Compare commits
54 Commits
upload_doc
...
users/sind
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b432bd38eb | ||
|
|
ecb39dd99f | ||
|
|
51782f1157 | ||
|
|
425e375d50 | ||
|
|
55846b98bd | ||
|
|
3d02f07262 | ||
|
|
449118a1bf | ||
|
|
8405dbe8da | ||
|
|
19100ec437 | ||
|
|
ebd40cb9b0 | ||
|
|
986dbe7d54 | ||
|
|
143f7d8f2c | ||
|
|
b646f9f4cb | ||
|
|
0f52db73e7 | ||
|
|
9c1b9e6ff6 | ||
|
|
19041ffedd | ||
|
|
ceb66ed5b8 | ||
|
|
96b88ac344 | ||
|
|
bcedf0a29f | ||
|
|
67133017ce | ||
|
|
b5d7ab0a30 | ||
|
|
6dba4937ce | ||
|
|
1a3ca94efb | ||
|
|
9f7783f3f9 | ||
|
|
daa26d289b | ||
|
|
879cb08949 | ||
|
|
92f43c28a7 | ||
|
|
5f0c7bcea2 | ||
|
|
fa55d528ad | ||
|
|
c873fed7aa | ||
|
|
7ec5290293 | ||
|
|
4005128211 | ||
|
|
38d13cc74e | ||
|
|
4ab93a5a32 | ||
|
|
44e25c0769 | ||
|
|
8ea8f0230f | ||
|
|
655b998b84 | ||
|
|
9e2f6a9c89 | ||
|
|
42e11d5160 | ||
|
|
10037d844e | ||
|
|
13434715b3 | ||
|
|
676434cac5 | ||
|
|
3d5580865a | ||
|
|
7375cc717c | ||
|
|
de3e56bb99 | ||
|
|
53cd78452b | ||
|
|
fb6eb635c1 | ||
|
|
fb6c0caca6 | ||
|
|
e9f3c64239 | ||
|
|
bb6ee5deec | ||
|
|
5796b28045 | ||
|
|
9635d14599 | ||
|
|
c58d5765c2 | ||
|
|
708f4316b4 |
@@ -144,4 +144,5 @@ src/Explorer/Notebook/NotebookRenderer/decorators/kbd-shortcuts/index.tsx
|
|||||||
src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx
|
src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx
|
||||||
src/Explorer/Tree/ResourceTreeAdapter.tsx
|
src/Explorer/Tree/ResourceTreeAdapter.tsx
|
||||||
__mocks__/monaco-editor.ts
|
__mocks__/monaco-editor.ts
|
||||||
src/Explorer/Tree/ResourceTree.tsx
|
src/Explorer/Tree/ResourceTree.tsx
|
||||||
|
src/Utils/PriorityBasedExecutionUtils.ts
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
<svg width="10" height="14" viewBox="0 0 10 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="12" height="14" viewBox="0 0 12 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M10 2.5C10 3.88071 7.76142 5 5 5C2.23858 5 0 3.88071 0 2.5C0 1.11929 2.23858 0 5 0C7.76142 0 10 1.11929 10 2.5ZM0 11.5V4.487C1.057 5.413 2.864 6 5 6C7.136 6 8.943 5.413 10 4.487V11.5C10 12.925 7.851 14 5 14C2.149 14 0 12.925 0 11.5Z" fill="#0078D4"/>
|
<path d="M10 2.5C10 3.88071 7.76142 5 5 5C2.23858 5 0 3.88071 0 2.5C0 1.11929 2.23858 0 5 0C7.76142 0 10 1.11929 10 2.5ZM0 11.5V4.487C1.057 5.413 2.864 6 5 6C7.136 6 8.943 5.413 10 4.487V11.5C10 12.925 7.851 14 5 14C2.149 14 0 12.925 0 11.5Z" fill="#0078D4"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 363 B After Width: | Height: | Size: 363 B |
@@ -1,3 +1,3 @@
|
|||||||
<svg width="13" height="17" viewBox="0 0 13 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="12" height="14" viewBox="0 0 12 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M4.21207 0C3.73772 0 3.32085 0.314446 3.19054 0.770537L0.940937 8.64415C0.747029 9.32283 1.25663 9.99841 1.96246 9.99841H3.22974L2.06002 14.6773C1.79611 15.7329 3.10089 16.4551 3.85526 15.6726L12.5318 6.81506L12.5354 6.81137C13.1762 6.1436 12.7152 5 11.7688 5H9.20509L10.4665 1.40582L10.469 1.39836C10.6983 0.710426 10.1863 0 9.46114 0H4.21207Z" fill="#0078D4"/>
|
<path d="M4.21207 0C3.73772 0 3.32085 0.314446 3.19054 0.770537L0.940937 8.64415C0.747029 9.32283 1.25663 9.99841 1.96246 9.99841H3.22974L2.06002 14.6773C1.79611 15.7329 3.10089 16.4551 3.85526 15.6726L12.5318 6.81506L12.5354 6.81137C13.1762 6.1436 12.7152 5 11.7688 5H9.20509L10.4665 1.40582L10.469 1.39836C10.6983 0.710426 10.1863 0 9.46114 0H4.21207Z" fill="#0078D4"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 475 B After Width: | Height: | Size: 475 B |
34
images/CopilotSidebarLogo.svg
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.79997 1.36139L9.59997 1.54445V6.72957L6.71997 7.39864L5.75998 8.71588V10.7866C5.75998 12.178 6.51265 13.4605 7.72739 14.139L10.4421 15.6554L5.75997 18.1318L3.81352 18.4092L1.96738 17.378C0.75264 16.6995 -3.05176e-05 15.417 -3.05176e-05 14.0256V7.97332C-3.05176e-05 6.58155 0.753043 5.29871 1.9683 4.62034L7.3904 1.59367L7.38722 1.59591L7.79997 1.36139Z" fill="url(#paint0_radial_1157_162756)"/>
|
||||||
|
<path d="M7.79997 1.36139L9.59997 1.54445V6.72957L6.71997 7.39864L5.75998 8.71588V10.7866C5.75998 12.178 6.51265 13.4605 7.72739 14.139L10.4421 15.6554L5.75997 18.1318L3.81352 18.4092L1.96738 17.378C0.75264 16.6995 -3.05176e-05 15.417 -3.05176e-05 14.0256V7.97332C-3.05176e-05 6.58155 0.753043 5.29871 1.9683 4.62034L7.3904 1.59367L7.38722 1.59591L7.79997 1.36139Z" fill="url(#paint1_linear_1157_162756)"/>
|
||||||
|
<path d="M13.4397 8.11841L18.2397 10.9984L19.1997 11.9584V14.025C19.1997 15.4164 18.447 16.6989 17.2323 17.3774L11.4723 20.5948C10.3085 21.2448 8.89088 21.2448 7.72711 20.5948L1.96711 17.3774C1.85515 17.3149 1.74713 17.2472 1.64325 17.1748L1.96708 17.3557C3.13085 18.0057 4.54849 18.0057 5.71226 17.3557L11.4723 14.1383C12.687 13.4598 13.4397 12.1773 13.4397 10.7859V8.11841Z" fill="url(#paint2_radial_1157_162756)"/>
|
||||||
|
<path d="M13.4397 8.11841L18.2397 10.9984L19.1997 11.9584V14.025C19.1997 15.4164 18.447 16.6989 17.2323 17.3774L11.4723 20.5948C10.3085 21.2448 8.89088 21.2448 7.72711 20.5948L1.96711 17.3774C1.85515 17.3149 1.74713 17.2472 1.64325 17.1748L1.96708 17.3557C3.13085 18.0057 4.54849 18.0057 5.71226 17.3557L11.4723 14.1383C12.687 13.4598 13.4397 12.1773 13.4397 10.7859V8.11841Z" fill="url(#paint3_linear_1157_162756)"/>
|
||||||
|
<path d="M17.2316 4.62014L11.4716 1.40484C10.3083 0.755475 8.89151 0.755475 7.72821 1.40484L7.38921 1.59407C6.37448 2.30824 5.75989 3.47665 5.75989 4.73397V8.74548L7.72821 7.64674C8.89151 6.99738 10.3083 6.99738 11.4716 7.64674L17.2316 10.862C18.4192 11.525 19.1654 12.7652 19.1987 14.1202C19.1995 14.0886 19.1999 14.057 19.1999 14.0254V7.97311C19.1999 6.58134 18.4468 5.29851 17.2316 4.62014Z" fill="url(#paint4_radial_1157_162756)"/>
|
||||||
|
<path d="M17.2316 4.62014L11.4716 1.40484C10.3083 0.755475 8.89151 0.755475 7.72821 1.40484L7.38921 1.59407C6.37448 2.30824 5.75989 3.47665 5.75989 4.73397V8.74548L7.72821 7.64674C8.89151 6.99738 10.3083 6.99738 11.4716 7.64674L17.2316 10.862C18.4192 11.525 19.1654 12.7652 19.1987 14.1202C19.1995 14.0886 19.1999 14.057 19.1999 14.0254V7.97311C19.1999 6.58134 18.4468 5.29851 17.2316 4.62014Z" fill="url(#paint5_linear_1157_162756)"/>
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="paint0_radial_1157_162756" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(8.1032 6.84805) rotate(112.544) scale(14.0711 11.584)">
|
||||||
|
<stop offset="0.206732" stop-color="#4995D0"/>
|
||||||
|
<stop offset="0.875628" stop-color="#0078D4"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="paint1_linear_1157_162756" x1="6.63744" y1="16.8883" x2="5.87214" y2="15.464" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.9999" stop-color="#0078D4"/>
|
||||||
|
<stop offset="1" stop-color="#0078D4" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="paint2_radial_1157_162756" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(4.13968 15.6306) rotate(-3.9995) scale(14.7718 9.27561)">
|
||||||
|
<stop offset="0.140029" stop-color="#80C8FF"/>
|
||||||
|
<stop offset="0.952721" stop-color="#0078D4"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="paint3_linear_1157_162756" x1="16.1362" y1="10.2483" x2="15.3298" y2="11.6531" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.9999" stop-color="#3F8AC3"/>
|
||||||
|
<stop offset="1" stop-color="#8C66BA" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="paint4_radial_1157_162756" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(15.9793 10.7505) rotate(-138.33) scale(13.6809 7.80282)">
|
||||||
|
<stop stop-color="#7BC6FF"/>
|
||||||
|
<stop offset="0.839255" stop-color="#0078D4"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="paint5_linear_1157_162756" x1="5.75989" y1="6.14584" x2="7.32799" y2="6.14584" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0.9999" stop-color="#0078D4"/>
|
||||||
|
<stop offset="1" stop-color="#436DCD" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.2 KiB |
29
images/CopilotSidebarWelcomeIllustration.svg
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<svg width="121" height="94" viewBox="0 0 121 94" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path opacity="0.3" d="M3.25029 80.7474H97.1699C97.8805 80.7474 98.5648 80.471 99.078 79.9578C99.5913 79.4446 99.8676 78.7603 99.8676 78.0497V13.8575C99.8676 12.3704 98.3148 11.0018 96.2224 11.0018L4.53992 12.3968C4.18461 12.3968 3.84247 12.4626 3.51348 12.5942C3.18449 12.7389 2.89498 12.9231 2.64495 13.1863C2.39492 13.4364 2.19752 13.7259 2.05277 14.0549C1.90801 14.3838 1.85538 14.7392 1.85538 15.0813L0.552582 78.076C0.552582 78.7866 0.82893 79.4709 1.34215 79.971C1.85537 80.471 2.53967 80.7474 3.25029 80.7474Z" fill="#1F1D20"/>
|
||||||
|
<path d="M96.9332 9.80411H2.67139C1.19752 9.80411 0 11.0016 0 12.4755V76.7993C0 78.2732 1.19752 79.4707 2.67139 79.4707H96.9463C98.4202 79.4707 99.6177 78.2732 99.6177 76.7993V12.4755C99.6045 11.0016 98.407 9.80411 96.9332 9.80411Z" fill="url(#paint0_linear_1157_163007)"/>
|
||||||
|
<path d="M95.6963 13.7121H50.1379V74.904H95.6963V13.7121Z" fill="white"/>
|
||||||
|
<path d="M50.7829 13.7121H5.22461V74.904H50.7829V13.7121Z" fill="#CDCDD0"/>
|
||||||
|
<path d="M14.3309 0V60.6918C14.3309 60.6918 26.4903 59.4811 36.3468 64.4291C46.2165 69.3771 50.7829 74.8514 50.7829 74.8514V14.1465C50.7829 14.1465 45.0979 6.90876 36.3468 4.23737C29.2275 2.05289 14.3309 0 14.3309 0Z" fill="#EAEAEA"/>
|
||||||
|
<path d="M59.1387 42.9135L56.2699 41.6633L53.388 42.9135V9.80411H59.1387V42.9135Z" fill="url(#paint1_linear_1157_163007)"/>
|
||||||
|
<path opacity="0.15" d="M86.7212 25.6883H66.7713C65.9028 25.6883 65.2053 26.3989 65.2053 27.2543C65.2053 28.1228 65.9159 28.8202 66.7713 28.8202H86.7212C87.5897 28.8202 88.2872 28.1096 88.2872 27.2543C88.3003 26.3857 87.5897 25.6883 86.7212 25.6883Z" fill="#1F1D20"/>
|
||||||
|
<path opacity="0.15" d="M86.7212 32.5045H66.7713C65.9028 32.5045 65.2053 33.2151 65.2053 34.0705C65.2053 34.939 65.9159 35.6365 66.7713 35.6365H86.7212C87.5897 35.6365 88.2872 34.9258 88.2872 34.0705C88.3003 33.2019 87.5897 32.5045 86.7212 32.5045Z" fill="#1F1D20"/>
|
||||||
|
<path opacity="0.15" fill-rule="evenodd" clip-rule="evenodd" d="M66.7715 39.3336H86.695C87.5636 39.3336 88.261 40.0442 88.261 40.8995C88.261 41.7681 87.5504 42.4655 86.695 42.4655H66.7715C65.903 42.4655 65.2055 41.7549 65.2055 40.8995C65.1923 40.0442 65.8898 39.3336 66.7715 39.3336Z" fill="#1F1D20"/>
|
||||||
|
<path d="M86.7212 25.1614H66.7713C65.9028 25.1614 65.2053 25.872 65.2053 26.7273C65.2053 27.5959 65.9159 28.2933 66.7713 28.2933H86.7212C87.5897 28.2933 88.2872 27.5827 88.2872 26.7273C88.3003 25.8588 87.5897 25.1614 86.7212 25.1614Z" fill="#50E6FF"/>
|
||||||
|
<path d="M86.7212 31.9917H66.7713C65.9028 31.9917 65.2053 32.7023 65.2053 33.5577C65.2053 34.4262 65.9159 35.1237 66.7713 35.1237H86.7212C87.5897 35.1237 88.2872 34.4131 88.2872 33.5577C88.3003 32.6892 87.5897 31.9917 86.7212 31.9917Z" fill="#32B0E7"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M66.7715 38.808H86.695C87.5636 38.808 88.261 39.5186 88.261 40.3739C88.261 41.2425 87.5504 41.9399 86.695 41.9399H66.7715C65.903 41.9531 65.2055 41.2293 65.2055 40.3739C65.1923 39.5186 65.8898 38.808 66.7715 38.808Z" fill="#185A97"/>
|
||||||
|
<path opacity="0.2" d="M120.295 60.2875H80.476V85.9653C80.476 87.0514 81.3515 87.9269 82.4375 87.9269H86.6156V94L92.733 87.9269H118.333C119.419 87.9269 120.295 87.0514 120.295 85.9653V60.2875Z" fill="#1F1D21"/>
|
||||||
|
<path d="M118.965 58.6804H81.8058C81.0744 58.6804 80.476 59.2788 80.476 60.0103V84.9898C80.476 85.7213 81.0744 86.3197 81.8058 86.3197H86.6156V92.3928L92.733 86.3197H118.965C119.696 86.3197 120.295 85.7213 120.295 84.9898V60.0103C120.295 59.2788 119.696 58.6804 118.965 58.6804Z" fill="#49C8EF"/>
|
||||||
|
<path d="M106.564 67.8017H94.1846C93.697 67.8017 93.298 68.2006 93.298 68.6883C93.298 69.1759 93.697 69.5748 94.1846 69.5748H106.564C107.051 69.5748 107.45 69.1759 107.45 68.6883C107.45 68.2006 107.051 67.8017 106.564 67.8017Z" fill="#C3F1FF"/>
|
||||||
|
<path d="M106.564 71.6913H94.1846C93.697 71.6913 93.298 72.0903 93.298 72.5779C93.298 73.0655 93.697 73.4645 94.1846 73.4645H106.564C107.051 73.4645 107.45 73.0655 107.45 72.5779C107.45 72.1014 107.051 71.6913 106.564 71.6913Z" fill="#C3F1FF"/>
|
||||||
|
<path d="M106.564 75.6026H94.1846C93.697 75.6026 93.298 76.0016 93.298 76.4892C93.298 76.9768 93.697 77.3758 94.1846 77.3758H106.564C107.051 77.3758 107.45 76.9768 107.45 76.4892C107.45 75.9905 107.051 75.6026 106.564 75.6026Z" fill="#C3F1FF"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_1157_163007" x1="49.8133" y1="14.2868" x2="49.8133" y2="85.0398" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#007ED8"/>
|
||||||
|
<stop offset="0.7065" stop-color="#002D4C"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint1_linear_1157_163007" x1="36.5785" y1="-9.69372" x2="104.813" y2="115.29" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#007ED8"/>
|
||||||
|
<stop offset="0.7065" stop-color="#002D4C"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.6 KiB |
3
images/CopilotTabIcon.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="22" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12.5849 1.28273C11.0125 0.262814 8.98746 0.262813 7.41509 1.28273L2.16509 4.68813C0.8149 5.56393 0 7.06384 0 8.6732V13.3268C0 14.9361 0.814898 16.4361 2.16509 17.3119L7.41509 20.7173C8.98746 21.7372 11.0125 21.7372 12.5849 20.7173L17.8349 17.3119C19.1851 16.4361 20 14.9361 20 13.3268V8.6732C20 7.06384 19.1851 5.56393 17.8349 4.68813L12.5849 1.28273ZM9.50097 2.05627C10.2759 1.93601 11.0848 2.09764 11.7686 2.54117L17.0186 5.94657C17.9424 6.54581 18.5 7.57205 18.5 8.6732V11.3955C18.287 11.1926 18.0465 11.0117 17.7802 10.8585L11.8647 7.4571C10.7104 6.79343 9.29086 6.79152 8.13486 7.45209L7.5 7.81487V4.41942C7.5 3.54181 8.01028 2.74428 8.80712 2.37651L9.50097 2.05627ZM17.8644 15.2257L17.7932 15.3503C17.5776 15.6214 17.3172 15.8597 17.0186 16.0534L11.7686 19.4588C10.6928 20.1567 9.30721 20.1567 8.23138 19.4588L5.86807 17.9259C6.11557 17.8361 6.35595 17.7193 6.58487 17.5754L12.2457 14.0172C13.3374 13.3309 14 12.1318 14 10.8423V10.4152L17.0324 12.1589C18.1077 12.7771 18.4798 14.1489 17.8644 15.2257ZM12.5 9.55272V10.8423C12.5 11.616 12.1025 12.3354 11.4474 12.7472L10.0303 13.6379L8.57078 12.7398C7.90535 12.3303 7.5 11.6049 7.5 10.8235V9.54249L8.87907 8.75445C9.57267 8.35811 10.4244 8.35926 11.1169 8.75746L12.5 9.55272ZM8.61445 14.5279L5.78662 16.3054C5.02045 16.787 4.04031 16.7627 3.29894 16.2437L2.5521 15.721C1.88767 15.1111 1.5 14.245 1.5 13.3268V8.6732C1.5 7.57205 2.05756 6.5458 2.98138 5.94657L6.0268 3.97116C6.00907 4.11873 6 4.26836 6 4.41942V10.8235C6 12.1258 6.67558 13.3348 7.78463 14.0173L8.61445 14.5279Z" fill="#0078D4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
3
images/StopGenerating.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="14" height="13" viewBox="0 0 14 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3 0C1.61929 0 0.5 1.11929 0.5 2.5V9.5C0.5 10.8807 1.61929 12 3 12H4.75716C4.50353 11.6929 4.28261 11.3578 4.09971 11H3C2.17157 11 1.5 10.3284 1.5 9.5V2.5C1.5 1.67157 2.17157 1 3 1H10C10.8284 1 11.5 1.67157 11.5 2.5V3.59971C11.8578 3.78261 12.1929 4.00353 12.5 4.25716V2.5C12.5 1.11929 11.3807 0 10 0H3ZM9 13C11.4853 13 13.5 10.9853 13.5 8.5C13.5 6.01472 11.4853 4 9 4C6.51472 4 4.5 6.01472 4.5 8.5C4.5 10.9853 6.51472 13 9 13ZM10.8536 6.64645C11.0488 6.84171 11.0488 7.15829 10.8536 7.35355L9.70711 8.5L10.8536 9.64645C11.0488 9.84171 11.0488 10.1583 10.8536 10.3536C10.6583 10.5488 10.3417 10.5488 10.1464 10.3536L9 9.20711L7.85355 10.3536C7.65829 10.5488 7.34171 10.5488 7.14645 10.3536C6.95118 10.1583 6.95118 9.84171 7.14645 9.64645L8.29289 8.5L7.14645 7.35355C6.95118 7.15829 6.95118 6.84171 7.14645 6.64645C7.34171 6.45118 7.65829 6.45118 7.85355 6.64645L9 7.79289L10.1464 6.64645C10.3417 6.45118 10.6583 6.45118 10.8536 6.64645Z" fill="#0078D4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -108,7 +108,7 @@ table.storage thead td {
|
|||||||
background-color: @BaseLight;
|
background-color: @BaseLight;
|
||||||
/*[{datatable-header-background}]*/
|
/*[{datatable-header-background}]*/
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: #808080;
|
color: #333333;
|
||||||
/*[{datatable-header-text}]*/
|
/*[{datatable-header-text}]*/
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ input::-webkit-inner-spin-button {
|
|||||||
.advanced-options-panel .advanced-options .select .select-options-link {
|
.advanced-options-panel .advanced-options .select .select-options-link {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
outline: none;
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-panel .row .column-headers .Field {
|
.query-panel .row .column-headers .Field {
|
||||||
|
|||||||
@@ -28,4 +28,4 @@
|
|||||||
.clickDisabled {
|
.clickDisabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
package-lock.json
generated
@@ -164,6 +164,7 @@
|
|||||||
"jest": "26.6.3",
|
"jest": "26.6.3",
|
||||||
"jest-canvas-mock": "2.3.1",
|
"jest-canvas-mock": "2.3.1",
|
||||||
"jest-playwright-preset": "1.5.1",
|
"jest-playwright-preset": "1.5.1",
|
||||||
|
"jest-react-hooks-shallow": "1.5.1",
|
||||||
"jest-trx-results-processor": "0.0.7",
|
"jest-trx-results-processor": "0.0.7",
|
||||||
"less": "3.8.1",
|
"less": "3.8.1",
|
||||||
"less-loader": "4.1.0",
|
"less-loader": "4.1.0",
|
||||||
@@ -19578,6 +19579,16 @@
|
|||||||
"node": ">=8.9.0"
|
"node": ">=8.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jest-react-hooks-shallow": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://msazure.pkgs.visualstudio.com/_packaging/AzurePortal/npm/registry/jest-react-hooks-shallow/-/jest-react-hooks-shallow-1.5.1.tgz",
|
||||||
|
"integrity": "sha1-1SI4EGG7nf9WcfhLBT0LPxy+N/k=",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jest-regex-util": {
|
"node_modules/jest-regex-util": {
|
||||||
"version": "24.9.0",
|
"version": "24.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz",
|
||||||
@@ -48608,6 +48619,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"jest-react-hooks-shallow": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://msazure.pkgs.visualstudio.com/_packaging/AzurePortal/npm/registry/jest-react-hooks-shallow/-/jest-react-hooks-shallow-1.5.1.tgz",
|
||||||
|
"integrity": "sha1-1SI4EGG7nf9WcfhLBT0LPxy+N/k=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"react": "^16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"jest-regex-util": {
|
"jest-regex-util": {
|
||||||
"version": "24.9.0",
|
"version": "24.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz",
|
||||||
|
|||||||
@@ -160,6 +160,7 @@
|
|||||||
"jest": "26.6.3",
|
"jest": "26.6.3",
|
||||||
"jest-canvas-mock": "2.3.1",
|
"jest-canvas-mock": "2.3.1",
|
||||||
"jest-playwright-preset": "1.5.1",
|
"jest-playwright-preset": "1.5.1",
|
||||||
|
"jest-react-hooks-shallow": "1.5.1",
|
||||||
"jest-trx-results-processor": "0.0.7",
|
"jest-trx-results-processor": "0.0.7",
|
||||||
"less": "3.8.1",
|
"less": "3.8.1",
|
||||||
"less-loader": "4.1.0",
|
"less-loader": "4.1.0",
|
||||||
@@ -232,4 +233,4 @@
|
|||||||
"prettier": {
|
"prettier": {
|
||||||
"printWidth": 120
|
"printWidth": 120
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -358,6 +358,7 @@ export enum ContainerStatusType {
|
|||||||
|
|
||||||
export enum PoolIdType {
|
export enum PoolIdType {
|
||||||
DefaultPoolId = "default",
|
DefaultPoolId = "default",
|
||||||
|
QueryCopilot = "query-copilot",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmulatorMasterKey =
|
export const EmulatorMasterKey =
|
||||||
@@ -429,30 +430,101 @@ export class JunoEndpoints {
|
|||||||
public static readonly Stage = "https://tools-staging.cosmos.azure.com";
|
public static readonly Stage = "https://tools-staging.cosmos.azure.com";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class PriorityLevel {
|
||||||
|
public static readonly High = "high";
|
||||||
|
public static readonly Low = "low";
|
||||||
|
public static readonly Default = "low";
|
||||||
|
}
|
||||||
|
|
||||||
export const QueryCopilotSampleDatabaseId = "CopilotSampleDb";
|
export const QueryCopilotSampleDatabaseId = "CopilotSampleDb";
|
||||||
export const QueryCopilotSampleContainerId = "SampleContainer";
|
export const QueryCopilotSampleContainerId = "SampleContainer";
|
||||||
|
|
||||||
export const QueryCopilotSampleContainerSchema = {
|
export const QueryCopilotSampleContainerSchema = {
|
||||||
product: {
|
product: {
|
||||||
sampleData: {
|
sampleData: {
|
||||||
id: "de6fadec-0384-43c8-93ea-16c0170b5460",
|
id: "c415e70f-9bf5-4cda-aebe-a290cb8b94c2",
|
||||||
name: "Premium Phone Mini (Red)",
|
name: "Amazing Phone 3000 (Black)",
|
||||||
price: 652.04,
|
price: 223.33,
|
||||||
category: "Electronics",
|
category: "Electronics",
|
||||||
description:
|
description:
|
||||||
"This Premium Phone Mini (Red) is designed by the company under agreement with the FCC, so we'd give it a pass in either direction, but no one should be using this handset without a compatible handset. All in all, this phone is one of the most affordable Android handsets out there at $100. Check them out.\n\n9. HTC One M9 4",
|
"This Amazing Phone 3000 (Black) is made of black metal! It has a very well made aluminum body and it feels very comfortable. We loved the sound that comes out of it! Also, the design of the phone was a little loose at first because I was using the camera and felt uncomfortable wearing it. The phone is actually made slightly smaller than these photos! This is due to the addition of a 3.3mm filter",
|
||||||
stock: 74,
|
stock: 84,
|
||||||
countryOfOrigin: "Mexico",
|
countryOfOrigin: "USA",
|
||||||
firstAvailable: "2018-07-07 17:42:28",
|
firstAvailable: "2018-09-07 19:41:44",
|
||||||
priceHistory: [592.81],
|
priceHistory: [238.68, 234.7, 221.49, 205.88, 220.15],
|
||||||
customerRatings: [
|
customerRatings: [
|
||||||
{ username: "dannyhowell", stars: 1, date: "2022-03-12 17:01:23", verifiedUser: true },
|
{
|
||||||
{ username: "lindsay26", stars: 1, date: "2022-12-29 07:18:20", verifiedUser: false },
|
username: "steven66",
|
||||||
{ username: "smithmiguel", stars: 3, date: "2022-09-08 11:43:27", verifiedUser: false },
|
firstName: "Carol",
|
||||||
{ username: "julie07", stars: 3, date: "2021-03-14 23:54:10", verifiedUser: true },
|
gender: "female",
|
||||||
{ username: "kelly93", stars: 3, date: "2022-11-05 12:45:43", verifiedUser: false },
|
lastName: "Shelton",
|
||||||
{ username: "katherinereynolds", stars: 2, date: "2022-09-14 11:49:36", verifiedUser: false },
|
age: "25-35",
|
||||||
{ username: "chandlerkenneth", stars: 1, date: "2022-01-14 12:14:43", verifiedUser: true },
|
area: "suburban",
|
||||||
|
address: "261 Collins Burgs Apt. 332\nNorth Taylor, NM 32268",
|
||||||
|
stars: 5,
|
||||||
|
date: "2021-04-22 13:42:14",
|
||||||
|
verifiedUser: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "khudson",
|
||||||
|
firstName: "Ronald",
|
||||||
|
gender: "male",
|
||||||
|
lastName: "Webb",
|
||||||
|
age: "18-24",
|
||||||
|
area: "suburban",
|
||||||
|
address: "9912 Parker Court Apt. 068\nNorth Austin, HI 76225",
|
||||||
|
stars: 5,
|
||||||
|
date: "2021-02-07 07:00:22",
|
||||||
|
verifiedUser: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "lfrancis",
|
||||||
|
firstName: "Brady",
|
||||||
|
gender: "male",
|
||||||
|
lastName: "Wright",
|
||||||
|
age: "35-45",
|
||||||
|
area: "urban",
|
||||||
|
address: "PSC 5437, Box 3159\nAPO AA 26385",
|
||||||
|
stars: 2,
|
||||||
|
date: "2022-02-23 21:40:10",
|
||||||
|
verifiedUser: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "nicolemartinez",
|
||||||
|
firstName: "Megan",
|
||||||
|
gender: "female",
|
||||||
|
lastName: "Tran",
|
||||||
|
age: "18-24",
|
||||||
|
area: "rural",
|
||||||
|
address: "7445 Salazar Brooks\nNew Sarah, PW 18097",
|
||||||
|
stars: 4,
|
||||||
|
date: "2021-09-01 22:21:40",
|
||||||
|
verifiedUser: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "uguzman",
|
||||||
|
firstName: "Deanna",
|
||||||
|
gender: "female",
|
||||||
|
lastName: "Campbell",
|
||||||
|
age: "18-24",
|
||||||
|
area: "urban",
|
||||||
|
address: "41104 Moreno Fort Suite 872\nPort Michaelbury, AK 48712",
|
||||||
|
stars: 1,
|
||||||
|
date: "2022-03-07 02:23:14",
|
||||||
|
verifiedUser: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "rebeccahunt",
|
||||||
|
firstName: "Jared",
|
||||||
|
gender: "male",
|
||||||
|
lastName: "Lopez",
|
||||||
|
age: "18-24",
|
||||||
|
area: "rural",
|
||||||
|
address: "392 Morgan Village Apt. 785\nGreenshire, CT 05921",
|
||||||
|
stars: 5,
|
||||||
|
date: "2021-04-17 04:17:49",
|
||||||
|
verifiedUser: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
rareProperty: true,
|
rareProperty: true,
|
||||||
},
|
},
|
||||||
@@ -494,6 +566,24 @@ export const QueryCopilotSampleContainerSchema = {
|
|||||||
username: {
|
username: {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
|
firstName: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
gender: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
lastName: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
age: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
area: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
stars: {
|
stars: {
|
||||||
type: "number",
|
type: "number",
|
||||||
},
|
},
|
||||||
@@ -516,3 +606,69 @@ export const QueryCopilotSampleContainerSchema = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ShortenedQueryCopilotSampleContainerSchema = {
|
||||||
|
containerSchema: {
|
||||||
|
product: {
|
||||||
|
sampleData: {
|
||||||
|
categoryName: "Components, Saddles",
|
||||||
|
|
||||||
|
name: "LL Road Seat/Saddle",
|
||||||
|
|
||||||
|
price: 27.12,
|
||||||
|
|
||||||
|
tags: [
|
||||||
|
{
|
||||||
|
id: "0573D684-9140-4DEE-89AF-4E4A90E65666",
|
||||||
|
|
||||||
|
name: "Tag-113",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "6C2F05C8-1E61-4912-BE1A-C67A378429BB",
|
||||||
|
|
||||||
|
name: "Tag-5",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
schema: {
|
||||||
|
properties: {
|
||||||
|
categoryName: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
|
||||||
|
name: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
|
||||||
|
price: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
|
||||||
|
tags: {
|
||||||
|
items: {
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
|
||||||
|
name: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
type: "object",
|
||||||
|
},
|
||||||
|
|
||||||
|
type: "array",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
type: "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
userPrompt: "find all products",
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import { userContext } from "../UserContext";
|
|||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||||
import { getErrorMessage } from "./ErrorHandlingUtils";
|
import { getErrorMessage } from "./ErrorHandlingUtils";
|
||||||
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
|
import { PriorityLevel } from "../Common/Constants";
|
||||||
|
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
||||||
|
|
||||||
const _global = typeof self === "undefined" ? window : self;
|
const _global = typeof self === "undefined" ? window : self;
|
||||||
|
|
||||||
@@ -105,6 +108,13 @@ export function client(): Cosmos.CosmosClient {
|
|||||||
if (configContext.PROXY_PATH !== undefined) {
|
if (configContext.PROXY_PATH !== undefined) {
|
||||||
(options as any).plugins = [{ on: "request", plugin: requestPlugin }];
|
(options as any).plugins = [{ on: "request", plugin: requestPlugin }];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userContext.features.enablePriorityBasedThrottling && userContext.apiType === "SQL") {
|
||||||
|
const plugins = (options as any).plugins || [];
|
||||||
|
plugins.push({ on: "request", plugin: PriorityBasedExecutionUtils.requestPlugin });
|
||||||
|
(options as any).plugins = plugins;
|
||||||
|
}
|
||||||
|
|
||||||
_client = new Cosmos.CosmosClient(options);
|
_client = new Cosmos.CosmosClient(options);
|
||||||
return _client;
|
return _client;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
|
|||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label={entityValueLabel && entityValueLabel}
|
label={entityValueLabel && entityValueLabel}
|
||||||
id="entityTimeId"
|
|
||||||
autoFocus
|
autoFocus
|
||||||
type="time"
|
type="time"
|
||||||
value={entityTimeValue}
|
value={entityTimeValue}
|
||||||
@@ -54,7 +53,6 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
|
|||||||
<TextField
|
<TextField
|
||||||
label={entityValueLabel && entityValueLabel}
|
label={entityValueLabel && entityValueLabel}
|
||||||
className="addEntityTextField"
|
className="addEntityTextField"
|
||||||
id="entityValueId"
|
|
||||||
disabled={isEntityValueDisable}
|
disabled={isEntityValueDisable}
|
||||||
type={entityValueType}
|
type={entityValueType}
|
||||||
placeholder={entityValuePlaceholder}
|
placeholder={entityValuePlaceholder}
|
||||||
|
|||||||
@@ -106,7 +106,6 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
|||||||
<Stack horizontal tokens={sectionStackTokens}>
|
<Stack horizontal tokens={sectionStackTokens}>
|
||||||
<TextField
|
<TextField
|
||||||
label={entityPropertyLabel && entityPropertyLabel}
|
label={entityPropertyLabel && entityPropertyLabel}
|
||||||
id="entityPropertyId"
|
|
||||||
autoFocus
|
autoFocus
|
||||||
disabled={isPropertyTypeDisable}
|
disabled={isPropertyTypeDisable}
|
||||||
placeholder={entityPropertyPlaceHolder}
|
placeholder={entityPropertyPlaceHolder}
|
||||||
@@ -120,7 +119,6 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
|||||||
onChange={onEntityTypeChange}
|
onChange={onEntityTypeChange}
|
||||||
options={options}
|
options={options}
|
||||||
disabled={isPropertyTypeDisable}
|
disabled={isPropertyTypeDisable}
|
||||||
id="entityTypeId"
|
|
||||||
styles={dropdownStyles}
|
styles={dropdownStyles}
|
||||||
/>
|
/>
|
||||||
<EntityValue
|
<EntityValue
|
||||||
@@ -137,7 +135,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
|||||||
/>
|
/>
|
||||||
{!isEntityValueDisable && (
|
{!isEntityValueDisable && (
|
||||||
<TooltipHost content="Edit property" id="editTooltip">
|
<TooltipHost content="Edit property" id="editTooltip">
|
||||||
<div tabIndex={0}>
|
<div>
|
||||||
<Image
|
<Image
|
||||||
{...imageProps}
|
{...imageProps}
|
||||||
src={EditIcon}
|
src={EditIcon}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
allowedAadEndpoints,
|
allowedAadEndpoints,
|
||||||
allowedArcadiaEndpoints,
|
allowedArcadiaEndpoints,
|
||||||
allowedArmEndpoints,
|
|
||||||
allowedBackendEndpoints,
|
|
||||||
allowedEmulatorEndpoints,
|
allowedEmulatorEndpoints,
|
||||||
allowedGraphEndpoints,
|
allowedGraphEndpoints,
|
||||||
allowedHostedExplorerEndpoints,
|
allowedHostedExplorerEndpoints,
|
||||||
allowedJunoOrigins,
|
allowedJunoOrigins,
|
||||||
allowedMongoBackendEndpoints,
|
allowedMongoBackendEndpoints,
|
||||||
allowedMsalRedirectEndpoints,
|
allowedMsalRedirectEndpoints,
|
||||||
|
defaultAllowedArmEndpoints,
|
||||||
|
defaultAllowedBackendEndpoints,
|
||||||
validateEndpoint,
|
validateEndpoint,
|
||||||
} from "Utils/EndpointValidation";
|
} from "Utils/EndpointValidation";
|
||||||
|
|
||||||
@@ -20,6 +20,8 @@ export enum Platform {
|
|||||||
|
|
||||||
export interface ConfigContext {
|
export interface ConfigContext {
|
||||||
platform: Platform;
|
platform: Platform;
|
||||||
|
allowedArmEndpoints: ReadonlyArray<string>;
|
||||||
|
allowedBackendEndpoints: ReadonlyArray<string>;
|
||||||
allowedParentFrameOrigins: ReadonlyArray<string>;
|
allowedParentFrameOrigins: ReadonlyArray<string>;
|
||||||
gitSha?: string;
|
gitSha?: string;
|
||||||
proxyPath?: string;
|
proxyPath?: string;
|
||||||
@@ -49,6 +51,8 @@ export interface ConfigContext {
|
|||||||
// Default configuration
|
// Default configuration
|
||||||
let configContext: Readonly<ConfigContext> = {
|
let configContext: Readonly<ConfigContext> = {
|
||||||
platform: Platform.Portal,
|
platform: Platform.Portal,
|
||||||
|
allowedArmEndpoints: defaultAllowedArmEndpoints,
|
||||||
|
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
|
||||||
allowedParentFrameOrigins: [
|
allowedParentFrameOrigins: [
|
||||||
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
|
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
|
||||||
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
|
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
|
||||||
@@ -77,7 +81,7 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
|
|
||||||
export function resetConfigContext(): void {
|
export function resetConfigContext(): void {
|
||||||
if (process.env.NODE_ENV !== "test") {
|
if (process.env.NODE_ENV !== "test") {
|
||||||
throw new Error("resetConfigContext can only becalled in a test environment");
|
throw new Error("resetConfigContext can only be called in a test environment");
|
||||||
}
|
}
|
||||||
configContext = {} as ConfigContext;
|
configContext = {} as ConfigContext;
|
||||||
}
|
}
|
||||||
@@ -87,7 +91,7 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.ARM_ENDPOINT, allowedArmEndpoints)) {
|
if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) {
|
||||||
delete newContext.ARM_ENDPOINT;
|
delete newContext.ARM_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +111,12 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
delete newContext.ARCADIA_ENDPOINT;
|
delete newContext.ARCADIA_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.BACKEND_ENDPOINT, allowedBackendEndpoints)) {
|
if (
|
||||||
|
!validateEndpoint(
|
||||||
|
newContext.BACKEND_ENDPOINT,
|
||||||
|
configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints
|
||||||
|
)
|
||||||
|
) {
|
||||||
delete newContext.BACKEND_ENDPOINT;
|
delete newContext.BACKEND_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +139,7 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
Object.assign(configContext, newContext);
|
Object.assign(configContext, newContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Injected for local develpment. These will be removed in the production bundle by webpack
|
// Injected for local development. These will be removed in the production bundle by webpack
|
||||||
if (process.env.NODE_ENV === "development") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
const port: string = process.env.PORT || "1234";
|
const port: string = process.env.PORT || "1234";
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
|
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import AddCollectionIcon from "../../images/AddCollection.svg";
|
import AddCollectionIcon from "../../images/AddCollection.svg";
|
||||||
@@ -144,11 +147,24 @@ export const createCollectionContextMenuButton = (
|
|||||||
export const createSampleCollectionContextMenuButton = (): TreeNodeMenuItem[] => {
|
export const createSampleCollectionContextMenuButton = (): TreeNodeMenuItem[] => {
|
||||||
const items: TreeNodeMenuItem[] = [];
|
const items: TreeNodeMenuItem[] = [];
|
||||||
if (userContext.apiType === "SQL") {
|
if (userContext.apiType === "SQL") {
|
||||||
items.push({
|
const copilotVersion = userContext.features.copilotVersion;
|
||||||
iconSrc: AddSqlQueryIcon,
|
if (copilotVersion === "v1.0") {
|
||||||
onClick: () => useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot),
|
items.push({
|
||||||
label: "New SQL Query",
|
iconSrc: AddSqlQueryIcon,
|
||||||
});
|
onClick: () => {
|
||||||
|
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
|
||||||
|
traceOpen(Action.OpenQueryCopilotFromNewQuery, { apiType: userContext.apiType });
|
||||||
|
},
|
||||||
|
label: "New SQL Query",
|
||||||
|
});
|
||||||
|
} else if (copilotVersion === "v2.0") {
|
||||||
|
const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection;
|
||||||
|
items.push({
|
||||||
|
iconSrc: AddSqlQueryIcon,
|
||||||
|
onClick: () => sampleCollection && sampleCollection.onNewQueryClick(sampleCollection, undefined),
|
||||||
|
label: "New SQL Query",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export class AccordionComponent extends React.Component<AccordionComponentProps>
|
|||||||
export interface AccordionItemComponentProps {
|
export interface AccordionItemComponentProps {
|
||||||
title: string;
|
title: string;
|
||||||
isExpanded?: boolean;
|
isExpanded?: boolean;
|
||||||
|
containerStyles?: React.CSSProperties;
|
||||||
styles?: React.CSSProperties;
|
styles?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,9 +55,9 @@ export class AccordionItemComponent extends React.Component<AccordionItemCompone
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
const { styles } = this.props;
|
const { containerStyles, styles } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className="accordionItemContainer">
|
<div className="accordionItemContainer" style={{ ...containerStyles }}>
|
||||||
<div className="accordionItemHeader" onClick={this.onHeaderClick} onKeyPress={this.onHeaderKeyPress}>
|
<div className="accordionItemHeader" onClick={this.onHeaderClick} onKeyPress={this.onHeaderKeyPress}>
|
||||||
{this.renderCollapseExpandIcon()}
|
{this.renderCollapseExpandIcon()}
|
||||||
{this.props.title}
|
{this.props.title}
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
|||||||
onClick={this.toggleCollapsed}
|
onClick={this.toggleCollapsed}
|
||||||
onKeyPress={this.onKeyPress}
|
onKeyPress={this.onKeyPress}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-name="Advanced"
|
|
||||||
role="button"
|
role="button"
|
||||||
aria-expanded={this.state.isExpanded}
|
aria-expanded={this.state.isExpanded}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ exports[`CollapsibleSectionComponent renders 1`] = `
|
|||||||
<Fragment>
|
<Fragment>
|
||||||
<Stack
|
<Stack
|
||||||
aria-expanded={true}
|
aria-expanded={true}
|
||||||
aria-name="Advanced"
|
|
||||||
className="collapsibleSection"
|
className="collapsibleSection"
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
|||||||
@@ -31,6 +31,13 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
|||||||
|
|
||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
this.createEditor(this.configureEditor.bind(this));
|
this.createEditor(this.configureEditor.bind(this));
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const suggestionWidget = this.editor?.getDomNode()?.querySelector(".suggest-widget") as HTMLElement;
|
||||||
|
if (suggestionWidget) {
|
||||||
|
suggestionWidget.style.display = "none";
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(previous: EditorReactProps) {
|
public componentDidUpdate(previous: EditorReactProps) {
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
onChange={(event, newInput?: string) => onThroughputValueChange(newInput)}
|
onChange={(event, newInput?: string) => onThroughputValueChange(newInput)}
|
||||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||||
min={AutoPilotUtils.autoPilotThroughput1K}
|
min={AutoPilotUtils.autoPilotThroughput1K}
|
||||||
|
max={isSharded ? Number.MAX_SAFE_INTEGER.toString() : "10000"}
|
||||||
value={throughput.toString()}
|
value={throughput.toString()}
|
||||||
ariaLabel={`${isDatabase ? "Database" : getCollectionName()} max RU/s`}
|
ariaLabel={`${isDatabase ? "Database" : getCollectionName()} max RU/s`}
|
||||||
required={true}
|
required={true}
|
||||||
|
|||||||
@@ -1644,6 +1644,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
errorMessage=""
|
errorMessage=""
|
||||||
id="autoscaleRUValueField"
|
id="autoscaleRUValueField"
|
||||||
key=".0:$.2"
|
key=".0:$.2"
|
||||||
|
max="9007199254740991"
|
||||||
min={1000}
|
min={1000}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
required={true}
|
required={true}
|
||||||
@@ -1667,6 +1668,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
deferredValidationTime={200}
|
deferredValidationTime={200}
|
||||||
errorMessage=""
|
errorMessage=""
|
||||||
id="autoscaleRUValueField"
|
id="autoscaleRUValueField"
|
||||||
|
max="9007199254740991"
|
||||||
min={1000}
|
min={1000}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
required={true}
|
required={true}
|
||||||
@@ -1964,6 +1966,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
aria-label="Container max RU/s"
|
aria-label="Container max RU/s"
|
||||||
className="ms-TextField-field field-64"
|
className="ms-TextField-field field-64"
|
||||||
id="autoscaleRUValueField"
|
id="autoscaleRUValueField"
|
||||||
|
max="9007199254740991"
|
||||||
min={1000}
|
min={1000}
|
||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { Link } from "@fluentui/react/lib/Link";
|
import { Link } from "@fluentui/react/lib/Link";
|
||||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||||
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
|
import { Platform } from "ConfigContext";
|
||||||
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { IGalleryItem } from "Juno/JunoClient";
|
import { IGalleryItem } from "Juno/JunoClient";
|
||||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
@@ -23,7 +26,7 @@ import { PhoenixClient } from "../Phoenix/PhoenixClient";
|
|||||||
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
||||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../UserContext";
|
import { isAccountNewerThanThresholdInMs, userContext } from "../UserContext";
|
||||||
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
||||||
import { stringToBlob } from "../Utils/BlobUtils";
|
import { stringToBlob } from "../Utils/BlobUtils";
|
||||||
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
||||||
@@ -258,6 +261,45 @@ export default class Explorer {
|
|||||||
// TODO: return result
|
// TODO: return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getRandomInt(max: number) {
|
||||||
|
return Math.floor(Math.random() * max);
|
||||||
|
}
|
||||||
|
|
||||||
|
public openNPSSurveyDialog(): void {
|
||||||
|
if (!Platform.Portal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NINETY_DAYS_IN_MS = 7776000000;
|
||||||
|
const ONE_DAY_IN_MS = 86400000;
|
||||||
|
const isAccountNewerThanNinetyDays = isAccountNewerThanThresholdInMs(
|
||||||
|
userContext.databaseAccount?.systemData?.createdAt || "",
|
||||||
|
NINETY_DAYS_IN_MS
|
||||||
|
);
|
||||||
|
|
||||||
|
// Try Cosmos DB subscription - survey shown to random 25% of users at day 1 in Data Explorer.
|
||||||
|
if (userContext.isTryCosmosDBSubscription) {
|
||||||
|
if (
|
||||||
|
isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", ONE_DAY_IN_MS) &&
|
||||||
|
this.getRandomInt(100) < 25
|
||||||
|
) {
|
||||||
|
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// An existing account is lesser than 90 days old. For existing account show to random 10 % of users in Data Explorer.
|
||||||
|
if (isAccountNewerThanNinetyDays) {
|
||||||
|
if (this.getRandomInt(100) < 10) {
|
||||||
|
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// An existing account is greater than 90 days. For existing account show to random 25 % of users in Data Explorer.
|
||||||
|
if (this.getRandomInt(100) < 25) {
|
||||||
|
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async refreshDatabaseForResourceToken(): Promise<void> {
|
public async refreshDatabaseForResourceToken(): Promise<void> {
|
||||||
const databaseId = userContext.parsedResourceToken?.databaseId;
|
const databaseId = userContext.parsedResourceToken?.databaseId;
|
||||||
const collectionId = userContext.parsedResourceToken?.collectionId;
|
const collectionId = userContext.parsedResourceToken?.collectionId;
|
||||||
@@ -343,7 +385,7 @@ export default class Explorer {
|
|||||||
this._isInitializingNotebooks = false;
|
this._isInitializingNotebooks = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async allocateContainer(): Promise<void> {
|
public async allocateContainer(poolId: PoolIdType): Promise<void> {
|
||||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||||
const isAllocating = useNotebook.getState().isAllocating;
|
const isAllocating = useNotebook.getState().isAllocating;
|
||||||
if (
|
if (
|
||||||
@@ -353,7 +395,7 @@ export default class Explorer {
|
|||||||
) {
|
) {
|
||||||
const provisionData: IProvisionData = {
|
const provisionData: IProvisionData = {
|
||||||
cosmosEndpoint: userContext?.databaseAccount?.properties?.documentEndpoint,
|
cosmosEndpoint: userContext?.databaseAccount?.properties?.documentEndpoint,
|
||||||
poolId: PoolIdType.DefaultPoolId,
|
poolId: poolId === PoolIdType.DefaultPoolId ? undefined : poolId,
|
||||||
};
|
};
|
||||||
const connectionStatus: ContainerConnectionInfo = {
|
const connectionStatus: ContainerConnectionInfo = {
|
||||||
status: ConnectionStatusType.Connecting,
|
status: ConnectionStatusType.Connecting,
|
||||||
@@ -608,6 +650,8 @@ export default class Explorer {
|
|||||||
private _initSettings() {
|
private _initSettings() {
|
||||||
if (!ExplorerSettings.hasSettingsDefined()) {
|
if (!ExplorerSettings.hasSettingsDefined()) {
|
||||||
ExplorerSettings.createDefaultSettings();
|
ExplorerSettings.createDefaultSettings();
|
||||||
|
} else {
|
||||||
|
ExplorerSettings.ensurePriorityLevel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -711,7 +755,7 @@ export default class Explorer {
|
|||||||
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
|
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
|
||||||
}
|
}
|
||||||
if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenixNotebooks) {
|
if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenixNotebooks) {
|
||||||
await this.allocateContainer();
|
await this.allocateContainer(PoolIdType.DefaultPoolId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const notebookTabs = useTabs
|
const notebookTabs = useTabs
|
||||||
@@ -936,7 +980,7 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||||
if (isGithubTree) {
|
if (isGithubTree) {
|
||||||
await this.allocateContainer();
|
await this.allocateContainer(PoolIdType.DefaultPoolId);
|
||||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||||
this.createNewNoteBook(parent, isGithubTree);
|
this.createNewNoteBook(parent, isGithubTree);
|
||||||
} else {
|
} else {
|
||||||
@@ -945,7 +989,7 @@ export default class Explorer {
|
|||||||
undefined,
|
undefined,
|
||||||
"Create",
|
"Create",
|
||||||
async () => {
|
async () => {
|
||||||
await this.allocateContainer();
|
await this.allocateContainer(PoolIdType.DefaultPoolId);
|
||||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||||
this.createNewNoteBook(parent, isGithubTree);
|
this.createNewNoteBook(parent, isGithubTree);
|
||||||
},
|
},
|
||||||
@@ -1024,7 +1068,7 @@ export default class Explorer {
|
|||||||
|
|
||||||
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
|
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
|
||||||
if (useNotebook.getState().isPhoenixFeatures) {
|
if (useNotebook.getState().isPhoenixFeatures) {
|
||||||
await this.allocateContainer();
|
await this.allocateContainer(PoolIdType.DefaultPoolId);
|
||||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||||
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
|
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
|
||||||
this.connectToNotebookTerminal(kind);
|
this.connectToNotebookTerminal(kind);
|
||||||
@@ -1168,7 +1212,7 @@ export default class Explorer {
|
|||||||
await useNotebook.getState().getPhoenixStatus();
|
await useNotebook.getState().getPhoenixStatus();
|
||||||
}
|
}
|
||||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||||
await this.allocateContainer();
|
await this.allocateContainer(PoolIdType.DefaultPoolId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb
|
// We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb
|
||||||
@@ -1205,7 +1249,7 @@ export default class Explorer {
|
|||||||
undefined,
|
undefined,
|
||||||
"Upload",
|
"Upload",
|
||||||
async () => {
|
async () => {
|
||||||
await this.allocateContainer();
|
await this.allocateContainer(PoolIdType.DefaultPoolId);
|
||||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||||
this.uploadFilePanel(parent);
|
this.uploadFilePanel(parent);
|
||||||
},
|
},
|
||||||
@@ -1291,7 +1335,7 @@ export default class Explorer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection);
|
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
|
||||||
useDatabases.setState({ sampleDataResourceTokenCollection });
|
useDatabases.setState({ sampleDataResourceTokenCollection });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
*/
|
*/
|
||||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
import * as React from "react";
|
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
|
import * as React from "react";
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
import { ConnectionStatusType, StyleConstants } from "../../../Common/Constants";
|
import { ConnectionStatusType, PoolIdType, StyleConstants } from "../../../Common/Constants";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
@@ -76,7 +76,9 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
(useNotebook.getState().isPhoenixNotebooks || useNotebook.getState().isPhoenixFeatures) &&
|
(useNotebook.getState().isPhoenixNotebooks || useNotebook.getState().isPhoenixFeatures) &&
|
||||||
connectionInfo?.status !== ConnectionStatusType.Connect
|
connectionInfo?.status !== ConnectionStatusType.Connect
|
||||||
) {
|
) {
|
||||||
uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus(container, "connectionStatus"));
|
uiFabricControlButtons.unshift(
|
||||||
|
CommandBarUtil.createConnectionStatus(container, PoolIdType.DefaultPoolId, "connectionStatus")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
|
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import AddCollectionIcon from "../../../../images/AddCollection.svg";
|
import AddCollectionIcon from "../../../../images/AddCollection.svg";
|
||||||
@@ -268,7 +270,6 @@ function createNewCollectionGroup(container: Explorer): CommandButtonComponentPr
|
|||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
id: "createNewContainerCommandButton",
|
id: "createNewContainerCommandButton",
|
||||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,7 +315,6 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
|
|||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,6 +328,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
|
|||||||
onCommandClick: () => {
|
onCommandClick: () => {
|
||||||
if (useSelectedNode.getState().isQueryCopilotCollectionSelected()) {
|
if (useSelectedNode.getState().isQueryCopilotCollectionSelected()) {
|
||||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
|
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
|
||||||
|
traceOpen(Action.OpenQueryCopilotFromNewQuery, { apiType: userContext.apiType });
|
||||||
} else {
|
} else {
|
||||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||||
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
|
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
|
||||||
@@ -493,7 +494,7 @@ function createOpenTerminalButton(container: Explorer): CommandButtonComponentPr
|
|||||||
onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Default),
|
onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Default),
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: false,
|
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import _ from "underscore";
|
import _ from "underscore";
|
||||||
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
|
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
|
||||||
import { StyleConstants } from "../../../Common/Constants";
|
import { PoolIdType, StyleConstants } from "../../../Common/Constants";
|
||||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
@@ -204,9 +204,9 @@ export const createMemoryTracker = (key: string): ICommandBarItemProps => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createConnectionStatus = (container: Explorer, key: string): ICommandBarItemProps => {
|
export const createConnectionStatus = (container: Explorer, poolId: PoolIdType, key: string): ICommandBarItemProps => {
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
onRender: () => <ConnectionStatus container={container} />,
|
onRender: () => <ConnectionStatus container={container} poolId={poolId} />,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,14 +14,15 @@ import { useId } from "@fluentui/react-hooks";
|
|||||||
import { ActionButton, DefaultButton } from "@fluentui/react/lib/Button";
|
import { ActionButton, DefaultButton } from "@fluentui/react/lib/Button";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import "../../../../less/hostedexplorer.less";
|
import "../../../../less/hostedexplorer.less";
|
||||||
import { ConnectionStatusType, ContainerStatusType, Notebook } from "../../../Common/Constants";
|
import { ConnectionStatusType, ContainerStatusType, Notebook, PoolIdType } from "../../../Common/Constants";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { useNotebook } from "../../Notebook/useNotebook";
|
import { useNotebook } from "../../Notebook/useNotebook";
|
||||||
import "../CommandBar/ConnectionStatusComponent.less";
|
import "../CommandBar/ConnectionStatusComponent.less";
|
||||||
interface Props {
|
interface Props {
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
|
poolId: PoolIdType;
|
||||||
}
|
}
|
||||||
export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Element => {
|
export const ConnectionStatus: React.FC<Props> = ({ container, poolId }: Props): JSX.Element => {
|
||||||
const connectionInfo = useNotebook((state) => state.connectionInfo);
|
const connectionInfo = useNotebook((state) => state.connectionInfo);
|
||||||
const [second, setSecond] = React.useState("00");
|
const [second, setSecond] = React.useState("00");
|
||||||
const [minute, setMinute] = React.useState("00");
|
const [minute, setMinute] = React.useState("00");
|
||||||
@@ -93,7 +94,7 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
|
|||||||
(connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.Reconnect)
|
(connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.Reconnect)
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<ActionButton className="commandReactBtn" onClick={() => container.allocateContainer()}>
|
<ActionButton className="commandReactBtn" onClick={() => container.allocateContainer(poolId)}>
|
||||||
<TooltipHost content={toolTipContent}>
|
<TooltipHost content={toolTipContent}>
|
||||||
<Stack className="connectionStatusContainer" horizontal>
|
<Stack className="connectionStatusContainer" horizontal>
|
||||||
<Icon iconName="ConnectVirtualMachine" className="connectIcon" />
|
<Icon iconName="ConnectVirtualMachine" className="connectIcon" />
|
||||||
@@ -133,7 +134,9 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
|
|||||||
id={buttonId}
|
id={buttonId}
|
||||||
className={connectionInfo.status === ConnectionStatusType.Failed ? "commandReactBtn" : "connectedReactBtn"}
|
className={connectionInfo.status === ConnectionStatusType.Failed ? "commandReactBtn" : "connectedReactBtn"}
|
||||||
onClick={(e: React.MouseEvent<HTMLSpanElement>) =>
|
onClick={(e: React.MouseEvent<HTMLSpanElement>) =>
|
||||||
connectionInfo.status === ConnectionStatusType.Failed ? container.allocateContainer() : e.preventDefault()
|
connectionInfo.status === ConnectionStatusType.Failed
|
||||||
|
? container.allocateContainer(poolId)
|
||||||
|
: e.preventDefault()
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Stack className="connectionStatusContainer" horizontal>
|
<Stack className="connectionStatusContainer" horizontal>
|
||||||
|
|||||||
@@ -122,7 +122,9 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
{userContext.features.pr && <PrPreview pr={userContext.features.pr} />}
|
{userContext.features.pr && <PrPreview pr={userContext.features.pr} />}
|
||||||
<span className="consoleSplitter" />
|
<span className="consoleSplitter" />
|
||||||
<span className="headerStatus">
|
<span className="headerStatus">
|
||||||
<span className="headerStatusEllipsis">{this.state.headerStatus}</span>
|
<span className="headerStatusEllipsis" aria-live="assertive" aria-atomic="true">
|
||||||
|
{this.state.headerStatus}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
className="headerStatus"
|
className="headerStatus"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
aria-atomic="true"
|
||||||
|
aria-live="assertive"
|
||||||
className="headerStatusEllipsis"
|
className="headerStatusEllipsis"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@@ -225,6 +227,8 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
className="headerStatus"
|
className="headerStatus"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
aria-atomic="true"
|
||||||
|
aria-live="assertive"
|
||||||
className="headerStatusEllipsis"
|
className="headerStatusEllipsis"
|
||||||
>
|
>
|
||||||
message
|
message
|
||||||
|
|||||||
@@ -1425,6 +1425,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
this.setState({ isExecuting: false });
|
this.setState({ isExecuting: false });
|
||||||
TelemetryProcessor.traceSuccess(Action.CreateCollection, telemetryData, startKey);
|
TelemetryProcessor.traceSuccess(Action.CreateCollection, telemetryData, startKey);
|
||||||
useSidePanel.getState().closeSidePanel();
|
useSidePanel.getState().closeSidePanel();
|
||||||
|
// open NPS Survey Dialog once the collection is created
|
||||||
|
this.props.explorer.openNPSSurveyDialog();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage: string = getErrorMessage(error);
|
const errorMessage: string = getErrorMessage(error);
|
||||||
this.setState({ isExecuting: false, errorMessage, showErrorDetails: true });
|
this.setState({ isExecuting: false, errorMessage, showErrorDetails: true });
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { Checkbox, Dropdown, IDropdownOption, Link, Stack, Text, TextField } fro
|
|||||||
import * as Constants from "Common/Constants";
|
import * as Constants from "Common/Constants";
|
||||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
|
||||||
import React, { FunctionComponent, useState } from "react";
|
|
||||||
import * as SharedConstants from "Shared/Constants";
|
import * as SharedConstants from "Shared/Constants";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
||||||
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
|
import React, { FunctionComponent, useState } from "react";
|
||||||
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
|
import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
|
||||||
@@ -291,7 +291,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
|||||||
styles={getTextFieldStyles({ fontSize: 12, width: 150 })}
|
styles={getTextFieldStyles({ fontSize: 12, width: 150 })}
|
||||||
aria-required="true"
|
aria-required="true"
|
||||||
required={true}
|
required={true}
|
||||||
ariaLabel="addCollection-tableId"
|
ariaLabel="addCollection-table Id Create table"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
pattern="[^/?#\\]*[^/?# \\]"
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { IDropdownOption } from "@fluentui/react";
|
import { IDropdownOption } from "@fluentui/react";
|
||||||
import React, { FormEvent, FunctionComponent, useEffect, useState } from "react";
|
import React, { FormEvent, FunctionComponent, useEffect, useState } from "react";
|
||||||
import { HttpStatusCodes } from "../../../Common/Constants";
|
import { HttpStatusCodes, PoolIdType } from "../../../Common/Constants";
|
||||||
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
|
||||||
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
|
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
|
||||||
import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient";
|
import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient";
|
||||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||||
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem";
|
||||||
import { useNotebook } from "../../Notebook/useNotebook";
|
import { useNotebook } from "../../Notebook/useNotebook";
|
||||||
@@ -109,7 +109,7 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
|
|||||||
};
|
};
|
||||||
isGithubTree = false;
|
isGithubTree = false;
|
||||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||||
await container.allocateContainer();
|
await container.allocateContainer(PoolIdType.DefaultPoolId);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { Text, TextField } from "@fluentui/react";
|
import { Text, TextField } from "@fluentui/react";
|
||||||
import { Areas } from "Common/Constants";
|
import { Areas } from "Common/Constants";
|
||||||
import { deleteCollection } from "Common/dataAccess/deleteCollection";
|
|
||||||
import DeleteFeedback from "Common/DeleteFeedback";
|
import DeleteFeedback from "Common/DeleteFeedback";
|
||||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||||
|
import { deleteCollection } from "Common/dataAccess/deleteCollection";
|
||||||
import { Collection } from "Contracts/ViewModels";
|
import { Collection } from "Contracts/ViewModels";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
|
||||||
import { useTabs } from "hooks/useTabs";
|
|
||||||
import React, { FunctionComponent, useState } from "react";
|
|
||||||
import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility";
|
import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility";
|
||||||
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { getCollectionName } from "Utils/APITypeUtils";
|
import { getCollectionName } from "Utils/APITypeUtils";
|
||||||
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
|
||||||
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
|
import { useTabs } from "hooks/useTabs";
|
||||||
|
import React, { FunctionComponent, useState } from "react";
|
||||||
import { useDatabases } from "../../useDatabases";
|
import { useDatabases } from "../../useDatabases";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||||
@@ -126,6 +126,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
|||||||
setInputCollectionName(newInput);
|
setInputCollectionName(newInput);
|
||||||
}}
|
}}
|
||||||
ariaLabel={confirmContainer}
|
ariaLabel={confirmContainer}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{shouldRecordFeedback() && (
|
{shouldRecordFeedback() && (
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
id="confirmCollectionId"
|
id="confirmCollectionId"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
|
required={true}
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"fieldGroup": Object {
|
"fieldGroup": Object {
|
||||||
@@ -59,6 +60,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||||||
deferredValidationTime={200}
|
deferredValidationTime={200}
|
||||||
id="confirmCollectionId"
|
id="confirmCollectionId"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
|
required={true}
|
||||||
resizable={true}
|
resizable={true}
|
||||||
styles={[Function]}
|
styles={[Function]}
|
||||||
theme={
|
theme={
|
||||||
@@ -338,7 +340,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||||||
value=""
|
value=""
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ms-TextField root-55"
|
className="ms-TextField is-required root-55"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ms-TextField-wrapper"
|
className="ms-TextField-wrapper"
|
||||||
@@ -356,6 +358,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
onInput={[Function]}
|
onInput={[Function]}
|
||||||
|
required={true}
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
|||||||
setDatabaseInput(newInput);
|
setDatabaseInput(newInput);
|
||||||
}}
|
}}
|
||||||
ariaLabel={confirmDatabase}
|
ariaLabel={confirmDatabase}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isLastNonEmptyDatabase() && (
|
{isLastNonEmptyDatabase() && (
|
||||||
|
|||||||
@@ -112,6 +112,9 @@
|
|||||||
margin-top: 28px;
|
margin-top: 28px;
|
||||||
margin-left: 4px !important;
|
margin-left: 4px !important;
|
||||||
}
|
}
|
||||||
|
.addRemoveIcon [alt="editEntity"]:focus,.addRemoveIconLabel [alt="editEntity"]:focus{
|
||||||
|
border:1px dashed #605E5C
|
||||||
|
}
|
||||||
.addNewParamStyle {
|
.addNewParamStyle {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
margin-left: 5px !important;
|
margin-left: 5px !important;
|
||||||
|
|||||||
@@ -41,12 +41,18 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
? LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism)
|
? LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism)
|
||||||
: Constants.Queries.DefaultMaxDegreeOfParallelism
|
: Constants.Queries.DefaultMaxDegreeOfParallelism
|
||||||
);
|
);
|
||||||
|
const [priorityLevel, setPriorityLevel] = useState<string>(
|
||||||
|
LocalStorageUtility.hasItem(StorageKey.PriorityLevel)
|
||||||
|
? LocalStorageUtility.getEntryString(StorageKey.PriorityLevel)
|
||||||
|
: Constants.PriorityLevel.Default
|
||||||
|
);
|
||||||
const explorerVersion = configContext.gitSha;
|
const explorerVersion = configContext.gitSha;
|
||||||
const shouldShowQueryPageOptions = userContext.apiType === "SQL";
|
const shouldShowQueryPageOptions = userContext.apiType === "SQL";
|
||||||
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin";
|
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin";
|
||||||
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
|
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
|
||||||
const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
|
const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
|
||||||
|
const shouldShowPriorityLevelOption =
|
||||||
|
userContext.features.enablePriorityBasedThrottling && userContext.apiType === "SQL";
|
||||||
const handlerOnSubmit = (e: MouseEvent<HTMLButtonElement>) => {
|
const handlerOnSubmit = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
setIsExecuting(true);
|
setIsExecuting(true);
|
||||||
|
|
||||||
@@ -58,6 +64,7 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
LocalStorageUtility.setEntryString(StorageKey.ContainerPaginationEnabled, containerPaginationEnabled.toString());
|
LocalStorageUtility.setEntryString(StorageKey.ContainerPaginationEnabled, containerPaginationEnabled.toString());
|
||||||
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
|
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
|
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
|
||||||
|
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, priorityLevel.toString());
|
||||||
|
|
||||||
if (shouldShowGraphAutoVizOption) {
|
if (shouldShowGraphAutoVizOption) {
|
||||||
LocalStorageUtility.setEntryBoolean(
|
LocalStorageUtility.setEntryBoolean(
|
||||||
@@ -76,6 +83,7 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
StorageKey.MaxDegreeOfParellism
|
StorageKey.MaxDegreeOfParellism
|
||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
|
logConsoleInfo(`Updated priority level setting to ${LocalStorageUtility.getEntryString(StorageKey.PriorityLevel)}`);
|
||||||
|
|
||||||
if (shouldShowGraphAutoVizOption) {
|
if (shouldShowGraphAutoVizOption) {
|
||||||
logConsoleInfo(
|
logConsoleInfo(
|
||||||
@@ -116,6 +124,18 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
{ key: "true", text: "JSON" },
|
{ key: "true", text: "JSON" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const priorityLevelOptionList: IChoiceGroupOption[] = [
|
||||||
|
{ key: Constants.PriorityLevel.Low, text: "Low" },
|
||||||
|
{ key: Constants.PriorityLevel.High, text: "High" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleOnPriorityLevelOptionChange = (
|
||||||
|
ev: React.FormEvent<HTMLInputElement>,
|
||||||
|
option: IChoiceGroupOption
|
||||||
|
): void => {
|
||||||
|
setPriorityLevel(option.key);
|
||||||
|
};
|
||||||
|
|
||||||
const handleOnPageOptionChange = (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption): void => {
|
const handleOnPageOptionChange = (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption): void => {
|
||||||
setPageOption(option.key);
|
setPageOption(option.key);
|
||||||
};
|
};
|
||||||
@@ -260,6 +280,29 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{shouldShowPriorityLevelOption && (
|
||||||
|
<div className="settingsSection">
|
||||||
|
<div className="settingsSectionPart">
|
||||||
|
<fieldset>
|
||||||
|
<legend id="priorityLevel" className="settingsSectionLabel legendLabel">
|
||||||
|
Priority Level
|
||||||
|
</legend>
|
||||||
|
<InfoTooltip>
|
||||||
|
Sets the priority level for data-plane requests from Data Explorer when using Priority-Based
|
||||||
|
Execution. If "None" is selected, Data Explorer will not specify priority level, and the
|
||||||
|
server-side default priority level will be used.
|
||||||
|
</InfoTooltip>
|
||||||
|
<ChoiceGroup
|
||||||
|
ariaLabelledBy="priorityLevel"
|
||||||
|
selectedKey={priorityLevel}
|
||||||
|
options={priorityLevelOptionList}
|
||||||
|
styles={choiceButtonStyles}
|
||||||
|
onChange={handleOnPriorityLevelOptionChange}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{shouldShowGraphAutoVizOption && (
|
{shouldShowGraphAutoVizOption && (
|
||||||
<div className="settingsSection">
|
<div className="settingsSection">
|
||||||
<div className="settingsSectionPart">
|
<div className="settingsSectionPart">
|
||||||
|
|||||||
@@ -371,6 +371,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
id="confirmDatabaseId"
|
id="confirmDatabaseId"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
|
required={true}
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"fieldGroup": Object {
|
"fieldGroup": Object {
|
||||||
@@ -385,6 +386,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
deferredValidationTime={200}
|
deferredValidationTime={200}
|
||||||
id="confirmDatabaseId"
|
id="confirmDatabaseId"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
|
required={true}
|
||||||
resizable={true}
|
resizable={true}
|
||||||
styles={[Function]}
|
styles={[Function]}
|
||||||
theme={
|
theme={
|
||||||
@@ -663,7 +665,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
validateOnLoad={true}
|
validateOnLoad={true}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ms-TextField root-58"
|
className="ms-TextField is-required root-58"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ms-TextField-wrapper"
|
className="ms-TextField-wrapper"
|
||||||
@@ -681,6 +683,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
onInput={[Function]}
|
onInput={[Function]}
|
||||||
|
required={true}
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import { Checkbox, ChoiceGroup, DefaultButton, IconButton, PrimaryButton, TextField } from "@fluentui/react";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
||||||
|
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
|
import { getUserEmail } from "Utils/UserUtils";
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
jest.mock("Utils/UserUtils");
|
||||||
|
(getUserEmail as jest.Mock).mockResolvedValue("test@email.com");
|
||||||
|
|
||||||
|
jest.mock("Explorer/QueryCopilot/Shared/QueryCopilotClient");
|
||||||
|
SubmitFeedback as jest.Mock;
|
||||||
|
|
||||||
|
describe("Query Copilot Feedback Modal snapshot test", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
it("shoud render and match snapshot", () => {
|
||||||
|
useQueryCopilot.getState().openFeedbackModal("test query", false, "test prompt");
|
||||||
|
|
||||||
|
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
|
||||||
|
|
||||||
|
expect(wrapper.props().isOpen).toBeTruthy();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should close on cancel click", () => {
|
||||||
|
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
|
||||||
|
|
||||||
|
const cancelButton = wrapper.find(IconButton);
|
||||||
|
cancelButton.simulate("click");
|
||||||
|
wrapper.setProps({});
|
||||||
|
|
||||||
|
expect(wrapper.props().isOpen).toBeFalsy();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get user unput", () => {
|
||||||
|
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
|
||||||
|
const testUserInput = "test user input";
|
||||||
|
|
||||||
|
const userInput = wrapper.find(TextField).first();
|
||||||
|
userInput.simulate("change", {}, testUserInput);
|
||||||
|
|
||||||
|
expect(wrapper.find(TextField).first().props().value).toEqual(testUserInput);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should record user contact choice no", () => {
|
||||||
|
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
|
||||||
|
const contactAllowed = wrapper.find(ChoiceGroup);
|
||||||
|
|
||||||
|
contactAllowed.simulate("change", {}, { key: "no" });
|
||||||
|
|
||||||
|
expect(getUserEmail).toHaveBeenCalledTimes(3);
|
||||||
|
expect(wrapper.find(ChoiceGroup).props().selectedKey).toEqual("no");
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should record user contact choice yes", () => {
|
||||||
|
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
|
||||||
|
const contactAllowed = wrapper.find(ChoiceGroup);
|
||||||
|
|
||||||
|
contactAllowed.simulate("change", {}, { key: "yes" });
|
||||||
|
|
||||||
|
expect(getUserEmail).toHaveBeenCalledTimes(4);
|
||||||
|
expect(wrapper.find(ChoiceGroup).props().selectedKey).toEqual("yes");
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not render dont show again button", () => {
|
||||||
|
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
|
||||||
|
|
||||||
|
const dontShowAgain = wrapper.find(Checkbox);
|
||||||
|
|
||||||
|
expect(dontShowAgain).toHaveLength(0);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render dont show again button and check it ", () => {
|
||||||
|
useQueryCopilot.getState().openFeedbackModal("test query", true, "test prompt");
|
||||||
|
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
|
||||||
|
|
||||||
|
const dontShowAgain = wrapper.find(Checkbox);
|
||||||
|
dontShowAgain.simulate("change", {}, true);
|
||||||
|
|
||||||
|
expect(wrapper.find(Checkbox)).toHaveLength(1);
|
||||||
|
expect(wrapper.find(Checkbox).first().props().checked).toBeTruthy();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should cancel submission", () => {
|
||||||
|
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={new Explorer()} />);
|
||||||
|
|
||||||
|
const cancelButton = wrapper.find(DefaultButton);
|
||||||
|
cancelButton.simulate("click");
|
||||||
|
wrapper.setProps({});
|
||||||
|
|
||||||
|
expect(wrapper.props().isOpen).toBeFalsy();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should submit submission", () => {
|
||||||
|
const explorer = new Explorer();
|
||||||
|
const wrapper = shallow(<QueryCopilotFeedbackModal explorer={explorer} />);
|
||||||
|
|
||||||
|
const submitButton = wrapper.find(PrimaryButton);
|
||||||
|
submitButton.simulate("click");
|
||||||
|
wrapper.setProps({});
|
||||||
|
|
||||||
|
expect(SubmitFeedback).toHaveBeenCalledTimes(1);
|
||||||
|
expect(SubmitFeedback).toHaveBeenCalledWith({
|
||||||
|
params: {
|
||||||
|
likeQuery: false,
|
||||||
|
generatedQuery: "",
|
||||||
|
userPrompt: "",
|
||||||
|
description: "",
|
||||||
|
contact: getUserEmail(),
|
||||||
|
},
|
||||||
|
explorer: explorer,
|
||||||
|
});
|
||||||
|
expect(wrapper.props().isOpen).toBeFalsy();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -10,12 +10,13 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { getUserEmail } from "../../Utils/UserUtils";
|
import { getUserEmail } from "../../../Utils/UserUtils";
|
||||||
|
|
||||||
export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
|
export const QueryCopilotFeedbackModal = ({ explorer }: { explorer: Explorer }): JSX.Element => {
|
||||||
const {
|
const {
|
||||||
generatedQuery,
|
generatedQuery,
|
||||||
userPrompt,
|
userPrompt,
|
||||||
@@ -28,6 +29,7 @@ export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
|
|||||||
const [description, setDescription] = React.useState<string>("");
|
const [description, setDescription] = React.useState<string>("");
|
||||||
const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false);
|
const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false);
|
||||||
const [contact, setContact] = React.useState<string>(getUserEmail());
|
const [contact, setContact] = React.useState<string>(getUserEmail());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={showFeedbackModal}>
|
<Modal isOpen={showFeedbackModal}>
|
||||||
<Stack style={{ padding: 24 }}>
|
<Stack style={{ padding: 24 }}>
|
||||||
@@ -77,11 +79,13 @@ export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
|
|||||||
}}
|
}}
|
||||||
></ChoiceGroup>
|
></ChoiceGroup>
|
||||||
<Text style={{ fontSize: 12, marginBottom: 14 }}>
|
<Text style={{ fontSize: 12, marginBottom: 14 }}>
|
||||||
By pressing submit, your feedback will be used to improve Microsoft products and services. IT admins for your
|
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the{" "}
|
||||||
organization will be able to view and manage your feedback data.{" "}
|
{
|
||||||
<Link href="" target="_blank">
|
<Link href="https://privacy.microsoft.com/privacystatement" target="_blank">
|
||||||
Privacy statement
|
Privacy statement
|
||||||
</Link>
|
</Link>
|
||||||
|
}{" "}
|
||||||
|
for more information.
|
||||||
</Text>
|
</Text>
|
||||||
{likeQuery && (
|
{likeQuery && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -97,7 +101,10 @@ export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
closeFeedbackModal();
|
closeFeedbackModal();
|
||||||
setHideFeedbackModalForLikedQueries(doNotShowAgainChecked);
|
setHideFeedbackModalForLikedQueries(doNotShowAgainChecked);
|
||||||
submitFeedback({ generatedQuery, likeQuery, description, userPrompt, contact });
|
SubmitFeedback({
|
||||||
|
params: { generatedQuery, likeQuery, description, userPrompt, contact },
|
||||||
|
explorer: explorer,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Submit
|
Submit
|
||||||
32
src/Explorer/QueryCopilot/Modal/WelcomeModal.test.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { shallow } from "enzyme";
|
||||||
|
import { withHooks } from "jest-react-hooks-shallow";
|
||||||
|
import React from "react";
|
||||||
|
import { WelcomeModal } from "./WelcomeModal";
|
||||||
|
|
||||||
|
describe("Query Copilot Welcome Modal snapshot test", () => {
|
||||||
|
it("should render when isOpen is true", () => {
|
||||||
|
withHooks(() => {
|
||||||
|
const spy = jest.spyOn(localStorage, "setItem");
|
||||||
|
spy.mockClear();
|
||||||
|
const wrapper = shallow(<WelcomeModal visible={true} />);
|
||||||
|
|
||||||
|
expect(wrapper.props().children.props.isOpen).toBeTruthy();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(spy).toHaveBeenLastCalledWith("hideWelcomeModal", "true");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("should not render when isOpen is false", () => {
|
||||||
|
withHooks(() => {
|
||||||
|
const spy = jest.spyOn(localStorage, "setItem");
|
||||||
|
spy.mockClear();
|
||||||
|
const wrapper = shallow(<WelcomeModal visible={false} />);
|
||||||
|
|
||||||
|
expect(wrapper.props().children.props.isOpen).toBeFalsy();
|
||||||
|
|
||||||
|
expect(spy).not.toHaveBeenCalled();
|
||||||
|
expect(spy.mock.instances.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -42,8 +42,8 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack horizontalAlign="center">
|
<Stack horizontalAlign="center">
|
||||||
<Stack.Item align="center">
|
<Stack.Item align="center" style={{ textAlign: "center" }}>
|
||||||
<Text className="title bold">Welcome to Copilot in CosmosDB</Text>
|
<Text className="title bold">Welcome to Query Copilot for Azure Cosmos DB (Private Preview)</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item align="center" className="text">
|
<Stack.Item align="center" className="text">
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
@@ -52,7 +52,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
|
|||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem align="start">
|
<StackItem align="start">
|
||||||
<Text className="bold">
|
<Text className="bold">
|
||||||
Let copilot do the work for you
|
Let Query Copilot do the work for you
|
||||||
<br />
|
<br />
|
||||||
</Text>
|
</Text>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
@@ -60,7 +60,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
|
|||||||
<Text>
|
<Text>
|
||||||
Ask Copilot to generate a query by describing the query in your words.
|
Ask Copilot to generate a query by describing the query in your words.
|
||||||
<br />
|
<br />
|
||||||
<Link href="">Learn more</Link>
|
<Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
|
||||||
</Text>
|
</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item align="center" className="text">
|
<Stack.Item align="center" className="text">
|
||||||
@@ -78,7 +78,7 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
|
|||||||
<Text>
|
<Text>
|
||||||
AI-generated content can have mistakes. Make sure it’s accurate and appropriate before using it.
|
AI-generated content can have mistakes. Make sure it’s accurate and appropriate before using it.
|
||||||
<br />
|
<br />
|
||||||
<Link href="">Read preview terms</Link>
|
<Link href="http://aka.ms/cdb-copilot-preview-terms">Read preview terms</Link>
|
||||||
</Text>
|
</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item align="center" className="text">
|
<Stack.Item align="center" className="text">
|
||||||
@@ -88,15 +88,16 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
|
|||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem align="start">
|
<StackItem align="start">
|
||||||
<Text className="bold">
|
<Text className="bold">
|
||||||
Copilot currently works only a sample database
|
Query Copilot works on a sample database.
|
||||||
<br />
|
<br />
|
||||||
</Text>
|
</Text>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Text>
|
<Text>
|
||||||
Copilot is setup on a sample database we have configured for you at no cost
|
While in Private Preview, Query Copilot is setup to work on sample database we have configured for you
|
||||||
|
at no cost.
|
||||||
<br />
|
<br />
|
||||||
<Link href="">Learn more</Link>
|
<Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
|
||||||
</Text>
|
</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is true 1`] = `
|
||||||
|
<Fragment>
|
||||||
|
<Modal
|
||||||
|
isBlocking={false}
|
||||||
|
isOpen={true}
|
||||||
|
onDismiss={[Function]}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="modalContentPadding"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
grow={4}
|
||||||
|
horizontal={true}
|
||||||
|
horizontalAlign="end"
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<Image
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
className="exitPadding"
|
||||||
|
grow={1}
|
||||||
|
horizontal={true}
|
||||||
|
horizontalAlign="end"
|
||||||
|
verticalAlign="start"
|
||||||
|
>
|
||||||
|
<StackItem
|
||||||
|
className="previewMargin"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
className="preview"
|
||||||
|
>
|
||||||
|
Preview
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem>
|
||||||
|
<CustomizedIconButton
|
||||||
|
ariaLabel="Exit"
|
||||||
|
className="exitIcon"
|
||||||
|
iconProps={
|
||||||
|
Object {
|
||||||
|
"iconName": "Cancel",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClick={[Function]}
|
||||||
|
title="Exit"
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
horizontalAlign="center"
|
||||||
|
>
|
||||||
|
<StackItem
|
||||||
|
align="center"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"textAlign": "center",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
className="title bold"
|
||||||
|
>
|
||||||
|
Welcome to Query Copilot for Azure Cosmos DB (Private Preview)
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem
|
||||||
|
align="center"
|
||||||
|
className="text"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
>
|
||||||
|
<StackItem
|
||||||
|
align="start"
|
||||||
|
className="imageTextPadding"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
className="bold"
|
||||||
|
>
|
||||||
|
Let Query Copilot do the work for you
|
||||||
|
<br />
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
<Text>
|
||||||
|
Ask Copilot to generate a query by describing the query in your words.
|
||||||
|
<br />
|
||||||
|
<StyledLinkBase
|
||||||
|
href="http://aka.ms/cdb-copilot-learn-more"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</StyledLinkBase>
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem
|
||||||
|
align="center"
|
||||||
|
className="text"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
>
|
||||||
|
<StackItem
|
||||||
|
align="start"
|
||||||
|
className="imageTextPadding"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
className="bold"
|
||||||
|
>
|
||||||
|
Use your judgement
|
||||||
|
<br />
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
<Text>
|
||||||
|
AI-generated content can have mistakes. Make sure it’s accurate and appropriate before using it.
|
||||||
|
<br />
|
||||||
|
<StyledLinkBase
|
||||||
|
href="http://aka.ms/cdb-copilot-preview-terms"
|
||||||
|
>
|
||||||
|
Read preview terms
|
||||||
|
</StyledLinkBase>
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem
|
||||||
|
align="center"
|
||||||
|
className="text"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
>
|
||||||
|
<StackItem
|
||||||
|
align="start"
|
||||||
|
className="imageTextPadding"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
className="bold"
|
||||||
|
>
|
||||||
|
Query Copilot works on a sample database.
|
||||||
|
<br />
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
<Text>
|
||||||
|
While in Private Preview, Query Copilot is setup to work on sample database we have configured for you at no cost.
|
||||||
|
<br />
|
||||||
|
<StyledLinkBase
|
||||||
|
href="http://aka.ms/cdb-copilot-learn-more"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</StyledLinkBase>
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
className="buttonPadding"
|
||||||
|
>
|
||||||
|
<StackItem
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
<CustomizedPrimaryButton
|
||||||
|
className="tryButton"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
Try Copilot
|
||||||
|
</CustomizedPrimaryButton>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
</Fragment>
|
||||||
|
`;
|
||||||
@@ -1,11 +1,48 @@
|
|||||||
|
import { IconButton } from "@fluentui/react";
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { any } from "underscore";
|
|
||||||
import { CopyPopup } from "./CopyPopup";
|
import { CopyPopup } from "./CopyPopup";
|
||||||
|
|
||||||
describe("Copy Popup snapshot test", () => {
|
describe("Copy Popup snapshot test", () => {
|
||||||
|
const setShowCopyPopupMock = jest.fn();
|
||||||
it("should render when showCopyPopup is true", () => {
|
it("should render when showCopyPopup is true", () => {
|
||||||
const wrapper = shallow(<CopyPopup showCopyPopup={true} setShowCopyPopup={() => any} />);
|
const wrapper = shallow(<CopyPopup showCopyPopup={true} setShowCopyPopup={setShowCopyPopupMock} />);
|
||||||
|
expect(wrapper.exists()).toBe(true);
|
||||||
|
expect(wrapper.prop("setShowCopyPopup")).toBeUndefined();
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should render when showCopyPopup is false", () => {
|
||||||
|
const wrapper = shallow(<CopyPopup showCopyPopup={false} setShowCopyPopup={setShowCopyPopupMock} />);
|
||||||
|
expect(wrapper.prop("showCopyPopup")).toBeFalsy();
|
||||||
|
expect(wrapper.prop("setShowCopyPopup")).toBeUndefined();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call setShowCopyPopup(false) when close button is clicked", () => {
|
||||||
|
const wrapper = shallow(<CopyPopup showCopyPopup={true} setShowCopyPopup={setShowCopyPopupMock} />);
|
||||||
|
|
||||||
|
const closeButton = wrapper.find(IconButton);
|
||||||
|
closeButton.props().onClick?.({} as React.MouseEvent<HTMLButtonElement, MouseEvent>);
|
||||||
|
|
||||||
|
expect(setShowCopyPopupMock).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have the correct inline styles", () => {
|
||||||
|
const wrapper = shallow(<CopyPopup showCopyPopup={true} setShowCopyPopup={setShowCopyPopupMock} />);
|
||||||
|
|
||||||
|
const stackStyle = wrapper.find("Stack").first().props().style;
|
||||||
|
|
||||||
|
expect(stackStyle).toEqual({
|
||||||
|
position: "fixed",
|
||||||
|
width: 345,
|
||||||
|
height: 66,
|
||||||
|
padding: 10,
|
||||||
|
gap: 5,
|
||||||
|
top: 75,
|
||||||
|
right: 20,
|
||||||
|
background: "#FFFFFF",
|
||||||
|
boxShadow: "0 2px 6px rgba(0, 0, 0, 0.16)",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,19 +1,113 @@
|
|||||||
import { shallow } from "enzyme";
|
import { mount, shallow } from "enzyme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { any } from "underscore";
|
|
||||||
import { DeletePopup } from "./DeletePopup";
|
import { DeletePopup } from "./DeletePopup";
|
||||||
|
|
||||||
describe("Delete Popup snapshot test", () => {
|
describe("Delete Popup snapshot test", () => {
|
||||||
|
const setShowDeletePopupMock = jest.fn();
|
||||||
|
const setQueryMock = jest.fn();
|
||||||
|
const clearFeedbackMock = jest.fn();
|
||||||
|
const showFeedbackBarMock = jest.fn();
|
||||||
|
|
||||||
it("should render when showDeletePopup is true", () => {
|
it("should render when showDeletePopup is true", () => {
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
<DeletePopup
|
<DeletePopup
|
||||||
showDeletePopup={true}
|
showDeletePopup={true}
|
||||||
setShowDeletePopup={() => any}
|
setShowDeletePopup={setShowDeletePopupMock}
|
||||||
setQuery={() => any}
|
setQuery={setQueryMock}
|
||||||
clearFeedback={() => any}
|
clearFeedback={clearFeedbackMock}
|
||||||
showFeedbackBar={() => any}
|
showFeedbackBar={showFeedbackBarMock}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
expect(wrapper.find("Modal").prop("isOpen")).toBeTruthy();
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not render when showDeletePopup is false", () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<DeletePopup
|
||||||
|
showDeletePopup={false}
|
||||||
|
setShowDeletePopup={setShowDeletePopupMock}
|
||||||
|
setQuery={setQueryMock}
|
||||||
|
clearFeedback={clearFeedbackMock}
|
||||||
|
showFeedbackBar={showFeedbackBarMock}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(wrapper.props().children.props.showDeletePopup).toBeFalsy();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call setQuery with an empty string and setShowDeletePopup(false) when delete button is clicked", () => {
|
||||||
|
const wrapper = mount(
|
||||||
|
<DeletePopup
|
||||||
|
showDeletePopup={true}
|
||||||
|
setShowDeletePopup={setShowDeletePopupMock}
|
||||||
|
setQuery={setQueryMock}
|
||||||
|
clearFeedback={clearFeedbackMock}
|
||||||
|
showFeedbackBar={showFeedbackBarMock}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
wrapper.find("PrimaryButton").simulate("click");
|
||||||
|
|
||||||
|
expect(setQueryMock).toHaveBeenCalledWith("");
|
||||||
|
expect(setShowDeletePopupMock).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call setShowDeletePopup(false) when close button is clicked", () => {
|
||||||
|
const setShowDeletePopupMock = jest.fn();
|
||||||
|
const wrapper = mount(
|
||||||
|
<DeletePopup
|
||||||
|
showDeletePopup={true}
|
||||||
|
setShowDeletePopup={setShowDeletePopupMock}
|
||||||
|
setQuery={setQueryMock}
|
||||||
|
clearFeedback={clearFeedbackMock}
|
||||||
|
showFeedbackBar={showFeedbackBarMock}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
wrapper.find("DefaultButton").at(1).simulate("click");
|
||||||
|
|
||||||
|
expect(setShowDeletePopupMock).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render the appropriate text content", () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<DeletePopup
|
||||||
|
showDeletePopup={true}
|
||||||
|
setShowDeletePopup={setShowDeletePopupMock}
|
||||||
|
setQuery={setQueryMock}
|
||||||
|
clearFeedback={clearFeedbackMock}
|
||||||
|
showFeedbackBar={showFeedbackBarMock}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const textContent = wrapper
|
||||||
|
.find("Text")
|
||||||
|
.map((text, index) => <React.Fragment key={index}>{text.props().children}</React.Fragment>);
|
||||||
|
|
||||||
|
expect(textContent).toEqual([
|
||||||
|
<React.Fragment key={0}>
|
||||||
|
<b>Delete code?</b>
|
||||||
|
</React.Fragment>,
|
||||||
|
<React.Fragment key={1}>
|
||||||
|
This will clear the query from the query builder pane along with all comments and also reset the prompt pane
|
||||||
|
</React.Fragment>,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have the correct inline style", () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<DeletePopup
|
||||||
|
showDeletePopup={true}
|
||||||
|
setShowDeletePopup={setShowDeletePopupMock}
|
||||||
|
setQuery={setQueryMock}
|
||||||
|
clearFeedback={clearFeedbackMock}
|
||||||
|
showFeedbackBar={showFeedbackBarMock}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const stackStyle = wrapper.find("Stack[style]").props().style;
|
||||||
|
|
||||||
|
expect(stackStyle).toEqual({ padding: "16px 24px", height: "auto" });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Copy Popup snapshot test should render when showCopyPopup is false 1`] = `<Fragment />`;
|
||||||
|
|
||||||
exports[`Copy Popup snapshot test should render when showCopyPopup is true 1`] = `
|
exports[`Copy Popup snapshot test should render when showCopyPopup is true 1`] = `
|
||||||
<Stack
|
<Stack
|
||||||
style={
|
style={
|
||||||
|
|||||||
@@ -1,5 +1,83 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Delete Popup snapshot test should not render when showDeletePopup is false 1`] = `
|
||||||
|
<Modal
|
||||||
|
isOpen={false}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"main": Object {
|
||||||
|
"minHeight": "122px",
|
||||||
|
"minWidth": "880px",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"height": "auto",
|
||||||
|
"padding": "16px 24px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": "18px",
|
||||||
|
"height": 24,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<b>
|
||||||
|
Delete code?
|
||||||
|
</b>
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"marginBottom": 20,
|
||||||
|
"marginTop": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
This will clear the query from the query builder pane along with all comments and also reset the prompt pane
|
||||||
|
</Text>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
horizontalAlign="start"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CustomizedPrimaryButton
|
||||||
|
onClick={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"height": 24,
|
||||||
|
"padding": "0px 20px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</CustomizedPrimaryButton>
|
||||||
|
<CustomizedDefaultButton
|
||||||
|
onClick={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"height": 24,
|
||||||
|
"padding": "0px 20px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</CustomizedDefaultButton>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`Delete Popup snapshot test should render when showDeletePopup is true 1`] = `
|
exports[`Delete Popup snapshot test should render when showDeletePopup is true 1`] = `
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import { QueryCopilotTab } from "./QueryCopilotTab";
|
|||||||
|
|
||||||
describe("Query copilot tab snapshot test", () => {
|
describe("Query copilot tab snapshot test", () => {
|
||||||
it("should render with initial input", () => {
|
it("should render with initial input", () => {
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(<QueryCopilotTab explorer={new Explorer()} />);
|
||||||
<QueryCopilotTab initialInput="Write a query to return all records in this table" explorer={new Explorer()} />
|
|
||||||
);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { FeedOptions } from "@azure/cosmos";
|
||||||
import {
|
import {
|
||||||
Callout,
|
Callout,
|
||||||
CommandBarButton,
|
CommandBarButton,
|
||||||
@@ -18,28 +18,32 @@ import {
|
|||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { useBoolean } from "@fluentui/react-hooks";
|
import { useBoolean } from "@fluentui/react-hooks";
|
||||||
import {
|
import {
|
||||||
|
PoolIdType,
|
||||||
QueryCopilotSampleContainerId,
|
QueryCopilotSampleContainerId,
|
||||||
QueryCopilotSampleContainerSchema,
|
QueryCopilotSampleContainerSchema,
|
||||||
QueryCopilotSampleDatabaseId,
|
ShortenedQueryCopilotSampleContainerSchema,
|
||||||
} from "Common/Constants";
|
} from "Common/Constants";
|
||||||
import { getErrorMessage, handleError } from "Common/ErrorHandlingUtils";
|
import { getErrorMessage, handleError } from "Common/ErrorHandlingUtils";
|
||||||
import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility";
|
import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility";
|
||||||
import { MinimalQueryIterator } from "Common/IteratorUtilities";
|
import { MinimalQueryIterator } from "Common/IteratorUtilities";
|
||||||
import { sampleDataClient } from "Common/SampleDataClient";
|
import { createUri } from "Common/UrlUtility";
|
||||||
import { getCommonQueryOptions } from "Common/dataAccess/queryDocuments";
|
|
||||||
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
|
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
|
||||||
import { QueryResults } from "Contracts/ViewModels";
|
import { QueryResults } from "Contracts/ViewModels";
|
||||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
||||||
import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal";
|
import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal";
|
||||||
import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
|
import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
|
||||||
import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup";
|
import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup";
|
||||||
import { submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/SamplePrompts/SamplePrompts";
|
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
|
import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
|
import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/Shared/SamplePrompts/SamplePrompts";
|
||||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||||
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
|
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
@@ -50,7 +54,6 @@ import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
|||||||
import HintIcon from "../../../images/Hint.svg";
|
import HintIcon from "../../../images/Hint.svg";
|
||||||
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
||||||
import RecentIcon from "../../../images/Recent.svg";
|
import RecentIcon from "../../../images/Recent.svg";
|
||||||
import SamplePromptsIcon from "../../../images/SamplePromptsIcon.svg";
|
|
||||||
import XErrorMessage from "../../../images/X-errorMessage.svg";
|
import XErrorMessage from "../../../images/X-errorMessage.svg";
|
||||||
import SaveQueryIcon from "../../../images/save-cosmos.svg";
|
import SaveQueryIcon from "../../../images/save-cosmos.svg";
|
||||||
import { useTabs } from "../../hooks/useTabs";
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
@@ -60,49 +63,57 @@ interface SuggestedPrompt {
|
|||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueryCopilotTabProps {
|
|
||||||
initialInput: string;
|
|
||||||
explorer: Explorer;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GenerateSQLQueryResponse {
|
|
||||||
apiVersion: string;
|
|
||||||
sql: string;
|
|
||||||
explanation: string;
|
|
||||||
generateStart: string;
|
|
||||||
generateEnd: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const promptStyles: IButtonStyles = {
|
const promptStyles: IButtonStyles = {
|
||||||
root: { border: 0, selectors: { ":hover": { outline: "1px dashed #605e5c" } } },
|
root: { border: 0, selectors: { ":hover": { outline: "1px dashed #605e5c" } } },
|
||||||
label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 },
|
label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
|
||||||
initialInput,
|
|
||||||
explorer,
|
|
||||||
}: QueryCopilotTabProps): JSX.Element => {
|
|
||||||
const hideFeedbackModalForLikedQueries = useQueryCopilot((state) => state.hideFeedbackModalForLikedQueries);
|
|
||||||
const [userPrompt, setUserPrompt] = useState<string>(initialInput || "");
|
|
||||||
const [generatedQuery, setGeneratedQuery] = useState<string>("");
|
|
||||||
const [query, setQuery] = useState<string>("");
|
|
||||||
const [selectedQuery, setSelectedQuery] = useState<string>("");
|
|
||||||
const [isGeneratingQuery, setIsGeneratingQuery] = useState<boolean>(false);
|
|
||||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
|
||||||
const [likeQuery, setLikeQuery] = useState<boolean>();
|
|
||||||
const [dislikeQuery, setDislikeQuery] = useState<boolean>();
|
|
||||||
const [showCallout, setShowCallout] = useState<boolean>(false);
|
|
||||||
const [showSamplePrompts, setShowSamplePrompts] = useState<boolean>(false);
|
|
||||||
const [queryIterator, setQueryIterator] = useState<MinimalQueryIterator>();
|
|
||||||
const [queryResults, setQueryResults] = useState<QueryResults>();
|
|
||||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
|
||||||
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false);
|
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false);
|
||||||
const inputEdited = useRef(false);
|
const inputEdited = useRef(false);
|
||||||
const [isSamplePromptsOpen, setIsSamplePromptsOpen] = useState<boolean>(false);
|
const {
|
||||||
const [showDeletePopup, setShowDeletePopup] = useState<boolean>(false);
|
hideFeedbackModalForLikedQueries,
|
||||||
const [showFeedbackBar, setShowFeedbackBar] = useState<boolean>(false);
|
userPrompt,
|
||||||
const [showCopyPopup, setshowCopyPopup] = useState<boolean>(false);
|
setUserPrompt,
|
||||||
const [showErrorMessageBar, setShowErrorMessageBar] = useState<boolean>(false);
|
generatedQuery,
|
||||||
|
setGeneratedQuery,
|
||||||
|
query,
|
||||||
|
setQuery,
|
||||||
|
selectedQuery,
|
||||||
|
setSelectedQuery,
|
||||||
|
isGeneratingQuery,
|
||||||
|
setIsGeneratingQuery,
|
||||||
|
isExecuting,
|
||||||
|
setIsExecuting,
|
||||||
|
likeQuery,
|
||||||
|
setLikeQuery,
|
||||||
|
dislikeQuery,
|
||||||
|
setDislikeQuery,
|
||||||
|
showCallout,
|
||||||
|
setShowCallout,
|
||||||
|
showSamplePrompts,
|
||||||
|
setShowSamplePrompts,
|
||||||
|
queryIterator,
|
||||||
|
setQueryIterator,
|
||||||
|
queryResults,
|
||||||
|
setQueryResults,
|
||||||
|
errorMessage,
|
||||||
|
setErrorMessage,
|
||||||
|
isSamplePromptsOpen,
|
||||||
|
setIsSamplePromptsOpen,
|
||||||
|
showDeletePopup,
|
||||||
|
setShowDeletePopup,
|
||||||
|
showFeedbackBar,
|
||||||
|
setShowFeedbackBar,
|
||||||
|
showCopyPopup,
|
||||||
|
setshowCopyPopup,
|
||||||
|
showErrorMessageBar,
|
||||||
|
setShowErrorMessageBar,
|
||||||
|
generatedQueryComments,
|
||||||
|
setGeneratedQueryComments,
|
||||||
|
shouldAllocateContainer,
|
||||||
|
setShouldAllocateContainer,
|
||||||
|
} = useQueryCopilot();
|
||||||
|
|
||||||
const sampleProps: SamplePromptsProps = {
|
const sampleProps: SamplePromptsProps = {
|
||||||
isSamplePromptsOpen: isSamplePromptsOpen,
|
isSamplePromptsOpen: isSamplePromptsOpen,
|
||||||
@@ -128,12 +139,12 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`);
|
const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`);
|
||||||
const cachedHistories = cachedHistoriesString?.split(",");
|
const cachedHistories = cachedHistoriesString?.split("|");
|
||||||
const [histories, setHistories] = useState<string[]>(cachedHistories || []);
|
const [histories, setHistories] = useState<string[]>(cachedHistories || []);
|
||||||
const suggestedPrompts: SuggestedPrompt[] = [
|
const suggestedPrompts: SuggestedPrompt[] = [
|
||||||
{ id: 1, text: "Give me all customers whose names start with C" },
|
{ id: 1, text: 'Show all products that have the word "ultra" in the name or description' },
|
||||||
{ id: 2, text: "Show me all customers" },
|
{ id: 2, text: "What are all of the possible categories for the products, and their counts?" },
|
||||||
{ id: 3, text: "Show me all customers who bought a bike in 2019" },
|
{ id: 3, text: 'Show me all products that have been reviewed by someone with a username that contains "bob"' },
|
||||||
];
|
];
|
||||||
const [filteredHistories, setFilteredHistories] = useState<string[]>(histories);
|
const [filteredHistories, setFilteredHistories] = useState<string[]>(histories);
|
||||||
const [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState<SuggestedPrompt[]>(suggestedPrompts);
|
const [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState<SuggestedPrompt[]>(suggestedPrompts);
|
||||||
@@ -155,38 +166,69 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateHistories = (): void => {
|
const updateHistories = (): void => {
|
||||||
const newHistories = histories.length < 3 ? [userPrompt, ...histories] : [userPrompt, histories[1], histories[2]];
|
const formattedUserPrompt = userPrompt.replace(/\s+/g, " ").trim();
|
||||||
|
const existingHistories = histories.map((history) => history.replace(/\s+/g, " ").trim());
|
||||||
|
|
||||||
|
const updatedHistories = existingHistories.filter(
|
||||||
|
(history) => history.toLowerCase() !== formattedUserPrompt.toLowerCase()
|
||||||
|
);
|
||||||
|
const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)];
|
||||||
|
|
||||||
setHistories(newHistories);
|
setHistories(newHistories);
|
||||||
localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join(","));
|
localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join("|"));
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateSQLQuery = async (): Promise<void> => {
|
const generateSQLQuery = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
|
if (shouldAllocateContainer && userContext.features.enableCopilotPhoenixGateaway) {
|
||||||
|
await explorer.allocateContainer(PoolIdType.QueryCopilot);
|
||||||
|
setShouldAllocateContainer(false);
|
||||||
|
}
|
||||||
|
|
||||||
setIsGeneratingQuery(true);
|
setIsGeneratingQuery(true);
|
||||||
useTabs.getState().setIsTabExecuting(true);
|
useTabs.getState().setIsTabExecuting(true);
|
||||||
useTabs.getState().setIsQueryErrorThrown(false);
|
useTabs.getState().setIsQueryErrorThrown(false);
|
||||||
const payload = {
|
const payload = {
|
||||||
containerSchema: QueryCopilotSampleContainerSchema,
|
containerSchema: userContext.features.enableCopilotFullSchema
|
||||||
|
? QueryCopilotSampleContainerSchema
|
||||||
|
: ShortenedQueryCopilotSampleContainerSchema,
|
||||||
userPrompt: userPrompt,
|
userPrompt: userPrompt,
|
||||||
};
|
};
|
||||||
setShowDeletePopup(false);
|
setShowDeletePopup(false);
|
||||||
const response = await fetch("https://copilotorchestrater.azurewebsites.net/generateSQLQuery", {
|
useQueryCopilot.getState().refreshCorrelationId();
|
||||||
|
const serverInfo = useNotebook.getState().notebookServerInfo;
|
||||||
|
const queryUri = userContext.features.enableCopilotPhoenixGateaway
|
||||||
|
? createUri(serverInfo.notebookServerEndpoint, "generateSQLQuery")
|
||||||
|
: createUri("https://copilotorchestrater.azurewebsites.net/", "generateSQLQuery");
|
||||||
|
const response = await fetch(queryUri, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
|
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
|
|
||||||
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
|
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
|
||||||
if (generateSQLQueryResponse?.sql) {
|
if (response.status === 404) {
|
||||||
let query = `-- **Prompt:** ${userPrompt}\r\n`;
|
setShouldAllocateContainer(true);
|
||||||
if (generateSQLQueryResponse.explanation) {
|
}
|
||||||
query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`;
|
if (response.ok) {
|
||||||
|
if (generateSQLQueryResponse?.sql) {
|
||||||
|
let query = `-- **Prompt:** ${userPrompt}\r\n`;
|
||||||
|
if (generateSQLQueryResponse.explanation) {
|
||||||
|
query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`;
|
||||||
|
}
|
||||||
|
query += generateSQLQueryResponse.sql;
|
||||||
|
setQuery(query);
|
||||||
|
setGeneratedQuery(generateSQLQueryResponse.sql);
|
||||||
|
setGeneratedQueryComments(generateSQLQueryResponse.explanation);
|
||||||
|
setShowErrorMessageBar(false);
|
||||||
}
|
}
|
||||||
query += generateSQLQueryResponse.sql;
|
} else {
|
||||||
setQuery(query);
|
handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError");
|
||||||
setGeneratedQuery(generateSQLQueryResponse.sql);
|
useTabs.getState().setIsQueryErrorThrown(true);
|
||||||
|
setShowErrorMessageBar(true);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "executeNaturalLanguageQuery");
|
handleError(error, "executeNaturalLanguageQuery");
|
||||||
@@ -200,15 +242,14 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const querySampleDocuments = (query: string, options: FeedOptions): QueryIterator<ItemDefinition & Resource> => {
|
|
||||||
options = getCommonQueryOptions(options);
|
|
||||||
return sampleDataClient()
|
|
||||||
.database(QueryCopilotSampleDatabaseId)
|
|
||||||
.container(QueryCopilotSampleContainerId)
|
|
||||||
.items.query(query, options);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onExecuteQueryClick = async (): Promise<void> => {
|
const onExecuteQueryClick = async (): Promise<void> => {
|
||||||
|
traceStart(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
||||||
|
correlationId: useQueryCopilot.getState().correlationId,
|
||||||
|
userPrompt: userPrompt,
|
||||||
|
generatedQuery: generatedQuery,
|
||||||
|
generatedQueryComments: generatedQueryComments,
|
||||||
|
executedQuery: selectedQuery || query,
|
||||||
|
});
|
||||||
const queryToExecute = selectedQuery || query;
|
const queryToExecute = selectedQuery || query;
|
||||||
const queryIterator = querySampleDocuments(queryToExecute, {
|
const queryIterator = querySampleDocuments(queryToExecute, {
|
||||||
enableCrossPartitionQuery: shouldEnableCrossPartitionKey(),
|
enableCrossPartitionQuery: shouldEnableCrossPartitionKey(),
|
||||||
@@ -233,8 +274,16 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
|||||||
|
|
||||||
setQueryResults(queryResults);
|
setQueryResults(queryResults);
|
||||||
setErrorMessage("");
|
setErrorMessage("");
|
||||||
|
setShowErrorMessageBar(false);
|
||||||
|
traceSuccess(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
||||||
|
correlationId: useQueryCopilot.getState().correlationId,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
|
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
||||||
|
correlationId: useQueryCopilot.getState().correlationId,
|
||||||
|
errorMessage: errorMessage,
|
||||||
|
});
|
||||||
setErrorMessage(errorMessage);
|
setErrorMessage(errorMessage);
|
||||||
handleError(errorMessage, "executeQueryCopilotTab");
|
handleError(errorMessage, "executeQueryCopilotTab");
|
||||||
useTabs.getState().setIsQueryErrorThrown(true);
|
useTabs.getState().setIsQueryErrorThrown(true);
|
||||||
@@ -254,6 +303,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
|||||||
commandButtonLabel: executeQueryBtnLabel,
|
commandButtonLabel: executeQueryBtnLabel,
|
||||||
ariaLabel: executeQueryBtnLabel,
|
ariaLabel: executeQueryBtnLabel,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
|
disabled: query?.trim() === "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveQueryBtn = {
|
const saveQueryBtn = {
|
||||||
@@ -264,23 +314,26 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
|||||||
commandButtonLabel: "Save Query",
|
commandButtonLabel: "Save Query",
|
||||||
ariaLabel: "Save Query",
|
ariaLabel: "Save Query",
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
|
disabled: query?.trim() === "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const samplePromptsBtn = {
|
// Sample Prompts temporary disabled due current design
|
||||||
iconSrc: SamplePromptsIcon,
|
// const samplePromptsBtn = {
|
||||||
iconAlt: "Sample Prompts",
|
// iconSrc: SamplePromptsIcon,
|
||||||
onCommandClick: () => setIsSamplePromptsOpen(true),
|
// iconAlt: "Sample Prompts",
|
||||||
commandButtonLabel: "Sample Prompts",
|
// onCommandClick: () => setIsSamplePromptsOpen(true),
|
||||||
ariaLabel: "Sample Prompts",
|
// commandButtonLabel: "Sample Prompts",
|
||||||
hasPopup: false,
|
// ariaLabel: "Sample Prompts",
|
||||||
};
|
// hasPopup: false,
|
||||||
|
// };
|
||||||
|
|
||||||
return [executeQueryBtn, saveQueryBtn, samplePromptsBtn];
|
return [executeQueryBtn, saveQueryBtn];
|
||||||
};
|
};
|
||||||
const showTeachingBubble = (): void => {
|
const showTeachingBubble = (): void => {
|
||||||
if (!inputEdited.current) {
|
const shouldShowTeachingBubble = !inputEdited.current && userPrompt.trim() === "";
|
||||||
|
if (shouldShowTeachingBubble) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!inputEdited.current) {
|
if (shouldShowTeachingBubble) {
|
||||||
toggleCopilotTeachingBubbleVisible();
|
toggleCopilotTeachingBubbleVisible();
|
||||||
inputEdited.current = true;
|
inputEdited.current = true;
|
||||||
}
|
}
|
||||||
@@ -294,282 +347,308 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
|||||||
setShowCallout(false);
|
setShowCallout(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const startGenerateQueryProcess = () => {
|
||||||
|
updateHistories();
|
||||||
|
generateSQLQuery();
|
||||||
|
resetButtonState();
|
||||||
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
useCommandBar.getState().setContextButtons(getCommandbarButtons());
|
useCommandBar.getState().setContextButtons(getCommandbarButtons());
|
||||||
}, [query, selectedQuery]);
|
}, [query, selectedQuery]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (initialInput) {
|
|
||||||
generateSQLQuery();
|
|
||||||
}
|
|
||||||
showTeachingBubble();
|
showTeachingBubble();
|
||||||
useTabs.getState().setIsQueryErrorThrown(false);
|
useTabs.getState().setIsQueryErrorThrown(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack className="tab-pane" style={{ padding: 24, width: "100%", height: "100%" }}>
|
<Stack className="tab-pane" style={{ padding: 24, width: "100%" }}>
|
||||||
<Stack horizontal verticalAlign="center">
|
<div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}>
|
||||||
<Image src={CopilotIcon} />
|
<Stack horizontal verticalAlign="center">
|
||||||
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
|
<Image src={CopilotIcon} />
|
||||||
</Stack>
|
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
|
||||||
<Stack horizontal verticalAlign="center" style={{ marginTop: 16, width: "100%", position: "relative" }}>
|
</Stack>
|
||||||
<TextField
|
<Stack horizontal verticalAlign="center" style={{ marginTop: 16, width: "100%", position: "relative" }}>
|
||||||
id="naturalLanguageInput"
|
<TextField
|
||||||
value={userPrompt}
|
id="naturalLanguageInput"
|
||||||
onChange={handleUserPromptChange}
|
value={userPrompt}
|
||||||
onClick={() => {
|
onChange={handleUserPromptChange}
|
||||||
inputEdited.current = true;
|
onClick={() => {
|
||||||
setShowSamplePrompts(true);
|
inputEdited.current = true;
|
||||||
}}
|
setShowSamplePrompts(true);
|
||||||
style={{ lineHeight: 30 }}
|
}}
|
||||||
styles={{ root: { width: "95%" } }}
|
onKeyDown={(e) => {
|
||||||
disabled={isGeneratingQuery}
|
if (e.key === "Enter") {
|
||||||
autoComplete="off"
|
startGenerateQueryProcess();
|
||||||
/>
|
}
|
||||||
{copilotTeachingBubbleVisible && (
|
}}
|
||||||
<TeachingBubble
|
style={{ lineHeight: 30 }}
|
||||||
calloutProps={{ directionalHint: DirectionalHint.bottomCenter }}
|
styles={{ root: { width: "95%" } }}
|
||||||
target="#naturalLanguageInput"
|
disabled={isGeneratingQuery}
|
||||||
hasCloseButton={true}
|
autoComplete="off"
|
||||||
closeButtonAriaLabel="Close"
|
/>
|
||||||
onDismiss={toggleCopilotTeachingBubbleVisible}
|
{copilotTeachingBubbleVisible && (
|
||||||
hasSmallHeadline={true}
|
<TeachingBubble
|
||||||
headline="Write a prompt"
|
calloutProps={{ directionalHint: DirectionalHint.bottomCenter }}
|
||||||
>
|
target="#naturalLanguageInput"
|
||||||
Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "}
|
hasCloseButton={true}
|
||||||
<Link
|
closeButtonAriaLabel="Close"
|
||||||
onClick={() => {
|
onDismiss={toggleCopilotTeachingBubbleVisible}
|
||||||
setShowSamplePrompts(true);
|
hasSmallHeadline={true}
|
||||||
toggleCopilotTeachingBubbleVisible();
|
headline="Write a prompt"
|
||||||
}}
|
|
||||||
style={{ color: "white", fontWeight: 600 }}
|
|
||||||
>
|
>
|
||||||
sample prompts
|
Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "}
|
||||||
</Link>{" "}
|
<Link
|
||||||
or write your own query
|
onClick={() => {
|
||||||
</TeachingBubble>
|
setShowSamplePrompts(true);
|
||||||
)}
|
toggleCopilotTeachingBubbleVisible();
|
||||||
<IconButton
|
|
||||||
iconProps={{ iconName: "Send" }}
|
|
||||||
disabled={isGeneratingQuery || !userPrompt.trim()}
|
|
||||||
style={{ marginLeft: 8 }}
|
|
||||||
onClick={() => {
|
|
||||||
updateHistories();
|
|
||||||
generateSQLQuery();
|
|
||||||
resetButtonState();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
|
|
||||||
{showSamplePrompts && (
|
|
||||||
<Callout
|
|
||||||
styles={{ root: { minWidth: 400 } }}
|
|
||||||
target="#naturalLanguageInput"
|
|
||||||
isBeakVisible={false}
|
|
||||||
onDismiss={() => setShowSamplePrompts(false)}
|
|
||||||
directionalHintFixed={true}
|
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
|
||||||
alignTargetEdge={true}
|
|
||||||
gapSpace={4}
|
|
||||||
>
|
|
||||||
<Stack>
|
|
||||||
{filteredHistories?.length > 0 && (
|
|
||||||
<Stack>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "#0078D4",
|
|
||||||
marginLeft: 16,
|
|
||||||
padding: "4px 0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Recent
|
|
||||||
</Text>
|
|
||||||
{filteredHistories.map((history, i) => (
|
|
||||||
<DefaultButton
|
|
||||||
key={i}
|
|
||||||
onClick={() => {
|
|
||||||
setUserPrompt(history);
|
|
||||||
setShowSamplePrompts(false);
|
|
||||||
}}
|
|
||||||
onRenderIcon={() => <Image src={RecentIcon} />}
|
|
||||||
styles={promptStyles}
|
|
||||||
>
|
|
||||||
{history}
|
|
||||||
</DefaultButton>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "#0078D4",
|
|
||||||
marginLeft: 16,
|
|
||||||
padding: "4px 0",
|
|
||||||
}}
|
}}
|
||||||
|
style={{ color: "white", fontWeight: 600 }}
|
||||||
>
|
>
|
||||||
Suggested Prompts
|
sample prompts
|
||||||
</Text>
|
</Link>{" "}
|
||||||
{filteredSuggestedPrompts.map((prompt) => (
|
or write your own query
|
||||||
<DefaultButton
|
</TeachingBubble>
|
||||||
key={prompt.id}
|
|
||||||
onClick={() => {
|
|
||||||
setUserPrompt(prompt.text);
|
|
||||||
setShowSamplePrompts(false);
|
|
||||||
}}
|
|
||||||
onRenderIcon={() => <Image src={HintIcon} />}
|
|
||||||
styles={promptStyles}
|
|
||||||
>
|
|
||||||
{prompt.text}
|
|
||||||
</DefaultButton>
|
|
||||||
))}
|
|
||||||
<Separator
|
|
||||||
styles={{
|
|
||||||
root: {
|
|
||||||
selectors: { "::before": { background: "#E1DFDD" } },
|
|
||||||
padding: 0,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
fontSize: 14,
|
|
||||||
marginLeft: 16,
|
|
||||||
padding: "4px 0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Learn about{" "}
|
|
||||||
<Link target="_blank" href="">
|
|
||||||
writing effective prompts
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Callout>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Text style={{ marginTop: 8, marginBottom: 24, fontSize: 12 }}>
|
|
||||||
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "}
|
|
||||||
<Link href="" target="_blank">
|
|
||||||
Read preview terms
|
|
||||||
</Link>
|
|
||||||
{showErrorMessageBar && (
|
|
||||||
<Stack style={{ backgroundColor: "#FEF0F1", padding: "4px 8px" }} horizontal verticalAlign="center">
|
|
||||||
<Image src={XErrorMessage} style={{ marginRight: "8px" }} />
|
|
||||||
<Text style={{ fontSize: 12 }}>
|
|
||||||
We ran into an error and were not able to execute query. Please try again after sometime
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{showFeedbackBar && (
|
|
||||||
<Stack style={{ backgroundColor: "#FFF8F0", padding: "2px 8px" }} horizontal verticalAlign="center">
|
|
||||||
<Text style={{ fontWeight: 600, fontSize: 12 }}>Provide feedback on the query generated</Text>
|
|
||||||
{showCallout && !hideFeedbackModalForLikedQueries && (
|
|
||||||
<Callout
|
|
||||||
style={{ padding: 8 }}
|
|
||||||
target="#likeBtn"
|
|
||||||
onDismiss={() => {
|
|
||||||
setShowCallout(false);
|
|
||||||
submitFeedback({ generatedQuery, likeQuery, description: "", userPrompt: userPrompt });
|
|
||||||
}}
|
|
||||||
directionalHint={DirectionalHint.topCenter}
|
|
||||||
>
|
|
||||||
<Text>
|
|
||||||
Thank you. Need to give{" "}
|
|
||||||
<Link
|
|
||||||
onClick={() => {
|
|
||||||
setShowCallout(false);
|
|
||||||
useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userPrompt);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
more feedback?
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
</Callout>
|
|
||||||
)}
|
)}
|
||||||
<IconButton
|
<IconButton
|
||||||
id="likeBtn"
|
iconProps={{ iconName: "Send" }}
|
||||||
style={{ marginLeft: 20 }}
|
disabled={isGeneratingQuery || !userPrompt.trim()}
|
||||||
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
|
style={{ marginLeft: 8 }}
|
||||||
onClick={() => {
|
onClick={() => startGenerateQueryProcess()}
|
||||||
setShowCallout(!likeQuery);
|
|
||||||
setLikeQuery(!likeQuery);
|
|
||||||
if (dislikeQuery) {
|
|
||||||
setDislikeQuery(!dislikeQuery);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<IconButton
|
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
|
||||||
style={{ margin: "0 10px" }}
|
{showSamplePrompts && (
|
||||||
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
|
<Callout
|
||||||
onClick={() => {
|
styles={{ root: { minWidth: 400 } }}
|
||||||
if (!dislikeQuery) {
|
target="#naturalLanguageInput"
|
||||||
useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userPrompt);
|
isBeakVisible={false}
|
||||||
setLikeQuery(false);
|
onDismiss={() => setShowSamplePrompts(false)}
|
||||||
}
|
directionalHintFixed={true}
|
||||||
setDislikeQuery(!dislikeQuery);
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
setShowCallout(false);
|
alignTargetEdge={true}
|
||||||
}}
|
gapSpace={4}
|
||||||
/>
|
>
|
||||||
<Separator vertical style={{ color: "#EDEBE9" }} />
|
<Stack>
|
||||||
<CommandBarButton
|
{filteredHistories?.length > 0 && (
|
||||||
onClick={copyGeneratedCode}
|
<Stack>
|
||||||
iconProps={{ iconName: "Copy" }}
|
<Text
|
||||||
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
style={{
|
||||||
>
|
width: "100%",
|
||||||
Copy code
|
fontSize: 14,
|
||||||
</CommandBarButton>
|
fontWeight: 600,
|
||||||
<CommandBarButton
|
color: "#0078D4",
|
||||||
onClick={() => {
|
marginLeft: 16,
|
||||||
setShowDeletePopup(true);
|
padding: "4px 0",
|
||||||
}}
|
}}
|
||||||
iconProps={{ iconName: "Delete" }}
|
>
|
||||||
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
Recent
|
||||||
>
|
</Text>
|
||||||
Delete code
|
{filteredHistories.map((history, i) => (
|
||||||
</CommandBarButton>
|
<DefaultButton
|
||||||
|
key={i}
|
||||||
|
onClick={() => {
|
||||||
|
setUserPrompt(history);
|
||||||
|
setShowSamplePrompts(false);
|
||||||
|
}}
|
||||||
|
onRenderIcon={() => <Image src={RecentIcon} />}
|
||||||
|
styles={promptStyles}
|
||||||
|
>
|
||||||
|
{history}
|
||||||
|
</DefaultButton>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
{filteredSuggestedPrompts?.length > 0 && (
|
||||||
|
<Stack>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "#0078D4",
|
||||||
|
marginLeft: 16,
|
||||||
|
padding: "4px 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Suggested Prompts
|
||||||
|
</Text>
|
||||||
|
{filteredSuggestedPrompts.map((prompt) => (
|
||||||
|
<DefaultButton
|
||||||
|
key={prompt.id}
|
||||||
|
onClick={() => {
|
||||||
|
setUserPrompt(prompt.text);
|
||||||
|
setShowSamplePrompts(false);
|
||||||
|
}}
|
||||||
|
onRenderIcon={() => <Image src={HintIcon} />}
|
||||||
|
styles={promptStyles}
|
||||||
|
>
|
||||||
|
{prompt.text}
|
||||||
|
</DefaultButton>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
{(filteredHistories?.length > 0 || filteredSuggestedPrompts?.length > 0) && (
|
||||||
|
<Stack>
|
||||||
|
<Separator
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
selectors: { "::before": { background: "#E1DFDD" } },
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
fontSize: 14,
|
||||||
|
marginLeft: 16,
|
||||||
|
padding: "4px 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Learn about{" "}
|
||||||
|
<Link target="_blank" href="http://aka.ms/cdb-copilot-writing">
|
||||||
|
writing effective prompts
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Callout>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
|
||||||
|
|
||||||
<Stack className="tabPaneContentContainer">
|
<Stack style={{ marginTop: 8, marginBottom: 24 }}>
|
||||||
<SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}>
|
<Text style={{ fontSize: 12 }}>
|
||||||
<EditorReact
|
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "}
|
||||||
language={"sql"}
|
<Link href="http://aka.ms/cdb-copilot-preview-terms" target="_blank">
|
||||||
content={query}
|
Read preview terms
|
||||||
isReadOnly={false}
|
</Link>
|
||||||
ariaLabel={"Editing Query"}
|
{showErrorMessageBar && (
|
||||||
lineNumbers={"on"}
|
<Stack style={{ backgroundColor: "#FEF0F1", padding: "4px 8px" }} horizontal verticalAlign="center">
|
||||||
onContentChanged={(newQuery: string) => setQuery(newQuery)}
|
<Image src={XErrorMessage} style={{ marginRight: "8px" }} />
|
||||||
onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)}
|
<Text style={{ fontSize: 12 }}>
|
||||||
|
We ran into an error and were not able to execute query. Please try again after sometime
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{showFeedbackBar && (
|
||||||
|
<Stack style={{ backgroundColor: "#FFF8F0", padding: "2px 8px" }} horizontal verticalAlign="center">
|
||||||
|
<Text style={{ fontWeight: 600, fontSize: 12 }}>Provide feedback on the query generated</Text>
|
||||||
|
{showCallout && !hideFeedbackModalForLikedQueries && (
|
||||||
|
<Callout
|
||||||
|
style={{ padding: 8 }}
|
||||||
|
target="#likeBtn"
|
||||||
|
onDismiss={() => {
|
||||||
|
setShowCallout(false);
|
||||||
|
SubmitFeedback({
|
||||||
|
params: {
|
||||||
|
generatedQuery: generatedQuery,
|
||||||
|
likeQuery: likeQuery,
|
||||||
|
description: "",
|
||||||
|
userPrompt: userPrompt,
|
||||||
|
},
|
||||||
|
explorer: explorer,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
directionalHint={DirectionalHint.topCenter}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
Thank you. Need to give{" "}
|
||||||
|
<Link
|
||||||
|
onClick={() => {
|
||||||
|
setShowCallout(false);
|
||||||
|
useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userPrompt);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
more feedback?
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
</Callout>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
id="likeBtn"
|
||||||
|
style={{ marginLeft: 20 }}
|
||||||
|
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
|
||||||
|
onClick={() => {
|
||||||
|
setShowCallout(!likeQuery);
|
||||||
|
setLikeQuery(!likeQuery);
|
||||||
|
if (dislikeQuery) {
|
||||||
|
setDislikeQuery(!dislikeQuery);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
style={{ margin: "0 10px" }}
|
||||||
|
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
|
||||||
|
onClick={() => {
|
||||||
|
if (!dislikeQuery) {
|
||||||
|
useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userPrompt);
|
||||||
|
setLikeQuery(false);
|
||||||
|
}
|
||||||
|
setDislikeQuery(!dislikeQuery);
|
||||||
|
setShowCallout(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Separator vertical style={{ color: "#EDEBE9" }} />
|
||||||
|
<CommandBarButton
|
||||||
|
onClick={copyGeneratedCode}
|
||||||
|
iconProps={{ iconName: "Copy" }}
|
||||||
|
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
||||||
|
>
|
||||||
|
Copy code
|
||||||
|
</CommandBarButton>
|
||||||
|
<CommandBarButton
|
||||||
|
onClick={() => {
|
||||||
|
setShowDeletePopup(true);
|
||||||
|
}}
|
||||||
|
iconProps={{ iconName: "Delete" }}
|
||||||
|
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
||||||
|
>
|
||||||
|
Delete code
|
||||||
|
</CommandBarButton>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Stack className="tabPaneContentContainer">
|
||||||
|
<SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}>
|
||||||
|
<EditorReact
|
||||||
|
language={"sql"}
|
||||||
|
content={query}
|
||||||
|
isReadOnly={false}
|
||||||
|
ariaLabel={"Editing Query"}
|
||||||
|
lineNumbers={"on"}
|
||||||
|
onContentChanged={(newQuery: string) => setQuery(newQuery)}
|
||||||
|
onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)}
|
||||||
|
/>
|
||||||
|
<QueryResultSection
|
||||||
|
isMongoDB={false}
|
||||||
|
queryEditorContent={selectedQuery || query}
|
||||||
|
error={errorMessage}
|
||||||
|
queryResults={queryResults}
|
||||||
|
isExecuting={isExecuting}
|
||||||
|
executeQueryDocumentsPage={(firstItemIndex: number) =>
|
||||||
|
queryDocumentsPerPage(firstItemIndex, queryIterator)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SplitterLayout>
|
||||||
|
</Stack>
|
||||||
|
<WelcomeModal visible={localStorage.getItem("hideWelcomeModal") !== "true"} />
|
||||||
|
{isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />}
|
||||||
|
{query !== "" && query.trim().length !== 0 && (
|
||||||
|
<DeletePopup
|
||||||
|
showDeletePopup={showDeletePopup}
|
||||||
|
setShowDeletePopup={setShowDeletePopup}
|
||||||
|
setQuery={setQuery}
|
||||||
|
clearFeedback={resetButtonState}
|
||||||
|
showFeedbackBar={setShowFeedbackBar}
|
||||||
/>
|
/>
|
||||||
<QueryResultSection
|
)}
|
||||||
isMongoDB={false}
|
<CopyPopup showCopyPopup={showCopyPopup} setShowCopyPopup={setshowCopyPopup} />
|
||||||
queryEditorContent={selectedQuery || query}
|
</div>
|
||||||
error={errorMessage}
|
|
||||||
queryResults={queryResults}
|
|
||||||
isExecuting={isExecuting}
|
|
||||||
executeQueryDocumentsPage={(firstItemIndex: number) => queryDocumentsPerPage(firstItemIndex, queryIterator)}
|
|
||||||
/>
|
|
||||||
</SplitterLayout>
|
|
||||||
</Stack>
|
|
||||||
<WelcomeModal visible={localStorage.getItem("hideWelcomeModal") !== "true"} />
|
|
||||||
{isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />}
|
|
||||||
{query !== "" && query.trim().length !== 0 && (
|
|
||||||
<DeletePopup
|
|
||||||
showDeletePopup={showDeletePopup}
|
|
||||||
setShowDeletePopup={setShowDeletePopup}
|
|
||||||
setQuery={setQuery}
|
|
||||||
clearFeedback={resetButtonState}
|
|
||||||
showFeedbackBar={setShowFeedbackBar}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<CopyPopup showCopyPopup={showCopyPopup} setShowCopyPopup={setshowCopyPopup} />
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
154
src/Explorer/QueryCopilot/QueryCopilotUtilities.test.tsx
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import { FeedOptions } from "@azure/cosmos";
|
||||||
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
|
import { sampleDataClient } from "Common/SampleDataClient";
|
||||||
|
import * as commonUtils from "Common/dataAccess/queryDocuments";
|
||||||
|
import DocumentId from "Explorer/Tree/DocumentId";
|
||||||
|
import { querySampleDocuments, readSampleDocument } from "./QueryCopilotUtilities";
|
||||||
|
|
||||||
|
jest.mock("Explorer/Tree/DocumentId", () => {
|
||||||
|
return jest.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
id: jest.fn(),
|
||||||
|
loadDocument: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock("Utils/NotificationConsoleUtils", () => ({
|
||||||
|
logConsoleProgress: jest.fn().mockReturnValue((): void => undefined),
|
||||||
|
logConsoleError: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("@azure/cosmos", () => ({
|
||||||
|
FeedOptions: jest.fn(),
|
||||||
|
QueryIterator: jest.fn(),
|
||||||
|
Constants: {
|
||||||
|
HttpHeaders: {},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
jest.mock("Common/ErrorHandlingUtils", () => ({
|
||||||
|
handleError: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Common/dataAccess/queryDocuments", () => ({
|
||||||
|
getCommonQueryOptions: jest.fn((options) => options),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Common/SampleDataClient");
|
||||||
|
|
||||||
|
jest.mock("node-fetch");
|
||||||
|
|
||||||
|
jest.mock("Explorer/Explorer", () => {
|
||||||
|
class MockExplorer {
|
||||||
|
allocateContainer = jest.fn().mockResolvedValueOnce({});
|
||||||
|
}
|
||||||
|
return MockExplorer;
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock("hooks/useQueryCopilot", () => {
|
||||||
|
const mockQueryCopilotStore = {
|
||||||
|
shouldAllocateContainer: true,
|
||||||
|
setShouldAllocateContainer: jest.fn(),
|
||||||
|
correlationId: "mocked-correlation-id",
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
useQueryCopilot: jest.fn(() => mockQueryCopilotStore),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("QueryCopilotUtilities", () => {
|
||||||
|
beforeEach(() => jest.clearAllMocks());
|
||||||
|
|
||||||
|
describe("querySampleDocuments", () => {
|
||||||
|
(sampleDataClient as jest.Mock).mockReturnValue({
|
||||||
|
database: jest.fn().mockReturnValue({
|
||||||
|
container: jest.fn().mockReturnValue({
|
||||||
|
items: {
|
||||||
|
query: jest.fn().mockReturnValue([]),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls getCommonQueryOptions with the provided options", () => {
|
||||||
|
const query = "sample query";
|
||||||
|
const options: FeedOptions = { maxItemCount: 10 };
|
||||||
|
|
||||||
|
querySampleDocuments(query, options);
|
||||||
|
|
||||||
|
expect(commonUtils.getCommonQueryOptions).toHaveBeenCalledWith(options);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the result of items.query method", () => {
|
||||||
|
const query = "sample query";
|
||||||
|
const options: FeedOptions = { maxItemCount: 10 };
|
||||||
|
const mockResult = [
|
||||||
|
{ id: 1, name: "Document 1" },
|
||||||
|
{ id: 2, name: "Document 2" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock the items.query method to return the mockResult
|
||||||
|
(sampleDataClient().database("CopilotSampleDb").container("SampleContainer").items
|
||||||
|
.query as jest.Mock).mockReturnValue(mockResult);
|
||||||
|
|
||||||
|
const result = querySampleDocuments(query, options);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("readSampleDocument", () => {
|
||||||
|
it("should call the read method with the correct parameters", async () => {
|
||||||
|
(sampleDataClient as jest.Mock).mockReturnValue({
|
||||||
|
database: jest.fn().mockReturnValue({
|
||||||
|
container: jest.fn().mockReturnValue({
|
||||||
|
item: jest.fn().mockReturnValue({
|
||||||
|
read: jest.fn().mockResolvedValue({
|
||||||
|
resource: {},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const documentId = new DocumentId(null, "DocumentId", []);
|
||||||
|
const expectedResponse = {};
|
||||||
|
|
||||||
|
const result = await readSampleDocument(documentId);
|
||||||
|
|
||||||
|
expect(sampleDataClient).toHaveBeenCalled();
|
||||||
|
expect(sampleDataClient().database).toHaveBeenCalledWith("CopilotSampleDb");
|
||||||
|
expect(sampleDataClient().database("CopilotSampleDb").container).toHaveBeenCalledWith("SampleContainer");
|
||||||
|
expect(
|
||||||
|
sampleDataClient().database("CopilotSampleDb").container("SampleContainer").item("DocumentId", undefined).read
|
||||||
|
).toHaveBeenCalled();
|
||||||
|
expect(result).toEqual(expectedResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle an error and re-throw it", async () => {
|
||||||
|
(sampleDataClient as jest.Mock).mockReturnValue({
|
||||||
|
database: jest.fn().mockReturnValue({
|
||||||
|
container: jest.fn().mockReturnValue({
|
||||||
|
item: jest.fn().mockReturnValue({
|
||||||
|
read: jest.fn().mockRejectedValue(new Error("Mock error")),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorMock = new Error("Mock error");
|
||||||
|
const documentId = new DocumentId(null, "DocumentId", []);
|
||||||
|
|
||||||
|
await expect(readSampleDocument(documentId)).rejects.toStrictEqual(errorMock);
|
||||||
|
|
||||||
|
expect(sampleDataClient).toHaveBeenCalled();
|
||||||
|
expect(sampleDataClient().database).toHaveBeenCalledWith("CopilotSampleDb");
|
||||||
|
expect(sampleDataClient().database("CopilotSampleDb").container).toHaveBeenCalledWith("SampleContainer");
|
||||||
|
expect(
|
||||||
|
sampleDataClient().database("CopilotSampleDb").container("SampleContainer").item("DocumentId", undefined).read
|
||||||
|
).toHaveBeenCalled();
|
||||||
|
expect(handleError).toHaveBeenCalledWith(errorMock, "ReadDocument", expect.any(String));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,36 +1,35 @@
|
|||||||
import { QueryCopilotSampleContainerSchema } from "Common/Constants";
|
import { FeedOptions, Item, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
|
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
||||||
import { handleError } from "Common/ErrorHandlingUtils";
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
|
import { sampleDataClient } from "Common/SampleDataClient";
|
||||||
|
import { getPartitionKeyValue } from "Common/dataAccess/getPartitionKeyValue";
|
||||||
|
import { getCommonQueryOptions } from "Common/dataAccess/queryDocuments";
|
||||||
|
import DocumentId from "Explorer/Tree/DocumentId";
|
||||||
|
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
||||||
|
|
||||||
interface FeedbackParams {
|
export const querySampleDocuments = (query: string, options: FeedOptions): QueryIterator<ItemDefinition & Resource> => {
|
||||||
likeQuery: boolean;
|
options = getCommonQueryOptions(options);
|
||||||
generatedQuery: string;
|
return sampleDataClient()
|
||||||
userPrompt: string;
|
.database(QueryCopilotSampleDatabaseId)
|
||||||
description?: string;
|
.container(QueryCopilotSampleContainerId)
|
||||||
contact?: string;
|
.items.query(query, options);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export const readSampleDocument = async (documentId: DocumentId): Promise<Item> => {
|
||||||
|
const clearMessage = logConsoleProgress(`Reading item ${documentId.id()}`);
|
||||||
|
|
||||||
export const submitFeedback = async (params: FeedbackParams): Promise<void> => {
|
|
||||||
try {
|
try {
|
||||||
const { likeQuery, generatedQuery, userPrompt, description, contact } = params;
|
const response = await sampleDataClient()
|
||||||
const payload = {
|
.database(QueryCopilotSampleDatabaseId)
|
||||||
containerSchema: QueryCopilotSampleContainerSchema,
|
.container(QueryCopilotSampleContainerId)
|
||||||
like: likeQuery ? "like" : "dislike",
|
.item(documentId.id(), getPartitionKeyValue(documentId))
|
||||||
generatedSql: generatedQuery,
|
.read();
|
||||||
userPrompt,
|
|
||||||
description: description || "",
|
|
||||||
contact: contact || "",
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch("https://copilotorchestrater.azurewebsites.net/feedback", {
|
return response?.resource;
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"content-type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
});
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(response);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "copilotSubmitFeedback");
|
handleError(error, "ReadDocument", `Failed to read item ${documentId.id()}`);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
clearMessage();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import { shallow } from "enzyme";
|
|
||||||
import React from "react";
|
|
||||||
import { SamplePrompts, SamplePromptsProps } from "./SamplePrompts";
|
|
||||||
|
|
||||||
describe("Sample Prompts snapshot test", () => {
|
|
||||||
it("should render properly if isSamplePromptsOpen is true", () => {
|
|
||||||
const sampleProps: SamplePromptsProps = {
|
|
||||||
isSamplePromptsOpen: true,
|
|
||||||
setIsSamplePromptsOpen: () => undefined,
|
|
||||||
setTextBox: () => undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render properly if isSamplePromptsOpen is false", () => {
|
|
||||||
const sampleProps: SamplePromptsProps = {
|
|
||||||
isSamplePromptsOpen: false,
|
|
||||||
setIsSamplePromptsOpen: () => undefined,
|
|
||||||
setTextBox: () => undefined,
|
|
||||||
};
|
|
||||||
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
151
src/Explorer/QueryCopilot/Shared/QueryCopilotClient.test.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import { QueryCopilotSampleContainerSchema, ShortenedQueryCopilotSampleContainerSchema } from "Common/Constants";
|
||||||
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
|
import { createUri } from "Common/UrlUtility";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
|
import { SubmitFeedback } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
|
|
||||||
|
jest.mock("@azure/cosmos", () => ({
|
||||||
|
Constants: {
|
||||||
|
HttpHeaders: {},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Common/ErrorHandlingUtils", () => ({
|
||||||
|
handleError: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Common/SampleDataClient");
|
||||||
|
|
||||||
|
jest.mock("node-fetch");
|
||||||
|
|
||||||
|
jest.mock("Explorer/Explorer", () => {
|
||||||
|
class MockExplorer {
|
||||||
|
allocateContainer = jest.fn().mockResolvedValueOnce({});
|
||||||
|
}
|
||||||
|
return MockExplorer;
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock("hooks/useQueryCopilot", () => {
|
||||||
|
const mockQueryCopilotStore = {
|
||||||
|
shouldAllocateContainer: true,
|
||||||
|
setShouldAllocateContainer: jest.fn(),
|
||||||
|
correlationId: "mocked-correlation-id",
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
useQueryCopilot: jest.fn(() => mockQueryCopilotStore),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Query Copilot Client", () => {
|
||||||
|
beforeEach(() => jest.clearAllMocks());
|
||||||
|
|
||||||
|
describe("submitFeedback", () => {
|
||||||
|
const payload = {
|
||||||
|
like: "like",
|
||||||
|
generatedSql: "GeneratedQuery",
|
||||||
|
userPrompt: "UserPrompt",
|
||||||
|
description: "Description",
|
||||||
|
contact: "Contact",
|
||||||
|
containerSchema: userContext.features.enableCopilotFullSchema
|
||||||
|
? QueryCopilotSampleContainerSchema
|
||||||
|
: ShortenedQueryCopilotSampleContainerSchema,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockStore = useNotebook.getState();
|
||||||
|
beforeEach(() => {
|
||||||
|
mockStore.notebookServerInfo = {
|
||||||
|
notebookServerEndpoint: "mocked-endpoint",
|
||||||
|
authToken: "mocked-token",
|
||||||
|
forwardingId: "mocked-forwarding-id",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const feedbackUri = userContext.features.enableCopilotPhoenixGateaway
|
||||||
|
? createUri(useNotebook.getState().notebookServerInfo.notebookServerEndpoint, "feedback")
|
||||||
|
: createUri("https://copilotorchestrater.azurewebsites.net/", "feedback");
|
||||||
|
|
||||||
|
it("should call fetch with the payload with like", async () => {
|
||||||
|
const mockFetch = jest.fn().mockResolvedValueOnce({});
|
||||||
|
|
||||||
|
globalThis.fetch = mockFetch;
|
||||||
|
await SubmitFeedback({
|
||||||
|
params: {
|
||||||
|
likeQuery: true,
|
||||||
|
generatedQuery: "GeneratedQuery",
|
||||||
|
userPrompt: "UserPrompt",
|
||||||
|
description: "Description",
|
||||||
|
contact: "Contact",
|
||||||
|
},
|
||||||
|
explorer: new Explorer(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
|
feedbackUri,
|
||||||
|
expect.objectContaining({
|
||||||
|
headers: expect.objectContaining({
|
||||||
|
"x-ms-correlationid": "mocked-correlation-id",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const actualBody = JSON.parse(mockFetch.mock.calls[0][1].body);
|
||||||
|
expect(actualBody).toEqual(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call fetch with the payload with unlike and empty parameters", async () => {
|
||||||
|
payload.like = "dislike";
|
||||||
|
payload.description = "";
|
||||||
|
payload.contact = "";
|
||||||
|
const mockFetch = jest.fn().mockResolvedValueOnce({});
|
||||||
|
|
||||||
|
globalThis.fetch = mockFetch;
|
||||||
|
|
||||||
|
await SubmitFeedback({
|
||||||
|
params: {
|
||||||
|
likeQuery: false,
|
||||||
|
generatedQuery: "GeneratedQuery",
|
||||||
|
userPrompt: "UserPrompt",
|
||||||
|
description: undefined,
|
||||||
|
contact: undefined,
|
||||||
|
},
|
||||||
|
explorer: new Explorer(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
|
feedbackUri,
|
||||||
|
expect.objectContaining({
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"x-ms-correlationid": "mocked-correlation-id",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const actualBody = JSON.parse(mockFetch.mock.calls[0][1].body);
|
||||||
|
expect(actualBody).toEqual(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle errors and call handleError", async () => {
|
||||||
|
globalThis.fetch = jest.fn().mockRejectedValueOnce(new Error("Mock error"));
|
||||||
|
|
||||||
|
await SubmitFeedback({
|
||||||
|
params: {
|
||||||
|
likeQuery: true,
|
||||||
|
generatedQuery: "GeneratedQuery",
|
||||||
|
userPrompt: "UserPrompt",
|
||||||
|
description: "Description",
|
||||||
|
contact: "Contact",
|
||||||
|
},
|
||||||
|
explorer: new Explorer(),
|
||||||
|
}).catch((error) => {
|
||||||
|
expect(error.message).toEqual("Mock error");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(handleError).toHaveBeenCalledWith(new Error("Mock error"), expect.any(String));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
132
src/Explorer/QueryCopilot/Shared/QueryCopilotClient.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import {
|
||||||
|
PoolIdType,
|
||||||
|
QueryCopilotSampleContainerSchema,
|
||||||
|
ShortenedQueryCopilotSampleContainerSchema,
|
||||||
|
} from "Common/Constants";
|
||||||
|
import { handleError } from "Common/ErrorHandlingUtils";
|
||||||
|
import { createUri } from "Common/UrlUtility";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
|
import { FeedbackParams, GenerateSQLQueryResponse } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
import { useTabs } from "hooks/useTabs";
|
||||||
|
|
||||||
|
export const SendQueryRequest = async ({
|
||||||
|
userPrompt,
|
||||||
|
explorer,
|
||||||
|
}: {
|
||||||
|
userPrompt: string;
|
||||||
|
explorer: Explorer;
|
||||||
|
}): Promise<void> => {
|
||||||
|
if (userPrompt.trim() !== "") {
|
||||||
|
useQueryCopilot.getState().setIsGeneratingQuery(true);
|
||||||
|
useTabs.getState().setIsTabExecuting(true);
|
||||||
|
useTabs.getState().setIsQueryErrorThrown(false);
|
||||||
|
useQueryCopilot
|
||||||
|
.getState()
|
||||||
|
.setChatMessages([...useQueryCopilot.getState().chatMessages, { source: 0, message: userPrompt }]);
|
||||||
|
try {
|
||||||
|
if (useQueryCopilot.getState().shouldAllocateContainer && userContext.features.enableCopilotPhoenixGateaway) {
|
||||||
|
await explorer.allocateContainer(PoolIdType.DefaultPoolId);
|
||||||
|
useQueryCopilot.getState().setShouldAllocateContainer(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
useQueryCopilot.getState().refreshCorrelationId();
|
||||||
|
const serverInfo = useNotebook.getState().notebookServerInfo;
|
||||||
|
|
||||||
|
const queryUri = userContext.features.enableCopilotPhoenixGateaway
|
||||||
|
? createUri(serverInfo.notebookServerEndpoint, "generateSQLQuery")
|
||||||
|
: createUri("https://copilotorchestrater.azurewebsites.net/", "generateSQLQuery");
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
containerSchema: userContext.features.enableCopilotFullSchema
|
||||||
|
? QueryCopilotSampleContainerSchema
|
||||||
|
: ShortenedQueryCopilotSampleContainerSchema,
|
||||||
|
userPrompt: userPrompt,
|
||||||
|
};
|
||||||
|
const response = await fetch(queryUri, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
|
||||||
|
if (response.status === 404) {
|
||||||
|
useQueryCopilot.getState().setShouldAllocateContainer(true);
|
||||||
|
}
|
||||||
|
if (response.ok) {
|
||||||
|
if (generateSQLQueryResponse?.sql) {
|
||||||
|
let query = `Here is a query which will help you with provided prompt.\r\n **Prompt:** ${userPrompt}`;
|
||||||
|
query += `\r\n${generateSQLQueryResponse.sql}`;
|
||||||
|
useQueryCopilot
|
||||||
|
.getState()
|
||||||
|
.setChatMessages([
|
||||||
|
...useQueryCopilot.getState().chatMessages,
|
||||||
|
{ source: 1, message: query, explanation: generateSQLQueryResponse.explanation },
|
||||||
|
]);
|
||||||
|
useQueryCopilot.getState().setGeneratedQuery(generateSQLQueryResponse.sql);
|
||||||
|
useQueryCopilot.getState().setGeneratedQueryComments(generateSQLQueryResponse.explanation);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError");
|
||||||
|
useTabs.getState().setIsQueryErrorThrown(true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, "executeNaturalLanguageQuery");
|
||||||
|
useTabs.getState().setIsQueryErrorThrown(true);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
useQueryCopilot.getState().setUserPrompt("");
|
||||||
|
useQueryCopilot.getState().setIsGeneratingQuery(false);
|
||||||
|
useTabs.getState().setIsTabExecuting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SubmitFeedback = async ({
|
||||||
|
params,
|
||||||
|
explorer,
|
||||||
|
}: {
|
||||||
|
params: FeedbackParams;
|
||||||
|
explorer: Explorer;
|
||||||
|
}): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { likeQuery, generatedQuery, userPrompt, description, contact } = params;
|
||||||
|
const { correlationId, shouldAllocateContainer, setShouldAllocateContainer } = useQueryCopilot();
|
||||||
|
const payload = {
|
||||||
|
containerSchema: userContext.features.enableCopilotFullSchema
|
||||||
|
? QueryCopilotSampleContainerSchema
|
||||||
|
: ShortenedQueryCopilotSampleContainerSchema,
|
||||||
|
like: likeQuery ? "like" : "dislike",
|
||||||
|
generatedSql: generatedQuery,
|
||||||
|
userPrompt,
|
||||||
|
description: description || "",
|
||||||
|
contact: contact || "",
|
||||||
|
};
|
||||||
|
if (shouldAllocateContainer && userContext.features.enableCopilotPhoenixGateaway) {
|
||||||
|
await explorer.allocateContainer(PoolIdType.DefaultPoolId);
|
||||||
|
setShouldAllocateContainer(false);
|
||||||
|
}
|
||||||
|
const serverInfo = useNotebook.getState().notebookServerInfo;
|
||||||
|
const feedbackUri = userContext.features.enableCopilotPhoenixGateaway
|
||||||
|
? createUri(serverInfo.notebookServerEndpoint, "feedback")
|
||||||
|
: createUri("https://copilotorchestrater.azurewebsites.net/", "feedback");
|
||||||
|
const response = await fetch(feedbackUri, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"x-ms-correlationid": correlationId,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
if (response.status === 404) {
|
||||||
|
setShouldAllocateContainer(true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, "copilotSubmitFeedback");
|
||||||
|
}
|
||||||
|
};
|
||||||
32
src/Explorer/QueryCopilot/Shared/QueryCopilotInterfaces.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
|
||||||
|
export interface GenerateSQLQueryResponse {
|
||||||
|
apiVersion: string;
|
||||||
|
sql: string;
|
||||||
|
explanation: string;
|
||||||
|
generateStart: string;
|
||||||
|
generateEnd: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MessageSource {
|
||||||
|
User,
|
||||||
|
AI,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CopilotMessage {
|
||||||
|
source: MessageSource;
|
||||||
|
message: string;
|
||||||
|
explanation?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FeedbackParams {
|
||||||
|
likeQuery: boolean;
|
||||||
|
generatedQuery: string;
|
||||||
|
userPrompt: string;
|
||||||
|
description?: string;
|
||||||
|
contact?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryCopilotProps {
|
||||||
|
explorer: Explorer;
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { DefaultButton, IconButton } from "@fluentui/react";
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import React from "react";
|
||||||
|
import { SamplePrompts, SamplePromptsProps } from "./SamplePrompts";
|
||||||
|
|
||||||
|
describe("Sample Prompts snapshot test", () => {
|
||||||
|
const setTextBoxMock = jest.fn();
|
||||||
|
const setIsSamplePromptsOpenMock = jest.fn();
|
||||||
|
const sampleProps: SamplePromptsProps = {
|
||||||
|
isSamplePromptsOpen: true,
|
||||||
|
setIsSamplePromptsOpen: setIsSamplePromptsOpenMock,
|
||||||
|
setTextBox: setTextBoxMock,
|
||||||
|
};
|
||||||
|
beforeEach(() => jest.clearAllMocks());
|
||||||
|
|
||||||
|
it("should render properly if isSamplePromptsOpen is true", () => {
|
||||||
|
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render properly if isSamplePromptsOpen is false", () => {
|
||||||
|
sampleProps.isSamplePromptsOpen = false;
|
||||||
|
|
||||||
|
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call setTextBox and setIsSamplePromptsOpen(false) when a button is clicked", () => {
|
||||||
|
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
|
||||||
|
|
||||||
|
wrapper.find(DefaultButton).at(0).simulate("click");
|
||||||
|
expect(setTextBoxMock).toHaveBeenCalledWith("Show me products less than 100 dolars");
|
||||||
|
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
|
||||||
|
|
||||||
|
wrapper.find(DefaultButton).at(3).simulate("click");
|
||||||
|
expect(setTextBoxMock).toHaveBeenCalledWith(
|
||||||
|
"Write a query to return all records in this table created in the last thirty days"
|
||||||
|
);
|
||||||
|
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call setIsSamplePromptsOpen(false) when the close button is clicked", () => {
|
||||||
|
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
|
||||||
|
|
||||||
|
wrapper.find(IconButton).first().simulate("click");
|
||||||
|
|
||||||
|
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call setTextBox and setIsSamplePromptsOpen(false) when a simple prompt button is clicked", () => {
|
||||||
|
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
|
||||||
|
|
||||||
|
wrapper.find(DefaultButton).at(0).simulate("click");
|
||||||
|
expect(setTextBoxMock).toHaveBeenCalledWith("Show me products less than 100 dolars");
|
||||||
|
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
|
||||||
|
|
||||||
|
wrapper.find(DefaultButton).at(1).simulate("click");
|
||||||
|
expect(setTextBoxMock).toHaveBeenCalledWith("Show schema");
|
||||||
|
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call setTextBox and setIsSamplePromptsOpen(false) when an intermediate prompt button is clicked", () => {
|
||||||
|
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
|
||||||
|
|
||||||
|
wrapper.find(DefaultButton).at(2).simulate("click");
|
||||||
|
expect(setTextBoxMock).toHaveBeenCalledWith(
|
||||||
|
"Show items with a description that contains a number between 0 and 99 inclusive."
|
||||||
|
);
|
||||||
|
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
|
||||||
|
|
||||||
|
wrapper.find(DefaultButton).at(3).simulate("click");
|
||||||
|
expect(setTextBoxMock).toHaveBeenCalledWith(
|
||||||
|
"Write a query to return all records in this table created in the last thirty days"
|
||||||
|
);
|
||||||
|
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call setTextBox and setIsSamplePromptsOpen(false) when a complex prompt button is clicked", () => {
|
||||||
|
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
|
||||||
|
|
||||||
|
wrapper.find(DefaultButton).at(4).simulate("click");
|
||||||
|
expect(setTextBoxMock).toHaveBeenCalledWith("Show all the products that customer Bob has reviewed.");
|
||||||
|
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
|
||||||
|
|
||||||
|
wrapper.find(DefaultButton).at(5).simulate("click");
|
||||||
|
expect(setTextBoxMock).toHaveBeenCalledWith("Which computers are more than 300 dollars and less than 400 dollars?");
|
||||||
|
expect(setIsSamplePromptsOpenMock).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { DefaultButton, FontIcon, IconButton, Image, Modal, Stack, Text } from "@fluentui/react";
|
import { DefaultButton, FontIcon, IconButton, Image, Modal, Stack, Text } from "@fluentui/react";
|
||||||
import React, { Dispatch, SetStateAction } from "react";
|
import React, { Dispatch, SetStateAction } from "react";
|
||||||
import ComplexPrompts from "../../../../images/ComplexPrompts.svg";
|
import ComplexPrompts from "../../../../../images/ComplexPrompts.svg";
|
||||||
import IntermediatePrompts from "../../../../images/IntermediatePrompts.svg";
|
import IntermediatePrompts from "../../../../../images/IntermediatePrompts.svg";
|
||||||
import SimplePrompts from "../../../../images/SimplePrompts.svg";
|
import SimplePrompts from "../../../../../images/SimplePrompts.svg";
|
||||||
|
|
||||||
export interface SamplePromptsProps {
|
export interface SamplePromptsProps {
|
||||||
isSamplePromptsOpen: boolean;
|
isSamplePromptsOpen: boolean;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
@keyframes loadingAnimation {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { DefaultButton } from "@fluentui/react";
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import React from "react";
|
||||||
|
import { RetrievingBubble } from "./RetrievingBubble";
|
||||||
|
|
||||||
|
const mockUseQueryCopilot = {
|
||||||
|
isGeneratingQuery: false,
|
||||||
|
setIsGeneratingQuery: jest.fn(),
|
||||||
|
isGeneratingExplanation: false,
|
||||||
|
setIsGeneratingExplanation: jest.fn(),
|
||||||
|
shouldIncludeInMessages: true,
|
||||||
|
setShouldIncludeInMessages: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock("hooks/useQueryCopilot", () => ({
|
||||||
|
useQueryCopilot: jest.fn(() => mockUseQueryCopilot),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("RetrievingBubble", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
mockUseQueryCopilot.isGeneratingQuery = false;
|
||||||
|
mockUseQueryCopilot.isGeneratingExplanation = false;
|
||||||
|
mockUseQueryCopilot.setIsGeneratingQuery.mockClear();
|
||||||
|
mockUseQueryCopilot.setIsGeneratingExplanation.mockClear();
|
||||||
|
mockUseQueryCopilot.setShouldIncludeInMessages.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render properly when isGeneratingQuery is true", () => {
|
||||||
|
mockUseQueryCopilot.isGeneratingQuery = true;
|
||||||
|
const wrapper = shallow(<RetrievingBubble />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render properly when isGeneratingExplanation is true", () => {
|
||||||
|
mockUseQueryCopilot.isGeneratingExplanation = true;
|
||||||
|
const wrapper = shallow(<RetrievingBubble />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when isGeneratingQuery is true clicking stop generating button invokes the correct callbacks", () => {
|
||||||
|
mockUseQueryCopilot.isGeneratingQuery = true;
|
||||||
|
|
||||||
|
const wrapper = shallow(<RetrievingBubble />);
|
||||||
|
wrapper.find(DefaultButton).at(0).simulate("click");
|
||||||
|
|
||||||
|
expect(mockUseQueryCopilot.setIsGeneratingQuery).toHaveBeenCalledWith(false);
|
||||||
|
expect(mockUseQueryCopilot.setIsGeneratingExplanation).toHaveBeenCalledTimes(0);
|
||||||
|
expect(mockUseQueryCopilot.setShouldIncludeInMessages).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when isGeneratingExplanation is true clicking stop generating button invokes the correct callbacks", () => {
|
||||||
|
mockUseQueryCopilot.isGeneratingExplanation = true;
|
||||||
|
|
||||||
|
const wrapper = shallow(<RetrievingBubble />);
|
||||||
|
wrapper.find(DefaultButton).at(0).simulate("click");
|
||||||
|
|
||||||
|
expect(mockUseQueryCopilot.setIsGeneratingQuery).toHaveBeenCalledTimes(0);
|
||||||
|
expect(mockUseQueryCopilot.setIsGeneratingExplanation).toHaveBeenCalledWith(false);
|
||||||
|
expect(mockUseQueryCopilot.setShouldIncludeInMessages).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { DefaultButton, Image, Stack, Text } from "@fluentui/react";
|
||||||
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
import React from "react";
|
||||||
|
import StopGeneratingIcon from "../../../../../../images/StopGenerating.svg";
|
||||||
|
import "./RetrievingBubble.css";
|
||||||
|
|
||||||
|
export const RetrievingBubble = (): JSX.Element => {
|
||||||
|
const {
|
||||||
|
isGeneratingQuery,
|
||||||
|
setIsGeneratingQuery,
|
||||||
|
isGeneratingExplanation,
|
||||||
|
setIsGeneratingExplanation,
|
||||||
|
shouldIncludeInMessages,
|
||||||
|
setShouldIncludeInMessages,
|
||||||
|
} = useQueryCopilot();
|
||||||
|
|
||||||
|
const stopGenerating = () => {
|
||||||
|
if (isGeneratingQuery) {
|
||||||
|
setIsGeneratingQuery(false);
|
||||||
|
}
|
||||||
|
if (isGeneratingExplanation) {
|
||||||
|
setIsGeneratingExplanation(false);
|
||||||
|
}
|
||||||
|
if (shouldIncludeInMessages) {
|
||||||
|
setShouldIncludeInMessages(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const bubbleContent = (bubbleType: string) => {
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
horizontalAlign="end"
|
||||||
|
verticalAlign="end"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "10px",
|
||||||
|
margin: "10px",
|
||||||
|
backgroundColor: "#FAFAFA",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "46px",
|
||||||
|
backgroundColor: "white",
|
||||||
|
padding: "12px 16px 16px 16px",
|
||||||
|
gap: "12px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Retriveing {bubbleType}
|
||||||
|
</Text>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "4px",
|
||||||
|
backgroundColor: "#E6E6E6",
|
||||||
|
borderRadius: "4px",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "50%",
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: "#0078D4",
|
||||||
|
animation: "loadingAnimation 2s linear infinite",
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<Stack
|
||||||
|
horizontalAlign="center"
|
||||||
|
verticalAlign="center"
|
||||||
|
style={{ marginTop: "8px", gap: "8px", alignItems: "center" }}
|
||||||
|
>
|
||||||
|
<DefaultButton
|
||||||
|
onClick={stopGenerating}
|
||||||
|
styles={{ root: { border: "none", background: "none", padding: 0, color: "#424242" } }}
|
||||||
|
style={{ color: "#424242" }}
|
||||||
|
onRenderIcon={() => <Image src={StopGeneratingIcon} />}
|
||||||
|
>
|
||||||
|
Stop generating
|
||||||
|
</DefaultButton>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isGeneratingQuery && bubbleContent("queries")}
|
||||||
|
{isGeneratingExplanation && bubbleContent("explanation")}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`RetrievingBubble should render properly when isGeneratingExplanation is true 1`] = `
|
||||||
|
<Fragment>
|
||||||
|
<Stack
|
||||||
|
horizontalAlign="end"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"alignItems": "center",
|
||||||
|
"backgroundColor": "#FAFAFA",
|
||||||
|
"borderRadius": "8px",
|
||||||
|
"display": "flex",
|
||||||
|
"margin": "10px",
|
||||||
|
"padding": "10px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verticalAlign="end"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "white",
|
||||||
|
"borderRadius": "8px",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"gap": "12px",
|
||||||
|
"height": "46px",
|
||||||
|
"padding": "12px 16px 16px 16px",
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Retriveing
|
||||||
|
explanation
|
||||||
|
</Text>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#E6E6E6",
|
||||||
|
"borderRadius": "4px",
|
||||||
|
"height": "4px",
|
||||||
|
"overflow": "hidden",
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"animation": "loadingAnimation 2s linear infinite",
|
||||||
|
"backgroundColor": "#0078D4",
|
||||||
|
"height": "100%",
|
||||||
|
"width": "50%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Stack
|
||||||
|
horizontalAlign="center"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"alignItems": "center",
|
||||||
|
"gap": "8px",
|
||||||
|
"marginTop": "8px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verticalAlign="center"
|
||||||
|
>
|
||||||
|
<CustomizedDefaultButton
|
||||||
|
onClick={[Function]}
|
||||||
|
onRenderIcon={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"color": "#424242",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"background": "none",
|
||||||
|
"border": "none",
|
||||||
|
"color": "#424242",
|
||||||
|
"padding": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Stop generating
|
||||||
|
</CustomizedDefaultButton>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Fragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`RetrievingBubble should render properly when isGeneratingQuery is true 1`] = `
|
||||||
|
<Fragment>
|
||||||
|
<Stack
|
||||||
|
horizontalAlign="end"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"alignItems": "center",
|
||||||
|
"backgroundColor": "#FAFAFA",
|
||||||
|
"borderRadius": "8px",
|
||||||
|
"display": "flex",
|
||||||
|
"margin": "10px",
|
||||||
|
"padding": "10px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verticalAlign="end"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "white",
|
||||||
|
"borderRadius": "8px",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"gap": "12px",
|
||||||
|
"height": "46px",
|
||||||
|
"padding": "12px 16px 16px 16px",
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Retriveing
|
||||||
|
queries
|
||||||
|
</Text>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#E6E6E6",
|
||||||
|
"borderRadius": "4px",
|
||||||
|
"height": "4px",
|
||||||
|
"overflow": "hidden",
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"animation": "loadingAnimation 2s linear infinite",
|
||||||
|
"backgroundColor": "#0078D4",
|
||||||
|
"height": "100%",
|
||||||
|
"width": "50%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Stack
|
||||||
|
horizontalAlign="center"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"alignItems": "center",
|
||||||
|
"gap": "8px",
|
||||||
|
"marginTop": "8px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verticalAlign="center"
|
||||||
|
>
|
||||||
|
<CustomizedDefaultButton
|
||||||
|
onClick={[Function]}
|
||||||
|
onRenderIcon={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"color": "#424242",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"background": "none",
|
||||||
|
"border": "none",
|
||||||
|
"color": "#424242",
|
||||||
|
"padding": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Stop generating
|
||||||
|
</CustomizedDefaultButton>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Fragment>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Text } from "@fluentui/react";
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
import React from "react";
|
||||||
|
import { SampleBubble } from "./SampleBubble";
|
||||||
|
|
||||||
|
describe("Sample Bubble snapshot test", () => {
|
||||||
|
it("should render", () => {
|
||||||
|
const wrapper = shallow(<SampleBubble />);
|
||||||
|
|
||||||
|
const sampleInputs = wrapper.find(Text);
|
||||||
|
|
||||||
|
expect(sampleInputs.length).toEqual(2);
|
||||||
|
expect(useQueryCopilot.getState().userPrompt).toEqual("");
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
it("should render and be clicked", () => {
|
||||||
|
const wrapper = shallow(<SampleBubble />);
|
||||||
|
|
||||||
|
const firstSampleInput = wrapper.find(Text).first();
|
||||||
|
firstSampleInput.simulate("click", {}, "");
|
||||||
|
|
||||||
|
expect(useQueryCopilot.getState().userPrompt).toEqual(expect.any(String));
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
42
src/Explorer/QueryCopilot/V2/Bubbles/Sample/SampleBubble.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { Stack, Text } from "@fluentui/react";
|
||||||
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export const SampleBubble: React.FC = (): JSX.Element => {
|
||||||
|
const { setUserPrompt } = useQueryCopilot();
|
||||||
|
|
||||||
|
const sampleChatMessages: string[] = [
|
||||||
|
"Write a query to return last 10 records in the database",
|
||||||
|
'Write a query to return all records in this table created in the last thirty days which also have the record owner as "Contoso"',
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
horizontalAlign="end"
|
||||||
|
verticalAlign="end"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "10px",
|
||||||
|
margin: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sampleChatMessages.map((text, index) => (
|
||||||
|
<Text
|
||||||
|
key={index}
|
||||||
|
onClick={() => setUserPrompt(text)}
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
border: "1.5px solid #B0BEFF",
|
||||||
|
width: "100%",
|
||||||
|
padding: "2px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
marginBottom: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Sample Bubble snapshot test should render 1`] = `
|
||||||
|
<Stack
|
||||||
|
horizontalAlign="end"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"alignItems": "center",
|
||||||
|
"display": "flex",
|
||||||
|
"margin": "10px",
|
||||||
|
"padding": "10px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verticalAlign="end"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
key="0"
|
||||||
|
onClick={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"border": "1.5px solid #B0BEFF",
|
||||||
|
"borderRadius": "4px",
|
||||||
|
"cursor": "pointer",
|
||||||
|
"marginBottom": "5px",
|
||||||
|
"padding": "2px",
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Write a query to return last 10 records in the database
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
key="1"
|
||||||
|
onClick={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"border": "1.5px solid #B0BEFF",
|
||||||
|
"borderRadius": "4px",
|
||||||
|
"cursor": "pointer",
|
||||||
|
"marginBottom": "5px",
|
||||||
|
"padding": "2px",
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Write a query to return all records in this table created in the last thirty days which also have the record owner as "Contoso"
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Sample Bubble snapshot test should render and be clicked 1`] = `
|
||||||
|
<Stack
|
||||||
|
horizontalAlign="end"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"alignItems": "center",
|
||||||
|
"display": "flex",
|
||||||
|
"margin": "10px",
|
||||||
|
"padding": "10px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verticalAlign="end"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
key="0"
|
||||||
|
onClick={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"border": "1.5px solid #B0BEFF",
|
||||||
|
"borderRadius": "4px",
|
||||||
|
"cursor": "pointer",
|
||||||
|
"marginBottom": "5px",
|
||||||
|
"padding": "2px",
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Write a query to return last 10 records in the database
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
key="1"
|
||||||
|
onClick={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"border": "1.5px solid #B0BEFF",
|
||||||
|
"borderRadius": "4px",
|
||||||
|
"cursor": "pointer",
|
||||||
|
"marginBottom": "5px",
|
||||||
|
"padding": "2px",
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Write a query to return all records in this table created in the last thirty days which also have the record owner as "Contoso"
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { shallow } from "enzyme";
|
||||||
|
import React from "react";
|
||||||
|
import { WelcomeBubble } from "./WelcomeBubble";
|
||||||
|
|
||||||
|
const mockedDate = new Date(2023, 7, 15);
|
||||||
|
jest.spyOn(global, "Date").mockImplementation(() => (mockedDate as unknown) as string);
|
||||||
|
|
||||||
|
describe("Welcome Bubble snapshot test", () => {
|
||||||
|
it("should render ", () => {
|
||||||
|
const wrapper = shallow(<WelcomeBubble />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { Stack, Text } from "@fluentui/react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export const WelcomeBubble: React.FC = (): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<Stack horizontalAlign="center" style={{ color: "#707070" }} tokens={{ padding: 8, childrenGap: 8 }}>
|
||||||
|
{new Date().toLocaleDateString("en-US", {
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
})}{" "}
|
||||||
|
{new Date().toLocaleTimeString("en-US", {
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "numeric",
|
||||||
|
hour12: true,
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
tokens={{ padding: 16, childrenGap: 12 }}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "white",
|
||||||
|
margin: "5px 10px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text variant="medium">
|
||||||
|
Hello, I am Cosmos Db's copilot assistant. I can help you do the following things:
|
||||||
|
</Text>
|
||||||
|
<Stack tokens={{ childrenGap: 8 }}>
|
||||||
|
<Stack horizontal style={{ marginLeft: "15px" }} tokens={{ childrenGap: 8 }}>
|
||||||
|
<Text style={{ fontSize: "16px", lineHeight: "16px", verticalAlign: "middle" }}>•</Text>
|
||||||
|
<Text style={{ verticalAlign: "middle" }}>Generate queries based upon prompt you suggest</Text>
|
||||||
|
</Stack>
|
||||||
|
<Stack horizontal style={{ marginLeft: "15px" }} tokens={{ childrenGap: 8 }}>
|
||||||
|
<Text style={{ fontSize: "16px", lineHeight: "16px", verticalAlign: "middle" }}>•</Text>
|
||||||
|
<Text style={{ verticalAlign: "middle" }}>
|
||||||
|
Explain and provide alternate queries for a query suggested by you
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Stack horizontal style={{ marginLeft: "15px" }} tokens={{ childrenGap: 8 }}>
|
||||||
|
<Text style={{ fontSize: "16px", lineHeight: "16px", verticalAlign: "middle" }}>•</Text>
|
||||||
|
<Text style={{ verticalAlign: "middle" }}>Help answer questions about Cosmos DB</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<Text variant="medium">
|
||||||
|
To get started, ask me a question or use one of the sample prompts to generate a query. AI-generated content
|
||||||
|
may be incorrect.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Welcome Bubble snapshot test should render 1`] = `
|
||||||
|
<Stack>
|
||||||
|
<Stack
|
||||||
|
horizontalAlign="center"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"color": "#707070",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 8,
|
||||||
|
"padding": 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
August 15
|
||||||
|
|
||||||
|
12:00 AM
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "white",
|
||||||
|
"borderRadius": "8px",
|
||||||
|
"margin": "5px 10px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 12,
|
||||||
|
"padding": 16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
variant="medium"
|
||||||
|
>
|
||||||
|
Hello, I am Cosmos Db's copilot assistant. I can help you do the following things:
|
||||||
|
</Text>
|
||||||
|
<Stack
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"marginLeft": "15px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": "16px",
|
||||||
|
"lineHeight": "16px",
|
||||||
|
"verticalAlign": "middle",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
•
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"verticalAlign": "middle",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Generate queries based upon prompt you suggest
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"marginLeft": "15px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": "16px",
|
||||||
|
"lineHeight": "16px",
|
||||||
|
"verticalAlign": "middle",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
•
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"verticalAlign": "middle",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Explain and provide alternate queries for a query suggested by you
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"marginLeft": "15px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": "16px",
|
||||||
|
"lineHeight": "16px",
|
||||||
|
"verticalAlign": "middle",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
•
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"verticalAlign": "middle",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Help answer questions about Cosmos DB
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<Text
|
||||||
|
variant="medium"
|
||||||
|
>
|
||||||
|
To get started, ask me a question or use one of the sample prompts to generate a query. AI-generated content may be incorrect.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
`;
|
||||||
112
src/Explorer/QueryCopilot/V2/Footer/Footer.test.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { IconButton, Image, TextField } from "@fluentui/react";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { SendQueryRequest } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
import React from "react";
|
||||||
|
import { act } from "react-dom/test-utils";
|
||||||
|
import { Footer } from "./Footer";
|
||||||
|
|
||||||
|
jest.mock("@azure/cosmos", () => ({
|
||||||
|
Constants: {
|
||||||
|
HttpHeaders: {},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Common/SampleDataClient");
|
||||||
|
|
||||||
|
jest.mock("Explorer/Explorer");
|
||||||
|
|
||||||
|
jest.mock("node-fetch");
|
||||||
|
|
||||||
|
jest.mock("Common/ErrorHandlingUtils", () => ({
|
||||||
|
handleError: jest.fn(),
|
||||||
|
getErrorMessage: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("Explorer/QueryCopilot/Shared/QueryCopilotClient");
|
||||||
|
|
||||||
|
describe("Footer snapshot test", () => {
|
||||||
|
const testMessage = "test message";
|
||||||
|
|
||||||
|
const initialStoreState = useQueryCopilot.getState();
|
||||||
|
beforeEach(() => {
|
||||||
|
useQueryCopilot.setState(initialStoreState, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should open sample prompts on button click", () => {
|
||||||
|
const wrapper = shallow(<Footer explorer={new Explorer()} />);
|
||||||
|
|
||||||
|
const samplePromptsImage = wrapper.find(Image).first();
|
||||||
|
samplePromptsImage.simulate("click", {});
|
||||||
|
|
||||||
|
expect(useQueryCopilot.getState().isSamplePromptsOpen).toBeTruthy();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update user input", () => {
|
||||||
|
const wrapper = shallow(<Footer explorer={new Explorer()} />);
|
||||||
|
const newInput = "some new input";
|
||||||
|
|
||||||
|
const textInput = wrapper.find(TextField).first();
|
||||||
|
textInput.simulate("change", {}, newInput);
|
||||||
|
|
||||||
|
expect(useQueryCopilot.getState().userPrompt).toEqual(newInput);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should pass text with enter key", async () => {
|
||||||
|
useQueryCopilot.getState().setUserPrompt(testMessage);
|
||||||
|
const wrapper = shallow(<Footer explorer={new Explorer()} />);
|
||||||
|
|
||||||
|
const textInput = wrapper.find(TextField).first();
|
||||||
|
await act(async () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
textInput.simulate("keydown", { key: "Enter", shiftKey: false, preventDefault: () => {} });
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
expect(SendQueryRequest).toHaveBeenCalledWith({ userPrompt: testMessage, explorer: expect.any(Explorer) });
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not pass text with non enter key", () => {
|
||||||
|
const wrapper = shallow(<Footer explorer={new Explorer()} />);
|
||||||
|
|
||||||
|
const textInput = wrapper.find(TextField).first();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
textInput.simulate("keydown", { key: "K", shiftKey: false, preventDefault: () => {} });
|
||||||
|
|
||||||
|
expect(useQueryCopilot.getState().chatMessages).toEqual([]);
|
||||||
|
expect(useQueryCopilot.getState().userPrompt).toEqual("");
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not pass if no text", () => {
|
||||||
|
const wrapper = shallow(<Footer explorer={new Explorer()} />);
|
||||||
|
|
||||||
|
const textInput = wrapper.find(TextField).first();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
textInput.simulate("keydown", { key: "Enter", shiftKey: false, preventDefault: () => {} });
|
||||||
|
|
||||||
|
expect(useQueryCopilot.getState().chatMessages).toEqual([]);
|
||||||
|
expect(useQueryCopilot.getState().userPrompt).toEqual("");
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should pass text with icon button", async () => {
|
||||||
|
useQueryCopilot.getState().setUserPrompt(testMessage);
|
||||||
|
const wrapper = shallow(<Footer explorer={new Explorer()} />);
|
||||||
|
|
||||||
|
const iconButton = wrapper.find(IconButton).first();
|
||||||
|
await act(async () => {
|
||||||
|
iconButton.simulate("click", {});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
expect(SendQueryRequest).toHaveBeenCalledWith({ userPrompt: testMessage, explorer: expect.any(Explorer) });
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
82
src/Explorer/QueryCopilot/V2/Footer/Footer.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { IButtonStyles, IconButton, Image, Stack, TextField } from "@fluentui/react";
|
||||||
|
import { SendQueryRequest } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
|
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
import React from "react";
|
||||||
|
import HintIcon from "../../../../../images/Hint.svg";
|
||||||
|
import { SamplePrompts, SamplePromptsProps } from "../../Shared/SamplePrompts/SamplePrompts";
|
||||||
|
|
||||||
|
export const Footer: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
|
||||||
|
const {
|
||||||
|
userPrompt,
|
||||||
|
setUserPrompt,
|
||||||
|
isSamplePromptsOpen,
|
||||||
|
setIsSamplePromptsOpen,
|
||||||
|
isGeneratingQuery,
|
||||||
|
} = useQueryCopilot();
|
||||||
|
|
||||||
|
const promptStyles: IButtonStyles = {
|
||||||
|
root: { border: "5px", selectors: { ":hover": { outline: "1px dashed #605e5c" } } },
|
||||||
|
label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const sampleProps: SamplePromptsProps = {
|
||||||
|
isSamplePromptsOpen: isSamplePromptsOpen,
|
||||||
|
setIsSamplePromptsOpen: setIsSamplePromptsOpen,
|
||||||
|
setTextBox: setUserPrompt,
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEnterKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (event.key === "Enter" && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
startSentMessageProcess();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startSentMessageProcess = async () => {
|
||||||
|
await SendQueryRequest({ userPrompt, explorer });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
horizontal
|
||||||
|
horizontalAlign="end"
|
||||||
|
verticalAlign="end"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
borderRadius: "20px",
|
||||||
|
background: "white",
|
||||||
|
padding: "5px",
|
||||||
|
margin: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Image src={HintIcon} styles={promptStyles} onClick={() => setIsSamplePromptsOpen(true)} />
|
||||||
|
<SamplePrompts sampleProps={sampleProps} />
|
||||||
|
</Stack>
|
||||||
|
<TextField
|
||||||
|
placeholder="Write your own prompt or ask a question"
|
||||||
|
value={userPrompt}
|
||||||
|
onChange={(_, newValue) => setUserPrompt(newValue)}
|
||||||
|
onKeyDown={handleEnterKeyPress}
|
||||||
|
multiline
|
||||||
|
resizable={false}
|
||||||
|
disabled={isGeneratingQuery}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
width: "100%",
|
||||||
|
height: "80px",
|
||||||
|
borderRadius: "20px",
|
||||||
|
padding: "8px",
|
||||||
|
border: "none",
|
||||||
|
outline: "none",
|
||||||
|
marginLeft: "10px",
|
||||||
|
},
|
||||||
|
fieldGroup: { border: "none" },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton disabled={isGeneratingQuery} iconProps={{ iconName: "Send" }} onClick={startSentMessageProcess} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,511 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Footer snapshot test should not pass if no text 1`] = `
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
horizontalAlign="end"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"alignItems": "center",
|
||||||
|
"background": "white",
|
||||||
|
"borderRadius": "20px",
|
||||||
|
"display": "flex",
|
||||||
|
"margin": "5px",
|
||||||
|
"padding": "5px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verticalAlign="end"
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Image
|
||||||
|
onClick={[Function]}
|
||||||
|
src=""
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"label": Object {
|
||||||
|
"fontWeight": 400,
|
||||||
|
"paddingLeft": 8,
|
||||||
|
"textAlign": "left",
|
||||||
|
},
|
||||||
|
"root": Object {
|
||||||
|
"border": "5px",
|
||||||
|
"selectors": Object {
|
||||||
|
":hover": Object {
|
||||||
|
"outline": "1px dashed #605e5c",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<SamplePrompts
|
||||||
|
sampleProps={
|
||||||
|
Object {
|
||||||
|
"isSamplePromptsOpen": false,
|
||||||
|
"setIsSamplePromptsOpen": [Function],
|
||||||
|
"setTextBox": [Function],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<StyledTextFieldBase
|
||||||
|
disabled={false}
|
||||||
|
multiline={true}
|
||||||
|
onChange={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
placeholder="Write your own prompt or ask a question"
|
||||||
|
resizable={false}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"fieldGroup": Object {
|
||||||
|
"border": "none",
|
||||||
|
},
|
||||||
|
"root": Object {
|
||||||
|
"border": "none",
|
||||||
|
"borderRadius": "20px",
|
||||||
|
"height": "80px",
|
||||||
|
"marginLeft": "10px",
|
||||||
|
"outline": "none",
|
||||||
|
"padding": "8px",
|
||||||
|
"width": "100%",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<CustomizedIconButton
|
||||||
|
disabled={false}
|
||||||
|
iconProps={
|
||||||
|
Object {
|
||||||
|
"iconName": "Send",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClick={[Function]}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Footer snapshot test should not pass text with non enter key 1`] = `
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
horizontalAlign="end"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"alignItems": "center",
|
||||||
|
"background": "white",
|
||||||
|
"borderRadius": "20px",
|
||||||
|
"display": "flex",
|
||||||
|
"margin": "5px",
|
||||||
|
"padding": "5px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verticalAlign="end"
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Image
|
||||||
|
onClick={[Function]}
|
||||||
|
src=""
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"label": Object {
|
||||||
|
"fontWeight": 400,
|
||||||
|
"paddingLeft": 8,
|
||||||
|
"textAlign": "left",
|
||||||
|
},
|
||||||
|
"root": Object {
|
||||||
|
"border": "5px",
|
||||||
|
"selectors": Object {
|
||||||
|
":hover": Object {
|
||||||
|
"outline": "1px dashed #605e5c",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<SamplePrompts
|
||||||
|
sampleProps={
|
||||||
|
Object {
|
||||||
|
"isSamplePromptsOpen": false,
|
||||||
|
"setIsSamplePromptsOpen": [Function],
|
||||||
|
"setTextBox": [Function],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<StyledTextFieldBase
|
||||||
|
disabled={false}
|
||||||
|
multiline={true}
|
||||||
|
onChange={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
placeholder="Write your own prompt or ask a question"
|
||||||
|
resizable={false}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"fieldGroup": Object {
|
||||||
|
"border": "none",
|
||||||
|
},
|
||||||
|
"root": Object {
|
||||||
|
"border": "none",
|
||||||
|
"borderRadius": "20px",
|
||||||
|
"height": "80px",
|
||||||
|
"marginLeft": "10px",
|
||||||
|
"outline": "none",
|
||||||
|
"padding": "8px",
|
||||||
|
"width": "100%",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<CustomizedIconButton
|
||||||
|
disabled={false}
|
||||||
|
iconProps={
|
||||||
|
Object {
|
||||||
|
"iconName": "Send",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClick={[Function]}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Footer snapshot test should open sample prompts on button click 1`] = `
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
horizontalAlign="end"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"alignItems": "center",
|
||||||
|
"background": "white",
|
||||||
|
"borderRadius": "20px",
|
||||||
|
"display": "flex",
|
||||||
|
"margin": "5px",
|
||||||
|
"padding": "5px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verticalAlign="end"
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Image
|
||||||
|
onClick={[Function]}
|
||||||
|
src=""
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"label": Object {
|
||||||
|
"fontWeight": 400,
|
||||||
|
"paddingLeft": 8,
|
||||||
|
"textAlign": "left",
|
||||||
|
},
|
||||||
|
"root": Object {
|
||||||
|
"border": "5px",
|
||||||
|
"selectors": Object {
|
||||||
|
":hover": Object {
|
||||||
|
"outline": "1px dashed #605e5c",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<SamplePrompts
|
||||||
|
sampleProps={
|
||||||
|
Object {
|
||||||
|
"isSamplePromptsOpen": false,
|
||||||
|
"setIsSamplePromptsOpen": [Function],
|
||||||
|
"setTextBox": [Function],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<StyledTextFieldBase
|
||||||
|
disabled={false}
|
||||||
|
multiline={true}
|
||||||
|
onChange={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
placeholder="Write your own prompt or ask a question"
|
||||||
|
resizable={false}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"fieldGroup": Object {
|
||||||
|
"border": "none",
|
||||||
|
},
|
||||||
|
"root": Object {
|
||||||
|
"border": "none",
|
||||||
|
"borderRadius": "20px",
|
||||||
|
"height": "80px",
|
||||||
|
"marginLeft": "10px",
|
||||||
|
"outline": "none",
|
||||||
|
"padding": "8px",
|
||||||
|
"width": "100%",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<CustomizedIconButton
|
||||||
|
disabled={false}
|
||||||
|
iconProps={
|
||||||
|
Object {
|
||||||
|
"iconName": "Send",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClick={[Function]}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Footer snapshot test should pass text with enter key 1`] = `
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
horizontalAlign="end"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"alignItems": "center",
|
||||||
|
"background": "white",
|
||||||
|
"borderRadius": "20px",
|
||||||
|
"display": "flex",
|
||||||
|
"margin": "5px",
|
||||||
|
"padding": "5px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verticalAlign="end"
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Image
|
||||||
|
onClick={[Function]}
|
||||||
|
src=""
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"label": Object {
|
||||||
|
"fontWeight": 400,
|
||||||
|
"paddingLeft": 8,
|
||||||
|
"textAlign": "left",
|
||||||
|
},
|
||||||
|
"root": Object {
|
||||||
|
"border": "5px",
|
||||||
|
"selectors": Object {
|
||||||
|
":hover": Object {
|
||||||
|
"outline": "1px dashed #605e5c",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<SamplePrompts
|
||||||
|
sampleProps={
|
||||||
|
Object {
|
||||||
|
"isSamplePromptsOpen": false,
|
||||||
|
"setIsSamplePromptsOpen": [Function],
|
||||||
|
"setTextBox": [Function],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<StyledTextFieldBase
|
||||||
|
disabled={false}
|
||||||
|
multiline={true}
|
||||||
|
onChange={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
placeholder="Write your own prompt or ask a question"
|
||||||
|
resizable={false}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"fieldGroup": Object {
|
||||||
|
"border": "none",
|
||||||
|
},
|
||||||
|
"root": Object {
|
||||||
|
"border": "none",
|
||||||
|
"borderRadius": "20px",
|
||||||
|
"height": "80px",
|
||||||
|
"marginLeft": "10px",
|
||||||
|
"outline": "none",
|
||||||
|
"padding": "8px",
|
||||||
|
"width": "100%",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="test message"
|
||||||
|
/>
|
||||||
|
<CustomizedIconButton
|
||||||
|
disabled={false}
|
||||||
|
iconProps={
|
||||||
|
Object {
|
||||||
|
"iconName": "Send",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClick={[Function]}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Footer snapshot test should pass text with icon button 1`] = `
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
horizontalAlign="end"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"alignItems": "center",
|
||||||
|
"background": "white",
|
||||||
|
"borderRadius": "20px",
|
||||||
|
"display": "flex",
|
||||||
|
"margin": "5px",
|
||||||
|
"padding": "5px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verticalAlign="end"
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Image
|
||||||
|
onClick={[Function]}
|
||||||
|
src=""
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"label": Object {
|
||||||
|
"fontWeight": 400,
|
||||||
|
"paddingLeft": 8,
|
||||||
|
"textAlign": "left",
|
||||||
|
},
|
||||||
|
"root": Object {
|
||||||
|
"border": "5px",
|
||||||
|
"selectors": Object {
|
||||||
|
":hover": Object {
|
||||||
|
"outline": "1px dashed #605e5c",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<SamplePrompts
|
||||||
|
sampleProps={
|
||||||
|
Object {
|
||||||
|
"isSamplePromptsOpen": false,
|
||||||
|
"setIsSamplePromptsOpen": [Function],
|
||||||
|
"setTextBox": [Function],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<StyledTextFieldBase
|
||||||
|
disabled={false}
|
||||||
|
multiline={true}
|
||||||
|
onChange={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
placeholder="Write your own prompt or ask a question"
|
||||||
|
resizable={false}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"fieldGroup": Object {
|
||||||
|
"border": "none",
|
||||||
|
},
|
||||||
|
"root": Object {
|
||||||
|
"border": "none",
|
||||||
|
"borderRadius": "20px",
|
||||||
|
"height": "80px",
|
||||||
|
"marginLeft": "10px",
|
||||||
|
"outline": "none",
|
||||||
|
"padding": "8px",
|
||||||
|
"width": "100%",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value="test message"
|
||||||
|
/>
|
||||||
|
<CustomizedIconButton
|
||||||
|
disabled={false}
|
||||||
|
iconProps={
|
||||||
|
Object {
|
||||||
|
"iconName": "Send",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClick={[Function]}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Footer snapshot test should update user input 1`] = `
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
horizontalAlign="end"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"alignItems": "center",
|
||||||
|
"background": "white",
|
||||||
|
"borderRadius": "20px",
|
||||||
|
"display": "flex",
|
||||||
|
"margin": "5px",
|
||||||
|
"padding": "5px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verticalAlign="end"
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Image
|
||||||
|
onClick={[Function]}
|
||||||
|
src=""
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"label": Object {
|
||||||
|
"fontWeight": 400,
|
||||||
|
"paddingLeft": 8,
|
||||||
|
"textAlign": "left",
|
||||||
|
},
|
||||||
|
"root": Object {
|
||||||
|
"border": "5px",
|
||||||
|
"selectors": Object {
|
||||||
|
":hover": Object {
|
||||||
|
"outline": "1px dashed #605e5c",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<SamplePrompts
|
||||||
|
sampleProps={
|
||||||
|
Object {
|
||||||
|
"isSamplePromptsOpen": false,
|
||||||
|
"setIsSamplePromptsOpen": [Function],
|
||||||
|
"setTextBox": [Function],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<StyledTextFieldBase
|
||||||
|
disabled={false}
|
||||||
|
multiline={true}
|
||||||
|
onChange={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
placeholder="Write your own prompt or ask a question"
|
||||||
|
resizable={false}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"fieldGroup": Object {
|
||||||
|
"border": "none",
|
||||||
|
},
|
||||||
|
"root": Object {
|
||||||
|
"border": "none",
|
||||||
|
"borderRadius": "20px",
|
||||||
|
"height": "80px",
|
||||||
|
"marginLeft": "10px",
|
||||||
|
"outline": "none",
|
||||||
|
"padding": "8px",
|
||||||
|
"width": "100%",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<CustomizedIconButton
|
||||||
|
disabled={false}
|
||||||
|
iconProps={
|
||||||
|
Object {
|
||||||
|
"iconName": "Send",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClick={[Function]}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
`;
|
||||||
17
src/Explorer/QueryCopilot/V2/Header/Header.test.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { IconButton } from "@fluentui/react";
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
import React from "react";
|
||||||
|
import { Header } from "./Header";
|
||||||
|
|
||||||
|
describe("Header snapshot test", () => {
|
||||||
|
it("should close on button click ", () => {
|
||||||
|
const wrapper = shallow(<Header />);
|
||||||
|
|
||||||
|
const iconButton = wrapper.find(IconButton).first();
|
||||||
|
iconButton.simulate("click", {});
|
||||||
|
|
||||||
|
expect(useQueryCopilot.getState().showCopilotSidebar).toBeFalsy();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
39
src/Explorer/QueryCopilot/V2/Header/Header.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { IconButton, Image, Stack, Text } from "@fluentui/react";
|
||||||
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
import React from "react";
|
||||||
|
import CopilotIcon from "../../../../../images/CopilotSidebarLogo.svg";
|
||||||
|
|
||||||
|
export const Header: React.FC = (): JSX.Element => {
|
||||||
|
const { setShowCopilotSidebar } = useQueryCopilot();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
style={{ margin: "15px 0px 0px 0px", padding: "5px", display: "flex", justifyContent: "space-between" }}
|
||||||
|
horizontal
|
||||||
|
verticalAlign="center"
|
||||||
|
>
|
||||||
|
<Stack horizontal verticalAlign="center">
|
||||||
|
<Image src={CopilotIcon} />
|
||||||
|
<Text style={{ marginLeft: "5px", fontWeight: "bold" }}>Copilot</Text>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
background: "#f0f0f0",
|
||||||
|
fontSize: "10px",
|
||||||
|
padding: "2px 4px",
|
||||||
|
marginLeft: "5px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Preview
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => setShowCopilotSidebar(false)}
|
||||||
|
iconProps={{ iconName: "Cancel" }}
|
||||||
|
title="Exit"
|
||||||
|
ariaLabel="Exit"
|
||||||
|
style={{ color: "#424242", verticalAlign: "middle" }}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Header snapshot test should close on button click 1`] = `
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"display": "flex",
|
||||||
|
"justifyContent": "space-between",
|
||||||
|
"margin": "15px 0px 0px 0px",
|
||||||
|
"padding": "5px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verticalAlign="center"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
verticalAlign="center"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"marginLeft": "5px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Copilot
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"background": "#f0f0f0",
|
||||||
|
"borderRadius": "8px",
|
||||||
|
"fontSize": "10px",
|
||||||
|
"marginLeft": "5px",
|
||||||
|
"padding": "2px 4px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Preview
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<CustomizedIconButton
|
||||||
|
ariaLabel="Exit"
|
||||||
|
iconProps={
|
||||||
|
Object {
|
||||||
|
"iconName": "Cancel",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClick={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"color": "#424242",
|
||||||
|
"verticalAlign": "middle",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
title="Exit"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { PrimaryButton } from "@fluentui/react";
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
import { withHooks } from "jest-react-hooks-shallow";
|
||||||
|
import React from "react";
|
||||||
|
import { WelcomeSidebarModal } from "./WelcomeSidebarModal";
|
||||||
|
|
||||||
|
describe("Welcome Sidebar Modal snapshot test", () => {
|
||||||
|
it("should close on button click ", () => {
|
||||||
|
withHooks(() => {
|
||||||
|
const wrapper = shallow(<WelcomeSidebarModal />);
|
||||||
|
const spy = jest.spyOn(localStorage, "setItem");
|
||||||
|
spy.mockClear();
|
||||||
|
|
||||||
|
const button = wrapper.find(PrimaryButton).first();
|
||||||
|
button.simulate("click", {});
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(spy).toHaveBeenLastCalledWith("showWelcomeSidebar", "false");
|
||||||
|
expect(useQueryCopilot.getState().showWelcomeSidebar).toBeFalsy();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("should not reneder with local storage key", () => {
|
||||||
|
withHooks(() => {
|
||||||
|
window.localStorage.setItem("showWelcomeSidebar", "false");
|
||||||
|
const wrapper = shallow(<WelcomeSidebarModal />);
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
130
src/Explorer/QueryCopilot/V2/Modal/WelcomeSidebarModal.tsx
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { Image, Link, PrimaryButton, Stack, Text } from "@fluentui/react";
|
||||||
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
import React from "react";
|
||||||
|
import Database from "../../../../../images/CopilotDatabase.svg";
|
||||||
|
import Flash from "../../../../../images/CopilotFlash.svg";
|
||||||
|
import CopilotSidebarWelcomeIllustration from "../../../../../images/CopilotSidebarWelcomeIllustration.svg";
|
||||||
|
import Thumb from "../../../../../images/CopilotThumb.svg";
|
||||||
|
|
||||||
|
export const WelcomeSidebarModal: React.FC = (): JSX.Element => {
|
||||||
|
const { showWelcomeSidebar, setShowWelcomeSidebar } = useQueryCopilot();
|
||||||
|
|
||||||
|
const hideModal = (): void => {
|
||||||
|
setShowWelcomeSidebar(false);
|
||||||
|
window.localStorage.setItem("showWelcomeSidebar", "false");
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const showWelcomeSidebar = window.localStorage.getItem("showWelcomeSidebar");
|
||||||
|
setShowWelcomeSidebar(showWelcomeSidebar && showWelcomeSidebar === "false" ? false : true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
showWelcomeSidebar && (
|
||||||
|
<Stack
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
overflow: "auto",
|
||||||
|
backgroundColor: "#FAFAFA",
|
||||||
|
flex: "1 0 100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "20px 10px",
|
||||||
|
padding: "20px",
|
||||||
|
maxHeight: "100%",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
borderRadius: "20px",
|
||||||
|
backgroundColor: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack horizontalAlign="center" verticalAlign="center">
|
||||||
|
<Image src={CopilotSidebarWelcomeIllustration} />
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack>
|
||||||
|
<Stack.Item align="center" style={{ marginBottom: "10px" }}>
|
||||||
|
<Text className="title bold">Welcome to Copilot in CosmosDB</Text>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item style={{ marginBottom: "15px" }}>
|
||||||
|
<Stack>
|
||||||
|
<Stack horizontal>
|
||||||
|
<Stack.Item align="start">
|
||||||
|
<Image src={Flash} />
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item align="center" style={{ marginLeft: "10px" }}>
|
||||||
|
<Text style={{ fontWeight: 600 }}>
|
||||||
|
Let copilot do the work for you
|
||||||
|
<br />
|
||||||
|
</Text>
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
<Stack.Item style={{ textAlign: "start", marginLeft: "25px" }}>
|
||||||
|
<Text>
|
||||||
|
Ask Copilot to generate a query by describing the query in your words.
|
||||||
|
<br />
|
||||||
|
<Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
|
||||||
|
</Text>
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item style={{ marginBottom: "15px" }}>
|
||||||
|
<Stack>
|
||||||
|
<Stack horizontal>
|
||||||
|
<Stack.Item align="start">
|
||||||
|
<Image src={Thumb} />
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item align="center" style={{ marginLeft: "10px" }}>
|
||||||
|
<Text style={{ fontWeight: 600 }}>
|
||||||
|
Use your judgement
|
||||||
|
<br />
|
||||||
|
</Text>
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
<Stack.Item style={{ textAlign: "start", marginLeft: "25px" }}>
|
||||||
|
<Text>
|
||||||
|
AI-generated content can have mistakes. Make sure it’s accurate and appropriate before using it.
|
||||||
|
<br />
|
||||||
|
<Link href="http://aka.ms/cdb-copilot-preview-terms">Read preview terms</Link>
|
||||||
|
</Text>
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item style={{ marginBottom: "15px" }}>
|
||||||
|
<Stack>
|
||||||
|
<Stack horizontal>
|
||||||
|
<Stack.Item align="start">
|
||||||
|
<Image src={Database} />
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item align="center" style={{ marginLeft: "10px" }}>
|
||||||
|
<Text style={{ fontWeight: 600 }}>
|
||||||
|
Copilot currently works only a sample database
|
||||||
|
<br />
|
||||||
|
</Text>
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
<Stack.Item style={{ textAlign: "start", marginLeft: "25px" }}>
|
||||||
|
<Text>
|
||||||
|
Copilot is setup on a sample database we have configured for you at no cost
|
||||||
|
<br />
|
||||||
|
<Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
|
||||||
|
</Text>
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack>
|
||||||
|
<Stack.Item align="center">
|
||||||
|
<PrimaryButton style={{ width: "224px", height: "32px" }} onClick={hideModal}>
|
||||||
|
Get Started
|
||||||
|
</PrimaryButton>
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Welcome Sidebar Modal snapshot test should close on button click 1`] = `""`;
|
||||||
|
|
||||||
|
exports[`Welcome Sidebar Modal snapshot test should not reneder with local storage key 1`] = `""`;
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Stack } from "@fluentui/react";
|
||||||
|
import Explorer from "Explorer/Explorer";
|
||||||
|
import { SampleBubble } from "Explorer/QueryCopilot/V2/Bubbles/Sample/SampleBubble";
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
import { withHooks } from "jest-react-hooks-shallow";
|
||||||
|
import React from "react";
|
||||||
|
import { QueryCopilotSidebar } from "./QueryCopilotSidebar";
|
||||||
|
|
||||||
|
describe("Query Copilot Sidebar snapshot test", () => {
|
||||||
|
const initialState = useQueryCopilot.getState();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
useQueryCopilot.setState(initialState, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render and set copilot used flag ", () => {
|
||||||
|
withHooks(() => {
|
||||||
|
useQueryCopilot.getState().setShowCopilotSidebar(true);
|
||||||
|
const wrapper = shallow(<QueryCopilotSidebar explorer={new Explorer()} />);
|
||||||
|
|
||||||
|
expect(useQueryCopilot.getState().wasCopilotUsed).toBeTruthy();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render and not set copilot used flag ", () => {
|
||||||
|
withHooks(() => {
|
||||||
|
const wrapper = shallow(<QueryCopilotSidebar explorer={new Explorer()} />);
|
||||||
|
|
||||||
|
expect(useQueryCopilot.getState().wasCopilotUsed).toBeFalsy();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render with chat messages", () => {
|
||||||
|
const message = "some test message";
|
||||||
|
useQueryCopilot.getState().setChatMessages([{ source: 0, message: message }]);
|
||||||
|
const wrapper = shallow(<QueryCopilotSidebar explorer={new Explorer()} />);
|
||||||
|
|
||||||
|
const messageContainer = wrapper.find(Stack).findWhere((x) => x.text() === message);
|
||||||
|
|
||||||
|
expect(messageContainer).toBeDefined();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render samples without messages", () => {
|
||||||
|
const wrapper = shallow(<QueryCopilotSidebar explorer={new Explorer()} />);
|
||||||
|
|
||||||
|
const sampleBubble = wrapper.find(SampleBubble);
|
||||||
|
|
||||||
|
expect(sampleBubble).toBeDefined();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
84
src/Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { Stack } from "@fluentui/react";
|
||||||
|
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
|
import { RetrievingBubble } from "Explorer/QueryCopilot/V2/Bubbles/Retriveing/RetrievingBubble";
|
||||||
|
import { SampleBubble } from "Explorer/QueryCopilot/V2/Bubbles/Sample/SampleBubble";
|
||||||
|
import { WelcomeBubble } from "Explorer/QueryCopilot/V2/Bubbles/Welcome/WelcomeBubble";
|
||||||
|
import { Footer } from "Explorer/QueryCopilot/V2/Footer/Footer";
|
||||||
|
import { Header } from "Explorer/QueryCopilot/V2/Header/Header";
|
||||||
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
import React from "react";
|
||||||
|
import { WelcomeSidebarModal } from "../Modal/WelcomeSidebarModal";
|
||||||
|
|
||||||
|
export const QueryCopilotSidebar: React.FC<QueryCopilotProps> = ({ explorer }: QueryCopilotProps): JSX.Element => {
|
||||||
|
const {
|
||||||
|
setWasCopilotUsed,
|
||||||
|
showCopilotSidebar,
|
||||||
|
chatMessages,
|
||||||
|
showWelcomeSidebar,
|
||||||
|
isGeneratingQuery,
|
||||||
|
} = useQueryCopilot();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (showCopilotSidebar) {
|
||||||
|
setWasCopilotUsed(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack style={{ width: "100%", height: "100%", backgroundColor: "#FAFAFA" }}>
|
||||||
|
<Header />
|
||||||
|
{showWelcomeSidebar ? (
|
||||||
|
<WelcomeSidebarModal />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Stack
|
||||||
|
style={{
|
||||||
|
flexGrow: 1,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
overflowY: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<WelcomeBubble />
|
||||||
|
{chatMessages.map((message, index) =>
|
||||||
|
message.source === 0 ? (
|
||||||
|
<Stack
|
||||||
|
key={index}
|
||||||
|
horizontalAlign="center"
|
||||||
|
tokens={{ padding: 8, childrenGap: 8 }}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#E0E7FF",
|
||||||
|
borderRadius: "8px",
|
||||||
|
margin: "5px 10px",
|
||||||
|
textAlign: "start",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{message.message}
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Stack
|
||||||
|
key={index}
|
||||||
|
horizontalAlign="center"
|
||||||
|
tokens={{ padding: 8, childrenGap: 8 }}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "white",
|
||||||
|
borderRadius: "8px",
|
||||||
|
margin: "5px 10px",
|
||||||
|
textAlign: "start",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{message.message}
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
<RetrievingBubble />
|
||||||
|
|
||||||
|
{chatMessages.length === 0 && !isGeneratingQuery && <SampleBubble />}
|
||||||
|
</Stack>
|
||||||
|
<Footer explorer={explorer} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Query Copilot Sidebar snapshot test should render and not set copilot used flag 1`] = `
|
||||||
|
<Stack
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#FAFAFA",
|
||||||
|
"height": "100%",
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Header />
|
||||||
|
<WelcomeSidebarModal />
|
||||||
|
</Stack>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Query Copilot Sidebar snapshot test should render and set copilot used flag 1`] = `
|
||||||
|
<Stack
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#FAFAFA",
|
||||||
|
"height": "100%",
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Header />
|
||||||
|
<WelcomeSidebarModal />
|
||||||
|
</Stack>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Query Copilot Sidebar snapshot test should render samples without messages 1`] = `
|
||||||
|
<Stack
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#FAFAFA",
|
||||||
|
"height": "100%",
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Header />
|
||||||
|
<WelcomeSidebarModal />
|
||||||
|
</Stack>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Query Copilot Sidebar snapshot test should render with chat messages 1`] = `
|
||||||
|
<Stack
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"backgroundColor": "#FAFAFA",
|
||||||
|
"height": "100%",
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Header />
|
||||||
|
<WelcomeSidebarModal />
|
||||||
|
</Stack>
|
||||||
|
`;
|
||||||
@@ -5,133 +5,149 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
|||||||
className="tab-pane"
|
className="tab-pane"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"height": "100%",
|
|
||||||
"padding": 24,
|
"padding": 24,
|
||||||
"width": "100%",
|
"width": "100%",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Stack
|
<div
|
||||||
horizontal={true}
|
|
||||||
verticalAlign="center"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src=""
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"fontSize": 16,
|
|
||||||
"fontWeight": 600,
|
|
||||||
"marginLeft": 8,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Copilot
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<Stack
|
|
||||||
horizontal={true}
|
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"marginTop": 16,
|
"height": "100%",
|
||||||
"position": "relative",
|
"overflowY": "auto",
|
||||||
"width": "100%",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
verticalAlign="center"
|
|
||||||
>
|
|
||||||
<StyledTextFieldBase
|
|
||||||
autoComplete="off"
|
|
||||||
disabled={false}
|
|
||||||
id="naturalLanguageInput"
|
|
||||||
onChange={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"lineHeight": 30,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
styles={
|
|
||||||
Object {
|
|
||||||
"root": Object {
|
|
||||||
"width": "95%",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value="Write a query to return all records in this table"
|
|
||||||
/>
|
|
||||||
<CustomizedIconButton
|
|
||||||
disabled={false}
|
|
||||||
iconProps={
|
|
||||||
Object {
|
|
||||||
"iconName": "Send",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onClick={[Function]}
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"marginLeft": 8,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
<Text
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"fontSize": 12,
|
|
||||||
"marginBottom": 24,
|
|
||||||
"marginTop": 8,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
<StyledLinkBase
|
verticalAlign="center"
|
||||||
href=""
|
|
||||||
target="_blank"
|
|
||||||
>
|
>
|
||||||
Read preview terms
|
<Image
|
||||||
</StyledLinkBase>
|
src=""
|
||||||
</Text>
|
/>
|
||||||
<Stack
|
<Text
|
||||||
className="tabPaneContentContainer"
|
style={
|
||||||
>
|
Object {
|
||||||
<t
|
"fontSize": 16,
|
||||||
customClassName=""
|
"fontWeight": 600,
|
||||||
onDragEnd={null}
|
"marginLeft": 8,
|
||||||
onDragStart={null}
|
}
|
||||||
onSecondaryPaneSizeChange={null}
|
}
|
||||||
percentage={false}
|
>
|
||||||
primaryIndex={0}
|
Copilot
|
||||||
primaryMinSize={100}
|
</Text>
|
||||||
secondaryMinSize={200}
|
</Stack>
|
||||||
vertical={true}
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"marginTop": 16,
|
||||||
|
"position": "relative",
|
||||||
|
"width": "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verticalAlign="center"
|
||||||
>
|
>
|
||||||
<EditorReact
|
<StyledTextFieldBase
|
||||||
ariaLabel="Editing Query"
|
autoComplete="off"
|
||||||
content=""
|
disabled={false}
|
||||||
isReadOnly={false}
|
id="naturalLanguageInput"
|
||||||
language="sql"
|
onChange={[Function]}
|
||||||
lineNumbers="on"
|
onClick={[Function]}
|
||||||
onContentChanged={[Function]}
|
onKeyDown={[Function]}
|
||||||
onContentSelected={[Function]}
|
style={
|
||||||
|
Object {
|
||||||
|
"lineHeight": 30,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": "95%",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value=""
|
||||||
/>
|
/>
|
||||||
<QueryResultSection
|
<CustomizedIconButton
|
||||||
error=""
|
disabled={true}
|
||||||
executeQueryDocumentsPage={[Function]}
|
iconProps={
|
||||||
isExecuting={false}
|
Object {
|
||||||
isMongoDB={false}
|
"iconName": "Send",
|
||||||
queryEditorContent=""
|
}
|
||||||
|
}
|
||||||
|
onClick={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"marginLeft": 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</t>
|
</Stack>
|
||||||
</Stack>
|
<Stack
|
||||||
<WelcomeModal
|
style={
|
||||||
visible={true}
|
Object {
|
||||||
/>
|
"marginBottom": 24,
|
||||||
<CopyPopup
|
"marginTop": 8,
|
||||||
setShowCopyPopup={[Function]}
|
}
|
||||||
showCopyPopup={false}
|
}
|
||||||
/>
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": 12,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.
|
||||||
|
|
||||||
|
<StyledLinkBase
|
||||||
|
href="http://aka.ms/cdb-copilot-preview-terms"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Read preview terms
|
||||||
|
</StyledLinkBase>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
className="tabPaneContentContainer"
|
||||||
|
>
|
||||||
|
<t
|
||||||
|
customClassName=""
|
||||||
|
onDragEnd={null}
|
||||||
|
onDragStart={null}
|
||||||
|
onSecondaryPaneSizeChange={null}
|
||||||
|
percentage={false}
|
||||||
|
primaryIndex={0}
|
||||||
|
primaryMinSize={100}
|
||||||
|
secondaryMinSize={200}
|
||||||
|
vertical={true}
|
||||||
|
>
|
||||||
|
<EditorReact
|
||||||
|
ariaLabel="Editing Query"
|
||||||
|
content=""
|
||||||
|
isReadOnly={false}
|
||||||
|
language="sql"
|
||||||
|
lineNumbers="on"
|
||||||
|
onContentChanged={[Function]}
|
||||||
|
onContentSelected={[Function]}
|
||||||
|
/>
|
||||||
|
<QueryResultSection
|
||||||
|
error=""
|
||||||
|
executeQueryDocumentsPage={[Function]}
|
||||||
|
isExecuting={false}
|
||||||
|
isMongoDB={false}
|
||||||
|
queryEditorContent=""
|
||||||
|
/>
|
||||||
|
</t>
|
||||||
|
</Stack>
|
||||||
|
<WelcomeModal
|
||||||
|
visible={true}
|
||||||
|
/>
|
||||||
|
<CopyPopup
|
||||||
|
setShowCopyPopup={[Function]}
|
||||||
|
showCopyPopup={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -97,6 +97,12 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
() => this.setState({}),
|
() => this.setState({}),
|
||||||
(state) => state.showResetPasswordBubble
|
(state) => state.showResetPasswordBubble
|
||||||
),
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dispose: useDatabases.subscribe(
|
||||||
|
() => this.setState({}),
|
||||||
|
(state) => state.sampleDataResourceTokenCollection
|
||||||
|
),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -107,7 +113,11 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private getSplashScreenButtons = (): JSX.Element => {
|
private getSplashScreenButtons = (): JSX.Element => {
|
||||||
if (userContext.features.enableCopilot && userContext.apiType === "SQL") {
|
if (
|
||||||
|
useDatabases.getState().sampleDataResourceTokenCollection &&
|
||||||
|
userContext.features.enableCopilot &&
|
||||||
|
userContext.apiType === "SQL"
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<Stack style={{ width: "66%", cursor: "pointer", margin: "40px auto" }} tokens={{ childrenGap: 16 }}>
|
<Stack style={{ width: "66%", cursor: "pointer", margin: "40px auto" }} tokens={{ childrenGap: 16 }}>
|
||||||
<Stack horizontal tokens={{ childrenGap: 16 }}>
|
<Stack horizontal tokens={{ childrenGap: 16 }}>
|
||||||
@@ -137,7 +147,16 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
description={
|
description={
|
||||||
"Copilot is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
|
"Copilot is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
|
||||||
}
|
}
|
||||||
onClick={() => useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot)}
|
onClick={() => {
|
||||||
|
const copilotVersion = userContext.features.copilotVersion;
|
||||||
|
if (copilotVersion === "v1.0") {
|
||||||
|
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
|
||||||
|
} else if (copilotVersion === "v2.0") {
|
||||||
|
const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection;
|
||||||
|
sampleCollection.onNewQueryClick(sampleCollection, undefined);
|
||||||
|
}
|
||||||
|
traceOpen(Action.OpenQueryCopilotFromSplashScreen, { apiType: userContext.apiType });
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<SplashScreenButton
|
<SplashScreenButton
|
||||||
imgSrc={ConnectIcon}
|
imgSrc={ConnectIcon}
|
||||||
@@ -246,8 +265,9 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
<form className="connectExplorerFormContainer">
|
<form className="connectExplorerFormContainer">
|
||||||
<div className="splashScreenContainer">
|
<div className="splashScreenContainer">
|
||||||
<div className="splashScreen">
|
<div className="splashScreen">
|
||||||
<div
|
<h1
|
||||||
className="title"
|
className="title"
|
||||||
|
role="heading"
|
||||||
aria-label={
|
aria-label={
|
||||||
userContext.apiType === "Postgres"
|
userContext.apiType === "Postgres"
|
||||||
? "Welcome to Azure Cosmos DB for PostgreSQL"
|
? "Welcome to Azure Cosmos DB for PostgreSQL"
|
||||||
@@ -258,7 +278,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
? "Welcome to Azure Cosmos DB for PostgreSQL"
|
? "Welcome to Azure Cosmos DB for PostgreSQL"
|
||||||
: "Welcome to Azure Cosmos DB"}
|
: "Welcome to Azure Cosmos DB"}
|
||||||
<FeaturePanelLauncher />
|
<FeaturePanelLauncher />
|
||||||
</div>
|
</h1>
|
||||||
<div className="subtitle">
|
<div className="subtitle">
|
||||||
{userContext.apiType === "Postgres"
|
{userContext.apiType === "Postgres"
|
||||||
? "Get started with our sample datasets, documentation, and additional tools."
|
? "Get started with our sample datasets, documentation, and additional tools."
|
||||||
@@ -581,7 +601,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Link>
|
</Link>
|
||||||
<Image src={LinkIcon} alt=" " />
|
<Image src={LinkIcon} alt={item.title} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Text>{item.description}</Text>
|
<Text>{item.description}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -600,7 +620,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
<li key={`${item.title}${item.description}${index}`}>
|
<li key={`${item.title}${item.description}${index}`}>
|
||||||
<Stack style={{ marginBottom: 26 }}>
|
<Stack style={{ marginBottom: 26 }}>
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<Image style={{ marginRight: 8 }} src={item.iconSrc} />
|
<Image style={{ marginRight: 8 }} src={item.iconSrc} alt={item.title} />
|
||||||
<Link style={{ fontSize: 14 }} onClick={item.onClick} title={item.info}>
|
<Link style={{ fontSize: 14 }} onClick={item.onClick} title={item.info}>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -720,7 +740,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Link>
|
</Link>
|
||||||
<Image src={LinkIcon} />
|
<Image src={LinkIcon} alt={item.title} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Text>{item.description}</Text>
|
<Text>{item.description}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { extractPartitionKey, ItemDefinition, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { extractPartitionKey, ItemDefinition, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
|
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import Q from "q";
|
import Q from "q";
|
||||||
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
|
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
|
||||||
@@ -7,7 +8,12 @@ import NewDocumentIcon from "../../../images/NewDocument.svg";
|
|||||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||||
import UploadIcon from "../../../images/Upload_16x16.svg";
|
import UploadIcon from "../../../images/Upload_16x16.svg";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { DocumentsGridMetrics, KeyCodes } from "../../Common/Constants";
|
import {
|
||||||
|
DocumentsGridMetrics,
|
||||||
|
KeyCodes,
|
||||||
|
QueryCopilotSampleContainerId,
|
||||||
|
QueryCopilotSampleDatabaseId,
|
||||||
|
} from "../../Common/Constants";
|
||||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||||
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
|
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
|
||||||
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
|
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
|
||||||
@@ -71,6 +77,7 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
|
|
||||||
private _documentsIterator: QueryIterator<ItemDefinition & Resource>;
|
private _documentsIterator: QueryIterator<ItemDefinition & Resource>;
|
||||||
private _resourceTokenPartitionKey: string;
|
private _resourceTokenPartitionKey: string;
|
||||||
|
private _isQueryCopilotSampleContainer: boolean;
|
||||||
|
|
||||||
constructor(options: ViewModels.DocumentsTabOptions) {
|
constructor(options: ViewModels.DocumentsTabOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
@@ -317,6 +324,9 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
this.selectedDocumentContent.subscribe((newContent: string) => this._onEditorContentChange(newContent));
|
this.selectedDocumentContent.subscribe((newContent: string) => this._onEditorContentChange(newContent));
|
||||||
|
|
||||||
this.showPartitionKey = this._shouldShowPartitionKey();
|
this.showPartitionKey = this._shouldShowPartitionKey();
|
||||||
|
this._isQueryCopilotSampleContainer =
|
||||||
|
this.collection?.databaseId === QueryCopilotSampleDatabaseId &&
|
||||||
|
this.collection?.id() === QueryCopilotSampleContainerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _shouldShowPartitionKey(): boolean {
|
private _shouldShowPartitionKey(): boolean {
|
||||||
@@ -678,7 +688,6 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public createIterator(): QueryIterator<ItemDefinition & Resource> {
|
public createIterator(): QueryIterator<ItemDefinition & Resource> {
|
||||||
let filters = this.lastFilterContents();
|
|
||||||
const filter: string = this.filterContent().trim();
|
const filter: string = this.filterContent().trim();
|
||||||
const query: string = this.buildQuery(filter);
|
const query: string = this.buildQuery(filter);
|
||||||
let options: any = {};
|
let options: any = {};
|
||||||
@@ -688,12 +697,16 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
options.partitionKey = this._resourceTokenPartitionKey;
|
options.partitionKey = this._resourceTokenPartitionKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
return queryDocuments(this.collection.databaseId, this.collection.id(), query, options);
|
return this._isQueryCopilotSampleContainer
|
||||||
|
? querySampleDocuments(query, options)
|
||||||
|
: queryDocuments(this.collection.databaseId, this.collection.id(), query, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async selectDocument(documentId: DocumentId): Promise<void> {
|
public async selectDocument(documentId: DocumentId): Promise<void> {
|
||||||
this.selectedDocumentId(documentId);
|
this.selectedDocumentId(documentId);
|
||||||
const content = await readDocument(this.collection, documentId);
|
const content = await (this._isQueryCopilotSampleContainer
|
||||||
|
? readSampleDocument(documentId)
|
||||||
|
: readDocument(this.collection, documentId));
|
||||||
this.initDocumentEditor(documentId, content);
|
this.initDocumentEditor(documentId, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -810,7 +823,7 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: !this.newDocumentButton.enabled(),
|
disabled: !this.newDocumentButton.enabled() || useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||||
id: "mongoNewDocumentBtn",
|
id: "mongoNewDocumentBtn",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -824,7 +837,8 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: !this.saveNewDocumentButton.enabled(),
|
disabled:
|
||||||
|
!this.saveNewDocumentButton.enabled() || useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -837,7 +851,9 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: !this.discardNewDocumentChangesButton.enabled(),
|
disabled:
|
||||||
|
!this.discardNewDocumentChangesButton.enabled() ||
|
||||||
|
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -850,7 +866,8 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: !this.saveExistingDocumentButton.enabled(),
|
disabled:
|
||||||
|
!this.saveExistingDocumentButton.enabled() || useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -863,7 +880,9 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: !this.discardExisitingDocumentChangesButton.enabled(),
|
disabled:
|
||||||
|
!this.discardExisitingDocumentChangesButton.enabled() ||
|
||||||
|
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -876,7 +895,9 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: !this.deleteExisitingDocumentButton.enabled(),
|
disabled:
|
||||||
|
!this.deleteExisitingDocumentButton.enabled() ||
|
||||||
|
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -920,7 +941,9 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
disabled: useSelectedNode.getState().isDatabaseNodeOrNoneSelected(),
|
disabled:
|
||||||
|
useSelectedNode.getState().isDatabaseNodeOrNoneSelected() ||
|
||||||
|
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
34
src/Explorer/Tabs/QueryTab/QueryTabComponent.test.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { fireEvent, render } from "@testing-library/react";
|
||||||
|
import QueryTabComponent, { IQueryTabComponentProps } from "Explorer/Tabs/QueryTab/QueryTabComponent";
|
||||||
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
jest.mock("Explorer/Controls/Editor/EditorReact");
|
||||||
|
|
||||||
|
describe("QueryTabComponent", () => {
|
||||||
|
const mockStore = useQueryCopilot.getState();
|
||||||
|
beforeEach(() => {
|
||||||
|
mockStore.showCopilotSidebar = false;
|
||||||
|
mockStore.setShowCopilotSidebar = jest.fn();
|
||||||
|
});
|
||||||
|
beforeEach(() => jest.clearAllMocks());
|
||||||
|
|
||||||
|
it("should launch Copilot when ALT+C is pressed", () => {
|
||||||
|
const propsMock: Readonly<IQueryTabComponentProps> = ({
|
||||||
|
collection: { databaseId: "CopilotSampleDb" },
|
||||||
|
onTabAccessor: () => jest.fn(),
|
||||||
|
isExecutionError: false,
|
||||||
|
tabId: "mockTabId",
|
||||||
|
tabsBaseInstance: {
|
||||||
|
updateNavbarWithTabsButtons: () => jest.fn(),
|
||||||
|
},
|
||||||
|
} as unknown) as IQueryTabComponentProps;
|
||||||
|
|
||||||
|
const { container } = render(<QueryTabComponent {...propsMock} />);
|
||||||
|
|
||||||
|
const launchCopilotButton = container.querySelector(".queryEditorWatermarkText");
|
||||||
|
fireEvent.keyDown(launchCopilotButton, { key: "c", altKey: true });
|
||||||
|
|
||||||
|
expect(mockStore.setShowCopilotSidebar).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
import { FeedOptions } from "@azure/cosmos";
|
import { FeedOptions } from "@azure/cosmos";
|
||||||
|
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
||||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||||
|
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
import SplitterLayout from "react-splitter-layout";
|
import SplitterLayout from "react-splitter-layout";
|
||||||
import "react-splitter-layout/lib/index.css";
|
import "react-splitter-layout/lib/index.css";
|
||||||
|
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
|
||||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||||
import SaveQueryIcon from "../../../../images/save-cosmos.svg";
|
import SaveQueryIcon from "../../../../images/save-cosmos.svg";
|
||||||
import { NormalizedEventKey } from "../../../Common/Constants";
|
import { NormalizedEventKey, QueryCopilotSampleDatabaseId } from "../../../Common/Constants";
|
||||||
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
|
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
|
||||||
import * as HeadersUtility from "../../../Common/HeadersUtility";
|
import * as HeadersUtility from "../../../Common/HeadersUtility";
|
||||||
import { MinimalQueryIterator } from "../../../Common/IteratorUtilities";
|
import { MinimalQueryIterator } from "../../../Common/IteratorUtilities";
|
||||||
@@ -72,15 +75,18 @@ interface IQueryTabStates {
|
|||||||
error: string;
|
error: string;
|
||||||
isExecutionError: boolean;
|
isExecutionError: boolean;
|
||||||
isExecuting: boolean;
|
isExecuting: boolean;
|
||||||
|
showCopilotSidebar: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> {
|
export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> {
|
||||||
public queryEditorId: string;
|
public queryEditorId: string;
|
||||||
public executeQueryButton: Button;
|
public executeQueryButton: Button;
|
||||||
public saveQueryButton: Button;
|
public saveQueryButton: Button;
|
||||||
|
public launchCopilotButton: Button;
|
||||||
public splitterId: string;
|
public splitterId: string;
|
||||||
public isPreferredApiMongoDB: boolean;
|
public isPreferredApiMongoDB: boolean;
|
||||||
public isCloseClicked: boolean;
|
public isCloseClicked: boolean;
|
||||||
|
public isCopilotTabActive: boolean;
|
||||||
private _iterator: MinimalQueryIterator;
|
private _iterator: MinimalQueryIterator;
|
||||||
|
|
||||||
constructor(props: IQueryTabComponentProps) {
|
constructor(props: IQueryTabComponentProps) {
|
||||||
@@ -94,11 +100,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
error: "",
|
error: "",
|
||||||
isExecutionError: this.props.isExecutionError,
|
isExecutionError: this.props.isExecutionError,
|
||||||
isExecuting: false,
|
isExecuting: false,
|
||||||
|
showCopilotSidebar: useQueryCopilot.getState().showCopilotSidebar,
|
||||||
};
|
};
|
||||||
this.isCloseClicked = false;
|
this.isCloseClicked = false;
|
||||||
this.splitterId = this.props.tabId + "_splitter";
|
this.splitterId = this.props.tabId + "_splitter";
|
||||||
this.queryEditorId = `queryeditor${this.props.tabId}`;
|
this.queryEditorId = `queryeditor${this.props.tabId}`;
|
||||||
this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB;
|
this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB;
|
||||||
|
this.isCopilotTabActive = QueryCopilotSampleDatabaseId === this.props.collection.databaseId;
|
||||||
|
|
||||||
this.executeQueryButton = {
|
this.executeQueryButton = {
|
||||||
enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0,
|
enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0,
|
||||||
@@ -111,6 +119,11 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
visible: isSaveQueryBtnEnabled,
|
visible: isSaveQueryBtnEnabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.launchCopilotButton = {
|
||||||
|
enabled: userContext.apiType === "SQL" && true,
|
||||||
|
visible: userContext.apiType === "SQL" && true,
|
||||||
|
};
|
||||||
|
|
||||||
this.props.tabsBaseInstance.updateNavbarWithTabsButtons();
|
this.props.tabsBaseInstance.updateNavbarWithTabsButtons();
|
||||||
props.onTabAccessor({
|
props.onTabAccessor({
|
||||||
onTabClickEvent: this.onTabClick.bind(this),
|
onTabClickEvent: this.onTabClick.bind(this),
|
||||||
@@ -121,6 +134,9 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
|
|
||||||
public onCloseClick(isClicked: boolean): void {
|
public onCloseClick(isClicked: boolean): void {
|
||||||
this.isCloseClicked = isClicked;
|
this.isCloseClicked = isClicked;
|
||||||
|
if (useQueryCopilot.getState().wasCopilotUsed && this.isCopilotTabActive) {
|
||||||
|
useQueryCopilot.getState().resetQueryCopilotStates();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCurrentEditorQuery(): string {
|
public getCurrentEditorQuery(): string {
|
||||||
@@ -146,6 +162,10 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={this.props.collection.container} />);
|
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={this.props.collection.container} />);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public launchQueryCopilotChat = (): void => {
|
||||||
|
useQueryCopilot.getState().setShowCopilotSidebar(!useQueryCopilot.getState().showCopilotSidebar);
|
||||||
|
};
|
||||||
|
|
||||||
public onSavedQueriesClick = (): void => {
|
public onSavedQueriesClick = (): void => {
|
||||||
useSidePanel
|
useSidePanel
|
||||||
.getState()
|
.getState()
|
||||||
@@ -164,6 +184,12 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public handleCopilotKeyDown = (event: KeyboardEvent): void => {
|
||||||
|
if (this.isCopilotTabActive && event.altKey && event.key === "c") {
|
||||||
|
this.launchQueryCopilotChat();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public onToggleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): boolean => {
|
public onToggleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): boolean => {
|
||||||
if (event.key === NormalizedEventKey.LeftArrow) {
|
if (event.key === NormalizedEventKey.LeftArrow) {
|
||||||
this.toggleResult();
|
this.toggleResult();
|
||||||
@@ -269,6 +295,39 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.launchCopilotButton.visible && this.isCopilotTabActive) {
|
||||||
|
const mainButtonLabel = "Launch Copilot";
|
||||||
|
const chatPaneLabel = "Open Copilot in chat pane (ALT+C)";
|
||||||
|
const copilotSettingLabel = "Copilot settings";
|
||||||
|
|
||||||
|
const openCopilotChatButton: CommandButtonComponentProps = {
|
||||||
|
iconAlt: chatPaneLabel,
|
||||||
|
onCommandClick: this.launchQueryCopilotChat,
|
||||||
|
commandButtonLabel: chatPaneLabel,
|
||||||
|
ariaLabel: chatPaneLabel,
|
||||||
|
hasPopup: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const copilotSettingsButton: CommandButtonComponentProps = {
|
||||||
|
iconAlt: copilotSettingLabel,
|
||||||
|
onCommandClick: () => undefined,
|
||||||
|
commandButtonLabel: copilotSettingLabel,
|
||||||
|
ariaLabel: copilotSettingLabel,
|
||||||
|
hasPopup: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const launchCopilotButton = {
|
||||||
|
iconSrc: LaunchCopilot,
|
||||||
|
iconAlt: mainButtonLabel,
|
||||||
|
onCommandClick: this.launchQueryCopilotChat,
|
||||||
|
commandButtonLabel: mainButtonLabel,
|
||||||
|
ariaLabel: mainButtonLabel,
|
||||||
|
hasPopup: false,
|
||||||
|
children: [openCopilotChatButton, copilotSettingsButton],
|
||||||
|
};
|
||||||
|
buttons.push(launchCopilotButton);
|
||||||
|
}
|
||||||
|
|
||||||
return buttons;
|
return buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,11 +365,25 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private unsubscribeCopilotSidebar: () => void;
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
|
this.unsubscribeCopilotSidebar = useQueryCopilot.subscribe((state: QueryCopilotState) => {
|
||||||
|
if (this.state.showCopilotSidebar !== state.showCopilotSidebar) {
|
||||||
|
this.setState({ showCopilotSidebar: state.showCopilotSidebar });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||||
|
document.addEventListener("keydown", this.handleCopilotKeyDown);
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): JSX.Element {
|
componentWillUnmount(): void {
|
||||||
|
this.unsubscribeCopilotSidebar();
|
||||||
|
document.removeEventListener("keydown", this.handleCopilotKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEditorAndQueryResult(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="tab-pane" id={this.props.tabId} role="tabpanel">
|
<div className="tab-pane" id={this.props.tabId} role="tabpanel">
|
||||||
@@ -343,4 +416,20 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render(): JSX.Element {
|
||||||
|
const shouldScaleElements = this.state.showCopilotSidebar && this.isCopilotTabActive;
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", flexDirection: "row", height: "100%" }}>
|
||||||
|
<div style={{ width: shouldScaleElements ? "70%" : "100%", height: "100%" }}>
|
||||||
|
{this.getEditorAndQueryResult()}
|
||||||
|
</div>
|
||||||
|
{shouldScaleElements && (
|
||||||
|
<div style={{ width: "30%", height: "100%" }}>
|
||||||
|
<QueryCopilotSidebar explorer={this.props.collection.container} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
import { Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
||||||
|
import { PoolIdType } from "Common/Constants";
|
||||||
import { configContext } from "ConfigContext";
|
import { configContext } from "ConfigContext";
|
||||||
import { NotebookWorkspaceConnectionInfo, PostgresFirewallRule } from "Contracts/DataModels";
|
import { NotebookWorkspaceConnectionInfo, PostgresFirewallRule } from "Contracts/DataModels";
|
||||||
import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent";
|
import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent";
|
||||||
@@ -51,7 +52,7 @@ export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: Quicks
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
explorer.allocateContainer();
|
explorer.allocateContainer(PoolIdType.DefaultPoolId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { ConnectTab } from "Explorer/Tabs/ConnectTab";
|
|||||||
import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
|
import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
|
||||||
import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
|
import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||||
@@ -68,7 +69,7 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
|||||||
function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?: ReactTabKind }) {
|
function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?: ReactTabKind }) {
|
||||||
const [hovering, setHovering] = useState(false);
|
const [hovering, setHovering] = useState(false);
|
||||||
const focusTab = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
|
const focusTab = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
|
||||||
const tabId = tab ? tab.tabId : "connect";
|
const tabId = tab ? tab.tabId : "";
|
||||||
|
|
||||||
const getReactTabTitle = (): ko.Observable<string> => {
|
const getReactTabTitle = (): ko.Observable<string> => {
|
||||||
if (tabKind === ReactTabKind.QueryCopilot) {
|
if (tabKind === ReactTabKind.QueryCopilot) {
|
||||||
@@ -126,7 +127,12 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="tabNavText">{useObservable(tab?.tabTitle || getReactTabTitle())}</span>
|
<span
|
||||||
|
className="tabNavText"
|
||||||
|
style={active ? { fontWeight: "bolder", borderBottom: "2px solid rgba(0,120,212,1)" } : {}}
|
||||||
|
>
|
||||||
|
{useObservable(tab?.tabTitle || getReactTabTitle())}
|
||||||
|
</span>
|
||||||
{tabKind !== ReactTabKind.Home && (
|
{tabKind !== ReactTabKind.Home && (
|
||||||
<span className="tabIconSection">
|
<span className="tabIconSection">
|
||||||
<CloseButton tab={tab} active={active} hovering={hovering} tabKind={tabKind} />
|
<CloseButton tab={tab} active={active} hovering={hovering} tabKind={tabKind} />
|
||||||
@@ -158,6 +164,7 @@ const CloseButton = ({
|
|||||||
onClick={(event: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
|
onClick={(event: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
tab ? tab.onCloseTabButtonClick() : useTabs.getState().closeReactTab(tabKind);
|
tab ? tab.onCloseTabButtonClick() : useTabs.getState().closeReactTab(tabKind);
|
||||||
|
tabKind === ReactTabKind.QueryCopilot && useQueryCopilot.getState().resetQueryCopilotStates();
|
||||||
}}
|
}}
|
||||||
tabIndex={active ? 0 : undefined}
|
tabIndex={active ? 0 : undefined}
|
||||||
onKeyPress={({ nativeEvent: e }) => tab.onKeyPressClose(undefined, e)}
|
onKeyPress={({ nativeEvent: e }) => tab.onKeyPressClose(undefined, e)}
|
||||||
@@ -251,7 +258,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
|||||||
case ReactTabKind.Quickstart:
|
case ReactTabKind.Quickstart:
|
||||||
return <QuickstartTab explorer={explorer} />;
|
return <QuickstartTab explorer={explorer} />;
|
||||||
case ReactTabKind.QueryCopilot:
|
case ReactTabKind.QueryCopilot:
|
||||||
return <QueryCopilotTab initialInput={useTabs.getState().queryCopilotTabInitialInput} explorer={explorer} />;
|
return <QueryCopilotTab explorer={explorer} />;
|
||||||
default:
|
default:
|
||||||
throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
|
throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { useNotebook } from "Explorer/Notebook/useNotebook";
|
|||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
|
import * as Logger from "../../Common/Logger";
|
||||||
|
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
|
||||||
import { bulkCreateDocument } from "../../Common/dataAccess/bulkCreateDocument";
|
import { bulkCreateDocument } from "../../Common/dataAccess/bulkCreateDocument";
|
||||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||||
import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize";
|
import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize";
|
||||||
@@ -10,19 +13,16 @@ import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer
|
|||||||
import { readStoredProcedures } from "../../Common/dataAccess/readStoredProcedures";
|
import { readStoredProcedures } from "../../Common/dataAccess/readStoredProcedures";
|
||||||
import { readTriggers } from "../../Common/dataAccess/readTriggers";
|
import { readTriggers } from "../../Common/dataAccess/readTriggers";
|
||||||
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
|
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
|
||||||
import * as Logger from "../../Common/Logger";
|
|
||||||
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
|
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { UploadDetailsRecord } from "../../Contracts/ViewModels";
|
import { UploadDetailsRecord } from "../../Contracts/ViewModels";
|
||||||
import { useTabs } from "../../hooks/useTabs";
|
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
|
|
||||||
import { isServerlessAccount } from "../../Utils/CapabilityUtils";
|
import { isServerlessAccount } from "../../Utils/CapabilityUtils";
|
||||||
import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
|
||||||
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient";
|
import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient";
|
||||||
@@ -526,7 +526,7 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
|
|
||||||
public onSchemaAnalyzerClick = async () => {
|
public onSchemaAnalyzerClick = async () => {
|
||||||
if (useNotebook.getState().isPhoenixFeatures) {
|
if (useNotebook.getState().isPhoenixFeatures) {
|
||||||
await this.container.allocateContainer();
|
await this.container.allocateContainer(Constants.PoolIdType.DefaultPoolId);
|
||||||
}
|
}
|
||||||
useSelectedNode.getState().setSelectedNode(this);
|
useSelectedNode.getState().setSelectedNode(this);
|
||||||
this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer);
|
this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer);
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import * as ko from "knockout";
|
|||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { useTabs } from "../../hooks/useTabs";
|
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import DocumentsTab from "../Tabs/DocumentsTab";
|
import DocumentsTab from "../Tabs/DocumentsTab";
|
||||||
import { NewQueryTab } from "../Tabs/QueryTab/QueryTab";
|
import { NewQueryTab } from "../Tabs/QueryTab/QueryTab";
|
||||||
@@ -28,8 +28,9 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
|||||||
public children: ko.ObservableArray<ViewModels.TreeNode>;
|
public children: ko.ObservableArray<ViewModels.TreeNode>;
|
||||||
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
|
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
|
||||||
public isCollectionExpanded: ko.Observable<boolean>;
|
public isCollectionExpanded: ko.Observable<boolean>;
|
||||||
|
public isSampleCollection?: boolean;
|
||||||
|
|
||||||
constructor(container: Explorer, databaseId: string, data: DataModels.Collection) {
|
constructor(container: Explorer, databaseId: string, data: DataModels.Collection, isSampleCollection?: boolean) {
|
||||||
this.nodeKind = "Collection";
|
this.nodeKind = "Collection";
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.databaseId = databaseId;
|
this.databaseId = databaseId;
|
||||||
@@ -42,6 +43,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
|||||||
this.children = ko.observableArray<ViewModels.TreeNode>([]);
|
this.children = ko.observableArray<ViewModels.TreeNode>([]);
|
||||||
this.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
|
this.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
|
||||||
this.isCollectionExpanded = ko.observable<boolean>(true);
|
this.isCollectionExpanded = ko.observable<boolean>(true);
|
||||||
|
this.isSampleCollection = isSampleCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public expandCollection(): void {
|
public expandCollection(): void {
|
||||||
@@ -139,7 +141,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
|||||||
|
|
||||||
documentsTab = new DocumentsTab({
|
documentsTab = new DocumentsTab({
|
||||||
partitionKey: this.partitionKey,
|
partitionKey: this.partitionKey,
|
||||||
resourceTokenPartitionKey: userContext.parsedResourceToken.partitionKey,
|
resourceTokenPartitionKey: userContext.parsedResourceToken?.partitionKey,
|
||||||
documentIds: ko.observableArray<DocumentId>([]),
|
documentIds: ko.observableArray<DocumentId>([]),
|
||||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||||
title: "Items",
|
title: "Items",
|
||||||
|
|||||||
@@ -790,14 +790,10 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
|||||||
{!isNotebookEnabled && isSampleDataEnabled && (
|
{!isNotebookEnabled && isSampleDataEnabled && (
|
||||||
<>
|
<>
|
||||||
<AccordionComponent>
|
<AccordionComponent>
|
||||||
<AccordionItemComponent
|
<AccordionItemComponent title={"MY DATA"} isExpanded={!gitHubNotebooksContentRoot}>
|
||||||
title={"MY DATA"}
|
|
||||||
isExpanded={!gitHubNotebooksContentRoot}
|
|
||||||
styles={{ maxHeight: 230 }}
|
|
||||||
>
|
|
||||||
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||||
</AccordionItemComponent>
|
</AccordionItemComponent>
|
||||||
<AccordionItemComponent title={"SAMPLE DATA"}>
|
<AccordionItemComponent title={"SAMPLE DATA"} containerStyles={{ display: "table" }}>
|
||||||
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
|
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
|
||||||
</AccordionItemComponent>
|
</AccordionItemComponent>
|
||||||
</AccordionComponent>
|
</AccordionComponent>
|
||||||
@@ -808,14 +804,10 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
|||||||
{isNotebookEnabled && isSampleDataEnabled && (
|
{isNotebookEnabled && isSampleDataEnabled && (
|
||||||
<>
|
<>
|
||||||
<AccordionComponent>
|
<AccordionComponent>
|
||||||
<AccordionItemComponent
|
<AccordionItemComponent title={"MY DATA"} isExpanded={!gitHubNotebooksContentRoot}>
|
||||||
title={"MY DATA"}
|
|
||||||
isExpanded={!gitHubNotebooksContentRoot}
|
|
||||||
styles={{ maxHeight: 130 }}
|
|
||||||
>
|
|
||||||
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||||
</AccordionItemComponent>
|
</AccordionItemComponent>
|
||||||
<AccordionItemComponent title={"SAMPLE DATA"}>
|
<AccordionItemComponent title={"SAMPLE DATA"} containerStyles={{ display: "table" }}>
|
||||||
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
|
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
|
||||||
</AccordionItemComponent>
|
</AccordionItemComponent>
|
||||||
<AccordionItemComponent title={"NOTEBOOKS"}>
|
<AccordionItemComponent title={"NOTEBOOKS"}>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdap
|
|||||||
import TabsBase from "Explorer/Tabs/TabsBase";
|
import TabsBase from "Explorer/Tabs/TabsBase";
|
||||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||||
import { useTabs } from "hooks/useTabs";
|
import { useTabs } from "hooks/useTabs";
|
||||||
import React, { useEffect, useState } from "react";
|
import React from "react";
|
||||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
@@ -14,51 +14,65 @@ export const SampleDataTree = ({
|
|||||||
}: {
|
}: {
|
||||||
sampleDataResourceTokenCollection: ViewModels.CollectionBase;
|
sampleDataResourceTokenCollection: ViewModels.CollectionBase;
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const [root, setRoot] = useState<TreeNode | undefined>(undefined);
|
const buildSampleDataTree = (): TreeNode => {
|
||||||
|
const updatedSampleTree: TreeNode = {
|
||||||
useEffect(() => {
|
label: sampleDataResourceTokenCollection.databaseId,
|
||||||
if (sampleDataResourceTokenCollection) {
|
isExpanded: false,
|
||||||
const updatedSampleTree: TreeNode = {
|
iconSrc: CosmosDBIcon,
|
||||||
label: sampleDataResourceTokenCollection.databaseId,
|
className: "databaseHeader",
|
||||||
isExpanded: false,
|
children: [
|
||||||
iconSrc: CosmosDBIcon,
|
{
|
||||||
className: "databaseHeader",
|
label: sampleDataResourceTokenCollection.id(),
|
||||||
children: [
|
iconSrc: CollectionIcon,
|
||||||
{
|
isExpanded: false,
|
||||||
label: sampleDataResourceTokenCollection.id(),
|
className: "collectionHeader",
|
||||||
iconSrc: CollectionIcon,
|
contextMenu: ResourceTreeContextMenuButtonFactory.createSampleCollectionContextMenuButton(),
|
||||||
isExpanded: false,
|
onClick: () => {
|
||||||
className: "dataResourceTree",
|
useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection);
|
||||||
contextMenu: ResourceTreeContextMenuButtonFactory.createSampleCollectionContextMenuButton(),
|
useCommandBar.getState().setContextButtons([]);
|
||||||
onClick: () => {
|
useTabs
|
||||||
// Rewritten version of expandCollapseCollection
|
.getState()
|
||||||
useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection);
|
.refreshActiveTab(
|
||||||
useCommandBar.getState().setContextButtons([]);
|
|
||||||
useTabs().refreshActiveTab(
|
|
||||||
(tab: TabsBase) =>
|
(tab: TabsBase) =>
|
||||||
tab.collection?.id() === sampleDataResourceTokenCollection.id() &&
|
tab.collection?.id() === sampleDataResourceTokenCollection.id() &&
|
||||||
tab.collection.databaseId === sampleDataResourceTokenCollection.databaseId
|
tab.collection.databaseId === sampleDataResourceTokenCollection.databaseId
|
||||||
);
|
);
|
||||||
},
|
|
||||||
isSelected: () =>
|
|
||||||
useSelectedNode
|
|
||||||
.getState()
|
|
||||||
.isDataNodeSelected(
|
|
||||||
sampleDataResourceTokenCollection.databaseId,
|
|
||||||
sampleDataResourceTokenCollection.id()
|
|
||||||
),
|
|
||||||
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
label: "Items",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
isSelected: () =>
|
||||||
};
|
useSelectedNode
|
||||||
setRoot(updatedSampleTree);
|
.getState()
|
||||||
}
|
.isDataNodeSelected(sampleDataResourceTokenCollection.databaseId, sampleDataResourceTokenCollection.id()),
|
||||||
}, [sampleDataResourceTokenCollection]);
|
onContextMenuOpen: () => useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: "Items",
|
||||||
|
onClick: () => sampleDataResourceTokenCollection.onDocumentDBDocumentsClick(),
|
||||||
|
contextMenu: ResourceTreeContextMenuButtonFactory.createSampleCollectionContextMenuButton(),
|
||||||
|
isSelected: () =>
|
||||||
|
useSelectedNode
|
||||||
|
.getState()
|
||||||
|
.isDataNodeSelected(
|
||||||
|
sampleDataResourceTokenCollection.databaseId,
|
||||||
|
sampleDataResourceTokenCollection.id(),
|
||||||
|
[ViewModels.CollectionTabKind.Documents]
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
return <TreeComponent className="dataResourceTree" rootNode={root || { label: "Sample data not initialized." }} />;
|
return {
|
||||||
|
label: undefined,
|
||||||
|
isExpanded: true,
|
||||||
|
children: [updatedSampleTree],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TreeComponent
|
||||||
|
className="dataResourceTree"
|
||||||
|
rootNode={sampleDataResourceTokenCollection ? buildSampleDataTree() : { label: "Sample data not initialized." }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -66,11 +66,12 @@ export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) =>
|
|||||||
return useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected;
|
return useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected;
|
||||||
},
|
},
|
||||||
isQueryCopilotCollectionSelected: (): boolean => {
|
isQueryCopilotCollectionSelected: (): boolean => {
|
||||||
const selectedNode = get().selectedNode;
|
const selectedNode = get().selectedNode as ViewModels.CollectionBase;
|
||||||
if (
|
if (
|
||||||
selectedNode &&
|
selectedNode &&
|
||||||
|
selectedNode.isSampleCollection &&
|
||||||
selectedNode.id() === QueryCopilotSampleContainerId &&
|
selectedNode.id() === QueryCopilotSampleContainerId &&
|
||||||
(selectedNode as ViewModels.Collection)?.databaseId === QueryCopilotSampleDatabaseId
|
selectedNode.databaseId === QueryCopilotSampleDatabaseId
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"Save": "Save",
|
"Save": "Save",
|
||||||
"Discard": "Discard",
|
"Discard": "Discard",
|
||||||
"Refresh": "Refesh"
|
"Refresh": "Refresh"
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ import "../externals/jquery.typeahead.min.css";
|
|||||||
import "../externals/jquery.typeahead.min.js";
|
import "../externals/jquery.typeahead.min.js";
|
||||||
// Image Dependencies
|
// Image Dependencies
|
||||||
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
import { QueryCopilotCarousel } from "Explorer/QueryCopilot/CopilotCarousel";
|
||||||
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/QueryCopilotFeedbackModal";
|
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
import "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
||||||
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
||||||
@@ -83,7 +83,7 @@ const App: React.FunctionComponent = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flexContainer">
|
<div className="flexContainer" aria-hidden="false">
|
||||||
<div id="divExplorer" className="flexContainer hideOverflows">
|
<div id="divExplorer" className="flexContainer hideOverflows">
|
||||||
<div id="freeTierTeachingBubble"> </div>
|
<div id="freeTierTeachingBubble"> </div>
|
||||||
{/* Main Command Bar - Start */}
|
{/* Main Command Bar - Start */}
|
||||||
@@ -128,7 +128,7 @@ const App: React.FunctionComponent = () => {
|
|||||||
{<SQLQuickstartTutorial />}
|
{<SQLQuickstartTutorial />}
|
||||||
{<MongoQuickstartTutorial />}
|
{<MongoQuickstartTutorial />}
|
||||||
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
|
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
|
||||||
{shouldShowModal && <QueryCopilotFeedbackModal />}
|
{shouldShowModal && <QueryCopilotFeedbackModal explorer={explorer} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { configContext } from "ConfigContext";
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
import promiseRetry, { AbortError } from "p-retry";
|
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
|
import { userContext } from "UserContext";
|
||||||
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation";
|
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation";
|
||||||
|
import promiseRetry, { AbortError } from "p-retry";
|
||||||
import {
|
import {
|
||||||
Areas,
|
Areas,
|
||||||
ConnectionStatusType,
|
ConnectionStatusType,
|
||||||
@@ -12,7 +14,6 @@ import {
|
|||||||
} from "../Common/Constants";
|
} from "../Common/Constants";
|
||||||
import { getErrorMessage, getErrorStack } from "../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../Common/ErrorHandlingUtils";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { configContext } from "../ConfigContext";
|
|
||||||
import {
|
import {
|
||||||
ContainerConnectionInfo,
|
ContainerConnectionInfo,
|
||||||
ContainerInfo,
|
ContainerInfo,
|
||||||
@@ -28,7 +29,6 @@ import {
|
|||||||
} from "../Contracts/DataModels";
|
} from "../Contracts/DataModels";
|
||||||
import { useNotebook } from "../Explorer/Notebook/useNotebook";
|
import { useNotebook } from "../Explorer/Notebook/useNotebook";
|
||||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../UserContext";
|
|
||||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||||
|
|
||||||
export class PhoenixClient {
|
export class PhoenixClient {
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ export type Features = {
|
|||||||
readonly enableLegacyMongoShellV2Debug: boolean;
|
readonly enableLegacyMongoShellV2Debug: boolean;
|
||||||
readonly loadLegacyMongoShellFromBE: boolean;
|
readonly loadLegacyMongoShellFromBE: boolean;
|
||||||
readonly enableCopilot: boolean;
|
readonly enableCopilot: boolean;
|
||||||
|
readonly enablePriorityBasedThrottling: boolean;
|
||||||
|
readonly copilotVersion?: string;
|
||||||
|
readonly enableCopilotPhoenixGateaway: boolean;
|
||||||
|
readonly enableCopilotFullSchema: boolean;
|
||||||
|
|
||||||
// can be set via both flight and feature flag
|
// can be set via both flight and feature flag
|
||||||
autoscaleDefault: boolean;
|
autoscaleDefault: boolean;
|
||||||
@@ -103,7 +107,11 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
enableLegacyMongoShellV2: "true" === get("enablelegacymongoshellv2"),
|
enableLegacyMongoShellV2: "true" === get("enablelegacymongoshellv2"),
|
||||||
enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"),
|
enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"),
|
||||||
loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"),
|
loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"),
|
||||||
|
enablePriorityBasedThrottling: "true" === get("enableprioritybasedthrottling"),
|
||||||
enableCopilot: "true" === get("enablecopilot"),
|
enableCopilot: "true" === get("enablecopilot"),
|
||||||
|
copilotVersion: get("copilotversion") ?? "v1.0",
|
||||||
|
enableCopilotPhoenixGateaway: "true" === get("enablecopilotphoenixgateaway"),
|
||||||
|
enableCopilotFullSchema: "true" === get("enablecopilotfullschema"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const createDefaultSettings = () => {
|
|||||||
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, Constants.Queries.itemsPerPage);
|
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, Constants.Queries.itemsPerPage);
|
||||||
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, "true");
|
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, "true");
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, Constants.Queries.DefaultMaxDegreeOfParallelism);
|
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, Constants.Queries.DefaultMaxDegreeOfParallelism);
|
||||||
|
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, Constants.PriorityLevel.Default);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hasSettingsDefined = (): boolean => {
|
export const hasSettingsDefined = (): boolean => {
|
||||||
@@ -15,3 +16,9 @@ export const hasSettingsDefined = (): boolean => {
|
|||||||
LocalStorageUtility.hasItem(StorageKey.MaxDegreeOfParellism)
|
LocalStorageUtility.hasItem(StorageKey.MaxDegreeOfParellism)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ensurePriorityLevel = () => {
|
||||||
|
if (!LocalStorageUtility.hasItem(StorageKey.PriorityLevel)) {
|
||||||
|
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, Constants.PriorityLevel.Default);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||