mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 10:51:30 +00:00
Compare commits
45 Commits
users/v-da
...
switch_to_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91c9c8fee7 | ||
|
|
5c21e9d5ee | ||
|
|
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 | ||
|
|
cf0c51337f | ||
|
|
1d6c0bbd1e | ||
|
|
ed9ee07e81 | ||
|
|
e9ea887fe7 | ||
|
|
b6d576b7b6 | ||
|
|
0eaa5d004b | ||
|
|
3bef1ed136 | ||
|
|
eb21193ae4 | ||
|
|
90178178c4 | ||
|
|
ebfc9d4b36 | ||
|
|
4742ee0e7b | ||
|
|
b98829109e | ||
|
|
ea4563f840 | ||
|
|
3a961870d9 |
3
images/CopilotDatabase.svg
Normal file
3
images/CopilotDatabase.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="10" height="14" viewBox="0 0 10 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 2.5C10 3.88071 7.76142 5 5 5C2.23858 5 0 3.88071 0 2.5C0 1.11929 2.23858 0 5 0C7.76142 0 10 1.11929 10 2.5ZM0 11.5V4.487C1.057 5.413 2.864 6 5 6C7.136 6 8.943 5.413 10 4.487V11.5C10 12.925 7.851 14 5 14C2.149 14 0 12.925 0 11.5Z" fill="#0078D4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 363 B |
3
images/CopilotFlash.svg
Normal file
3
images/CopilotFlash.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="13" height="17" viewBox="0 0 13 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.21207 0C3.73772 0 3.32085 0.314446 3.19054 0.770537L0.940937 8.64415C0.747029 9.32283 1.25663 9.99841 1.96246 9.99841H3.22974L2.06002 14.6773C1.79611 15.7329 3.10089 16.4551 3.85526 15.6726L12.5318 6.81506L12.5354 6.81137C13.1762 6.1436 12.7152 5 11.7688 5H9.20509L10.4665 1.40582L10.469 1.39836C10.6983 0.710426 10.1863 0 9.46114 0H4.21207Z" fill="#0078D4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 475 B |
3
images/CopilotThumb.svg
Normal file
3
images/CopilotThumb.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="14" viewBox="0 0 12 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.5806 0.051598C6.83006 -0.157 6.24411 0.402481 6.03494 0.923322C5.79411 1.52303 5.58243 1.94395 5.32941 2.44707C5.17287 2.75833 5.00052 3.10106 4.79565 3.53704C4.32141 4.54625 3.84755 5.19347 3.5035 5.58154C3.33128 5.77579 3.19093 5.90585 3.09835 5.98435C3.05204 6.02362 3.01761 6.05005 2.99704 6.0652L2.9809 6.07684L1.109 7.18119C0.272443 7.67473 -0.0885815 8.69797 0.253045 9.6072L0.773038 10.9912C0.989444 11.5671 1.45891 12.0114 2.04591 12.1958L7.40179 13.8781C8.73654 14.2974 10.1555 13.5397 10.5497 12.1973L11.9139 7.55127C12.29 6.2705 11.3298 4.9878 9.99492 4.9878H8.60995C8.67586 4.76117 8.74339 4.50906 8.80466 4.24751C8.93612 3.68641 9.04781 3.04484 9.03753 2.51008C9.02797 2.01293 8.97781 1.49126 8.77353 1.04807C8.55437 0.572583 8.1709 0.21566 7.5806 0.051598ZM2.9768 6.07969L2.97492 6.08097L2.9768 6.07969Z" fill="#0078D4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 952 B |
29
images/CopliotWelcomeIllustration.svg
Normal file
29
images/CopliotWelcomeIllustration.svg
Normal file
@@ -0,0 +1,29 @@
|
||||
<svg width="287" height="225" viewBox="0 0 287 225" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.3" d="M7.75456 192.647H231.829C233.524 192.647 235.156 191.988 236.381 190.764C237.605 189.539 238.265 187.907 238.265 186.211V33.061C238.265 29.5132 234.56 26.248 229.568 26.248L10.8314 29.576C9.98368 29.576 9.16738 29.733 8.38248 30.047C7.59758 30.3923 6.90687 30.8319 6.31034 31.4598C5.71381 32.0563 5.24287 32.747 4.89751 33.5319C4.55216 34.3168 4.42658 35.1645 4.42658 35.9808L1.31836 186.274C1.31836 187.969 1.97767 189.602 3.20212 190.795C4.42657 191.988 6.05917 192.647 7.75456 192.647Z" fill="#1F1D20"/>
|
||||
<path d="M231.264 23.3906H6.37342C2.85705 23.3906 0 26.2477 0 29.764V183.228C0 186.745 2.85705 189.602 6.37342 189.602H231.295C234.811 189.602 237.669 186.745 237.669 183.228V29.764C237.637 26.2477 234.78 23.3906 231.264 23.3906Z" fill="url(#paint0_linear_1151_179507)"/>
|
||||
<path d="M228.313 32.7148H119.62V178.707H228.313V32.7148Z" fill="white"/>
|
||||
<path d="M121.158 32.7148H12.4648V178.707H121.158V32.7148Z" fill="#CDCDD0"/>
|
||||
<path d="M34.1909 0V144.799C34.1909 144.799 63.2009 141.91 86.7166 153.715C110.264 165.52 121.158 178.581 121.158 178.581V33.7508C121.158 33.7508 107.595 16.483 86.7166 10.1095C69.7313 4.8978 34.1909 0 34.1909 0Z" fill="#EAEAEA"/>
|
||||
<path d="M141.094 102.383L134.249 99.4006L127.374 102.383V23.3906H141.094V102.383Z" fill="url(#paint1_linear_1151_179507)"/>
|
||||
<path opacity="0.15" d="M206.9 61.2871H159.304C157.231 61.2871 155.567 62.9825 155.567 65.0233C155.567 67.0954 157.263 68.7594 159.304 68.7594H206.9C208.972 68.7594 210.636 67.064 210.636 65.0233C210.668 62.9511 208.972 61.2871 206.9 61.2871Z" fill="#1F1D20"/>
|
||||
<path opacity="0.15" d="M206.9 77.5498H159.304C157.231 77.5498 155.567 79.2452 155.567 81.2859C155.567 83.3581 157.263 85.0221 159.304 85.0221H206.9C208.972 85.0221 210.636 83.3267 210.636 81.2859C210.668 79.2138 208.972 77.5498 206.9 77.5498Z" fill="#1F1D20"/>
|
||||
<path opacity="0.15" fill-rule="evenodd" clip-rule="evenodd" d="M159.304 93.8428H206.838C208.91 93.8428 210.574 95.5381 210.574 97.5789C210.574 99.651 208.878 101.315 206.838 101.315H159.304C157.232 101.315 155.568 99.6196 155.568 97.5789C155.536 95.5381 157.2 93.8428 159.304 93.8428Z" fill="#1F1D20"/>
|
||||
<path d="M206.9 60.0303H159.304C157.231 60.0303 155.567 61.7257 155.567 63.7664C155.567 65.8385 157.263 67.5025 159.304 67.5025H206.9C208.972 67.5025 210.636 65.8072 210.636 63.7664C210.668 61.6943 208.972 60.0303 206.9 60.0303Z" fill="#50E6FF"/>
|
||||
<path d="M206.9 76.3262H159.304C157.231 76.3262 155.567 78.0216 155.567 80.0623C155.567 82.1345 157.263 83.7984 159.304 83.7984H206.9C208.972 83.7984 210.636 82.1031 210.636 80.0623C210.668 77.9902 208.972 76.3262 206.9 76.3262Z" fill="#32B0E7"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M159.304 92.5889H206.838C208.91 92.5889 210.574 94.2843 210.574 96.325C210.574 98.3972 208.878 100.061 206.838 100.061H159.304C157.232 100.093 155.568 98.3658 155.568 96.325C155.536 94.2843 157.2 92.5889 159.304 92.5889Z" fill="#185A97"/>
|
||||
<path opacity="0.2" d="M287 143.834H192V205.096C192 207.687 194.089 209.776 196.68 209.776H206.648V224.265L221.243 209.776H282.32C284.911 209.776 287 207.687 287 205.096V143.834Z" fill="#1F1D21"/>
|
||||
<path d="M283.827 140H195.173C193.428 140 192 141.428 192 143.173V202.769C192 204.514 193.428 205.942 195.173 205.942H206.648V220.431L221.243 205.942H283.827C285.572 205.942 287 204.514 287 202.769V143.173C287 141.428 285.572 140 283.827 140Z" fill="#49C8EF"/>
|
||||
<path d="M254.24 161.762H224.706C223.543 161.762 222.591 162.714 222.591 163.877C222.591 165.04 223.543 165.992 224.706 165.992H254.24C255.403 165.992 256.355 165.04 256.355 163.877C256.355 162.714 255.403 161.762 254.24 161.762Z" fill="#C3F1FF"/>
|
||||
<path d="M254.24 171.042H224.706C223.543 171.042 222.591 171.994 222.591 173.157C222.591 174.321 223.543 175.272 224.706 175.272H254.24C255.403 175.272 256.355 174.321 256.355 173.157C256.355 172.02 255.403 171.042 254.24 171.042Z" fill="#C3F1FF"/>
|
||||
<path d="M254.24 180.373H224.706C223.543 180.373 222.591 181.325 222.591 182.488C222.591 183.652 223.543 184.604 224.706 184.604H254.24C255.403 184.604 256.355 183.652 256.355 182.488C256.355 181.298 255.403 180.373 254.24 180.373Z" fill="#C3F1FF"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1151_179507" x1="118.845" y1="34.0854" x2="118.845" y2="202.888" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#007ED8"/>
|
||||
<stop offset="0.7065" stop-color="#002D4C"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1151_179507" x1="87.2694" y1="-23.1274" x2="250.063" y2="275.059" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#007ED8"/>
|
||||
<stop offset="0.7065" stop-color="#002D4C"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
3
images/X-errorMessage.svg
Normal file
3
images/X-errorMessage.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 0C8.73438 0 9.44271 0.0963542 10.125 0.289062C10.8073 0.476562 11.4427 0.744792 12.0312 1.09375C12.625 1.44271 13.1641 1.86198 13.6484 2.35156C14.138 2.83594 14.5573 3.375 14.9062 3.96875C15.2552 4.55729 15.5234 5.19271 15.7109 5.875C15.9036 6.55729 16 7.26562 16 8C16 8.73438 15.9036 9.44271 15.7109 10.125C15.5234 10.8073 15.2552 11.4453 14.9062 12.0391C14.5573 12.6276 14.138 13.1667 13.6484 13.6562C13.1641 14.1406 12.625 14.5573 12.0312 14.9062C11.4427 15.2552 10.8073 15.526 10.125 15.7188C9.44271 15.9062 8.73438 16 8 16C7.26562 16 6.55729 15.9062 5.875 15.7188C5.19271 15.526 4.55469 15.2552 3.96094 14.9062C3.3724 14.5573 2.83333 14.1406 2.34375 13.6562C1.85938 13.1667 1.44271 12.6276 1.09375 12.0391C0.744792 11.4453 0.473958 10.8073 0.28125 10.125C0.09375 9.44271 0 8.73438 0 8C0 7.26562 0.09375 6.55729 0.28125 5.875C0.473958 5.19271 0.744792 4.55729 1.09375 3.96875C1.44271 3.375 1.85938 2.83594 2.34375 2.35156C2.83333 1.86198 3.3724 1.44271 3.96094 1.09375C4.55469 0.744792 5.19271 0.476562 5.875 0.289062C6.55729 0.0963542 7.26562 0 8 0ZM8 15C8.64583 15 9.26562 14.9167 9.85938 14.75C10.4583 14.5833 11.0156 14.349 11.5312 14.0469C12.0521 13.7396 12.5234 13.375 12.9453 12.9531C13.3724 12.526 13.737 12.0547 14.0391 11.5391C14.3464 11.0182 14.5833 10.4609 14.75 9.86719C14.9167 9.26823 15 8.64583 15 8C15 7.35417 14.9167 6.73438 14.75 6.14062C14.5833 5.54167 14.3464 4.98438 14.0391 4.46875C13.737 3.94792 13.3724 3.47656 12.9453 3.05469C12.5234 2.6276 12.0521 2.26302 11.5312 1.96094C11.0156 1.65365 10.4583 1.41667 9.85938 1.25C9.26562 1.08333 8.64583 1 8 1C7.35417 1 6.73177 1.08333 6.13281 1.25C5.53906 1.41667 4.98177 1.65365 4.46094 1.96094C3.94531 2.26302 3.47396 2.6276 3.04688 3.05469C2.625 3.47656 2.26042 3.94792 1.95312 4.46875C1.65104 4.98438 1.41667 5.54167 1.25 6.14062C1.08333 6.73438 1 7.35417 1 8C1 8.64583 1.08333 9.26823 1.25 9.86719C1.41667 10.4609 1.65104 11.0182 1.95312 11.5391C2.26042 12.0547 2.625 12.526 3.04688 12.9531C3.47396 13.375 3.94531 13.7396 4.46094 14.0469C4.98177 14.349 5.53906 14.5833 6.13281 14.75C6.73177 14.9167 7.35417 15 8 15ZM11.4609 5.24219L8.71094 8L11.4609 10.7578L10.7578 11.4609L8 8.71094L5.24219 11.4609L4.53906 10.7578L7.28906 8L4.53906 5.24219L5.24219 4.53906L8 7.28906L10.7578 4.53906L11.4609 5.24219Z" fill="#E00B1C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -230,7 +230,7 @@ input::-webkit-inner-spin-button {
|
||||
.advanced-options-panel .advanced-options .select .select-options-link {
|
||||
margin-left: 4px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.query-panel .row .column-headers .Field {
|
||||
|
||||
@@ -28,4 +28,4 @@
|
||||
.clickDisabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -164,6 +164,7 @@
|
||||
"jest": "26.6.3",
|
||||
"jest-canvas-mock": "2.3.1",
|
||||
"jest-playwright-preset": "1.5.1",
|
||||
"jest-react-hooks-shallow": "1.5.1",
|
||||
"jest-trx-results-processor": "0.0.7",
|
||||
"less": "3.8.1",
|
||||
"less-loader": "4.1.0",
|
||||
@@ -19578,6 +19579,16 @@
|
||||
"node": ">=8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-react-hooks-shallow": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://msazure.pkgs.visualstudio.com/_packaging/AzurePortal/npm/registry/jest-react-hooks-shallow/-/jest-react-hooks-shallow-1.5.1.tgz",
|
||||
"integrity": "sha1-1SI4EGG7nf9WcfhLBT0LPxy+N/k=",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"react": "^16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-regex-util": {
|
||||
"version": "24.9.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz",
|
||||
@@ -48608,6 +48619,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"jest-react-hooks-shallow": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://msazure.pkgs.visualstudio.com/_packaging/AzurePortal/npm/registry/jest-react-hooks-shallow/-/jest-react-hooks-shallow-1.5.1.tgz",
|
||||
"integrity": "sha1-1SI4EGG7nf9WcfhLBT0LPxy+N/k=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"react": "^16.8.0"
|
||||
}
|
||||
},
|
||||
"jest-regex-util": {
|
||||
"version": "24.9.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz",
|
||||
|
||||
@@ -160,6 +160,7 @@
|
||||
"jest": "26.6.3",
|
||||
"jest-canvas-mock": "2.3.1",
|
||||
"jest-playwright-preset": "1.5.1",
|
||||
"jest-react-hooks-shallow": "1.5.1",
|
||||
"jest-trx-results-processor": "0.0.7",
|
||||
"less": "3.8.1",
|
||||
"less-loader": "4.1.0",
|
||||
@@ -232,4 +233,4 @@
|
||||
"prettier": {
|
||||
"printWidth": 120
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,6 +358,7 @@ export enum ContainerStatusType {
|
||||
|
||||
export enum PoolIdType {
|
||||
DefaultPoolId = "default",
|
||||
QueryCopilot = "query-copilot",
|
||||
}
|
||||
|
||||
export const EmulatorMasterKey =
|
||||
@@ -435,24 +436,89 @@ export const QueryCopilotSampleContainerId = "SampleContainer";
|
||||
export const QueryCopilotSampleContainerSchema = {
|
||||
product: {
|
||||
sampleData: {
|
||||
id: "de6fadec-0384-43c8-93ea-16c0170b5460",
|
||||
name: "Premium Phone Mini (Red)",
|
||||
price: 652.04,
|
||||
id: "c415e70f-9bf5-4cda-aebe-a290cb8b94c2",
|
||||
name: "Amazing Phone 3000 (Black)",
|
||||
price: 223.33,
|
||||
category: "Electronics",
|
||||
description:
|
||||
"This Premium Phone Mini (Red) is designed by the company under agreement with the FCC, so we'd give it a pass in either direction, but no one should be using this handset without a compatible handset. All in all, this phone is one of the most affordable Android handsets out there at $100. Check them out.\n\n9. HTC One M9 4",
|
||||
stock: 74,
|
||||
countryOfOrigin: "Mexico",
|
||||
firstAvailable: "2018-07-07 17:42:28",
|
||||
priceHistory: [592.81],
|
||||
"This Amazing Phone 3000 (Black) is made of black metal! It has a very well made aluminum body and it feels very comfortable. We loved the sound that comes out of it! Also, the design of the phone was a little loose at first because I was using the camera and felt uncomfortable wearing it. The phone is actually made slightly smaller than these photos! This is due to the addition of a 3.3mm filter",
|
||||
stock: 84,
|
||||
countryOfOrigin: "USA",
|
||||
firstAvailable: "2018-09-07 19:41:44",
|
||||
priceHistory: [238.68, 234.7, 221.49, 205.88, 220.15],
|
||||
customerRatings: [
|
||||
{ username: "dannyhowell", stars: 1, date: "2022-03-12 17:01:23", verifiedUser: true },
|
||||
{ username: "lindsay26", stars: 1, date: "2022-12-29 07:18:20", verifiedUser: false },
|
||||
{ username: "smithmiguel", stars: 3, date: "2022-09-08 11:43:27", verifiedUser: false },
|
||||
{ username: "julie07", stars: 3, date: "2021-03-14 23:54:10", verifiedUser: true },
|
||||
{ username: "kelly93", stars: 3, date: "2022-11-05 12:45:43", verifiedUser: false },
|
||||
{ username: "katherinereynolds", stars: 2, date: "2022-09-14 11:49:36", verifiedUser: false },
|
||||
{ username: "chandlerkenneth", stars: 1, date: "2022-01-14 12:14:43", verifiedUser: true },
|
||||
{
|
||||
username: "steven66",
|
||||
firstName: "Carol",
|
||||
gender: "female",
|
||||
lastName: "Shelton",
|
||||
age: "25-35",
|
||||
area: "suburban",
|
||||
address: "261 Collins Burgs Apt. 332\nNorth Taylor, NM 32268",
|
||||
stars: 5,
|
||||
date: "2021-04-22 13:42:14",
|
||||
verifiedUser: true,
|
||||
},
|
||||
{
|
||||
username: "khudson",
|
||||
firstName: "Ronald",
|
||||
gender: "male",
|
||||
lastName: "Webb",
|
||||
age: "18-24",
|
||||
area: "suburban",
|
||||
address: "9912 Parker Court Apt. 068\nNorth Austin, HI 76225",
|
||||
stars: 5,
|
||||
date: "2021-02-07 07:00:22",
|
||||
verifiedUser: false,
|
||||
},
|
||||
{
|
||||
username: "lfrancis",
|
||||
firstName: "Brady",
|
||||
gender: "male",
|
||||
lastName: "Wright",
|
||||
age: "35-45",
|
||||
area: "urban",
|
||||
address: "PSC 5437, Box 3159\nAPO AA 26385",
|
||||
stars: 2,
|
||||
date: "2022-02-23 21:40:10",
|
||||
verifiedUser: false,
|
||||
},
|
||||
{
|
||||
username: "nicolemartinez",
|
||||
firstName: "Megan",
|
||||
gender: "female",
|
||||
lastName: "Tran",
|
||||
age: "18-24",
|
||||
area: "rural",
|
||||
address: "7445 Salazar Brooks\nNew Sarah, PW 18097",
|
||||
stars: 4,
|
||||
date: "2021-09-01 22:21:40",
|
||||
verifiedUser: false,
|
||||
},
|
||||
{
|
||||
username: "uguzman",
|
||||
firstName: "Deanna",
|
||||
gender: "female",
|
||||
lastName: "Campbell",
|
||||
age: "18-24",
|
||||
area: "urban",
|
||||
address: "41104 Moreno Fort Suite 872\nPort Michaelbury, AK 48712",
|
||||
stars: 1,
|
||||
date: "2022-03-07 02:23:14",
|
||||
verifiedUser: false,
|
||||
},
|
||||
{
|
||||
username: "rebeccahunt",
|
||||
firstName: "Jared",
|
||||
gender: "male",
|
||||
lastName: "Lopez",
|
||||
age: "18-24",
|
||||
area: "rural",
|
||||
address: "392 Morgan Village Apt. 785\nGreenshire, CT 05921",
|
||||
stars: 5,
|
||||
date: "2021-04-17 04:17:49",
|
||||
verifiedUser: false,
|
||||
},
|
||||
],
|
||||
rareProperty: true,
|
||||
},
|
||||
@@ -494,6 +560,24 @@ export const QueryCopilotSampleContainerSchema = {
|
||||
username: {
|
||||
type: "string",
|
||||
},
|
||||
firstName: {
|
||||
type: "string",
|
||||
},
|
||||
gender: {
|
||||
type: "string",
|
||||
},
|
||||
lastName: {
|
||||
type: "string",
|
||||
},
|
||||
age: {
|
||||
type: "string",
|
||||
},
|
||||
area: {
|
||||
type: "string",
|
||||
},
|
||||
address: {
|
||||
type: "string",
|
||||
},
|
||||
stars: {
|
||||
type: "number",
|
||||
},
|
||||
|
||||
@@ -137,7 +137,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
||||
/>
|
||||
{!isEntityValueDisable && (
|
||||
<TooltipHost content="Edit property" id="editTooltip">
|
||||
<div tabIndex={0}>
|
||||
<div>
|
||||
<Image
|
||||
{...imageProps}
|
||||
src={EditIcon}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import {
|
||||
allowedAadEndpoints,
|
||||
allowedArcadiaEndpoints,
|
||||
allowedArmEndpoints,
|
||||
allowedBackendEndpoints,
|
||||
allowedEmulatorEndpoints,
|
||||
allowedGraphEndpoints,
|
||||
allowedHostedExplorerEndpoints,
|
||||
allowedJunoOrigins,
|
||||
allowedMongoBackendEndpoints,
|
||||
allowedMsalRedirectEndpoints,
|
||||
defaultAllowedArmEndpoints,
|
||||
defaultAllowedBackendEndpoints,
|
||||
validateEndpoint,
|
||||
} from "Utils/EndpointValidation";
|
||||
|
||||
@@ -20,6 +20,8 @@ export enum Platform {
|
||||
|
||||
export interface ConfigContext {
|
||||
platform: Platform;
|
||||
allowedArmEndpoints: ReadonlyArray<string>;
|
||||
allowedBackendEndpoints: ReadonlyArray<string>;
|
||||
allowedParentFrameOrigins: ReadonlyArray<string>;
|
||||
gitSha?: string;
|
||||
proxyPath?: string;
|
||||
@@ -49,6 +51,8 @@ export interface ConfigContext {
|
||||
// Default configuration
|
||||
let configContext: Readonly<ConfigContext> = {
|
||||
platform: Platform.Portal,
|
||||
allowedArmEndpoints: defaultAllowedArmEndpoints,
|
||||
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
|
||||
allowedParentFrameOrigins: [
|
||||
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
|
||||
@@ -77,7 +81,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||
|
||||
export function resetConfigContext(): void {
|
||||
if (process.env.NODE_ENV !== "test") {
|
||||
throw new Error("resetConfigContext can only becalled in a test environment");
|
||||
throw new Error("resetConfigContext can only be called in a test environment");
|
||||
}
|
||||
configContext = {} as ConfigContext;
|
||||
}
|
||||
@@ -87,7 +91,7 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.ARM_ENDPOINT, allowedArmEndpoints)) {
|
||||
if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) {
|
||||
delete newContext.ARM_ENDPOINT;
|
||||
}
|
||||
|
||||
@@ -107,7 +111,12 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
delete newContext.ARCADIA_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.BACKEND_ENDPOINT, allowedBackendEndpoints)) {
|
||||
if (
|
||||
!validateEndpoint(
|
||||
newContext.BACKEND_ENDPOINT,
|
||||
configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints
|
||||
)
|
||||
) {
|
||||
delete newContext.BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
@@ -130,7 +139,7 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
Object.assign(configContext, newContext);
|
||||
}
|
||||
|
||||
// Injected for local develpment. These will be removed in the production bundle by webpack
|
||||
// Injected for local development. These will be removed in the production bundle by webpack
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
const port: string = process.env.PORT || "1234";
|
||||
updateConfigContext({
|
||||
|
||||
@@ -38,6 +38,7 @@ export enum MessageTypes {
|
||||
OpenPostgreSQLPasswordReset,
|
||||
OpenPostgresNetworkingBlade,
|
||||
OpenCosmosDBNetworkingBlade,
|
||||
DisplayNPSSurvey,
|
||||
}
|
||||
|
||||
export { Versions, ActionContracts, Diagnostics };
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
import React from "react";
|
||||
import AddCollectionIcon from "../../images/AddCollection.svg";
|
||||
import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg";
|
||||
@@ -140,6 +143,22 @@ export const createCollectionContextMenuButton = (
|
||||
return items;
|
||||
};
|
||||
|
||||
export const createSampleCollectionContextMenuButton = (): TreeNodeMenuItem[] => {
|
||||
const items: TreeNodeMenuItem[] = [];
|
||||
if (userContext.apiType === "SQL") {
|
||||
items.push({
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
onClick: () => {
|
||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
|
||||
traceOpen(Action.OpenQueryCopilotFromNewQuery, { apiType: userContext.apiType });
|
||||
},
|
||||
label: "New SQL Query",
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
export const createStoreProcedureContextMenuItems = (
|
||||
container: Explorer,
|
||||
storedProcedure: StoredProcedure
|
||||
|
||||
@@ -25,6 +25,8 @@ export class AccordionComponent extends React.Component<AccordionComponentProps>
|
||||
export interface AccordionItemComponentProps {
|
||||
title: string;
|
||||
isExpanded?: boolean;
|
||||
containerStyles?: React.CSSProperties;
|
||||
styles?: React.CSSProperties;
|
||||
}
|
||||
|
||||
interface AccordionItemComponentState {
|
||||
@@ -53,14 +55,19 @@ export class AccordionItemComponent extends React.Component<AccordionItemCompone
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const { containerStyles, styles } = this.props;
|
||||
return (
|
||||
<div className="accordionItemContainer">
|
||||
<div className="accordionItemContainer" style={{ ...containerStyles }}>
|
||||
<div className="accordionItemHeader" onClick={this.onHeaderClick} onKeyPress={this.onHeaderKeyPress}>
|
||||
{this.renderCollapseExpandIcon()}
|
||||
{this.props.title}
|
||||
</div>
|
||||
<div className="accordionItemContent">
|
||||
<AnimateHeight duration={AccordionItemComponent.durationMS} height={this.state.isExpanded ? "auto" : 0}>
|
||||
<AnimateHeight
|
||||
style={{ ...styles }}
|
||||
duration={AccordionItemComponent.durationMS}
|
||||
height={this.state.isExpanded ? "auto" : 0}
|
||||
>
|
||||
{this.props.children}
|
||||
</AnimateHeight>
|
||||
</div>
|
||||
|
||||
@@ -31,6 +31,13 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.createEditor(this.configureEditor.bind(this));
|
||||
|
||||
setTimeout(() => {
|
||||
const suggestionWidget = this.editor?.getDomNode()?.querySelector(".suggest-widget") as HTMLElement;
|
||||
if (suggestionWidget) {
|
||||
suggestionWidget.style.display = "none";
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
public componentDidUpdate(previous: EditorReactProps) {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Link } from "@fluentui/react/lib/Link";
|
||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||
import { sendMessage } from "Common/MessageHandler";
|
||||
import { Platform } from "ConfigContext";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import { IGalleryItem } from "Juno/JunoClient";
|
||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
||||
import * as ko from "knockout";
|
||||
@@ -23,7 +26,7 @@ import { PhoenixClient } from "../Phoenix/PhoenixClient";
|
||||
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../UserContext";
|
||||
import { isAccountNewerThanThresholdInMs, userContext } from "../UserContext";
|
||||
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
||||
import { stringToBlob } from "../Utils/BlobUtils";
|
||||
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
||||
@@ -258,6 +261,45 @@ export default class Explorer {
|
||||
// TODO: return result
|
||||
}
|
||||
|
||||
private getRandomInt(max: number) {
|
||||
return Math.floor(Math.random() * max);
|
||||
}
|
||||
|
||||
public openNPSSurveyDialog(): void {
|
||||
if (!Platform.Portal) {
|
||||
return;
|
||||
}
|
||||
|
||||
const NINETY_DAYS_IN_MS = 7776000000;
|
||||
const ONE_DAY_IN_MS = 86400000;
|
||||
const isAccountNewerThanNinetyDays = isAccountNewerThanThresholdInMs(
|
||||
userContext.databaseAccount?.systemData?.createdAt || "",
|
||||
NINETY_DAYS_IN_MS
|
||||
);
|
||||
|
||||
// Try Cosmos DB subscription - survey shown to random 25% of users at day 1 in Data Explorer.
|
||||
if (userContext.isTryCosmosDBSubscription) {
|
||||
if (
|
||||
isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", ONE_DAY_IN_MS) &&
|
||||
this.getRandomInt(100) < 25
|
||||
) {
|
||||
sendMessage(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(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(MessageTypes.DisplayNPSSurvey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async refreshDatabaseForResourceToken(): Promise<void> {
|
||||
const databaseId = userContext.parsedResourceToken?.databaseId;
|
||||
const collectionId = userContext.parsedResourceToken?.collectionId;
|
||||
@@ -353,7 +395,7 @@ export default class Explorer {
|
||||
) {
|
||||
const provisionData: IProvisionData = {
|
||||
cosmosEndpoint: userContext?.databaseAccount?.properties?.documentEndpoint,
|
||||
poolId: PoolIdType.DefaultPoolId,
|
||||
poolId: PoolIdType.QueryCopilot,
|
||||
};
|
||||
const connectionStatus: ContainerConnectionInfo = {
|
||||
status: ConnectionStatusType.Connecting,
|
||||
@@ -1291,7 +1333,9 @@ export default class Explorer {
|
||||
return;
|
||||
}
|
||||
|
||||
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection);
|
||||
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
|
||||
useDatabases.setState({ sampleDataResourceTokenCollection });
|
||||
|
||||
await this.allocateContainer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
import * as React from "react";
|
||||
import AddCollectionIcon from "../../../../images/AddCollection.svg";
|
||||
import AddDatabaseIcon from "../../../../images/AddDatabase.svg";
|
||||
@@ -8,23 +11,23 @@ import AddUdfIcon from "../../../../images/AddUdf.svg";
|
||||
import BrowseQueriesIcon from "../../../../images/BrowseQuery.svg";
|
||||
import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
|
||||
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
|
||||
import GitHubIcon from "../../../../images/github.svg";
|
||||
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
|
||||
import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg";
|
||||
import GitHubIcon from "../../../../images/github.svg";
|
||||
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
|
||||
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
|
||||
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
|
||||
import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg";
|
||||
import SettingsIcon from "../../../../images/settings_15x15.svg";
|
||||
import SynapseIcon from "../../../../images/synapse-link.svg";
|
||||
import { AuthType } from "../../../AuthType";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { configContext, Platform } from "../../../ConfigContext";
|
||||
import { Platform, configContext } from "../../../ConfigContext";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { JunoClient } from "../../../Juno/JunoClient";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
|
||||
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
import { useNotebook } from "../../Notebook/useNotebook";
|
||||
@@ -35,7 +38,7 @@ import { GitHubReposPanel } from "../../Panes/GitHubReposPanel/GitHubReposPanel"
|
||||
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
|
||||
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
import { SelectedNodeState } from "../../useSelectedNode";
|
||||
import { SelectedNodeState, useSelectedNode } from "../../useSelectedNode";
|
||||
|
||||
let counter = 0;
|
||||
|
||||
@@ -144,7 +147,9 @@ export function createStaticCommandBarButtons(
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
|
||||
disabled:
|
||||
useSelectedNode.getState().isQueryCopilotCollectionSelected() ||
|
||||
selectedNodeState.isDatabaseNodeOrNoneSelected(),
|
||||
};
|
||||
|
||||
newStoredProcedureBtn.children = createScriptCommandButtons(selectedNodeState);
|
||||
@@ -289,7 +294,8 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
|
||||
onCommandClick: () => container.openEnableSynapseLinkDialog(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: useNotebook.getState().isSynapseLinkUpdating,
|
||||
disabled:
|
||||
useSelectedNode.getState().isQueryCopilotCollectionSelected() || useNotebook.getState().isSynapseLinkUpdating,
|
||||
ariaLabel: label,
|
||||
};
|
||||
}
|
||||
@@ -320,8 +326,13 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
|
||||
if (useSelectedNode.getState().isQueryCopilotCollectionSelected()) {
|
||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
|
||||
traceOpen(Action.OpenQueryCopilotFromNewQuery, { apiType: userContext.apiType });
|
||||
} else {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewQueryClick(selectedCollection);
|
||||
}
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
@@ -366,7 +377,9 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
|
||||
disabled:
|
||||
useSelectedNode.getState().isQueryCopilotCollectionSelected() ||
|
||||
selectedNodeState.isDatabaseNodeOrNoneSelected(),
|
||||
};
|
||||
buttons.push(newStoredProcedureBtn);
|
||||
}
|
||||
@@ -383,7 +396,9 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
|
||||
disabled:
|
||||
useSelectedNode.getState().isQueryCopilotCollectionSelected() ||
|
||||
selectedNodeState.isDatabaseNodeOrNoneSelected(),
|
||||
};
|
||||
buttons.push(newUserDefinedFunctionBtn);
|
||||
}
|
||||
@@ -400,7 +415,9 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: selectedNodeState.isDatabaseNodeOrNoneSelected(),
|
||||
disabled:
|
||||
useSelectedNode.getState().isQueryCopilotCollectionSelected() ||
|
||||
selectedNodeState.isDatabaseNodeOrNoneSelected(),
|
||||
};
|
||||
buttons.push(newTriggerBtn);
|
||||
}
|
||||
@@ -424,7 +441,7 @@ function createNewNotebookButton(container: Explorer): CommandButtonComponentPro
|
||||
onCommandClick: () => container.onNewNotebookClicked(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
ariaLabel: label,
|
||||
};
|
||||
}
|
||||
@@ -437,7 +454,7 @@ function createuploadNotebookButton(container: Explorer): CommandButtonComponent
|
||||
onCommandClick: () => container.openUploadFilePanel(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
ariaLabel: label,
|
||||
};
|
||||
}
|
||||
@@ -452,7 +469,7 @@ function createOpenQueryButton(container: Explorer): CommandButtonComponentProps
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: false,
|
||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -465,7 +482,7 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: false,
|
||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -477,7 +494,7 @@ function createOpenTerminalButton(container: Explorer): CommandButtonComponentPr
|
||||
onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Default),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
ariaLabel: label,
|
||||
};
|
||||
}
|
||||
@@ -529,7 +546,8 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
|
||||
function createOpenPsqlTerminalButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Open PSQL Shell";
|
||||
const disableButton =
|
||||
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
|
||||
(!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled) ||
|
||||
useSelectedNode.getState().isQueryCopilotCollectionSelected();
|
||||
return {
|
||||
iconSrc: HostedTerminalIcon,
|
||||
iconAlt: label,
|
||||
@@ -556,7 +574,7 @@ function createNotebookWorkspaceResetButton(container: Explorer): CommandButtonC
|
||||
onCommandClick: () => container.resetNotebookWorkspace(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
ariaLabel: label,
|
||||
};
|
||||
}
|
||||
@@ -582,7 +600,7 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
ariaLabel: label,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,18 +5,18 @@
|
||||
import { Dropdown, IDropdownOption } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import AnimateHeight from "react-animate-height";
|
||||
import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif";
|
||||
import ClearIcon from "../../../../images/Clear-1.svg";
|
||||
import ChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
|
||||
import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x.png";
|
||||
import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif";
|
||||
import ErrorBlackIcon from "../../../../images/error_black.svg";
|
||||
import ErrorRedIcon from "../../../../images/error_red.svg";
|
||||
import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg";
|
||||
import InfoIcon from "../../../../images/info_color.svg";
|
||||
import LoadingIcon from "../../../../images/loading.svg";
|
||||
import ChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
|
||||
import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x.png";
|
||||
import { ClientDefaults, KeyCodes } from "../../../Common/Constants";
|
||||
import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
|
||||
import { ConsoleData, ConsoleDataType } from "./ConsoleData";
|
||||
|
||||
export interface NotificationConsoleComponentProps {
|
||||
@@ -256,6 +256,7 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
if (this.props.isConsoleExpanded && this.consoleHeaderElement) {
|
||||
this.consoleHeaderElement.focus();
|
||||
}
|
||||
useNotificationConsole.getState().setConsoleAnimationFinished(true);
|
||||
};
|
||||
|
||||
private updateConsoleData = (prevProps: NotificationConsoleComponentProps): void => {
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import {
|
||||
actions,
|
||||
AppState,
|
||||
castToSessionId,
|
||||
ContentRef,
|
||||
createKernelRef,
|
||||
JupyterHostRecordProps,
|
||||
ServerConfig as JupyterServerConfig,
|
||||
KernelInfo,
|
||||
KernelRef,
|
||||
RemoteKernelProps,
|
||||
actions,
|
||||
castToSessionId,
|
||||
createKernelRef,
|
||||
selectors,
|
||||
ServerConfig as JupyterServerConfig,
|
||||
} from "@nteract/core";
|
||||
import { Channels, childOf, createMessage, JupyterMessage, message, ofMessageType } from "@nteract/messaging";
|
||||
import { Channels, childOf, createMessage, message, ofMessageType } from "@nteract/messaging";
|
||||
import { defineConfigOption } from "@nteract/mythic-configuration";
|
||||
import { RecordOf } from "immutable";
|
||||
import { Action, AnyAction } from "redux";
|
||||
import { ofType, StateObservable } from "redux-observable";
|
||||
import { StateObservable, ofType } from "redux-observable";
|
||||
import { kernels, sessions } from "rx-jupyter";
|
||||
import { concat, EMPTY, from, interval, merge, Observable, Observer, of, Subject, Subscriber, timer } from "rxjs";
|
||||
import { EMPTY, Observable, Observer, Subject, Subscriber, concat, from, interval, merge, of, timer } from "rxjs";
|
||||
import {
|
||||
catchError,
|
||||
concatMap,
|
||||
@@ -35,17 +35,17 @@ import {
|
||||
import { webSocket } from "rxjs/webSocket";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { Areas } from "../../../Common/Constants";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { ActionModifiers, Action as TelemetryAction } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import { useDialog } from "../../Controls/Dialog";
|
||||
import * as FileSystemUtil from "../FileSystemUtil";
|
||||
import * as cdbActions from "../NotebookComponent/actions";
|
||||
import { NotebookContentProviderType, NotebookUtil } from "../NotebookUtil";
|
||||
import * as CdbActions from "./actions";
|
||||
import * as TextFile from "./contents/file/text-file";
|
||||
import { CdbAppState } from "./types";
|
||||
import { CdbAppState, JupyterMessage } from "./types";
|
||||
|
||||
interface NotebookServiceConfig extends JupyterServerConfig {
|
||||
userPuid?: string;
|
||||
@@ -106,10 +106,8 @@ const formWebSocketURL = (serverConfig: NotebookServiceConfig, kernelId: string,
|
||||
if (sessionId) {
|
||||
params.append("session_id", sessionId);
|
||||
}
|
||||
|
||||
const q = params.toString();
|
||||
const suffix = q !== "" ? `?${q}` : "";
|
||||
|
||||
const url = (serverConfig.endpoint.slice(0, -1) || "") + `api/kernels/${kernelId}/channels${suffix}`;
|
||||
|
||||
return url.replace(/^http(s)?/, "ws$1");
|
||||
@@ -241,10 +239,10 @@ const connect = (serverConfig: NotebookServiceConfig, kernelID: string, sessionI
|
||||
...message,
|
||||
header: {
|
||||
session: sessionID,
|
||||
token: serverConfig.token,
|
||||
...message.header,
|
||||
},
|
||||
};
|
||||
|
||||
wsSubject.next(sessionizedMessage);
|
||||
} else {
|
||||
console.error("Message must be an object, the app sent", message);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CellId } from "@nteract/commutable";
|
||||
import { AppState } from "@nteract/core";
|
||||
import { MessageType } from "@nteract/messaging";
|
||||
import * as Immutable from "immutable";
|
||||
import { Notebook } from "../../../Common/Constants";
|
||||
|
||||
@@ -53,3 +54,26 @@ export const makeCdbRecord = Immutable.Record<CdbRecordProps>({
|
||||
pendingSnapshotRequest: undefined,
|
||||
notebookSnapshotError: undefined,
|
||||
});
|
||||
|
||||
export interface JupyterMessage<MT extends MessageType = MessageType, C = any> {
|
||||
header: JupyterMessageHeader<MT>;
|
||||
parent_header:
|
||||
| JupyterMessageHeader<any>
|
||||
| {
|
||||
msg_id?: string;
|
||||
};
|
||||
metadata: object;
|
||||
content: C;
|
||||
channel: string;
|
||||
buffers?: Uint8Array | null;
|
||||
}
|
||||
|
||||
export interface JupyterMessageHeader<MT extends MessageType = MessageType> {
|
||||
msg_id: string;
|
||||
username: string;
|
||||
date: string;
|
||||
msg_type: MT;
|
||||
version: string;
|
||||
session: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
@@ -1425,6 +1425,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
this.setState({ isExecuting: false });
|
||||
TelemetryProcessor.traceSuccess(Action.CreateCollection, telemetryData, startKey);
|
||||
useSidePanel.getState().closeSidePanel();
|
||||
// open NPS Survey Dialog once the collection is created
|
||||
if (userContext.features.enableNPSSurvey) {
|
||||
this.props.explorer.openNPSSurveyDialog();
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage: string = getErrorMessage(error);
|
||||
this.setState({ isExecuting: false, errorMessage, showErrorDetails: true });
|
||||
|
||||
@@ -2,13 +2,13 @@ import { Checkbox, Dropdown, IDropdownOption, Link, Stack, Text, TextField } fro
|
||||
import * as Constants from "Common/Constants";
|
||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import * as SharedConstants from "Shared/Constants";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "UserContext";
|
||||
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
||||
import Explorer from "../../Explorer";
|
||||
import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
|
||||
@@ -291,7 +291,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
styles={getTextFieldStyles({ fontSize: 12, width: 150 })}
|
||||
aria-required="true"
|
||||
required={true}
|
||||
ariaLabel="addCollection-tableId"
|
||||
ariaLabel="addCollection-table Id Create table"
|
||||
autoComplete="off"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Text, TextField } from "@fluentui/react";
|
||||
import { Areas } from "Common/Constants";
|
||||
import { deleteCollection } from "Common/dataAccess/deleteCollection";
|
||||
import DeleteFeedback from "Common/DeleteFeedback";
|
||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||
import { deleteCollection } from "Common/dataAccess/deleteCollection";
|
||||
import { Collection } from "Contracts/ViewModels";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility";
|
||||
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "UserContext";
|
||||
import { getCollectionName } from "Utils/APITypeUtils";
|
||||
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
@@ -126,6 +126,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
setInputCollectionName(newInput);
|
||||
}}
|
||||
ariaLabel={confirmContainer}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{shouldRecordFeedback() && (
|
||||
|
||||
@@ -44,6 +44,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
autoFocus={true}
|
||||
id="confirmCollectionId"
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
@@ -59,6 +60,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
deferredValidationTime={200}
|
||||
id="confirmCollectionId"
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
resizable={true}
|
||||
styles={[Function]}
|
||||
theme={
|
||||
@@ -338,7 +340,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
value=""
|
||||
>
|
||||
<div
|
||||
className="ms-TextField root-55"
|
||||
className="ms-TextField is-required root-55"
|
||||
>
|
||||
<div
|
||||
className="ms-TextField-wrapper"
|
||||
@@ -356,6 +358,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onInput={[Function]}
|
||||
required={true}
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
|
||||
@@ -140,6 +140,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
setDatabaseInput(newInput);
|
||||
}}
|
||||
ariaLabel={confirmDatabase}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{isLastNonEmptyDatabase() && (
|
||||
|
||||
@@ -112,6 +112,9 @@
|
||||
margin-top: 28px;
|
||||
margin-left: 4px !important;
|
||||
}
|
||||
.addRemoveIcon [alt="editEntity"]:focus,.addRemoveIconLabel [alt="editEntity"]:focus{
|
||||
border:1px dashed #605E5C
|
||||
}
|
||||
.addNewParamStyle {
|
||||
margin-top: 5px;
|
||||
margin-left: 5px !important;
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface PanelContainerProps {
|
||||
panelContent?: JSX.Element;
|
||||
isConsoleExpanded: boolean;
|
||||
isOpen: boolean;
|
||||
isConsoleAnimationFinished?: boolean;
|
||||
panelWidth?: string;
|
||||
onRenderNavigationContent?: IRenderFunction<IPanelProps>;
|
||||
}
|
||||
@@ -28,7 +29,7 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
omponentDidMount(): void {
|
||||
window.addEventListener("resize", () => this.setState({ height: this.getPanelHeight() }));
|
||||
}
|
||||
|
||||
@@ -36,6 +37,15 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
|
||||
window.removeEventListener("resize", () => this.setState({ height: this.getPanelHeight() }));
|
||||
}
|
||||
|
||||
componentDidUpdate(): void {
|
||||
if (useNotificationConsole.getState().consoleAnimationFinished || this.state.height !== this.getPanelHeight()) {
|
||||
this.setState({
|
||||
height: this.getPanelHeight(),
|
||||
});
|
||||
useNotificationConsole.getState().setConsoleAnimationFinished(false);
|
||||
}
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
if (!this.props.panelContent) {
|
||||
return <></>;
|
||||
@@ -59,7 +69,7 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
|
||||
header: { padding: "0 0 8px 34px" },
|
||||
commands: { marginTop: 8 },
|
||||
}}
|
||||
style={{ height: this.getPanelHeight() }}
|
||||
style={{ height: this.state.height }}
|
||||
>
|
||||
{this.props.panelContent}
|
||||
</Panel>
|
||||
@@ -75,16 +85,22 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
|
||||
};
|
||||
|
||||
private getPanelHeight = (): string => {
|
||||
const consoleHeight = this.props.isConsoleExpanded
|
||||
? PanelContainerComponent.consoleContentHeight + PanelContainerComponent.consoleHeaderHeight
|
||||
: PanelContainerComponent.consoleHeaderHeight;
|
||||
const panelHeight = window.innerHeight - consoleHeight;
|
||||
return panelHeight + "px";
|
||||
const notificationConsole = document.getElementById("explorerNotificationConsole");
|
||||
if (notificationConsole) {
|
||||
return window.innerHeight - notificationConsole.clientHeight + "px";
|
||||
}
|
||||
return (
|
||||
window.innerHeight -
|
||||
(this.props.isConsoleExpanded
|
||||
? PanelContainerComponent.consoleContentHeight + PanelContainerComponent.consoleHeaderHeight
|
||||
: PanelContainerComponent.consoleHeaderHeight) +
|
||||
"px"
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const SidePanel: React.FC = () => {
|
||||
const isConsoleExpanded = useNotificationConsole((state) => state.isExpanded);
|
||||
const isConsoleAnimationFinished = useNotificationConsole((state) => state.consoleAnimationFinished);
|
||||
const { isOpen, panelContent, panelWidth, headerText } = useSidePanel((state) => {
|
||||
return {
|
||||
isOpen: state.isOpen,
|
||||
@@ -102,6 +118,7 @@ export const SidePanel: React.FC = () => {
|
||||
headerText={headerText}
|
||||
isConsoleExpanded={isConsoleExpanded}
|
||||
panelWidth={panelWidth}
|
||||
isConsoleAnimationFinished={isConsoleAnimationFinished}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { PrimaryButton } from "@fluentui/react";
|
||||
import React from "react";
|
||||
import React, { CSSProperties } from "react";
|
||||
|
||||
export interface PanelFooterProps {
|
||||
buttonLabel: string;
|
||||
isButtonDisabled?: boolean;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export const PanelFooterComponent: React.FunctionComponent<PanelFooterProps> = ({
|
||||
buttonLabel,
|
||||
isButtonDisabled,
|
||||
style,
|
||||
}: PanelFooterProps): JSX.Element => (
|
||||
<div className="panelFooter">
|
||||
<div className="panelFooter" style={style}>
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
id="sidePanelOkButton"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FunctionComponent, ReactNode } from "react";
|
||||
import React, { CSSProperties, FunctionComponent, ReactNode } from "react";
|
||||
import { PanelFooterComponent } from "../PanelFooterComponent";
|
||||
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
|
||||
import { PanelLoadingScreen } from "../PanelLoadingScreen";
|
||||
@@ -11,6 +11,7 @@ export interface RightPaneFormProps {
|
||||
isSubmitButtonHidden?: boolean;
|
||||
isSubmitButtonDisabled?: boolean;
|
||||
children?: ReactNode;
|
||||
footerStyle?: CSSProperties;
|
||||
}
|
||||
|
||||
export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
|
||||
@@ -21,6 +22,7 @@ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
|
||||
isSubmitButtonHidden = false,
|
||||
isSubmitButtonDisabled = false,
|
||||
children,
|
||||
footerStyle,
|
||||
}: RightPaneFormProps) => {
|
||||
const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
@@ -33,7 +35,11 @@ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
|
||||
{formError && <PanelInfoErrorComponent messageType="error" message={formError} showErrorDetails={true} />}
|
||||
{children}
|
||||
{!isSubmitButtonHidden && (
|
||||
<PanelFooterComponent buttonLabel={submitButtonText} isButtonDisabled={isSubmitButtonDisabled} />
|
||||
<PanelFooterComponent
|
||||
buttonLabel={submitButtonText}
|
||||
isButtonDisabled={isSubmitButtonDisabled}
|
||||
style={footerStyle}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
{isExecuting && <PanelLoadingScreen />}
|
||||
|
||||
@@ -139,10 +139,11 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
|
||||
onSubmit: () => {
|
||||
isSaveQueryEnabled() ? submit() : setupQueries();
|
||||
},
|
||||
footerStyle: isSaveQueryEnabled() ? { flexGrow: 0 } : {},
|
||||
};
|
||||
return (
|
||||
<RightPaneForm {...props}>
|
||||
<div className="panelFormWrapper">
|
||||
<div className="panelFormWrapper" style={{ flexGrow: 1 }}>
|
||||
<div className="panelMainContent">
|
||||
{!isSaveQueryEnabled() ? (
|
||||
<Text variant="small">{setupSaveQueriesText}</Text>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
exports[`Save Query Pane should render Default properly 1`] = `
|
||||
<RightPaneForm
|
||||
footerStyle={Object {}}
|
||||
formError=""
|
||||
isExecuting={false}
|
||||
onSubmit={[Function]}
|
||||
@@ -9,6 +10,11 @@ exports[`Save Query Pane should render Default properly 1`] = `
|
||||
>
|
||||
<div
|
||||
className="panelFormWrapper"
|
||||
style={
|
||||
Object {
|
||||
"flexGrow": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="panelMainContent"
|
||||
|
||||
@@ -2,12 +2,12 @@ import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton } from "@fluentui
|
||||
import * as Constants from "Common/Constants";
|
||||
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
||||
import { configContext } from "ConfigContext";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import React, { FunctionComponent, MouseEvent, useState } from "react";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import * as StringUtility from "Shared/StringUtility";
|
||||
import { userContext } from "UserContext";
|
||||
import { logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import React, { FunctionComponent, MouseEvent, useState } from "react";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
|
||||
export const SettingsPane: FunctionComponent = () => {
|
||||
@@ -29,7 +29,7 @@ export const SettingsPane: FunctionComponent = () => {
|
||||
const [crossPartitionQueryEnabled, setCrossPartitionQueryEnabled] = useState<boolean>(
|
||||
LocalStorageUtility.hasItem(StorageKey.IsCrossPartitionQueryEnabled)
|
||||
? LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
|
||||
: true
|
||||
: false
|
||||
);
|
||||
const [graphAutoVizDisabled, setGraphAutoVizDisabled] = useState<string>(
|
||||
LocalStorageUtility.hasItem(StorageKey.IsGraphAutoVizDisabled)
|
||||
|
||||
@@ -142,7 +142,7 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
</div>
|
||||
<StyledCheckboxBase
|
||||
ariaLabel="Enable cross partition query"
|
||||
checked={true}
|
||||
checked={false}
|
||||
className="padding"
|
||||
onChange={[Function]}
|
||||
styles={
|
||||
|
||||
@@ -371,6 +371,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
autoFocus={true}
|
||||
id="confirmDatabaseId"
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
@@ -385,6 +386,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
deferredValidationTime={200}
|
||||
id="confirmDatabaseId"
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
resizable={true}
|
||||
styles={[Function]}
|
||||
theme={
|
||||
@@ -663,7 +665,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
validateOnLoad={true}
|
||||
>
|
||||
<div
|
||||
className="ms-TextField root-58"
|
||||
className="ms-TextField is-required root-58"
|
||||
>
|
||||
<div
|
||||
className="ms-TextField-wrapper"
|
||||
@@ -681,6 +683,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onInput={[Function]}
|
||||
required={true}
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { Checkbox, ChoiceGroup, DefaultButton, IconButton, PrimaryButton, TextField } from "@fluentui/react";
|
||||
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
||||
import { submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||
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/QueryCopilotUtilities");
|
||||
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 />);
|
||||
|
||||
expect(wrapper.props().isOpen).toBeTruthy();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should close on cancel click", () => {
|
||||
const wrapper = shallow(<QueryCopilotFeedbackModal />);
|
||||
|
||||
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 />);
|
||||
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 />);
|
||||
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 />);
|
||||
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 />);
|
||||
|
||||
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 />);
|
||||
|
||||
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 />);
|
||||
|
||||
const cancelButton = wrapper.find(DefaultButton);
|
||||
cancelButton.simulate("click");
|
||||
wrapper.setProps({});
|
||||
|
||||
expect(wrapper.props().isOpen).toBeFalsy();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should submit submission", () => {
|
||||
const wrapper = shallow(<QueryCopilotFeedbackModal />);
|
||||
|
||||
const submitButton = wrapper.find(PrimaryButton);
|
||||
submitButton.simulate("click");
|
||||
wrapper.setProps({});
|
||||
|
||||
expect(submitFeedback).toHaveBeenCalledTimes(1);
|
||||
expect(submitFeedback).toHaveBeenCalledWith({
|
||||
likeQuery: false,
|
||||
generatedQuery: "",
|
||||
userPrompt: "",
|
||||
description: "",
|
||||
contact: getUserEmail(),
|
||||
});
|
||||
expect(wrapper.props().isOpen).toBeFalsy();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import React from "react";
|
||||
import { getUserEmail } from "../../../Utils/UserUtils";
|
||||
|
||||
export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
|
||||
const {
|
||||
@@ -26,6 +27,8 @@ export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
|
||||
const [isContactAllowed, setIsContactAllowed] = React.useState<boolean>(true);
|
||||
const [description, setDescription] = React.useState<string>("");
|
||||
const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false);
|
||||
const [contact, setContact] = React.useState<string>(getUserEmail());
|
||||
|
||||
return (
|
||||
<Modal isOpen={showFeedbackModal}>
|
||||
<Stack style={{ padding: 24 }}>
|
||||
@@ -69,12 +72,15 @@ export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
|
||||
{ key: "no", text: "No, do not contact me." },
|
||||
]}
|
||||
selectedKey={isContactAllowed ? "yes" : "no"}
|
||||
onChange={(_, option) => setIsContactAllowed(option.key === "yes")}
|
||||
onChange={(_, option) => {
|
||||
setIsContactAllowed(option.key === "yes");
|
||||
setContact(option.key === "yes" ? getUserEmail() : "");
|
||||
}}
|
||||
></ChoiceGroup>
|
||||
<Text style={{ fontSize: 12, marginBottom: 14 }}>
|
||||
By pressing submit, your feedback will be used to improve Microsoft products and services. IT admins for your
|
||||
organization will be able to view and manage your feedback data.{" "}
|
||||
<Link href="" target="_blank">
|
||||
<Link href="https://privacy.microsoft.com/privacystatement" target="_blank">
|
||||
Privacy statement
|
||||
</Link>
|
||||
</Text>
|
||||
@@ -92,7 +98,7 @@ export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
|
||||
onClick={() => {
|
||||
closeFeedbackModal();
|
||||
setHideFeedbackModalForLikedQueries(doNotShowAgainChecked);
|
||||
submitFeedback({ generatedQuery, likeQuery, description, userPrompt });
|
||||
submitFeedback({ generatedQuery, likeQuery, description, userPrompt, contact });
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
47
src/Explorer/QueryCopilot/Modal/WelcomeModal.css
Normal file
47
src/Explorer/QueryCopilot/Modal/WelcomeModal.css
Normal file
@@ -0,0 +1,47 @@
|
||||
.modalContentPadding {
|
||||
padding-top: 15px;
|
||||
width: 513px;
|
||||
height: 638px;
|
||||
}
|
||||
|
||||
.exitPadding {
|
||||
padding: 0px 7px 0px 0px;
|
||||
}
|
||||
|
||||
.previewMargin {
|
||||
margin: 8px 10px 0px 0;
|
||||
}
|
||||
|
||||
.preview {
|
||||
padding: 0px 4px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 8px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.exitIcon {
|
||||
margin: 3px 7px 0px 0px;
|
||||
color: #424242;
|
||||
}
|
||||
|
||||
.text {
|
||||
width: 348px;
|
||||
padding: 8px 16px 8px 16px;
|
||||
margin-left: 25px;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.imageTextPadding {
|
||||
padding: 0px 5px 0px 0px;
|
||||
}
|
||||
|
||||
.buttonPadding {
|
||||
padding: 15px 0px 0px 0px;
|
||||
}
|
||||
|
||||
.tryButton {
|
||||
border-radius: 4px;
|
||||
}
|
||||
32
src/Explorer/QueryCopilot/Modal/WelcomeModal.test.tsx
Normal file
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
114
src/Explorer/QueryCopilot/Modal/WelcomeModal.tsx
Normal file
114
src/Explorer/QueryCopilot/Modal/WelcomeModal.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { IconButton, Image, Link, Modal, PrimaryButton, Stack, StackItem, Text } from "@fluentui/react";
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import React from "react";
|
||||
import Database from "../../../../images/CopilotDatabase.svg";
|
||||
import Flash from "../../../../images/CopilotFlash.svg";
|
||||
import Thumb from "../../../../images/CopilotThumb.svg";
|
||||
import CoplilotWelcomeIllustration from "../../../../images/CopliotWelcomeIllustration.svg";
|
||||
import "./WelcomeModal.css";
|
||||
|
||||
export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element => {
|
||||
const [isModalVisible, { setFalse: hideModal }] = useBoolean(visible);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (visible) {
|
||||
window.localStorage.setItem("hideWelcomeModal", "true");
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal isOpen={isModalVisible} onDismiss={hideModal} isBlocking={false}>
|
||||
<Stack className="modalContentPadding">
|
||||
<Stack horizontal>
|
||||
<Stack horizontal grow={4} horizontalAlign="end">
|
||||
<Stack.Item>
|
||||
<Image src={CoplilotWelcomeIllustration} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack horizontal grow={1} horizontalAlign="end" verticalAlign="start" className="exitPadding">
|
||||
<Stack.Item className="previewMargin">
|
||||
<Text className="preview">Preview</Text>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<IconButton
|
||||
onClick={hideModal}
|
||||
iconProps={{ iconName: "Cancel" }}
|
||||
title="Exit"
|
||||
ariaLabel="Exit"
|
||||
className="exitIcon"
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack horizontalAlign="center">
|
||||
<Stack.Item align="center">
|
||||
<Text className="title bold">Welcome to Copilot in CosmosDB</Text>
|
||||
</Stack.Item>
|
||||
<Stack.Item align="center" className="text">
|
||||
<Stack horizontal>
|
||||
<StackItem align="start" className="imageTextPadding">
|
||||
<Image src={Flash} />
|
||||
</StackItem>
|
||||
<StackItem align="start">
|
||||
<Text className="bold">
|
||||
Let copilot do the work for you
|
||||
<br />
|
||||
</Text>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
<Text>
|
||||
Ask Copilot to generate a query by describing the query in your words.
|
||||
<br />
|
||||
<Link href="http://aka.ms/cdb-copilot-learn-more">Learn more</Link>
|
||||
</Text>
|
||||
</Stack.Item>
|
||||
<Stack.Item align="center" className="text">
|
||||
<Stack horizontal>
|
||||
<StackItem align="start" className="imageTextPadding">
|
||||
<Image src={Thumb} />
|
||||
</StackItem>
|
||||
<StackItem align="start">
|
||||
<Text className="bold">
|
||||
Use your judgement
|
||||
<br />
|
||||
</Text>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
<Text>
|
||||
AI-generated content can have mistakes. Make sure 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.Item align="center" className="text">
|
||||
<Stack horizontal>
|
||||
<StackItem align="start" className="imageTextPadding">
|
||||
<Image src={Database} />
|
||||
</StackItem>
|
||||
<StackItem align="start">
|
||||
<Text className="bold">
|
||||
Copilot currently works only a sample database
|
||||
<br />
|
||||
</Text>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
<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 className="buttonPadding">
|
||||
<Stack.Item align="center">
|
||||
<PrimaryButton onClick={hideModal} className="tryButton">
|
||||
Try Copilot
|
||||
</PrimaryButton>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,196 @@
|
||||
// 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"
|
||||
>
|
||||
<Text
|
||||
className="title bold"
|
||||
>
|
||||
Welcome to Copilot in CosmosDB
|
||||
</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 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"
|
||||
>
|
||||
Copilot currently works only a sample database
|
||||
<br />
|
||||
</Text>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
<Text>
|
||||
Copilot is setup on a sample database we have configured for you at no cost
|
||||
<br />
|
||||
<StyledLinkBase
|
||||
href="http://aka.ms/cdb-copilot-learn-more"
|
||||
>
|
||||
Learn more
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
<Stack
|
||||
className="buttonPadding"
|
||||
>
|
||||
<StackItem
|
||||
align="center"
|
||||
>
|
||||
<CustomizedPrimaryButton
|
||||
className="tryButton"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Try Copilot
|
||||
</CustomizedPrimaryButton>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -1,11 +1,48 @@
|
||||
import { IconButton } from "@fluentui/react";
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { any } from "underscore";
|
||||
import { CopyPopup } from "./CopyPopup";
|
||||
|
||||
describe("Copy Popup snapshot test", () => {
|
||||
const setShowCopyPopupMock = jest.fn();
|
||||
it("should render when showCopyPopup is true", () => {
|
||||
const wrapper = shallow(<CopyPopup showCopyPopup={true} setShowCopyPopup={() => any} />);
|
||||
const wrapper = shallow(<CopyPopup showCopyPopup={true} setShowCopyPopup={setShowCopyPopupMock} />);
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
expect(wrapper.prop("setShowCopyPopup")).toBeUndefined();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render when showCopyPopup is false", () => {
|
||||
const wrapper = shallow(<CopyPopup showCopyPopup={false} setShowCopyPopup={setShowCopyPopupMock} />);
|
||||
expect(wrapper.prop("showCopyPopup")).toBeFalsy();
|
||||
expect(wrapper.prop("setShowCopyPopup")).toBeUndefined();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should call setShowCopyPopup(false) when close button is clicked", () => {
|
||||
const wrapper = shallow(<CopyPopup showCopyPopup={true} setShowCopyPopup={setShowCopyPopupMock} />);
|
||||
|
||||
const closeButton = wrapper.find(IconButton);
|
||||
closeButton.props().onClick?.({} as React.MouseEvent<HTMLButtonElement, MouseEvent>);
|
||||
|
||||
expect(setShowCopyPopupMock).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("should have the correct inline styles", () => {
|
||||
const wrapper = shallow(<CopyPopup showCopyPopup={true} setShowCopyPopup={setShowCopyPopupMock} />);
|
||||
|
||||
const stackStyle = wrapper.find("Stack").first().props().style;
|
||||
|
||||
expect(stackStyle).toEqual({
|
||||
position: "fixed",
|
||||
width: 345,
|
||||
height: 66,
|
||||
padding: 10,
|
||||
gap: 5,
|
||||
top: 75,
|
||||
right: 20,
|
||||
background: "#FFFFFF",
|
||||
boxShadow: "0 2px 6px rgba(0, 0, 0, 0.16)",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,113 @@
|
||||
import { shallow } from "enzyme";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { any } from "underscore";
|
||||
import { DeletePopup } from "./DeletePopup";
|
||||
|
||||
describe("Delete Popup snapshot test", () => {
|
||||
const setShowDeletePopupMock = jest.fn();
|
||||
const setQueryMock = jest.fn();
|
||||
const clearFeedbackMock = jest.fn();
|
||||
const showFeedbackBarMock = jest.fn();
|
||||
|
||||
it("should render when showDeletePopup is true", () => {
|
||||
const wrapper = shallow(<DeletePopup showDeletePopup={true} setShowDeletePopup={() => any} setQuery={() => any} />);
|
||||
const wrapper = shallow(
|
||||
<DeletePopup
|
||||
showDeletePopup={true}
|
||||
setShowDeletePopup={setShowDeletePopupMock}
|
||||
setQuery={setQueryMock}
|
||||
clearFeedback={clearFeedbackMock}
|
||||
showFeedbackBar={showFeedbackBarMock}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find("Modal").prop("isOpen")).toBeTruthy();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should not render when showDeletePopup is false", () => {
|
||||
const wrapper = shallow(
|
||||
<DeletePopup
|
||||
showDeletePopup={false}
|
||||
setShowDeletePopup={setShowDeletePopupMock}
|
||||
setQuery={setQueryMock}
|
||||
clearFeedback={clearFeedbackMock}
|
||||
showFeedbackBar={showFeedbackBarMock}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.props().children.props.showDeletePopup).toBeFalsy();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should call setQuery with an empty string and setShowDeletePopup(false) when delete button is clicked", () => {
|
||||
const wrapper = mount(
|
||||
<DeletePopup
|
||||
showDeletePopup={true}
|
||||
setShowDeletePopup={setShowDeletePopupMock}
|
||||
setQuery={setQueryMock}
|
||||
clearFeedback={clearFeedbackMock}
|
||||
showFeedbackBar={showFeedbackBarMock}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.find("PrimaryButton").simulate("click");
|
||||
|
||||
expect(setQueryMock).toHaveBeenCalledWith("");
|
||||
expect(setShowDeletePopupMock).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("should call setShowDeletePopup(false) when close button is clicked", () => {
|
||||
const setShowDeletePopupMock = jest.fn();
|
||||
const wrapper = mount(
|
||||
<DeletePopup
|
||||
showDeletePopup={true}
|
||||
setShowDeletePopup={setShowDeletePopupMock}
|
||||
setQuery={setQueryMock}
|
||||
clearFeedback={clearFeedbackMock}
|
||||
showFeedbackBar={showFeedbackBarMock}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.find("DefaultButton").at(1).simulate("click");
|
||||
|
||||
expect(setShowDeletePopupMock).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("should render the appropriate text content", () => {
|
||||
const wrapper = shallow(
|
||||
<DeletePopup
|
||||
showDeletePopup={true}
|
||||
setShowDeletePopup={setShowDeletePopupMock}
|
||||
setQuery={setQueryMock}
|
||||
clearFeedback={clearFeedbackMock}
|
||||
showFeedbackBar={showFeedbackBarMock}
|
||||
/>
|
||||
);
|
||||
|
||||
const textContent = wrapper
|
||||
.find("Text")
|
||||
.map((text, index) => <React.Fragment key={index}>{text.props().children}</React.Fragment>);
|
||||
|
||||
expect(textContent).toEqual([
|
||||
<React.Fragment key={0}>
|
||||
<b>Delete code?</b>
|
||||
</React.Fragment>,
|
||||
<React.Fragment key={1}>
|
||||
This will clear the query from the query builder pane along with all comments and also reset the prompt pane
|
||||
</React.Fragment>,
|
||||
]);
|
||||
});
|
||||
|
||||
it("should have the correct inline style", () => {
|
||||
const wrapper = shallow(
|
||||
<DeletePopup
|
||||
showDeletePopup={true}
|
||||
setShowDeletePopup={setShowDeletePopupMock}
|
||||
setQuery={setQueryMock}
|
||||
clearFeedback={clearFeedbackMock}
|
||||
showFeedbackBar={showFeedbackBarMock}
|
||||
/>
|
||||
);
|
||||
|
||||
const stackStyle = wrapper.find("Stack[style]").props().style;
|
||||
|
||||
expect(stackStyle).toEqual({ padding: "16px 24px", height: "auto" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,14 +5,20 @@ export const DeletePopup = ({
|
||||
showDeletePopup,
|
||||
setShowDeletePopup,
|
||||
setQuery,
|
||||
clearFeedback,
|
||||
showFeedbackBar,
|
||||
}: {
|
||||
showDeletePopup: boolean;
|
||||
setShowDeletePopup: Dispatch<SetStateAction<boolean>>;
|
||||
setQuery: Dispatch<SetStateAction<string>>;
|
||||
clearFeedback: Dispatch<SetStateAction<void>>;
|
||||
showFeedbackBar: Dispatch<SetStateAction<boolean>>;
|
||||
}): JSX.Element => {
|
||||
const deleteCode = () => {
|
||||
setQuery("");
|
||||
setShowDeletePopup(false);
|
||||
clearFeedback();
|
||||
showFeedbackBar(false);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Copy Popup snapshot test should render when showCopyPopup is false 1`] = `<Fragment />`;
|
||||
|
||||
exports[`Copy Popup snapshot test should render when showCopyPopup is true 1`] = `
|
||||
<Stack
|
||||
style={
|
||||
|
||||
@@ -1,5 +1,83 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Delete Popup snapshot test should not render when showDeletePopup is false 1`] = `
|
||||
<Modal
|
||||
isOpen={false}
|
||||
styles={
|
||||
Object {
|
||||
"main": Object {
|
||||
"minHeight": "122px",
|
||||
"minWidth": "880px",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
style={
|
||||
Object {
|
||||
"height": "auto",
|
||||
"padding": "16px 24px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": "18px",
|
||||
"height": 24,
|
||||
}
|
||||
}
|
||||
>
|
||||
<b>
|
||||
Delete code?
|
||||
</b>
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"marginBottom": 20,
|
||||
"marginTop": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
This will clear the query from the query builder pane along with all comments and also reset the prompt pane
|
||||
</Text>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
horizontalAlign="start"
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedPrimaryButton
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": 24,
|
||||
"padding": "0px 20px",
|
||||
}
|
||||
}
|
||||
>
|
||||
Delete
|
||||
</CustomizedPrimaryButton>
|
||||
<CustomizedDefaultButton
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": 24,
|
||||
"padding": "0px 20px",
|
||||
}
|
||||
}
|
||||
>
|
||||
Close
|
||||
</CustomizedDefaultButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
`;
|
||||
|
||||
exports[`Delete Popup snapshot test should render when showDeletePopup is true 1`] = `
|
||||
<Modal
|
||||
isOpen={true}
|
||||
|
||||
@@ -5,9 +5,7 @@ import { QueryCopilotTab } from "./QueryCopilotTab";
|
||||
|
||||
describe("Query copilot tab snapshot test", () => {
|
||||
it("should render with initial input", () => {
|
||||
const wrapper = shallow(
|
||||
<QueryCopilotTab initialInput="Write a query to return all records in this table" explorer={new Explorer()} />
|
||||
);
|
||||
const wrapper = shallow(<QueryCopilotTab explorer={new Explorer()} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,46 +12,51 @@ import {
|
||||
Separator,
|
||||
Spinner,
|
||||
Stack,
|
||||
TeachingBubble,
|
||||
Text,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import {
|
||||
QueryCopilotSampleContainerId,
|
||||
QueryCopilotSampleContainerSchema,
|
||||
QueryCopilotSampleDatabaseId,
|
||||
} from "Common/Constants";
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import { QueryCopilotSampleContainerId, QueryCopilotSampleContainerSchema } from "Common/Constants";
|
||||
import { getErrorMessage, handleError } from "Common/ErrorHandlingUtils";
|
||||
import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility";
|
||||
import { MinimalQueryIterator } from "Common/IteratorUtilities";
|
||||
import { queryDocuments } from "Common/dataAccess/queryDocuments";
|
||||
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
|
||||
import { QueryResults } from "Contracts/ViewModels";
|
||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
||||
import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal";
|
||||
import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
|
||||
import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup";
|
||||
import { submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||
import { querySampleDocuments, submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||
import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/SamplePrompts/SamplePrompts";
|
||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "UserContext";
|
||||
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import React, { useState } from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import SplitterLayout from "react-splitter-layout";
|
||||
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
||||
import HintIcon from "../../../images/Hint.svg";
|
||||
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
||||
import RecentIcon from "../../../images/Recent.svg";
|
||||
import SamplePromptsIcon from "../../../images/SamplePromptsIcon.svg";
|
||||
import XErrorMessage from "../../../images/X-errorMessage.svg";
|
||||
import SaveQueryIcon from "../../../images/save-cosmos.svg";
|
||||
import { useTabs } from "../../hooks/useTabs";
|
||||
|
||||
interface SuggestedPrompt {
|
||||
id: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface QueryCopilotTabProps {
|
||||
initialInput: string;
|
||||
explorer: Explorer;
|
||||
}
|
||||
|
||||
@@ -68,27 +73,50 @@ const promptStyles: IButtonStyles = {
|
||||
label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 },
|
||||
};
|
||||
|
||||
export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
||||
initialInput,
|
||||
explorer,
|
||||
}: QueryCopilotTabProps): JSX.Element => {
|
||||
const hideFeedbackModalForLikedQueries = useQueryCopilot((state) => state.hideFeedbackModalForLikedQueries);
|
||||
const [userPrompt, setUserPrompt] = useState<string>(initialInput || "");
|
||||
const [generatedQuery, setGeneratedQuery] = useState<string>("");
|
||||
const [query, setQuery] = useState<string>("");
|
||||
const [selectedQuery, setSelectedQuery] = useState<string>("");
|
||||
const [isGeneratingQuery, setIsGeneratingQuery] = useState<boolean>(false);
|
||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||
const [likeQuery, setLikeQuery] = useState<boolean>();
|
||||
const [showCallout, setShowCallout] = useState<boolean>(false);
|
||||
const [showSamplePrompts, setShowSamplePrompts] = useState<boolean>(false);
|
||||
const [queryIterator, setQueryIterator] = useState<MinimalQueryIterator>();
|
||||
const [queryResults, setQueryResults] = useState<QueryResults>();
|
||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||
const [isSamplePromptsOpen, setIsSamplePromptsOpen] = useState<boolean>(false);
|
||||
const [showDeletePopup, setShowDeletePopup] = useState<boolean>(false);
|
||||
const [showFeedbackBar, setShowFeedbackBar] = useState<boolean>(false);
|
||||
const [showCopyPopup, setshowCopyPopup] = useState<boolean>(false);
|
||||
export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({ explorer }: QueryCopilotTabProps): JSX.Element => {
|
||||
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false);
|
||||
const inputEdited = useRef(false);
|
||||
const {
|
||||
hideFeedbackModalForLikedQueries,
|
||||
userPrompt,
|
||||
setUserPrompt,
|
||||
generatedQuery,
|
||||
setGeneratedQuery,
|
||||
query,
|
||||
setQuery,
|
||||
selectedQuery,
|
||||
setSelectedQuery,
|
||||
isGeneratingQuery,
|
||||
setIsGeneratingQuery,
|
||||
isExecuting,
|
||||
setIsExecuting,
|
||||
likeQuery,
|
||||
setLikeQuery,
|
||||
dislikeQuery,
|
||||
setDislikeQuery,
|
||||
showCallout,
|
||||
setShowCallout,
|
||||
showSamplePrompts,
|
||||
setShowSamplePrompts,
|
||||
queryIterator,
|
||||
setQueryIterator,
|
||||
queryResults,
|
||||
setQueryResults,
|
||||
errorMessage,
|
||||
setErrorMessage,
|
||||
isSamplePromptsOpen,
|
||||
setIsSamplePromptsOpen,
|
||||
showDeletePopup,
|
||||
setShowDeletePopup,
|
||||
showFeedbackBar,
|
||||
setShowFeedbackBar,
|
||||
showCopyPopup,
|
||||
setshowCopyPopup,
|
||||
showErrorMessageBar,
|
||||
setShowErrorMessageBar,
|
||||
generatedQueryComments,
|
||||
setGeneratedQueryComments,
|
||||
} = useQueryCopilot();
|
||||
|
||||
const sampleProps: SamplePromptsProps = {
|
||||
isSamplePromptsOpen: isSamplePromptsOpen,
|
||||
@@ -114,43 +142,88 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
||||
};
|
||||
|
||||
const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`);
|
||||
const cachedHistories = cachedHistoriesString?.split(",");
|
||||
const cachedHistories = cachedHistoriesString?.split("|");
|
||||
const [histories, setHistories] = useState<string[]>(cachedHistories || []);
|
||||
const suggestedPrompts: SuggestedPrompt[] = [
|
||||
{ id: 1, text: 'Show all products that have the word "ultra" in the name or description' },
|
||||
{ id: 2, text: "What are all of the possible categories for the products, and their counts?" },
|
||||
{ id: 3, text: 'Show me all products that have been reviewed by someone with a username that contains "bob"' },
|
||||
];
|
||||
const [filteredHistories, setFilteredHistories] = useState<string[]>(histories);
|
||||
const [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState<SuggestedPrompt[]>(suggestedPrompts);
|
||||
|
||||
const handleUserPromptChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
inputEdited.current = true;
|
||||
const { value } = event.target;
|
||||
setUserPrompt(value);
|
||||
|
||||
// Filter history prompts
|
||||
const filteredHistory = histories.filter((history) => history.toLowerCase().includes(value.toLowerCase()));
|
||||
setFilteredHistories(filteredHistory);
|
||||
|
||||
// Filter suggested prompts
|
||||
const filteredSuggested = suggestedPrompts.filter((prompt) =>
|
||||
prompt.text.toLowerCase().includes(value.toLowerCase())
|
||||
);
|
||||
setFilteredSuggestedPrompts(filteredSuggested);
|
||||
};
|
||||
|
||||
const updateHistories = (): void => {
|
||||
const newHistories = histories.length < 3 ? [userPrompt, ...histories] : [userPrompt, histories[1], histories[2]];
|
||||
const formattedUserPrompt = userPrompt.replace(/\s+/g, " ").trim();
|
||||
const existingHistories = histories.map((history) => history.replace(/\s+/g, " ").trim());
|
||||
|
||||
const updatedHistories = existingHistories.filter(
|
||||
(history) => history.toLowerCase() !== formattedUserPrompt.toLowerCase()
|
||||
);
|
||||
const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)];
|
||||
|
||||
setHistories(newHistories);
|
||||
localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join(","));
|
||||
localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join("|"));
|
||||
};
|
||||
|
||||
const generateSQLQuery = async (): Promise<void> => {
|
||||
try {
|
||||
setIsGeneratingQuery(true);
|
||||
useTabs.getState().setIsTabExecuting(true);
|
||||
useTabs.getState().setIsQueryErrorThrown(false);
|
||||
const payload = {
|
||||
containerSchema: QueryCopilotSampleContainerSchema,
|
||||
userPrompt: userPrompt,
|
||||
};
|
||||
setShowDeletePopup(false);
|
||||
const response = await fetch("https://copilotorchestrater.azurewebsites.net/generateSQLQuery", {
|
||||
useQueryCopilot.getState().refreshCorrelationId();
|
||||
const serverInfo = useNotebook.getState().notebookServerInfo;
|
||||
const response = await fetch(`${serverInfo.notebookServerEndpoint}generateSQLQuery`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
|
||||
if (generateSQLQueryResponse?.sql) {
|
||||
let query = `-- **Prompt:** ${userPrompt}\r\n`;
|
||||
if (generateSQLQueryResponse.explanation) {
|
||||
query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`;
|
||||
if (response.ok) {
|
||||
if (generateSQLQueryResponse?.sql) {
|
||||
let query = `-- **Prompt:** ${userPrompt}\r\n`;
|
||||
if (generateSQLQueryResponse.explanation) {
|
||||
query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`;
|
||||
}
|
||||
query += generateSQLQueryResponse.sql;
|
||||
setQuery(query);
|
||||
setGeneratedQuery(generateSQLQueryResponse.sql);
|
||||
setGeneratedQueryComments(generateSQLQueryResponse.explanation);
|
||||
setShowErrorMessageBar(false);
|
||||
}
|
||||
query += generateSQLQueryResponse.sql;
|
||||
setQuery(query);
|
||||
setGeneratedQuery(generateSQLQueryResponse.sql);
|
||||
} else {
|
||||
handleError(JSON.stringify(generateSQLQueryResponse), "copilotInternalServerError");
|
||||
useTabs.getState().setIsQueryErrorThrown(true);
|
||||
setShowErrorMessageBar(true);
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, "executeNaturalLanguageQuery");
|
||||
useTabs.getState().setIsQueryErrorThrown(true);
|
||||
setShowErrorMessageBar(true);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsGeneratingQuery(false);
|
||||
@@ -160,8 +233,15 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
||||
};
|
||||
|
||||
const onExecuteQueryClick = async (): Promise<void> => {
|
||||
traceStart(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
||||
correlationId: useQueryCopilot.getState().correlationId,
|
||||
userPrompt: userPrompt,
|
||||
generatedQuery: generatedQuery,
|
||||
generatedQueryComments: generatedQueryComments,
|
||||
executedQuery: selectedQuery || query,
|
||||
});
|
||||
const queryToExecute = selectedQuery || query;
|
||||
const queryIterator = queryDocuments(QueryCopilotSampleDatabaseId, QueryCopilotSampleContainerId, queryToExecute, {
|
||||
const queryIterator = querySampleDocuments(queryToExecute, {
|
||||
enableCrossPartitionQuery: shouldEnableCrossPartitionKey(),
|
||||
} as FeedOptions);
|
||||
setQueryIterator(queryIterator);
|
||||
@@ -175,6 +255,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
||||
try {
|
||||
setIsExecuting(true);
|
||||
useTabs.getState().setIsTabExecuting(true);
|
||||
useTabs.getState().setIsQueryErrorThrown(false);
|
||||
const queryResults: QueryResults = await queryPagesUntilContentPresent(
|
||||
firstItemIndex,
|
||||
async (firstItemIndex: number) =>
|
||||
@@ -183,10 +264,20 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
||||
|
||||
setQueryResults(queryResults);
|
||||
setErrorMessage("");
|
||||
setShowErrorMessageBar(false);
|
||||
traceSuccess(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
||||
correlationId: useQueryCopilot.getState().correlationId,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
traceFailure(Action.ExecuteQueryGeneratedFromQueryCopilot, {
|
||||
correlationId: useQueryCopilot.getState().correlationId,
|
||||
errorMessage: errorMessage,
|
||||
});
|
||||
setErrorMessage(errorMessage);
|
||||
handleError(errorMessage, "executeQueryCopilotTab");
|
||||
useTabs.getState().setIsQueryErrorThrown(true);
|
||||
setShowErrorMessageBar(true);
|
||||
} finally {
|
||||
setIsExecuting(false);
|
||||
useTabs.getState().setIsTabExecuting(false);
|
||||
@@ -202,6 +293,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
||||
commandButtonLabel: executeQueryBtnLabel,
|
||||
ariaLabel: executeQueryBtnLabel,
|
||||
hasPopup: false,
|
||||
disabled: query?.trim() === "",
|
||||
};
|
||||
|
||||
const saveQueryBtn = {
|
||||
@@ -212,248 +304,338 @@ export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
||||
commandButtonLabel: "Save Query",
|
||||
ariaLabel: "Save Query",
|
||||
hasPopup: false,
|
||||
disabled: query?.trim() === "",
|
||||
};
|
||||
|
||||
const samplePromptsBtn = {
|
||||
iconSrc: SamplePromptsIcon,
|
||||
iconAlt: "Sample Prompts",
|
||||
onCommandClick: () => setIsSamplePromptsOpen(true),
|
||||
commandButtonLabel: "Sample Prompts",
|
||||
ariaLabel: "Sample Prompts",
|
||||
hasPopup: false,
|
||||
};
|
||||
// Sample Prompts temporary disabled due current design
|
||||
// const samplePromptsBtn = {
|
||||
// iconSrc: SamplePromptsIcon,
|
||||
// iconAlt: "Sample Prompts",
|
||||
// onCommandClick: () => setIsSamplePromptsOpen(true),
|
||||
// commandButtonLabel: "Sample Prompts",
|
||||
// ariaLabel: "Sample Prompts",
|
||||
// hasPopup: false,
|
||||
// };
|
||||
|
||||
return [executeQueryBtn, saveQueryBtn, samplePromptsBtn];
|
||||
return [executeQueryBtn, saveQueryBtn];
|
||||
};
|
||||
const showTeachingBubble = (): void => {
|
||||
const shouldShowTeachingBubble = !inputEdited.current && userPrompt.trim() === "";
|
||||
if (shouldShowTeachingBubble) {
|
||||
setTimeout(() => {
|
||||
if (shouldShowTeachingBubble) {
|
||||
toggleCopilotTeachingBubbleVisible();
|
||||
inputEdited.current = true;
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
};
|
||||
|
||||
const resetButtonState = () => {
|
||||
setDislikeQuery(false);
|
||||
setLikeQuery(false);
|
||||
setShowCallout(false);
|
||||
};
|
||||
|
||||
const startGenerateQueryProcess = () => {
|
||||
updateHistories();
|
||||
generateSQLQuery();
|
||||
resetButtonState();
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
useCommandBar.getState().setContextButtons(getCommandbarButtons());
|
||||
}, [query, selectedQuery]);
|
||||
|
||||
return (
|
||||
<Stack className="tab-pane" style={{ padding: 24, width: "100%", height: "100%" }}>
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<Image src={CopilotIcon} />
|
||||
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
|
||||
</Stack>
|
||||
<Stack horizontal verticalAlign="center" style={{ marginTop: 16, width: "100%" }}>
|
||||
<TextField
|
||||
id="naturalLanguageInput"
|
||||
value={userPrompt}
|
||||
onChange={(_, newValue) => setUserPrompt(newValue)}
|
||||
style={{ lineHeight: 30 }}
|
||||
styles={{ root: { width: "95%" } }}
|
||||
disabled={isGeneratingQuery}
|
||||
onClick={() => setShowSamplePrompts(true)}
|
||||
/>
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Send" }}
|
||||
disabled={isGeneratingQuery}
|
||||
style={{ marginLeft: 8 }}
|
||||
onClick={() => {
|
||||
updateHistories();
|
||||
generateSQLQuery();
|
||||
}}
|
||||
/>
|
||||
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
|
||||
{showSamplePrompts && (
|
||||
<Callout
|
||||
styles={{ root: { minWidth: 400 } }}
|
||||
style={{ padding: "8px 0" }}
|
||||
target="#naturalLanguageInput"
|
||||
isBeakVisible={false}
|
||||
onDismiss={() => setShowSamplePrompts(false)}
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
>
|
||||
<Stack>
|
||||
{histories?.length > 0 && (
|
||||
<Stack>
|
||||
<Text
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: "#0078D4",
|
||||
marginLeft: 16,
|
||||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
Recent
|
||||
</Text>
|
||||
{histories.map((history, i) => (
|
||||
<DefaultButton
|
||||
key={i}
|
||||
onClick={() => {
|
||||
setUserPrompt(history);
|
||||
setShowSamplePrompts(false);
|
||||
}}
|
||||
onRenderIcon={() => <Image src={RecentIcon} />}
|
||||
styles={promptStyles}
|
||||
>
|
||||
{history}
|
||||
</DefaultButton>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
<Text
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: "#0078D4",
|
||||
marginLeft: 16,
|
||||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
Suggested Prompts
|
||||
</Text>
|
||||
<DefaultButton
|
||||
onClick={() => {
|
||||
setUserPrompt("Give me all customers whose names start with C");
|
||||
setShowSamplePrompts(false);
|
||||
}}
|
||||
onRenderIcon={() => <Image src={HintIcon} />}
|
||||
styles={promptStyles}
|
||||
>
|
||||
Give me all customers whose names start with C
|
||||
</DefaultButton>
|
||||
<DefaultButton
|
||||
onClick={() => {
|
||||
setUserPrompt("Show me all customers");
|
||||
setShowSamplePrompts(false);
|
||||
}}
|
||||
onRenderIcon={() => <Image src={HintIcon} />}
|
||||
styles={promptStyles}
|
||||
>
|
||||
Show me all customers
|
||||
</DefaultButton>
|
||||
<DefaultButton
|
||||
onClick={() => {
|
||||
setUserPrompt("Show me all customers who bought a bike in 2019");
|
||||
setShowSamplePrompts(false);
|
||||
}}
|
||||
onRenderIcon={() => <Image src={HintIcon} />}
|
||||
styles={promptStyles}
|
||||
>
|
||||
Show me all customers who bought a bike in 2019
|
||||
</DefaultButton>
|
||||
<Separator styles={{ root: { selectors: { "::before": { background: "#E1DFDD" } }, padding: 0 } }} />
|
||||
<Text
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: 14,
|
||||
marginLeft: 16,
|
||||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
Learn about{" "}
|
||||
<Link target="_blank" href="">
|
||||
writing effective prompts
|
||||
</Link>
|
||||
</Text>
|
||||
</Stack>
|
||||
</Callout>
|
||||
)}
|
||||
</Stack>
|
||||
<Text style={{ marginTop: 8, marginBottom: 24, fontSize: 12 }}>
|
||||
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "}
|
||||
<Link href="" target="_blank">
|
||||
Read preview terms
|
||||
</Link>
|
||||
</Text>
|
||||
React.useEffect(() => {
|
||||
showTeachingBubble();
|
||||
useTabs.getState().setIsQueryErrorThrown(false);
|
||||
}, []);
|
||||
|
||||
{showFeedbackBar ? (
|
||||
<Stack style={{ backgroundColor: "#FFF8F0", padding: "2px 8px" }} horizontal verticalAlign="center">
|
||||
<Text style={{ fontWeight: 600, fontSize: 12 }}>Provide feedback on the query generated</Text>
|
||||
{showCallout && !hideFeedbackModalForLikedQueries && (
|
||||
<Callout
|
||||
style={{ padding: 8 }}
|
||||
target="#likeBtn"
|
||||
onDismiss={() => {
|
||||
setShowCallout(false);
|
||||
submitFeedback({ generatedQuery, likeQuery, description: "", userPrompt: userPrompt });
|
||||
}}
|
||||
directionalHint={DirectionalHint.topCenter}
|
||||
return (
|
||||
<Stack className="tab-pane" style={{ padding: 24, width: "100%" }}>
|
||||
<div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}>
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<Image src={CopilotIcon} />
|
||||
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
|
||||
</Stack>
|
||||
<Stack horizontal verticalAlign="center" style={{ marginTop: 16, width: "100%", position: "relative" }}>
|
||||
<TextField
|
||||
id="naturalLanguageInput"
|
||||
value={userPrompt}
|
||||
onChange={handleUserPromptChange}
|
||||
onClick={() => {
|
||||
inputEdited.current = true;
|
||||
setShowSamplePrompts(true);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
startGenerateQueryProcess();
|
||||
}
|
||||
}}
|
||||
style={{ lineHeight: 30 }}
|
||||
styles={{ root: { width: "95%" } }}
|
||||
disabled={isGeneratingQuery}
|
||||
autoComplete="off"
|
||||
/>
|
||||
{copilotTeachingBubbleVisible && (
|
||||
<TeachingBubble
|
||||
calloutProps={{ directionalHint: DirectionalHint.bottomCenter }}
|
||||
target="#naturalLanguageInput"
|
||||
hasCloseButton={true}
|
||||
closeButtonAriaLabel="Close"
|
||||
onDismiss={toggleCopilotTeachingBubbleVisible}
|
||||
hasSmallHeadline={true}
|
||||
headline="Write a prompt"
|
||||
>
|
||||
<Text>
|
||||
Thank you. Need to give{" "}
|
||||
<Link
|
||||
onClick={() => {
|
||||
setShowCallout(false);
|
||||
useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userPrompt);
|
||||
}}
|
||||
>
|
||||
more feedback?
|
||||
</Link>
|
||||
</Text>
|
||||
</Callout>
|
||||
Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "}
|
||||
<Link
|
||||
onClick={() => {
|
||||
setShowSamplePrompts(true);
|
||||
toggleCopilotTeachingBubbleVisible();
|
||||
}}
|
||||
style={{ color: "white", fontWeight: 600 }}
|
||||
>
|
||||
sample prompts
|
||||
</Link>{" "}
|
||||
or write your own query
|
||||
</TeachingBubble>
|
||||
)}
|
||||
<IconButton
|
||||
id="likeBtn"
|
||||
style={{ marginLeft: 20 }}
|
||||
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
|
||||
onClick={() => {
|
||||
setLikeQuery(true);
|
||||
setShowCallout(true);
|
||||
}}
|
||||
iconProps={{ iconName: "Send" }}
|
||||
disabled={isGeneratingQuery || !userPrompt.trim()}
|
||||
style={{ marginLeft: 8 }}
|
||||
onClick={() => startGenerateQueryProcess()}
|
||||
/>
|
||||
<IconButton
|
||||
style={{ margin: "0 10px" }}
|
||||
iconProps={{ iconName: likeQuery === false ? "DislikeSolid" : "Dislike" }}
|
||||
onClick={() => {
|
||||
setLikeQuery(false);
|
||||
setShowCallout(false);
|
||||
useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userPrompt);
|
||||
}}
|
||||
/>
|
||||
<Separator vertical style={{ color: "#EDEBE9" }} />
|
||||
<CommandBarButton
|
||||
onClick={copyGeneratedCode}
|
||||
iconProps={{ iconName: "Copy" }}
|
||||
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
||||
>
|
||||
Copy code
|
||||
</CommandBarButton>
|
||||
<CommandBarButton
|
||||
onClick={() => setShowDeletePopup(true)}
|
||||
iconProps={{ iconName: "Delete" }}
|
||||
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
||||
>
|
||||
Delete code
|
||||
</CommandBarButton>
|
||||
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
|
||||
{showSamplePrompts && (
|
||||
<Callout
|
||||
styles={{ root: { minWidth: 400 } }}
|
||||
target="#naturalLanguageInput"
|
||||
isBeakVisible={false}
|
||||
onDismiss={() => setShowSamplePrompts(false)}
|
||||
directionalHintFixed={true}
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
alignTargetEdge={true}
|
||||
gapSpace={4}
|
||||
>
|
||||
<Stack>
|
||||
{filteredHistories?.length > 0 && (
|
||||
<Stack>
|
||||
<Text
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: "#0078D4",
|
||||
marginLeft: 16,
|
||||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
Recent
|
||||
</Text>
|
||||
{filteredHistories.map((history, i) => (
|
||||
<DefaultButton
|
||||
key={i}
|
||||
onClick={() => {
|
||||
setUserPrompt(history);
|
||||
setShowSamplePrompts(false);
|
||||
}}
|
||||
onRenderIcon={() => <Image src={RecentIcon} />}
|
||||
styles={promptStyles}
|
||||
>
|
||||
{history}
|
||||
</DefaultButton>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
{filteredSuggestedPrompts?.length > 0 && (
|
||||
<Stack>
|
||||
<Text
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: "#0078D4",
|
||||
marginLeft: 16,
|
||||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
Suggested Prompts
|
||||
</Text>
|
||||
{filteredSuggestedPrompts.map((prompt) => (
|
||||
<DefaultButton
|
||||
key={prompt.id}
|
||||
onClick={() => {
|
||||
setUserPrompt(prompt.text);
|
||||
setShowSamplePrompts(false);
|
||||
}}
|
||||
onRenderIcon={() => <Image src={HintIcon} />}
|
||||
styles={promptStyles}
|
||||
>
|
||||
{prompt.text}
|
||||
</DefaultButton>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
{(filteredHistories?.length > 0 || filteredSuggestedPrompts?.length > 0) && (
|
||||
<Stack>
|
||||
<Separator
|
||||
styles={{
|
||||
root: {
|
||||
selectors: { "::before": { background: "#E1DFDD" } },
|
||||
padding: 0,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: 14,
|
||||
marginLeft: 16,
|
||||
padding: "4px 0",
|
||||
}}
|
||||
>
|
||||
Learn about{" "}
|
||||
<Link target="_blank" href="http://aka.ms/cdb-copilot-writing">
|
||||
writing effective prompts
|
||||
</Link>
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Callout>
|
||||
)}
|
||||
</Stack>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<Stack className="tabPaneContentContainer">
|
||||
<SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}>
|
||||
<EditorReact
|
||||
language={"sql"}
|
||||
content={query}
|
||||
isReadOnly={false}
|
||||
ariaLabel={"Editing Query"}
|
||||
lineNumbers={"on"}
|
||||
onContentChanged={(newQuery: string) => setQuery(newQuery)}
|
||||
onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)}
|
||||
<Stack style={{ marginTop: 8, marginBottom: 24 }}>
|
||||
<Text style={{ fontSize: 12 }}>
|
||||
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "}
|
||||
<Link href="http://aka.ms/cdb-copilot-preview-terms" target="_blank">
|
||||
Read preview terms
|
||||
</Link>
|
||||
{showErrorMessageBar && (
|
||||
<Stack style={{ backgroundColor: "#FEF0F1", padding: "4px 8px" }} horizontal verticalAlign="center">
|
||||
<Image src={XErrorMessage} style={{ marginRight: "8px" }} />
|
||||
<Text style={{ fontSize: 12 }}>
|
||||
We ran into an error and were not able to execute query. Please try again after sometime
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
{showFeedbackBar && (
|
||||
<Stack style={{ backgroundColor: "#FFF8F0", padding: "2px 8px" }} horizontal verticalAlign="center">
|
||||
<Text style={{ fontWeight: 600, fontSize: 12 }}>Provide feedback on the query generated</Text>
|
||||
{showCallout && !hideFeedbackModalForLikedQueries && (
|
||||
<Callout
|
||||
style={{ padding: 8 }}
|
||||
target="#likeBtn"
|
||||
onDismiss={() => {
|
||||
setShowCallout(false);
|
||||
submitFeedback({
|
||||
generatedQuery: generatedQuery,
|
||||
likeQuery: 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
|
||||
id="likeBtn"
|
||||
style={{ marginLeft: 20 }}
|
||||
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
|
||||
onClick={() => {
|
||||
setShowCallout(!likeQuery);
|
||||
setLikeQuery(!likeQuery);
|
||||
if (dislikeQuery) {
|
||||
setDislikeQuery(!dislikeQuery);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
style={{ margin: "0 10px" }}
|
||||
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
|
||||
onClick={() => {
|
||||
if (!dislikeQuery) {
|
||||
useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userPrompt);
|
||||
setLikeQuery(false);
|
||||
}
|
||||
setDislikeQuery(!dislikeQuery);
|
||||
setShowCallout(false);
|
||||
}}
|
||||
/>
|
||||
<Separator vertical style={{ color: "#EDEBE9" }} />
|
||||
<CommandBarButton
|
||||
onClick={copyGeneratedCode}
|
||||
iconProps={{ iconName: "Copy" }}
|
||||
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
||||
>
|
||||
Copy code
|
||||
</CommandBarButton>
|
||||
<CommandBarButton
|
||||
onClick={() => {
|
||||
setShowDeletePopup(true);
|
||||
}}
|
||||
iconProps={{ iconName: "Delete" }}
|
||||
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
|
||||
>
|
||||
Delete code
|
||||
</CommandBarButton>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Stack className="tabPaneContentContainer">
|
||||
<SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}>
|
||||
<EditorReact
|
||||
language={"sql"}
|
||||
content={query}
|
||||
isReadOnly={false}
|
||||
ariaLabel={"Editing Query"}
|
||||
lineNumbers={"on"}
|
||||
onContentChanged={(newQuery: string) => setQuery(newQuery)}
|
||||
onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)}
|
||||
/>
|
||||
<QueryResultSection
|
||||
isMongoDB={false}
|
||||
queryEditorContent={selectedQuery || query}
|
||||
error={errorMessage}
|
||||
queryResults={queryResults}
|
||||
isExecuting={isExecuting}
|
||||
executeQueryDocumentsPage={(firstItemIndex: number) =>
|
||||
queryDocumentsPerPage(firstItemIndex, queryIterator)
|
||||
}
|
||||
/>
|
||||
</SplitterLayout>
|
||||
</Stack>
|
||||
<WelcomeModal visible={localStorage.getItem("hideWelcomeModal") !== "true"} />
|
||||
{isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />}
|
||||
{query !== "" && query.trim().length !== 0 && (
|
||||
<DeletePopup
|
||||
showDeletePopup={showDeletePopup}
|
||||
setShowDeletePopup={setShowDeletePopup}
|
||||
setQuery={setQuery}
|
||||
clearFeedback={resetButtonState}
|
||||
showFeedbackBar={setShowFeedbackBar}
|
||||
/>
|
||||
<QueryResultSection
|
||||
isMongoDB={false}
|
||||
queryEditorContent={selectedQuery || query}
|
||||
error={errorMessage}
|
||||
queryResults={queryResults}
|
||||
isExecuting={isExecuting}
|
||||
executeQueryDocumentsPage={(firstItemIndex: number) => queryDocumentsPerPage(firstItemIndex, queryIterator)}
|
||||
/>
|
||||
</SplitterLayout>
|
||||
</Stack>
|
||||
{isSamplePromptsOpen ? <SamplePrompts sampleProps={sampleProps} /> : <></>}
|
||||
{query !== "" && query.trim().length !== 0 ? (
|
||||
<DeletePopup showDeletePopup={showDeletePopup} setShowDeletePopup={setShowDeletePopup} setQuery={setQuery} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<CopyPopup showCopyPopup={showCopyPopup} setShowCopyPopup={setshowCopyPopup} />
|
||||
)}
|
||||
<CopyPopup showCopyPopup={showCopyPopup} setShowCopyPopup={setshowCopyPopup} />
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
225
src/Explorer/QueryCopilot/QueryCopilotUtilities.test.tsx
Normal file
225
src/Explorer/QueryCopilot/QueryCopilotUtilities.test.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import { FeedOptions } from "@azure/cosmos";
|
||||
import { QueryCopilotSampleContainerSchema } from "Common/Constants";
|
||||
import { handleError } from "Common/ErrorHandlingUtils";
|
||||
import { sampleDataClient } from "Common/SampleDataClient";
|
||||
import * as commonUtils from "Common/dataAccess/queryDocuments";
|
||||
import DocumentId from "Explorer/Tree/DocumentId";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { querySampleDocuments, readSampleDocument, submitFeedback } 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(),
|
||||
logConsoleError: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("@azure/cosmos", () => ({
|
||||
FeedOptions: jest.fn(),
|
||||
QueryIterator: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("Common/ErrorHandlingUtils", () => ({
|
||||
handleError: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("Utils/NotificationConsoleUtils", () => ({
|
||||
logConsoleProgress: jest.fn().mockReturnValue((): void => undefined),
|
||||
}));
|
||||
|
||||
jest.mock("Common/dataAccess/queryDocuments", () => ({
|
||||
getCommonQueryOptions: jest.fn((options) => options),
|
||||
}));
|
||||
|
||||
jest.mock("Common/SampleDataClient");
|
||||
|
||||
jest.mock("node-fetch");
|
||||
|
||||
describe("QueryCopilotUtilities", () => {
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
describe("submitFeedback", () => {
|
||||
const payload = {
|
||||
like: "like",
|
||||
generatedSql: "GeneratedQuery",
|
||||
userPrompt: "UserPrompt",
|
||||
description: "Description",
|
||||
contact: "Contact",
|
||||
containerSchema: QueryCopilotSampleContainerSchema,
|
||||
};
|
||||
|
||||
it("should call fetch with the payload with like", async () => {
|
||||
const mockFetch = jest.fn().mockResolvedValueOnce({});
|
||||
|
||||
globalThis.fetch = mockFetch;
|
||||
useQueryCopilot.getState().refreshCorrelationId();
|
||||
|
||||
await submitFeedback({
|
||||
likeQuery: true,
|
||||
generatedQuery: "GeneratedQuery",
|
||||
userPrompt: "UserPrompt",
|
||||
description: "Description",
|
||||
contact: "Contact",
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
"https://copilotorchestrater.azurewebsites.net/feedback",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
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;
|
||||
useQueryCopilot.getState().refreshCorrelationId();
|
||||
|
||||
await submitFeedback({
|
||||
likeQuery: false,
|
||||
generatedQuery: "GeneratedQuery",
|
||||
userPrompt: "UserPrompt",
|
||||
description: undefined,
|
||||
contact: undefined,
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
"https://copilotorchestrater.azurewebsites.net/feedback",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
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({
|
||||
likeQuery: true,
|
||||
generatedQuery: "GeneratedQuery",
|
||||
userPrompt: "UserPrompt",
|
||||
description: "Description",
|
||||
contact: "Contact",
|
||||
}).catch((error) => {
|
||||
expect(error.message).toEqual("Mock error");
|
||||
});
|
||||
|
||||
expect(handleError).toHaveBeenCalledWith(new Error("Mock error"), expect.any(String));
|
||||
});
|
||||
});
|
||||
|
||||
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,5 +1,16 @@
|
||||
import { QueryCopilotSampleContainerSchema } from "Common/Constants";
|
||||
import { FeedOptions, Item, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import {
|
||||
QueryCopilotSampleContainerId,
|
||||
QueryCopilotSampleContainerSchema,
|
||||
QueryCopilotSampleDatabaseId,
|
||||
} from "Common/Constants";
|
||||
import { handleError } from "Common/ErrorHandlingUtils";
|
||||
import { sampleDataClient } from "Common/SampleDataClient";
|
||||
import { getPartitionKeyValue } from "Common/dataAccess/getPartitionKeyValue";
|
||||
import { getCommonQueryOptions } from "Common/dataAccess/queryDocuments";
|
||||
import DocumentId from "Explorer/Tree/DocumentId";
|
||||
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
|
||||
interface FeedbackParams {
|
||||
likeQuery: boolean;
|
||||
@@ -21,16 +32,42 @@ export const submitFeedback = async (params: FeedbackParams): Promise<void> => {
|
||||
contact: contact || "",
|
||||
};
|
||||
|
||||
const response = await fetch("https://copilotorchestrater.azurewebsites.net/feedback", {
|
||||
await fetch("https://copilotorchestrater.azurewebsites.net/feedback", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-ms-correlationid": useQueryCopilot.getState().correlationId,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(response);
|
||||
} catch (error) {
|
||||
handleError(error, "copilotSubmitFeedback");
|
||||
}
|
||||
};
|
||||
|
||||
export const querySampleDocuments = (query: string, options: FeedOptions): QueryIterator<ItemDefinition & Resource> => {
|
||||
options = getCommonQueryOptions(options);
|
||||
return sampleDataClient()
|
||||
.database(QueryCopilotSampleDatabaseId)
|
||||
.container(QueryCopilotSampleContainerId)
|
||||
.items.query(query, options);
|
||||
};
|
||||
|
||||
export const readSampleDocument = async (documentId: DocumentId): Promise<Item> => {
|
||||
const clearMessage = logConsoleProgress(`Reading item ${documentId.id()}`);
|
||||
|
||||
try {
|
||||
const response = await sampleDataClient()
|
||||
.database(QueryCopilotSampleDatabaseId)
|
||||
.container(QueryCopilotSampleContainerId)
|
||||
.item(documentId.id(), getPartitionKeyValue(documentId))
|
||||
.read();
|
||||
|
||||
return response?.resource;
|
||||
} catch (error) {
|
||||
handleError(error, "ReadDocument", `Failed to read item ${documentId.id()}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,26 +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", () => {
|
||||
it("should render properly if isSamplePromptsOpen is true", () => {
|
||||
const sampleProps: SamplePromptsProps = {
|
||||
isSamplePromptsOpen: true,
|
||||
setIsSamplePromptsOpen: () => undefined,
|
||||
setTextBox: () => undefined,
|
||||
};
|
||||
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", () => {
|
||||
const sampleProps: SamplePromptsProps = {
|
||||
isSamplePromptsOpen: false,
|
||||
setIsSamplePromptsOpen: () => undefined,
|
||||
setTextBox: () => undefined,
|
||||
};
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,128 +5,149 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
||||
className="tab-pane"
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
"padding": 24,
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Image
|
||||
src=""
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 16,
|
||||
"fontWeight": 600,
|
||||
"marginLeft": 8,
|
||||
}
|
||||
}
|
||||
>
|
||||
Copilot
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"marginTop": 16,
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
disabled={false}
|
||||
id="naturalLanguageInput"
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"lineHeight": 30,
|
||||
}
|
||||
}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"width": "95%",
|
||||
},
|
||||
}
|
||||
}
|
||||
value="Write a query to return all records in this table"
|
||||
/>
|
||||
<CustomizedIconButton
|
||||
disabled={false}
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Send",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"marginLeft": 8,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 12,
|
||||
"marginBottom": 24,
|
||||
"marginTop": 8,
|
||||
"height": "100%",
|
||||
"overflowY": "auto",
|
||||
}
|
||||
}
|
||||
>
|
||||
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.
|
||||
|
||||
<StyledLinkBase
|
||||
href=""
|
||||
target="_blank"
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
>
|
||||
Read preview terms
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
<Stack
|
||||
className="tabPaneContentContainer"
|
||||
>
|
||||
<t
|
||||
customClassName=""
|
||||
onDragEnd={null}
|
||||
onDragStart={null}
|
||||
onSecondaryPaneSizeChange={null}
|
||||
percentage={false}
|
||||
primaryIndex={0}
|
||||
primaryMinSize={100}
|
||||
secondaryMinSize={200}
|
||||
vertical={true}
|
||||
<Image
|
||||
src=""
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 16,
|
||||
"fontWeight": 600,
|
||||
"marginLeft": 8,
|
||||
}
|
||||
}
|
||||
>
|
||||
Copilot
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
style={
|
||||
Object {
|
||||
"marginTop": 16,
|
||||
"position": "relative",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<EditorReact
|
||||
ariaLabel="Editing Query"
|
||||
content=""
|
||||
isReadOnly={false}
|
||||
language="sql"
|
||||
lineNumbers="on"
|
||||
onContentChanged={[Function]}
|
||||
onContentSelected={[Function]}
|
||||
<StyledTextFieldBase
|
||||
autoComplete="off"
|
||||
disabled={false}
|
||||
id="naturalLanguageInput"
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"lineHeight": 30,
|
||||
}
|
||||
}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"width": "95%",
|
||||
},
|
||||
}
|
||||
}
|
||||
value=""
|
||||
/>
|
||||
<QueryResultSection
|
||||
error=""
|
||||
executeQueryDocumentsPage={[Function]}
|
||||
isExecuting={false}
|
||||
isMongoDB={false}
|
||||
queryEditorContent=""
|
||||
<CustomizedIconButton
|
||||
disabled={true}
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Send",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"marginLeft": 8,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</t>
|
||||
</Stack>
|
||||
<CopyPopup
|
||||
setShowCopyPopup={[Function]}
|
||||
showCopyPopup={false}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
style={
|
||||
Object {
|
||||
"marginBottom": 24,
|
||||
"marginTop": 8,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.
|
||||
|
||||
<StyledLinkBase
|
||||
href="http://aka.ms/cdb-copilot-preview-terms"
|
||||
target="_blank"
|
||||
>
|
||||
Read preview terms
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack
|
||||
className="tabPaneContentContainer"
|
||||
>
|
||||
<t
|
||||
customClassName=""
|
||||
onDragEnd={null}
|
||||
onDragStart={null}
|
||||
onSecondaryPaneSizeChange={null}
|
||||
percentage={false}
|
||||
primaryIndex={0}
|
||||
primaryMinSize={100}
|
||||
secondaryMinSize={200}
|
||||
vertical={true}
|
||||
>
|
||||
<EditorReact
|
||||
ariaLabel="Editing Query"
|
||||
content=""
|
||||
isReadOnly={false}
|
||||
language="sql"
|
||||
lineNumbers="on"
|
||||
onContentChanged={[Function]}
|
||||
onContentSelected={[Function]}
|
||||
/>
|
||||
<QueryResultSection
|
||||
error=""
|
||||
executeQueryDocumentsPage={[Function]}
|
||||
isExecuting={false}
|
||||
isMongoDB={false}
|
||||
queryEditorContent=""
|
||||
/>
|
||||
</t>
|
||||
</Stack>
|
||||
<WelcomeModal
|
||||
visible={true}
|
||||
/>
|
||||
<CopyPopup
|
||||
setShowCopyPopup={[Function]}
|
||||
showCopyPopup={false}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
@@ -97,6 +97,12 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
() => this.setState({}),
|
||||
(state) => state.showResetPasswordBubble
|
||||
),
|
||||
},
|
||||
{
|
||||
dispose: useDatabases.subscribe(
|
||||
() => this.setState({}),
|
||||
(state) => state.sampleDataResourceTokenCollection
|
||||
),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -107,7 +113,11 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
};
|
||||
|
||||
private getSplashScreenButtons = (): JSX.Element => {
|
||||
if (userContext.features.enableCopilot && userContext.apiType === "SQL") {
|
||||
if (
|
||||
useDatabases.getState().sampleDataResourceTokenCollection &&
|
||||
userContext.features.enableCopilot &&
|
||||
userContext.apiType === "SQL"
|
||||
) {
|
||||
return (
|
||||
<Stack style={{ width: "66%", cursor: "pointer", margin: "40px auto" }} tokens={{ childrenGap: 16 }}>
|
||||
<Stack horizontal tokens={{ childrenGap: 16 }}>
|
||||
@@ -137,7 +147,10 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
description={
|
||||
"Copilot is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
|
||||
}
|
||||
onClick={() => useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot)}
|
||||
onClick={() => {
|
||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
|
||||
traceOpen(Action.OpenQueryCopilotFromSplashScreen, { apiType: userContext.apiType });
|
||||
}}
|
||||
/>
|
||||
<SplashScreenButton
|
||||
imgSrc={ConnectIcon}
|
||||
@@ -246,8 +259,9 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
<form className="connectExplorerFormContainer">
|
||||
<div className="splashScreenContainer">
|
||||
<div className="splashScreen">
|
||||
<div
|
||||
<h1
|
||||
className="title"
|
||||
role="heading"
|
||||
aria-label={
|
||||
userContext.apiType === "Postgres"
|
||||
? "Welcome to Azure Cosmos DB for PostgreSQL"
|
||||
@@ -258,7 +272,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
? "Welcome to Azure Cosmos DB for PostgreSQL"
|
||||
: "Welcome to Azure Cosmos DB"}
|
||||
<FeaturePanelLauncher />
|
||||
</div>
|
||||
</h1>
|
||||
<div className="subtitle">
|
||||
{userContext.apiType === "Postgres"
|
||||
? "Get started with our sample datasets, documentation, and additional tools."
|
||||
@@ -581,7 +595,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
<Image src={LinkIcon} alt=" " />
|
||||
<Image src={LinkIcon} alt={item.title} />
|
||||
</Stack>
|
||||
<Text>{item.description}</Text>
|
||||
</Stack>
|
||||
@@ -600,7 +614,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
<li key={`${item.title}${item.description}${index}`}>
|
||||
<Stack style={{ marginBottom: 26 }}>
|
||||
<Stack horizontal>
|
||||
<Image style={{ marginRight: 8 }} src={item.iconSrc} />
|
||||
<Image style={{ marginRight: 8 }} src={item.iconSrc} alt={item.title} />
|
||||
<Link style={{ fontSize: 14 }} onClick={item.onClick} title={item.info}>
|
||||
{item.title}
|
||||
</Link>
|
||||
@@ -720,7 +734,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
<Image src={LinkIcon} />
|
||||
<Image src={LinkIcon} alt={item.title} />
|
||||
</Stack>
|
||||
<Text>{item.description}</Text>
|
||||
</Stack>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { extractPartitionKey, ItemDefinition, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||
import * as ko from "knockout";
|
||||
import Q from "q";
|
||||
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
|
||||
@@ -7,7 +8,12 @@ import NewDocumentIcon from "../../../images/NewDocument.svg";
|
||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||
import UploadIcon from "../../../images/Upload_16x16.svg";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { DocumentsGridMetrics, KeyCodes } from "../../Common/Constants";
|
||||
import {
|
||||
DocumentsGridMetrics,
|
||||
KeyCodes,
|
||||
QueryCopilotSampleContainerId,
|
||||
QueryCopilotSampleDatabaseId,
|
||||
} from "../../Common/Constants";
|
||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
|
||||
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
|
||||
@@ -71,6 +77,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
|
||||
private _documentsIterator: QueryIterator<ItemDefinition & Resource>;
|
||||
private _resourceTokenPartitionKey: string;
|
||||
private _isQueryCopilotSampleContainer: boolean;
|
||||
|
||||
constructor(options: ViewModels.DocumentsTabOptions) {
|
||||
super(options);
|
||||
@@ -317,6 +324,9 @@ export default class DocumentsTab extends TabsBase {
|
||||
this.selectedDocumentContent.subscribe((newContent: string) => this._onEditorContentChange(newContent));
|
||||
|
||||
this.showPartitionKey = this._shouldShowPartitionKey();
|
||||
this._isQueryCopilotSampleContainer =
|
||||
this.collection?.databaseId === QueryCopilotSampleDatabaseId &&
|
||||
this.collection?.id() === QueryCopilotSampleContainerId;
|
||||
}
|
||||
|
||||
private _shouldShowPartitionKey(): boolean {
|
||||
@@ -678,7 +688,6 @@ export default class DocumentsTab extends TabsBase {
|
||||
}
|
||||
|
||||
public createIterator(): QueryIterator<ItemDefinition & Resource> {
|
||||
let filters = this.lastFilterContents();
|
||||
const filter: string = this.filterContent().trim();
|
||||
const query: string = this.buildQuery(filter);
|
||||
let options: any = {};
|
||||
@@ -688,12 +697,16 @@ export default class DocumentsTab extends TabsBase {
|
||||
options.partitionKey = this._resourceTokenPartitionKey;
|
||||
}
|
||||
|
||||
return queryDocuments(this.collection.databaseId, this.collection.id(), query, options);
|
||||
return this._isQueryCopilotSampleContainer
|
||||
? querySampleDocuments(query, options)
|
||||
: queryDocuments(this.collection.databaseId, this.collection.id(), query, options);
|
||||
}
|
||||
|
||||
public async selectDocument(documentId: DocumentId): Promise<void> {
|
||||
this.selectedDocumentId(documentId);
|
||||
const content = await readDocument(this.collection, documentId);
|
||||
const content = await (this._isQueryCopilotSampleContainer
|
||||
? readSampleDocument(documentId)
|
||||
: readDocument(this.collection, documentId));
|
||||
this.initDocumentEditor(documentId, content);
|
||||
}
|
||||
|
||||
@@ -810,7 +823,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.newDocumentButton.enabled(),
|
||||
disabled: !this.newDocumentButton.enabled() || useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
id: "mongoNewDocumentBtn",
|
||||
});
|
||||
}
|
||||
@@ -824,7 +837,8 @@ export default class DocumentsTab extends TabsBase {
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.saveNewDocumentButton.enabled(),
|
||||
disabled:
|
||||
!this.saveNewDocumentButton.enabled() || useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -837,7 +851,9 @@ export default class DocumentsTab extends TabsBase {
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.discardNewDocumentChangesButton.enabled(),
|
||||
disabled:
|
||||
!this.discardNewDocumentChangesButton.enabled() ||
|
||||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -850,7 +866,8 @@ export default class DocumentsTab extends TabsBase {
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.saveExistingDocumentButton.enabled(),
|
||||
disabled:
|
||||
!this.saveExistingDocumentButton.enabled() || useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -863,7 +880,9 @@ export default class DocumentsTab extends TabsBase {
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.discardExisitingDocumentChangesButton.enabled(),
|
||||
disabled:
|
||||
!this.discardExisitingDocumentChangesButton.enabled() ||
|
||||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -876,7 +895,9 @@ export default class DocumentsTab extends TabsBase {
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.deleteExisitingDocumentButton.enabled(),
|
||||
disabled:
|
||||
!this.deleteExisitingDocumentButton.enabled() ||
|
||||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -920,7 +941,9 @@ export default class DocumentsTab extends TabsBase {
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: useSelectedNode.getState().isDatabaseNodeOrNoneSelected(),
|
||||
disabled:
|
||||
useSelectedNode.getState().isDatabaseNodeOrNoneSelected() ||
|
||||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: Quicks
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path: firewallRulesUri,
|
||||
method: "GET",
|
||||
apiVersion: "2020-10-05-privatepreview",
|
||||
apiVersion: "2022-11-08",
|
||||
});
|
||||
const firewallRules: PostgresFirewallRule[] = response?.data?.value || response?.value || [];
|
||||
const isEnabled = firewallRules.some(
|
||||
|
||||
@@ -9,11 +9,13 @@ import { ConnectTab } from "Explorer/Tabs/ConnectTab";
|
||||
import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
|
||||
import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
|
||||
import { userContext } from "UserContext";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||
import ko from "knockout";
|
||||
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||
import loadingIcon from "../../../images/circular_loader_black_16x16.gif";
|
||||
import errorIcon from "../../../images/close-black.svg";
|
||||
import errorQuery from "../../../images/error_no_outline.svg";
|
||||
import { useObservable } from "../../hooks/useObservable";
|
||||
import { ReactTabKind, useTabs } from "../../hooks/useTabs";
|
||||
import TabsBase from "./TabsBase";
|
||||
@@ -67,7 +69,7 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
||||
function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?: ReactTabKind }) {
|
||||
const [hovering, setHovering] = useState(false);
|
||||
const focusTab = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
|
||||
const tabId = tab ? tab.tabId : "connect";
|
||||
const tabId = tab ? tab.tabId : "";
|
||||
|
||||
const getReactTabTitle = (): ko.Observable<string> => {
|
||||
if (tabKind === ReactTabKind.QueryCopilot) {
|
||||
@@ -116,6 +118,14 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
|
||||
{isTabExecuting(tab, tabKind) && (
|
||||
<img className="loadingIcon" title="Loading" src={loadingIcon} alt="Loading" />
|
||||
)}
|
||||
{isQueryErrorThrown(tab, tabKind) && (
|
||||
<img
|
||||
src={errorQuery}
|
||||
title="Error"
|
||||
alt="Error"
|
||||
style={{ marginTop: 4, marginLeft: 4, width: 10, height: 11 }}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
<span className="tabNavText">{useObservable(tab?.tabTitle || getReactTabTitle())}</span>
|
||||
{tabKind !== ReactTabKind.Home && (
|
||||
@@ -149,6 +159,7 @@ const CloseButton = ({
|
||||
onClick={(event: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
|
||||
event.stopPropagation();
|
||||
tab ? tab.onCloseTabButtonClick() : useTabs.getState().closeReactTab(tabKind);
|
||||
tabKind === ReactTabKind.QueryCopilot && useQueryCopilot.getState().resetQueryCopilotStates();
|
||||
}}
|
||||
tabIndex={active ? 0 : undefined}
|
||||
onKeyPress={({ nativeEvent: e }) => tab.onKeyPressClose(undefined, e)}
|
||||
@@ -220,6 +231,19 @@ const isTabExecuting = (tab?: Tab, tabKind?: ReactTabKind): boolean => {
|
||||
return false;
|
||||
};
|
||||
|
||||
const isQueryErrorThrown = (tab?: Tab, tabKind?: ReactTabKind): boolean => {
|
||||
if (
|
||||
!tab?.isExecuting &&
|
||||
tabKind !== undefined &&
|
||||
tabKind !== ReactTabKind.Home &&
|
||||
useTabs.getState()?.isQueryErrorThrown &&
|
||||
!useTabs.getState()?.isTabExecuting
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => {
|
||||
switch (activeReactTab) {
|
||||
case ReactTabKind.Connect:
|
||||
@@ -229,7 +253,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
||||
case ReactTabKind.Quickstart:
|
||||
return <QuickstartTab explorer={explorer} />;
|
||||
case ReactTabKind.QueryCopilot:
|
||||
return <QueryCopilotTab initialInput={useTabs.getState().queryCopilotTabInitialInput} explorer={explorer} />;
|
||||
return <QueryCopilotTab explorer={explorer} />;
|
||||
default:
|
||||
throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export default class TerminalTab extends TabsBase {
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path: firewallRulesUri,
|
||||
method: "GET",
|
||||
apiVersion: "2020-10-05-privatepreview",
|
||||
apiVersion: "2022-11-08",
|
||||
});
|
||||
const firewallRules: DataModels.PostgresFirewallRule[] = response?.data?.value || response?.value || [];
|
||||
const isEnabled = firewallRules.some(
|
||||
|
||||
@@ -2,10 +2,10 @@ import * as ko from "knockout";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { useTabs } from "../../hooks/useTabs";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { useTabs } from "../../hooks/useTabs";
|
||||
import Explorer from "../Explorer";
|
||||
import DocumentsTab from "../Tabs/DocumentsTab";
|
||||
import { NewQueryTab } from "../Tabs/QueryTab/QueryTab";
|
||||
@@ -28,8 +28,9 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
||||
public children: ko.ObservableArray<ViewModels.TreeNode>;
|
||||
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
|
||||
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.container = container;
|
||||
this.databaseId = databaseId;
|
||||
@@ -42,6 +43,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
||||
this.children = ko.observableArray<ViewModels.TreeNode>([]);
|
||||
this.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
|
||||
this.isCollectionExpanded = ko.observable<boolean>(true);
|
||||
this.isSampleCollection = isSampleCollection;
|
||||
}
|
||||
|
||||
public expandCollection(): void {
|
||||
@@ -139,7 +141,7 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
||||
|
||||
documentsTab = new DocumentsTab({
|
||||
partitionKey: this.partitionKey,
|
||||
resourceTokenPartitionKey: userContext.parsedResourceToken.partitionKey,
|
||||
resourceTokenPartitionKey: userContext.parsedResourceToken?.partitionKey,
|
||||
documentIds: ko.observableArray<DocumentId>([]),
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "Items",
|
||||
|
||||
@@ -793,7 +793,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
<AccordionItemComponent title={"MY DATA"} isExpanded={!gitHubNotebooksContentRoot}>
|
||||
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||
</AccordionItemComponent>
|
||||
<AccordionItemComponent title={"SAMPLE DATA"}>
|
||||
<AccordionItemComponent title={"SAMPLE DATA"} containerStyles={{ display: "table" }}>
|
||||
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
|
||||
</AccordionItemComponent>
|
||||
</AccordionComponent>
|
||||
@@ -807,7 +807,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
<AccordionItemComponent title={"MY DATA"} isExpanded={!gitHubNotebooksContentRoot}>
|
||||
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||
</AccordionItemComponent>
|
||||
<AccordionItemComponent title={"SAMPLE DATA"}>
|
||||
<AccordionItemComponent title={"SAMPLE DATA"} containerStyles={{ display: "table" }}>
|
||||
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
|
||||
</AccordionItemComponent>
|
||||
<AccordionItemComponent title={"NOTEBOOKS"}>
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import TabsBase from "Explorer/Tabs/TabsBase";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
import React from "react";
|
||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
|
||||
import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent";
|
||||
|
||||
export const SampleDataTree = ({
|
||||
@@ -9,33 +14,65 @@ export const SampleDataTree = ({
|
||||
}: {
|
||||
sampleDataResourceTokenCollection: ViewModels.CollectionBase;
|
||||
}): JSX.Element => {
|
||||
const [root, setRoot] = useState<TreeNode | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (sampleDataResourceTokenCollection) {
|
||||
const updatedSampleTree: TreeNode = {
|
||||
label: sampleDataResourceTokenCollection.databaseId,
|
||||
isExpanded: true,
|
||||
iconSrc: CosmosDBIcon,
|
||||
children: [
|
||||
{
|
||||
label: sampleDataResourceTokenCollection.id(),
|
||||
iconSrc: CollectionIcon,
|
||||
isExpanded: true,
|
||||
className: "collectionHeader",
|
||||
children: [
|
||||
{
|
||||
label: "Items",
|
||||
},
|
||||
],
|
||||
const buildSampleDataTree = (): TreeNode => {
|
||||
const updatedSampleTree: TreeNode = {
|
||||
label: sampleDataResourceTokenCollection.databaseId,
|
||||
isExpanded: false,
|
||||
iconSrc: CosmosDBIcon,
|
||||
className: "databaseHeader",
|
||||
children: [
|
||||
{
|
||||
label: sampleDataResourceTokenCollection.id(),
|
||||
iconSrc: CollectionIcon,
|
||||
isExpanded: false,
|
||||
className: "collectionHeader",
|
||||
contextMenu: ResourceTreeContextMenuButtonFactory.createSampleCollectionContextMenuButton(),
|
||||
onClick: () => {
|
||||
useSelectedNode.getState().setSelectedNode(sampleDataResourceTokenCollection);
|
||||
useCommandBar.getState().setContextButtons([]);
|
||||
useTabs
|
||||
.getState()
|
||||
.refreshActiveTab(
|
||||
(tab: TabsBase) =>
|
||||
tab.collection?.id() === sampleDataResourceTokenCollection.id() &&
|
||||
tab.collection.databaseId === sampleDataResourceTokenCollection.databaseId
|
||||
);
|
||||
},
|
||||
],
|
||||
};
|
||||
setRoot(updatedSampleTree);
|
||||
}
|
||||
}, [sampleDataResourceTokenCollection]);
|
||||
isSelected: () =>
|
||||
useSelectedNode
|
||||
.getState()
|
||||
.isDataNodeSelected(sampleDataResourceTokenCollection.databaseId, sampleDataResourceTokenCollection.id()),
|
||||
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 {
|
||||
label: undefined,
|
||||
isExpanded: true,
|
||||
children: [updatedSampleTree],
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<TreeComponent className="sampleDataResourceTree" rootNode={root || { label: "Sample data not initialized." }} />
|
||||
<TreeComponent
|
||||
className="dataResourceTree"
|
||||
rootNode={sampleDataResourceTokenCollection ? buildSampleDataTree() : { label: "Sample data not initialized." }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { ConnectionStatusType } from "Common/Constants";
|
||||
import { ConnectionStatusType, QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import create, { UseStore } from "zustand";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { useTabs } from "../hooks/useTabs";
|
||||
|
||||
export interface SelectedNodeState {
|
||||
selectedNode: ViewModels.TreeNode;
|
||||
setSelectedNode: (node: ViewModels.TreeNode) => void;
|
||||
@@ -15,6 +14,7 @@ export interface SelectedNodeState {
|
||||
subnodeKinds?: ViewModels.CollectionTabKind[]
|
||||
) => boolean;
|
||||
isConnectedToContainer: () => boolean;
|
||||
isQueryCopilotCollectionSelected: () => boolean;
|
||||
}
|
||||
|
||||
export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) => ({
|
||||
@@ -65,4 +65,16 @@ export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) =>
|
||||
isConnectedToContainer: (): boolean => {
|
||||
return useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected;
|
||||
},
|
||||
isQueryCopilotCollectionSelected: (): boolean => {
|
||||
const selectedNode = get().selectedNode as ViewModels.CollectionBase;
|
||||
if (
|
||||
selectedNode &&
|
||||
selectedNode.isSampleCollection &&
|
||||
selectedNode.id() === QueryCopilotSampleContainerId &&
|
||||
selectedNode.databaseId === QueryCopilotSampleDatabaseId
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -17,7 +17,8 @@ import "../externals/jquery.typeahead.min.css";
|
||||
import "../externals/jquery.typeahead.min.js";
|
||||
// Image Dependencies
|
||||
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 "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
||||
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
||||
import "../images/favicon.ico";
|
||||
@@ -63,6 +64,7 @@ const App: React.FunctionComponent = () => {
|
||||
const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState<boolean>(true);
|
||||
const isCarouselOpen = useCarousel((state) => state.shouldOpen);
|
||||
const isCopilotCarouselOpen = useCarousel((state) => state.showCopilotCarousel);
|
||||
const shouldShowModal = useQueryCopilot((state) => state.showFeedbackModal);
|
||||
|
||||
const config = useConfig();
|
||||
const explorer = useKnockoutExplorer(config?.platform);
|
||||
@@ -126,7 +128,7 @@ const App: React.FunctionComponent = () => {
|
||||
{<SQLQuickstartTutorial />}
|
||||
{<MongoQuickstartTutorial />}
|
||||
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
|
||||
{<QueryCopilotFeedbackModal />}
|
||||
{shouldShowModal && <QueryCopilotFeedbackModal />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation";
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
import {
|
||||
Areas,
|
||||
ConnectionStatusType,
|
||||
ContainerStatusType,
|
||||
HttpHeaders,
|
||||
HttpStatusCodes,
|
||||
JunoEndpoints,
|
||||
Notebook,
|
||||
} from "../Common/Constants";
|
||||
import { getErrorMessage, getErrorStack } from "../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import {
|
||||
ContainerConnectionInfo,
|
||||
ContainerInfo,
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
} from "../Contracts/DataModels";
|
||||
import { useNotebook } from "../Explorer/Notebook/useNotebook";
|
||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||
|
||||
export class PhoenixClient {
|
||||
@@ -231,8 +230,8 @@ export class PhoenixClient {
|
||||
throw new Error("The Phoenix client was not initialized properly: missing ARM resourcce id");
|
||||
}
|
||||
|
||||
const toolsEndpoint =
|
||||
userContext.features.phoenixEndpoint ?? userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
|
||||
const toolsEndpoint = JunoEndpoints.Test2;
|
||||
// userContext.features.phoenixEndpoint ?? userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
|
||||
|
||||
if (!validateEndpoint(toolsEndpoint, allowedJunoOrigins)) {
|
||||
const error = `${toolsEndpoint} not allowed as tools endpoint`;
|
||||
|
||||
@@ -36,6 +36,7 @@ export type Features = {
|
||||
readonly enableLegacyMongoShellV2Debug: boolean;
|
||||
readonly loadLegacyMongoShellFromBE: boolean;
|
||||
readonly enableCopilot: boolean;
|
||||
readonly enableNPSSurvey: boolean;
|
||||
|
||||
// can be set via both flight and feature flag
|
||||
autoscaleDefault: boolean;
|
||||
@@ -104,6 +105,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
||||
enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"),
|
||||
loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"),
|
||||
enableCopilot: "true" === get("enablecopilot"),
|
||||
enableNPSSurvey: "true" === get("enablenpssurvey"),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -131,6 +131,9 @@ export enum Action {
|
||||
LaunchUITour,
|
||||
CancelUITour,
|
||||
CompleteUITour,
|
||||
OpenQueryCopilotFromSplashScreen,
|
||||
OpenQueryCopilotFromNewQuery,
|
||||
ExecuteQueryGeneratedFromQueryCopilot,
|
||||
}
|
||||
|
||||
export const ActionModifiers = {
|
||||
|
||||
@@ -47,6 +47,10 @@ export class JupyterLabAppFactory {
|
||||
}
|
||||
|
||||
public async createTerminalApp(serverSettings: ServerConnection.ISettings): Promise<ITerminalConnection | undefined> {
|
||||
//Need to add this after we remove passing token through url
|
||||
//const configurationSettings: Partial<ServerConnection.ISettings> = serverSettings;
|
||||
//(configurationSettings.appendToken as boolean) = false;
|
||||
//serverSettings = ServerConnection.makeSettings(configurationSettings);
|
||||
const manager = new TerminalManager({
|
||||
serverSettings: serverSettings,
|
||||
});
|
||||
@@ -64,6 +68,11 @@ export class JupyterLabAppFactory {
|
||||
}
|
||||
}, this);
|
||||
|
||||
let internalSend = session.send;
|
||||
session.send = (message: IMessage) => {
|
||||
message?.content?.push(serverSettings?.token);
|
||||
internalSend.call(session, message);
|
||||
};
|
||||
const term = new Terminal(session, { theme: "dark", shutdownOnClose: true });
|
||||
|
||||
if (!term) {
|
||||
|
||||
@@ -91,7 +91,7 @@ const userContext: UserContext = {
|
||||
collectionCreationDefaults: CollectionCreationDefaults,
|
||||
};
|
||||
|
||||
function isAccountNewerThanThresholdInMs(createdAt: string, threshold: number) {
|
||||
export function isAccountNewerThanThresholdInMs(createdAt: string, threshold: number) {
|
||||
let createdAtMs: number = Date.parse(createdAt);
|
||||
if (isNaN(createdAtMs)) {
|
||||
createdAtMs = 0;
|
||||
|
||||
@@ -38,7 +38,7 @@ function validateEndpointInternal(
|
||||
return valid;
|
||||
}
|
||||
|
||||
export const allowedArmEndpoints: ReadonlyArray<string> = [
|
||||
export const defaultAllowedArmEndpoints: ReadonlyArray<string> = [
|
||||
"https://management.azure.com",
|
||||
"https://management.usgovcloudapi.net",
|
||||
"https://management.chinacloudapi.cn",
|
||||
@@ -46,7 +46,7 @@ export const allowedArmEndpoints: ReadonlyArray<string> = [
|
||||
|
||||
export const allowedAadEndpoints: ReadonlyArray<string> = ["https://login.microsoftonline.com/"];
|
||||
|
||||
export const allowedBackendEndpoints: ReadonlyArray<string> = [
|
||||
export const defaultAllowedBackendEndpoints: ReadonlyArray<string> = [
|
||||
"https://main.documentdb.ext.azure.com",
|
||||
"https://main.documentdb.ext.azure.cn",
|
||||
"https://main.documentdb.ext.azure.us",
|
||||
|
||||
@@ -18,7 +18,10 @@ export const getNetworkSettingsWarningMessage = (): string => {
|
||||
}
|
||||
|
||||
// public network access is disabled
|
||||
if (accountProperties.publicNetworkAccess !== "Enabled") {
|
||||
if (
|
||||
accountProperties.publicNetworkAccess !== "Enabled" &&
|
||||
accountProperties.publicNetworkAccess !== "SecuredByPerimeter"
|
||||
) {
|
||||
return "The Network settings for this account are preventing access from Data Explorer. Please enable public access to proceed.";
|
||||
}
|
||||
|
||||
|
||||
@@ -6,3 +6,9 @@ export const getFullName = (): string => {
|
||||
const { name } = decryptJWTToken(authorizationToken);
|
||||
return name;
|
||||
};
|
||||
|
||||
export const getUserEmail = (): string => {
|
||||
const { authorizationToken } = userContext;
|
||||
const { upn } = decryptJWTToken(authorizationToken);
|
||||
return upn;
|
||||
};
|
||||
|
||||
@@ -62,10 +62,6 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
|
||||
setExplorer(explorer);
|
||||
}
|
||||
}
|
||||
|
||||
if (userContext.features.enableCopilot) {
|
||||
await updateContextForSampleData();
|
||||
}
|
||||
};
|
||||
effect();
|
||||
}, [platform]);
|
||||
@@ -73,6 +69,9 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
|
||||
useEffect(() => {
|
||||
if (explorer) {
|
||||
applyExplorerBindings(explorer);
|
||||
if (userContext.features.enableCopilot) {
|
||||
updateContextForSampleData(explorer);
|
||||
}
|
||||
}
|
||||
}, [explorer]);
|
||||
|
||||
@@ -415,7 +414,7 @@ interface PortalMessage {
|
||||
inputs?: DataExplorerInputsFrame;
|
||||
}
|
||||
|
||||
async function updateContextForSampleData(): Promise<void> {
|
||||
async function updateContextForSampleData(explorer: Explorer): Promise<void> {
|
||||
if (!userContext.features.enableCopilot) {
|
||||
return;
|
||||
}
|
||||
@@ -435,6 +434,8 @@ async function updateContextForSampleData(): Promise<void> {
|
||||
const data: SampledataconnectionResponse = await response.json();
|
||||
const sampleDataConnectionInfo = parseResourceTokenConnectionString(data.connectionString);
|
||||
updateUserContext({ sampleDataConnectionInfo });
|
||||
|
||||
await explorer.refreshSampleData();
|
||||
}
|
||||
|
||||
interface SampledataconnectionResponse {
|
||||
|
||||
@@ -5,21 +5,26 @@ export interface NotificationConsoleState {
|
||||
isExpanded: boolean;
|
||||
inProgressConsoleDataIdToBeDeleted: string;
|
||||
consoleData: ConsoleData | undefined;
|
||||
consoleAnimationFinished: boolean;
|
||||
expandConsole: () => void;
|
||||
// TODO Remove this method. Add a `closeConsole` method instead
|
||||
setIsExpanded: (isExpanded: boolean) => void;
|
||||
// TODO These two methods badly need a refactor. Not very react friendly.
|
||||
setNotificationConsoleData: (consoleData: ConsoleData) => void;
|
||||
setInProgressConsoleDataIdToBeDeleted: (id: string) => void;
|
||||
setConsoleAnimationFinished: (consoleAnimationFinished: boolean) => void;
|
||||
}
|
||||
|
||||
export const useNotificationConsole: UseStore<NotificationConsoleState> = create((set) => ({
|
||||
isExpanded: false,
|
||||
consoleData: undefined,
|
||||
inProgressConsoleDataIdToBeDeleted: "",
|
||||
consoleAnimationFinished: false,
|
||||
expandConsole: () => set((state) => ({ ...state, isExpanded: true })),
|
||||
setIsExpanded: (isExpanded) => set((state) => ({ ...state, isExpanded })),
|
||||
setNotificationConsoleData: (consoleData: ConsoleData) => set((state) => ({ ...state, consoleData })),
|
||||
setInProgressConsoleDataIdToBeDeleted: (id: string) =>
|
||||
set((state) => ({ ...state, inProgressConsoleDataIdToBeDeleted: id })),
|
||||
setConsoleAnimationFinished: (consoleAnimationFinished: boolean) =>
|
||||
set({ consoleAnimationFinished: consoleAnimationFinished }),
|
||||
}));
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { MinimalQueryIterator } from "Common/IteratorUtilities";
|
||||
import { QueryResults } from "Contracts/ViewModels";
|
||||
import { guid } from "Explorer/Tables/Utilities";
|
||||
import create, { UseStore } from "zustand";
|
||||
|
||||
interface QueryCopilotState {
|
||||
@@ -6,20 +9,127 @@ interface QueryCopilotState {
|
||||
userPrompt: string;
|
||||
showFeedbackModal: boolean;
|
||||
hideFeedbackModalForLikedQueries: boolean;
|
||||
correlationId: string;
|
||||
query: string;
|
||||
selectedQuery: string;
|
||||
isGeneratingQuery: boolean;
|
||||
isExecuting: boolean;
|
||||
dislikeQuery: boolean | undefined;
|
||||
showCallout: boolean;
|
||||
showSamplePrompts: boolean;
|
||||
queryIterator: MinimalQueryIterator | undefined;
|
||||
queryResults: QueryResults | undefined;
|
||||
errorMessage: string;
|
||||
isSamplePromptsOpen: boolean;
|
||||
showDeletePopup: boolean;
|
||||
showFeedbackBar: boolean;
|
||||
showCopyPopup: boolean;
|
||||
showErrorMessageBar: boolean;
|
||||
generatedQueryComments: string;
|
||||
|
||||
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) => void;
|
||||
closeFeedbackModal: () => void;
|
||||
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) => void;
|
||||
refreshCorrelationId: () => void;
|
||||
setUserPrompt: (userPrompt: string) => void;
|
||||
setQuery: (query: string) => void;
|
||||
setGeneratedQuery: (generatedQuery: string) => void;
|
||||
setSelectedQuery: (selectedQuery: string) => void;
|
||||
setIsGeneratingQuery: (isGeneratingQuery: boolean) => void;
|
||||
setIsExecuting: (isExecuting: boolean) => void;
|
||||
setLikeQuery: (likeQuery: boolean) => void;
|
||||
setDislikeQuery: (dislikeQuery: boolean | undefined) => void;
|
||||
setShowCallout: (showCallout: boolean) => void;
|
||||
setShowSamplePrompts: (showSamplePrompts: boolean) => void;
|
||||
setQueryIterator: (queryIterator: MinimalQueryIterator | undefined) => void;
|
||||
setQueryResults: (queryResults: QueryResults | undefined) => void;
|
||||
setErrorMessage: (errorMessage: string) => void;
|
||||
setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => void;
|
||||
setShowDeletePopup: (showDeletePopup: boolean) => void;
|
||||
setShowFeedbackBar: (showFeedbackBar: boolean) => void;
|
||||
setshowCopyPopup: (showCopyPopup: boolean) => void;
|
||||
setShowErrorMessageBar: (showErrorMessageBar: boolean) => void;
|
||||
setGeneratedQueryComments: (generatedQueryComments: string) => void;
|
||||
resetQueryCopilotStates: () => void;
|
||||
}
|
||||
|
||||
export const useQueryCopilot: UseStore<QueryCopilotState> = create((set) => ({
|
||||
type QueryCopilotStore = UseStore<QueryCopilotState>;
|
||||
|
||||
export const useQueryCopilot: QueryCopilotStore = create((set) => ({
|
||||
generatedQuery: "",
|
||||
likeQuery: false,
|
||||
userPrompt: "",
|
||||
showFeedbackModal: false,
|
||||
hideFeedbackModalForLikedQueries: false,
|
||||
correlationId: "",
|
||||
query: "",
|
||||
selectedQuery: "",
|
||||
isGeneratingQuery: false,
|
||||
isExecuting: false,
|
||||
dislikeQuery: undefined,
|
||||
showCallout: false,
|
||||
showSamplePrompts: false,
|
||||
queryIterator: undefined,
|
||||
queryResults: undefined,
|
||||
errorMessage: "",
|
||||
isSamplePromptsOpen: false,
|
||||
showDeletePopup: false,
|
||||
showFeedbackBar: false,
|
||||
showCopyPopup: false,
|
||||
showErrorMessageBar: false,
|
||||
generatedQueryComments: "",
|
||||
|
||||
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) =>
|
||||
set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }),
|
||||
closeFeedbackModal: () => set({ generatedQuery: "", likeQuery: false, userPrompt: "", showFeedbackModal: false }),
|
||||
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) =>
|
||||
set({ hideFeedbackModalForLikedQueries }),
|
||||
refreshCorrelationId: () => set({ correlationId: guid() }),
|
||||
setUserPrompt: (userPrompt: string) => set({ userPrompt }),
|
||||
setQuery: (query: string) => set({ query }),
|
||||
setGeneratedQuery: (generatedQuery: string) => set({ generatedQuery }),
|
||||
setSelectedQuery: (selectedQuery: string) => set({ selectedQuery }),
|
||||
setIsGeneratingQuery: (isGeneratingQuery: boolean) => set({ isGeneratingQuery }),
|
||||
setIsExecuting: (isExecuting: boolean) => set({ isExecuting }),
|
||||
setLikeQuery: (likeQuery: boolean) => set({ likeQuery }),
|
||||
setDislikeQuery: (dislikeQuery: boolean | undefined) => set({ dislikeQuery }),
|
||||
setShowCallout: (showCallout: boolean) => set({ showCallout }),
|
||||
setShowSamplePrompts: (showSamplePrompts: boolean) => set({ showSamplePrompts }),
|
||||
setQueryIterator: (queryIterator: MinimalQueryIterator | undefined) => set({ queryIterator }),
|
||||
setQueryResults: (queryResults: QueryResults | undefined) => set({ queryResults }),
|
||||
setErrorMessage: (errorMessage: string) => set({ errorMessage }),
|
||||
setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => set({ isSamplePromptsOpen }),
|
||||
setShowDeletePopup: (showDeletePopup: boolean) => set({ showDeletePopup }),
|
||||
setShowFeedbackBar: (showFeedbackBar: boolean) => set({ showFeedbackBar }),
|
||||
setshowCopyPopup: (showCopyPopup: boolean) => set({ showCopyPopup }),
|
||||
setShowErrorMessageBar: (showErrorMessageBar: boolean) => set({ showErrorMessageBar }),
|
||||
setGeneratedQueryComments: (generatedQueryComments: string) => set({ generatedQueryComments }),
|
||||
|
||||
resetQueryCopilotStates: () => {
|
||||
set((state) => ({
|
||||
...state,
|
||||
generatedQuery: "",
|
||||
likeQuery: false,
|
||||
userPrompt: "",
|
||||
showFeedbackModal: false,
|
||||
hideFeedbackModalForLikedQueries: false,
|
||||
correlationId: "",
|
||||
query: "",
|
||||
selectedQuery: "",
|
||||
isGeneratingQuery: false,
|
||||
isExecuting: false,
|
||||
dislikeQuery: undefined,
|
||||
showCallout: false,
|
||||
showSamplePrompts: false,
|
||||
queryIterator: undefined,
|
||||
queryResults: undefined,
|
||||
errorMessage: "",
|
||||
isSamplePromptsOpen: false,
|
||||
showDeletePopup: false,
|
||||
showFeedbackBar: false,
|
||||
showCopyPopup: false,
|
||||
showErrorMessageBar: false,
|
||||
generatedQueryComments: "",
|
||||
}));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -12,6 +12,7 @@ interface TabsState {
|
||||
networkSettingsWarning: string;
|
||||
queryCopilotTabInitialInput: string;
|
||||
isTabExecuting: boolean;
|
||||
isQueryErrorThrown: boolean;
|
||||
activateTab: (tab: TabsBase) => void;
|
||||
activateNewTab: (tab: TabsBase) => void;
|
||||
activateReactTab: (tabkind: ReactTabKind) => void;
|
||||
@@ -26,6 +27,7 @@ interface TabsState {
|
||||
setNetworkSettingsWarning: (warningMessage: string) => void;
|
||||
setQueryCopilotTabInitialInput: (input: string) => void;
|
||||
setIsTabExecuting: (state: boolean) => void;
|
||||
setIsQueryErrorThrown: (state: boolean) => void;
|
||||
}
|
||||
|
||||
export enum ReactTabKind {
|
||||
@@ -43,6 +45,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||
networkSettingsWarning: "",
|
||||
queryCopilotTabInitialInput: "",
|
||||
isTabExecuting: false,
|
||||
isQueryErrorThrown: false,
|
||||
activateTab: (tab: TabsBase): void => {
|
||||
if (get().openedTabs.some((openedTab) => openedTab.tabId === tab.tabId)) {
|
||||
set({ activeTab: tab, activeReactTab: undefined });
|
||||
@@ -157,4 +160,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||
setIsTabExecuting: (state: boolean) => {
|
||||
set({ isTabExecuting: state });
|
||||
},
|
||||
setIsQueryErrorThrown: (state: boolean) => {
|
||||
set({ isQueryErrorThrown: state });
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { initializeIcons } from "@fluentui/react";
|
||||
import { configure } from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
import "jest-canvas-mock";
|
||||
import { initializeIcons } from "@fluentui/react";
|
||||
import enableHooks from "jest-react-hooks-shallow";
|
||||
import { TextDecoder, TextEncoder } from "util";
|
||||
configure({ adapter: new Adapter() });
|
||||
initializeIcons();
|
||||
@@ -10,6 +11,30 @@ if (typeof window.URL.createObjectURL === "undefined") {
|
||||
Object.defineProperty(window.URL, "createObjectURL", { value: () => {} });
|
||||
}
|
||||
|
||||
enableHooks(jest, { dontMockByDefault: true });
|
||||
|
||||
const localStorageMock = (function () {
|
||||
let store: { [key: string]: string } = {};
|
||||
return {
|
||||
getItem: function (key: string) {
|
||||
return store[key] || null;
|
||||
},
|
||||
setItem: function (key: string, value: string) {
|
||||
store[key] = value.toString();
|
||||
},
|
||||
removeItem: function (key: string) {
|
||||
delete store[key];
|
||||
},
|
||||
clear: function () {
|
||||
store = {};
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
Object.defineProperty(window, "localStorage", {
|
||||
value: localStorageMock,
|
||||
});
|
||||
|
||||
// TODO Remove when jquery and documentdbclient SDK are removed
|
||||
(<any>window).$ = (<any>window).jQuery = require("jquery");
|
||||
(<any>global).$ = (<any>global).$.jQuery = require("jquery");
|
||||
|
||||
@@ -16,8 +16,8 @@ test("Cassandra keyspace and table CRUD", async () => {
|
||||
await explorer.click('[data-test="New Table"]');
|
||||
await explorer.click('[aria-label="Keyspace id"]');
|
||||
await explorer.fill('[aria-label="Keyspace id"]', keyspaceId);
|
||||
await explorer.click('[aria-label="addCollection-tableId"]');
|
||||
await explorer.fill('[aria-label="addCollection-tableId"]', tableId);
|
||||
await explorer.click('[aria-label="addCollection-table Id Create table"]');
|
||||
await explorer.fill('[aria-label="addCollection-table Id Create table"]', tableId);
|
||||
await explorer.click("#sidePanelOkButton");
|
||||
await explorer.click(`.nodeItem >> text=${keyspaceId}`);
|
||||
await explorer.click(`[data-test="${tableId}"] [aria-label="More"]`);
|
||||
|
||||
@@ -39,7 +39,7 @@ test("Resource token", async () => {
|
||||
await page.type("input[class='inputToken']", resourceTokenConnectionString);
|
||||
await page.click("input[value='Connect']");
|
||||
await page.waitForSelector("iframe");
|
||||
const explorer = page.frame({
|
||||
const explorer = await page.frame({
|
||||
name: "explorer",
|
||||
});
|
||||
await explorer.textContent(`css=.dataResourceTree >> "${collectionId}"`);
|
||||
|
||||
Reference in New Issue
Block a user