Compare commits
20 Commits
defect2280
...
users/v-da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
deb33daae3 | ||
|
|
ceed162491 | ||
|
|
f3c96b91bd | ||
|
|
444f1b66fd | ||
|
|
42e24d0e99 | ||
|
|
c11b6838e5 | ||
|
|
1350122f76 | ||
|
|
7c88a4d65b | ||
|
|
0b6cb8ee3d | ||
|
|
15e8c66aa4 | ||
|
|
c606d95765 | ||
|
|
8092841ce5 | ||
|
|
4617fa9364 | ||
|
|
a282ad9242 | ||
|
|
b954b14f56 | ||
|
|
ed9c7dbb6b | ||
|
|
aadbb50e7d | ||
|
|
abff435e88 | ||
|
|
d1c4059320 | ||
|
|
9b214a3171 |
41
SECURITY.md
Normal file
@@ -0,0 +1,41 @@
|
||||
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.7 BLOCK -->
|
||||
|
||||
## Security
|
||||
|
||||
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
|
||||
|
||||
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
**Please do not report security vulnerabilities through public GitHub issues.**
|
||||
|
||||
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).
|
||||
|
||||
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).
|
||||
|
||||
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc).
|
||||
|
||||
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
|
||||
|
||||
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
|
||||
* Full paths of source file(s) related to the manifestation of the issue
|
||||
* The location of the affected source code (tag/branch/commit or direct URL)
|
||||
* Any special configuration required to reproduce the issue
|
||||
* Step-by-step instructions to reproduce the issue
|
||||
* Proof-of-concept or exploit code (if possible)
|
||||
* Impact of the issue, including how an attacker might exploit the issue
|
||||
|
||||
This information will help us triage your report more quickly.
|
||||
|
||||
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.
|
||||
|
||||
## Preferred Languages
|
||||
|
||||
We prefer all communications to be in English.
|
||||
|
||||
## Policy
|
||||
|
||||
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).
|
||||
|
||||
<!-- END MICROSOFT SECURITY.MD BLOCK -->
|
||||
3
images/ComplexPrompts.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.4852 1.13266C6.25055 1.69386 5.20039 2.5918 5.20039 3.8002V7.41652C5.39862 7.40568 5.59878 7.4002 5.80039 7.4002C6.00246 7.4002 6.20261 7.40569 6.40039 7.41649V5.83795C6.72565 6.08075 7.09627 6.29095 7.4852 6.46774C8.77401 7.05356 10.5123 7.4002 12.4004 7.4002C14.2885 7.4002 16.0268 7.05356 17.3156 6.46774C17.7045 6.29095 18.0752 6.08075 18.4004 5.83795V15.8002C18.4004 16.2486 17.9731 16.8507 16.819 17.3753C15.7191 17.8752 14.1574 18.2002 12.4005 18.2002L12.4004 18.7995C12.4004 19.0077 12.379 19.208 12.3392 19.4001L12.4004 19.4002C14.2885 19.4002 16.0268 19.0535 17.3156 18.4677C18.5503 17.9066 19.6004 17.0086 19.6004 15.8002V3.8002C19.6004 2.5918 18.5503 1.69386 17.3156 1.13266C16.0268 0.546827 14.2885 0.200195 12.4004 0.200195C10.5123 0.200195 8.77401 0.546827 7.4852 1.13266ZM7.98176 5.37529C6.82769 4.85071 6.40039 4.24866 6.40039 3.8002C6.40039 3.35173 6.82769 2.74968 7.98176 2.2251C9.08168 1.72513 10.6434 1.4002 12.4004 1.4002C14.1574 1.4002 15.7191 1.72513 16.819 2.2251C17.9731 2.74968 18.4004 3.35173 18.4004 3.8002C18.4004 4.24866 17.9731 4.85071 16.819 5.37529C15.7191 5.87526 14.1574 6.2002 12.4004 6.2002C10.6434 6.2002 9.08168 5.87526 7.98176 5.37529ZM6.40039 8.61851C6.76422 8.64085 7.11714 8.68327 7.4555 8.74374C7.93669 8.82972 8.38843 8.95219 8.80015 9.10529C10.2475 9.64345 11.2004 10.56 11.2004 11.6002C11.2004 13.257 8.78273 14.6002 5.80039 14.6002C2.81805 14.6002 0.400391 13.257 0.400391 11.6002C0.400391 10.056 2.50043 8.78432 5.20039 8.61851C5.39739 8.60641 5.59759 8.6002 5.80039 8.6002C6.00319 8.6002 6.20339 8.60641 6.40039 8.61851ZM11.1112 19.3454C10.6494 20.7416 8.44723 21.7995 5.80039 21.7995C2.81805 21.7995 0.400391 20.4564 0.400391 18.7995V14.0648C0.706223 14.3399 1.04834 14.5755 1.39925 14.7705C2.58615 15.4299 4.14455 15.8002 5.80039 15.8002C7.45623 15.8002 9.01463 15.4299 10.2015 14.7705C10.553 14.5752 10.8957 14.3391 11.202 14.0633C11.2015 15.2351 11.2008 16.9706 11.2005 18.1486V18.1508C11.2004 18.3942 11.2004 18.6137 11.2004 18.7995C11.2004 18.986 11.1698 19.1684 11.1112 19.3454Z" fill="#0078D4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
35
images/Copilot.svg
Normal file
@@ -0,0 +1,35 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="40" height="40" fill="white"/>
|
||||
<path d="M17 3.73926L20 4.04436V12.6862L15.2 13.8013L13.6 15.9967V19.4479C13.6 21.7669 14.8545 23.9045 16.879 25.0353L21.4036 27.5626L13.6 31.69L10.3559 32.1523L7.27902 30.4337C5.25445 29.3028 4 27.1652 4 24.8462V14.7591C4 12.4395 5.25512 10.3015 7.28055 9.17085L16.3174 4.12639L16.3121 4.13012L17 3.73926Z" fill="url(#paint0_radial_420_101763)"/>
|
||||
<path d="M17 3.73926L20 4.04436V12.6862L15.2 13.8013L13.6 15.9967V19.4479C13.6 21.7669 14.8545 23.9045 16.879 25.0353L21.4036 27.5626L13.6 31.69L10.3559 32.1523L7.27902 30.4337C5.25445 29.3028 4 27.1652 4 24.8462V14.7591C4 12.4395 5.25512 10.3015 7.28055 9.17085L16.3174 4.12639L16.3121 4.13012L17 3.73926Z" fill="url(#paint1_linear_420_101763)"/>
|
||||
<path d="M26.399 15.001L34.399 19.801L35.999 21.401V24.8452C35.999 27.1642 34.7446 29.3018 32.72 30.4327L23.12 35.7949C21.1804 36.8784 18.8177 36.8784 16.878 35.7949L7.27804 30.4327C7.09146 30.3284 6.91141 30.2157 6.73828 30.095L7.278 30.3965C9.21762 31.4799 11.5803 31.4799 13.52 30.3965L23.12 25.0342C25.1445 23.9033 26.399 21.7657 26.399 19.4467V15.001Z" fill="url(#paint2_radial_420_101763)"/>
|
||||
<path d="M26.399 15.001L34.399 19.801L35.999 21.401V24.8452C35.999 27.1642 34.7446 29.3018 32.72 30.4327L23.12 35.7949C21.1804 36.8784 18.8177 36.8784 16.878 35.7949L7.27804 30.4327C7.09146 30.3284 6.91141 30.2157 6.73828 30.095L7.278 30.3965C9.21762 31.4799 11.5803 31.4799 13.52 30.3965L23.12 25.0342C25.1445 23.9033 26.399 21.7657 26.399 19.4467V15.001Z" fill="url(#paint3_linear_420_101763)"/>
|
||||
<path d="M32.7191 9.17053L23.119 3.8117C21.1802 2.72943 18.819 2.72943 16.8802 3.8117L16.3151 4.1271C14.6239 5.31737 13.5996 7.26472 13.5996 9.36025V16.0461L16.8802 14.2149C18.819 13.1326 21.1802 13.1326 23.119 14.2149L32.7191 19.5737C34.6984 20.6786 35.9421 22.7456 35.9977 25.0039C35.999 24.9514 35.9996 24.8987 35.9996 24.8459V14.7588C35.9996 12.4392 34.7445 10.3011 32.7191 9.17053Z" fill="url(#paint4_radial_420_101763)"/>
|
||||
<path d="M32.7191 9.17053L23.119 3.8117C21.1802 2.72943 18.819 2.72943 16.8802 3.8117L16.3151 4.1271C14.6239 5.31737 13.5996 7.26472 13.5996 9.36025V16.0461L16.8802 14.2149C18.819 13.1326 21.1802 13.1326 23.119 14.2149L32.7191 19.5737C34.6984 20.6786 35.9421 22.7456 35.9977 25.0039C35.999 24.9514 35.9996 24.8987 35.9996 24.8459V14.7588C35.9996 12.4392 34.7445 10.3011 32.7191 9.17053Z" fill="url(#paint5_linear_420_101763)"/>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_420_101763" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(17.5054 12.8837) rotate(112.544) scale(23.4519 19.3067)">
|
||||
<stop offset="0.206732" stop-color="#45D586"/>
|
||||
<stop offset="0.875628" stop-color="#128245"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint1_linear_420_101763" x1="15.0625" y1="29.6174" x2="13.787" y2="27.2436" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.9999" stop-color="#0F773F"/>
|
||||
<stop offset="1" stop-color="#0078D4" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint2_radial_420_101763" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10.899 27.5214) rotate(-3.9995) scale(24.6197 15.4594)">
|
||||
<stop offset="0.140029" stop-color="#FBFF47"/>
|
||||
<stop offset="0.952721" stop-color="#54B228"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint3_linear_420_101763" x1="30.8932" y1="18.5508" x2="29.5491" y2="20.8921" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.9999" stop-color="#27770B"/>
|
||||
<stop offset="1" stop-color="#8C66BA" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint4_radial_420_101763" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(30.632 19.3878) rotate(-138.33) scale(22.8015 13.0047)">
|
||||
<stop stop-color="#95FEA0"/>
|
||||
<stop offset="0.839255" stop-color="#10B7B7"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint5_linear_420_101763" x1="13.5996" y1="11.7134" x2="16.2131" y2="11.7134" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.9999" stop-color="#0A7B7B"/>
|
||||
<stop offset="1" stop-color="#436DCD" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
3
images/Hint.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.39804 9.80882C4.57428 9.93313 4.78476 9.99968 5.00043 9.9993C5.21633 9.99946 5.42686 9.93197 5.60243 9.8063C5.77993 9.67582 5.91464 9.49552 5.98943 9.2883L6.43643 7.9153C6.55086 7.57101 6.74391 7.25811 7.00028 7.00139C7.25665 6.74467 7.56929 6.5512 7.91343 6.4363L9.30443 5.9853C9.45636 5.93095 9.59364 5.84214 9.70551 5.72586C9.81738 5.60957 9.9008 5.46896 9.94924 5.31503C9.99767 5.16111 10.0098 4.99806 9.98468 4.83867C9.95955 4.67927 9.89786 4.52786 9.80443 4.3963C9.67034 4.21038 9.47939 4.07314 9.26043 4.0053L7.88543 3.5583C7.54091 3.44407 7.22777 3.25111 6.97087 2.99472C6.71396 2.73833 6.52035 2.42558 6.40543 2.0813L5.95343 0.693301C5.88113 0.490997 5.74761 0.316236 5.57143 0.193301C5.43877 0.0995741 5.28607 0.0380931 5.12548 0.0137472C4.96489 -0.0105984 4.80083 0.00286224 4.64636 0.0530596C4.49188 0.103257 4.35125 0.188806 4.23564 0.302903C4.12004 0.417 4.03265 0.556497 3.98043 0.710301L3.52343 2.1103C3.40884 2.44513 3.21967 2.74954 2.97022 3.00055C2.72076 3.25157 2.41753 3.44263 2.08343 3.5593L0.692428 4.0073C0.540653 4.0617 0.403522 4.15048 0.291767 4.26669C0.180011 4.3829 0.0966621 4.5234 0.0482407 4.67719C-0.000180673 4.83097 -0.0123605 4.99388 0.0126534 5.15315C0.0376676 5.31243 0.0991972 5.46376 0.192428 5.5953C0.320272 5.77475 0.501046 5.90972 0.709428 5.9813L2.08343 6.4263C2.52354 6.57278 2.90999 6.84713 3.19343 7.2143C3.35585 7.42494 3.4813 7.66164 3.56443 7.9143L4.01643 9.3053C4.08846 9.50859 4.22179 9.68452 4.39804 9.80882ZM4.48343 2.3943L5.01043 1.0173L5.44943 2.3943C5.61312 2.88745 5.88991 3.33546 6.25767 3.70253C6.62544 4.0696 7.07397 4.34554 7.56743 4.5083L8.97343 5.0373L7.59143 5.4853C7.09866 5.6496 6.65095 5.92646 6.28382 6.29393C5.9167 6.66141 5.64026 7.10938 5.47643 7.6023L4.95343 8.9803L4.50443 7.6013C4.34335 7.10803 4.06943 6.65913 3.70443 6.2903C3.3356 5.92226 2.88653 5.64467 2.39243 5.4793L1.01443 4.9573L2.40043 4.5073C2.88672 4.33867 3.32775 4.06051 3.68943 3.6943C4.04901 3.3266 4.32049 2.88211 4.48343 2.3943ZM10.5353 13.8513C10.6713 13.9475 10.8337 13.9992 11.0003 13.9993C11.1654 13.9994 11.3264 13.9484 11.4613 13.8533C11.6008 13.7548 11.7058 13.6149 11.7613 13.4533L12.0093 12.6913C12.0625 12.5329 12.1515 12.3888 12.2693 12.2703C12.3867 12.1518 12.5307 12.063 12.6893 12.0113L13.4613 11.7593C13.619 11.7048 13.7557 11.6024 13.8523 11.4663C13.9257 11.3633 13.9736 11.2444 13.9921 11.1193C14.0106 10.9942 13.9992 10.8665 13.9588 10.7467C13.9184 10.6268 13.8501 10.5183 13.7597 10.4299C13.6692 10.3415 13.5591 10.2759 13.4383 10.2383L12.6743 9.98933C12.5162 9.93676 12.3724 9.84814 12.2544 9.73048C12.1364 9.61281 12.0473 9.46932 11.9943 9.31133L11.7423 8.53833C11.6886 8.38048 11.586 8.24387 11.4493 8.14833C11.3473 8.07538 11.2295 8.02744 11.1056 8.00838C10.9816 7.98932 10.8549 7.99967 10.7357 8.0386C10.6164 8.07753 10.508 8.14395 10.4192 8.23249C10.3304 8.32104 10.2636 8.42923 10.2243 8.54833L9.97731 9.31033C9.92502 9.46798 9.83747 9.61162 9.72131 9.73033C9.60657 9.8468 9.46665 9.93541 9.31231 9.98933L8.53931 10.2413C8.38025 10.2952 8.2422 10.3978 8.1447 10.5346C8.04721 10.6713 7.99522 10.8353 7.99611 11.0032C7.99699 11.1712 8.0507 11.3346 8.14963 11.4703C8.24856 11.606 8.38769 11.7071 8.54731 11.7593L9.31031 12.0063C9.46917 12.0597 9.61358 12.149 9.73231 12.2673C9.85053 12.3856 9.93896 12.5302 9.99031 12.6893L10.2433 13.4633C10.2981 13.6198 10.4001 13.7554 10.5353 13.8513ZM9.62231 11.0583L9.44331 10.9993L9.62731 10.9353C9.92907 10.8304 10.2027 10.6576 10.4273 10.4303C10.6537 10.2013 10.8248 9.92359 10.9273 9.61833L10.9853 9.44033L11.0443 9.62133C11.1463 9.92803 11.3185 10.2067 11.5471 10.4352C11.7757 10.6636 12.0545 10.8356 12.3613 10.9373L12.5563 11.0003L12.3763 11.0593C12.0689 11.1615 11.7898 11.3342 11.5611 11.5636C11.3324 11.793 11.1606 12.0727 11.0593 12.3803L11.0003 12.5613L10.9423 12.3803C10.8409 12.0722 10.6687 11.792 10.4394 11.5625C10.2102 11.3329 9.93033 11.1602 9.62231 11.0583Z" fill="#424242"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
3
images/IntermediatePrompts.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.799805 3.99971C0.799805 2.79131 1.8498 1.89371 3.0798 1.33211C4.6378 0.684806 6.313 0.367334 7.9998 0.39971C9.68664 0.367334 11.3618 0.684806 12.9198 1.33211C14.1498 1.89371 15.1998 2.79131 15.1998 3.99971V10.1304C14.827 10.1594 14.4604 10.2471 14.1138 10.3906C14.0755 10.4066 14.0375 10.423 13.9998 10.4403V6.03971C13.6631 6.28732 13.301 6.49849 12.9198 6.66971C11.3616 7.3162 9.6864 7.63285 7.9998 7.59971C6.313 7.63209 4.6378 7.31461 3.0798 6.66731C2.69864 6.49686 2.33662 6.28648 1.9998 6.03971V15.9997C1.9998 16.4485 2.427 17.0497 3.5814 17.5753C4.98209 18.1502 6.48605 18.4308 7.9998 18.3997L8.02536 18.4002C8.03904 18.4647 8.05452 18.529 8.0718 18.593L7.15982 19.5098C7.13596 19.5337 7.11282 19.5582 7.09042 19.5832C5.71356 19.507 4.35728 19.198 3.0798 18.6673C1.8498 18.1057 0.799805 17.2081 0.799805 15.9997V3.99971ZM1.9998 3.99971C1.9998 4.44851 2.427 5.04971 3.5814 5.57531C4.98209 6.15018 6.48605 6.4308 7.9998 6.39971C9.5136 6.4308 11.0176 6.15018 12.4182 5.57531C13.5726 5.04971 13.9998 4.44851 13.9998 3.99971C13.9998 3.55091 13.5726 2.94971 12.4182 2.42411C11.0176 1.84924 9.5136 1.56863 7.9998 1.59971C6.48605 1.56863 4.98209 1.84924 3.5814 2.42411C2.427 2.94971 1.9998 3.55091 1.9998 3.99971ZM19.0373 11.0246C19.15 10.9122 19.2133 10.7595 19.2134 10.6003C19.2136 10.441 19.1504 10.2883 19.0379 10.1756C18.9254 10.0629 18.7728 9.99963 18.6136 9.99952C18.4543 9.9994 18.3016 10.0626 18.1889 10.175L16.7657 11.5982C16.7506 11.6187 16.7366 11.64 16.7237 11.6618C16.2658 11.3886 15.7297 11.2755 15.2004 11.3407C14.6711 11.4058 14.1785 11.6456 13.8005 12.0218L13.0805 12.7418C12.8603 12.9626 12.7366 13.2616 12.7366 13.5734C12.7366 13.8853 12.8603 14.1843 13.0805 14.405L14.8109 16.1318C15.0317 16.3525 15.331 16.4764 15.6431 16.4764C15.9552 16.4764 16.2546 16.3525 16.4753 16.1318L17.1953 15.4118C17.5718 15.0338 17.8117 14.541 17.8769 14.0114C17.942 13.4817 17.829 12.9456 17.5553 12.4874C17.5777 12.4753 17.599 12.4612 17.6189 12.4454L19.0373 11.0246ZM12.2317 15.2498C12.1225 15.1405 11.9928 15.054 11.8501 14.9948C11.7074 14.9356 11.5546 14.9053 11.4001 14.9053C11.2457 14.9053 11.0927 14.9356 10.95 14.9948C10.8073 15.054 10.6777 15.1405 10.5685 15.2498L9.84852 15.9698C9.47196 16.3478 9.23208 16.8406 9.16692 17.3702C9.10176 17.8998 9.21492 18.436 9.48852 18.8942C9.46632 18.9066 9.44508 18.9206 9.42492 18.9362L8.00652 20.3582C7.89722 20.4714 7.83674 20.6229 7.8381 20.7802C7.83947 20.9376 7.90258 21.088 8.01384 21.1993C8.12508 21.3105 8.27556 21.3736 8.43288 21.375C8.5902 21.3763 8.74176 21.3158 8.85492 21.2066L10.2769 19.7834C10.2926 19.7631 10.3072 19.7419 10.3201 19.7198C10.778 19.993 11.3141 20.1061 11.8434 20.0409C12.3727 19.9756 12.8653 19.736 13.2433 19.3598L13.9633 18.6398C14.184 18.419 14.3078 18.1197 14.3078 17.8076C14.3078 17.4955 14.184 17.1961 13.9633 16.9754L12.2317 15.2498Z" fill="#0078D4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
34
images/QueryCopilotNewLogo.svg
Normal file
@@ -0,0 +1,34 @@
|
||||
<svg width="32" height="34" viewBox="0 0 32 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 0.739258L16 1.04436V9.68622L11.2 10.8013L9.60002 12.9967V16.4479C9.60002 18.7669 10.8545 20.9045 12.879 22.0353L17.4036 24.5626L9.6 28.69L6.35592 29.1523L3.27902 27.4337C1.25445 26.3028 0 24.1652 0 21.8462V11.7591C0 9.43952 1.25512 7.30147 3.28055 6.17085L12.3174 1.12639L12.3121 1.13012L13 0.739258Z" fill="url(#paint0_radial_857_143948)"/>
|
||||
<path d="M13 0.739258L16 1.04436V9.68622L11.2 10.8013L9.60002 12.9967V16.4479C9.60002 18.7669 10.8545 20.9045 12.879 22.0353L17.4036 24.5626L9.6 28.69L6.35592 29.1523L3.27902 27.4337C1.25445 26.3028 0 24.1652 0 21.8462V11.7591C0 9.43952 1.25512 7.30147 3.28055 6.17085L12.3174 1.12639L12.3121 1.13012L13 0.739258Z" fill="url(#paint1_linear_857_143948)"/>
|
||||
<path d="M22.399 12.001L30.399 16.801L31.999 18.401V21.8452C31.999 24.1642 30.7446 26.3018 28.72 27.4327L19.12 32.7949C17.1804 33.8784 14.8177 33.8784 12.878 32.7949L3.27804 27.4327C3.09146 27.3284 2.91141 27.2157 2.73828 27.095L3.278 27.3965C5.21762 28.4799 7.58035 28.4799 9.51996 27.3965L19.12 22.0342C21.1445 20.9033 22.399 18.7657 22.399 16.4467V12.001Z" fill="url(#paint2_radial_857_143948)"/>
|
||||
<path d="M22.399 12.001L30.399 16.801L31.999 18.401V21.8452C31.999 24.1642 30.7446 26.3018 28.72 27.4327L19.12 32.7949C17.1804 33.8784 14.8177 33.8784 12.878 32.7949L3.27804 27.4327C3.09146 27.3284 2.91141 27.2157 2.73828 27.095L3.278 27.3965C5.21762 28.4799 7.58035 28.4799 9.51996 27.3965L19.12 22.0342C21.1445 20.9033 22.399 18.7657 22.399 16.4467V12.001Z" fill="url(#paint3_linear_857_143948)"/>
|
||||
<path d="M28.7191 6.17053L19.119 0.811705C17.1802 -0.270568 14.819 -0.270568 12.8802 0.811704L12.3151 1.1271C10.6239 2.31737 9.59961 4.26472 9.59961 6.36025V13.0461L12.8802 11.2149C14.819 10.1326 17.1802 10.1326 19.119 11.2149L28.7191 16.5737C30.6984 17.6786 31.9421 19.7456 31.9977 22.0039C31.999 21.9514 31.9996 21.8987 31.9996 21.8459V11.7588C31.9996 9.4392 30.7445 7.30115 28.7191 6.17053Z" fill="url(#paint4_radial_857_143948)"/>
|
||||
<path d="M28.7191 6.17053L19.119 0.811705C17.1802 -0.270568 14.819 -0.270568 12.8802 0.811704L12.3151 1.1271C10.6239 2.31737 9.59961 4.26472 9.59961 6.36025V13.0461L12.8802 11.2149C14.819 10.1326 17.1802 10.1326 19.119 11.2149L28.7191 16.5737C30.6984 17.6786 31.9421 19.7456 31.9977 22.0039C31.999 21.9514 31.9996 21.8987 31.9996 21.8459V11.7588C31.9996 9.4392 30.7445 7.30115 28.7191 6.17053Z" fill="url(#paint5_linear_857_143948)"/>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_857_143948" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(13.5054 9.8837) rotate(112.544) scale(23.4519 19.3067)">
|
||||
<stop offset="0.206732" stop-color="#4995D0"/>
|
||||
<stop offset="0.875628" stop-color="#0078D4"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint1_linear_857_143948" x1="11.0625" y1="26.6174" x2="9.78695" y2="24.2436" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.9999" stop-color="#0078D4"/>
|
||||
<stop offset="1" stop-color="#0078D4" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint2_radial_857_143948" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(6.899 24.5214) rotate(-3.9995) scale(24.6197 15.4594)">
|
||||
<stop offset="0.140029" stop-color="#80C8FF"/>
|
||||
<stop offset="0.952721" stop-color="#0078D4"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint3_linear_857_143948" x1="26.8932" y1="15.5508" x2="25.5491" y2="17.8921" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.9999" stop-color="#3F8AC3"/>
|
||||
<stop offset="1" stop-color="#8C66BA" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint4_radial_857_143948" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(26.632 16.3878) rotate(-138.33) scale(22.8015 13.0047)">
|
||||
<stop stop-color="#7BC6FF"/>
|
||||
<stop offset="0.839255" stop-color="#0078D4"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint5_linear_857_143948" x1="9.59961" y1="8.71337" x2="12.2131" y2="8.71337" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.9999" stop-color="#0078D4"/>
|
||||
<stop offset="1" stop-color="#436DCD" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
3
images/Recent.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.9342 7.04998C15.4095 3.21975 11.8792 0.540074 8.04894 1.06475C6.10618 1.33087 4.46008 2.36951 3.37682 3.82964L3.25046 3.99997H6.49985C6.77599 3.99997 6.99985 4.22383 6.99985 4.49997C6.99985 4.77611 6.77599 4.99997 6.49985 4.99997H2.49985C2.22371 4.99997 1.99985 4.77611 1.99985 4.49997V0.499969C1.99985 0.223826 2.22371 -3.14998e-05 2.49985 -3.14998e-05C2.77599 -3.14998e-05 2.99985 0.223826 2.99985 0.499968V2.70729C4.22416 1.31847 5.93463 0.345036 7.91322 0.0740017C12.2906 -0.525627 16.3253 2.53686 16.9249 6.91426C17.5246 11.2917 14.4621 15.3263 10.0847 15.926C5.70727 16.5256 1.67259 13.4631 1.07296 9.08571C0.998819 8.54443 0.980672 8.00788 1.01426 7.48208C1.03186 7.2065 1.26953 6.99737 1.54511 7.01497C1.82069 7.03258 2.02982 7.27025 2.01222 7.54583C1.98287 8.00544 1.99867 8.47516 2.06371 8.94999C2.58839 12.7802 6.11873 15.4599 9.94896 14.9352C13.7792 14.4105 16.4589 10.8802 15.9342 7.04998Z" fill="#424242"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
3
images/SamplePromptsIcon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="20" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 5.5V4.75H0.75V5.5H0ZM0 3.25V2.5H0.75V3.25H0ZM2.25 3.25V2.5H12V3.25H2.25ZM0 1V0.25H0.75V1H0ZM2.25 0.25H12V1H2.25V0.25ZM2.25 5.5V4.75H12V5.5H2.25ZM0 7.75V7H0.75V7.75H0ZM2.25 7.75V7H12V7.75H2.25Z" fill="#0078D4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 325 B |
3
images/SimplePrompts.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="19" height="21" viewBox="0 0 19 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 4C0 3.30945 0.3153 2.70664 0.77423 2.22025C1.2294 1.73784 1.85301 1.33745 2.56668 1.01752C3.99575 0.3769 5.91738 0 8 0C10.0826 0 12.0042 0.3769 13.4333 1.01752C14.147 1.33745 14.7706 1.73784 15.2258 2.22025C15.6847 2.70664 16 3.30945 16 4V10.2572C15.5537 9.8886 15.0482 9.589 14.5 9.3749V6.39242C14.1791 6.61282 13.8194 6.80942 13.4333 6.98248C12.0042 7.6231 10.0826 8 8 8C5.91738 8 3.99575 7.6231 2.56668 6.98248C2.18063 6.80942 1.82094 6.61282 1.5 6.39242V16C1.5 16.2069 1.59044 16.4591 1.86525 16.7503C2.14382 17.0456 2.58195 17.3455 3.18027 17.6137C4.37519 18.1494 6.0786 18.5 8 18.5C8.2396 18.5 8.4759 18.4945 8.7081 18.484C9.3109 19.0579 10.0438 19.4964 10.858 19.7507C9.9673 19.9127 9.0023 20 8 20C5.91738 20 3.99575 19.6231 2.56668 18.9825C1.85301 18.6626 1.2294 18.2622 0.77423 17.7798C0.3153 17.2934 0 16.6906 0 16V4ZM1.5 4C1.5 4.20691 1.59044 4.45909 1.86525 4.75034C2.14382 5.04559 2.58195 5.3455 3.18027 5.61372C4.37519 6.14937 6.0786 6.5 8 6.5C9.9214 6.5 11.6248 6.14937 12.8197 5.61372C13.418 5.3455 13.8562 5.04559 14.1348 4.75034C14.4096 4.45909 14.5 4.20691 14.5 4C14.5 3.79309 14.4096 3.54091 14.1348 3.24966C13.8562 2.95441 13.418 2.65449 12.8197 2.38628C11.6248 1.85063 9.9214 1.5 8 1.5C6.0786 1.5 4.37519 1.85063 3.18027 2.38628C2.58195 2.65449 2.14382 2.95441 1.86525 3.24966C1.59044 3.54091 1.5 3.79309 1.5 4ZM12.5 19C13.4719 19 14.3718 18.6919 15.1074 18.1681L17.7197 20.7803C18.0126 21.0732 18.4874 21.0732 18.7803 20.7803C19.0732 20.4874 19.0732 20.0126 18.7803 19.7197L16.1681 17.1074C16.6919 16.3718 17 15.4719 17 14.5C17 12.0147 14.9853 10 12.5 10C10.0147 10 8 12.0147 8 14.5C8 16.9853 10.0147 19 12.5 19ZM12.5 17.5C10.8431 17.5 9.5 16.1569 9.5 14.5C9.5 12.8431 10.8431 11.5 12.5 11.5C14.1569 11.5 15.5 12.8431 15.5 14.5C15.5 16.1569 14.1569 17.5 12.5 17.5Z" fill="#0078D4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
3
images/YoutubePlaceholder.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="334" height="154" viewBox="0 0 334 154" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.5" y="0.5" width="333" height="153" fill="#F3F2F1" stroke="#E1DFDD"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 188 B |
4
images/successfulPopup.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14Z" fill="#57A300"/>
|
||||
<path d="M3.10891 7.25414C3.04416 7.18502 3.01091 7.09402 3.01441 6.99952C3.01791 6.90414 3.05816 6.81664 3.12816 6.75102L3.85879 6.07552C3.92529 6.01514 4.01016 5.98189 4.09854 5.98189C4.19654 5.98189 4.29104 6.02302 4.35754 6.09477L6.22654 8.10027L9.55766 3.83464C9.62504 3.74802 9.72653 3.69727 9.83678 3.69727C9.91553 3.69727 9.98991 3.72264 10.0529 3.77077L10.8457 4.38239C10.997 4.49439 11.0294 4.71227 10.9157 4.86714L6.65616 10.321C6.49079 10.5328 6.17491 10.5468 5.99116 10.3499L3.10891 7.25414Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 756 B |
@@ -37,7 +37,7 @@ module.exports = {
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 25,
|
||||
functions: 25,
|
||||
functions: 24,
|
||||
lines: 28,
|
||||
statements: 28,
|
||||
},
|
||||
|
||||
@@ -650,4 +650,4 @@ tr:hover td.nameColumnText {
|
||||
|
||||
.context-menu-item.icon-customize-columns {
|
||||
background-image: url(../../images/Options.svg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@
|
||||
"strict:find": "node ./strict-null-checks/find.js",
|
||||
"strict:add": "node ./strict-null-checks/auto-add.js",
|
||||
"compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks",
|
||||
"generateARMClients": "npx ts-node --compiler-options '{\"module\":\"commonjs\"}' utils/armClientGenerator/generator.ts"
|
||||
"generateARMClients": "npx ts-node utils/armClientGenerator/generator.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
11484
sampleData/queryCopilotSampleData.json
Normal file
@@ -428,3 +428,91 @@ export class JunoEndpoints {
|
||||
public static readonly Prod = "https://tools.cosmos.azure.com";
|
||||
public static readonly Stage = "https://tools-staging.cosmos.azure.com";
|
||||
}
|
||||
|
||||
export const QueryCopilotSampleDatabaseId = "CopilotSampleDb";
|
||||
export const QueryCopilotSampleContainerId = "SampleContainer";
|
||||
|
||||
export const QueryCopilotSampleContainerSchema = {
|
||||
product: {
|
||||
sampleData: {
|
||||
id: "de6fadec-0384-43c8-93ea-16c0170b5460",
|
||||
name: "Premium Phone Mini (Red)",
|
||||
price: 652.04,
|
||||
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],
|
||||
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 },
|
||||
],
|
||||
rareProperty: true,
|
||||
},
|
||||
schema: {
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
},
|
||||
price: {
|
||||
type: "number",
|
||||
},
|
||||
category: {
|
||||
type: "string",
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
},
|
||||
stock: {
|
||||
type: "number",
|
||||
},
|
||||
countryOfOrigin: {
|
||||
type: "string",
|
||||
},
|
||||
firstAvailable: {
|
||||
type: "string",
|
||||
},
|
||||
priceHistory: {
|
||||
items: {
|
||||
type: "number",
|
||||
},
|
||||
type: "array",
|
||||
},
|
||||
customerRatings: {
|
||||
items: {
|
||||
properties: {
|
||||
username: {
|
||||
type: "string",
|
||||
},
|
||||
stars: {
|
||||
type: "number",
|
||||
},
|
||||
date: {
|
||||
type: "string",
|
||||
},
|
||||
verifiedUser: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
},
|
||||
type: "array",
|
||||
},
|
||||
rareProperty: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
26
src/Common/SampleDataClient.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as Cosmos from "@azure/cosmos";
|
||||
import { userContext } from "UserContext";
|
||||
|
||||
let _sampleDataclient: Cosmos.CosmosClient;
|
||||
|
||||
export function sampleDataClient(): Cosmos.CosmosClient {
|
||||
if (_sampleDataclient) {
|
||||
return _sampleDataclient;
|
||||
}
|
||||
|
||||
const sampleDataConnectionInfo = userContext.sampleDataConnectionInfo;
|
||||
const options: Cosmos.CosmosClientOptions = {
|
||||
endpoint: sampleDataConnectionInfo.accountEndpoint,
|
||||
tokenProvider: async () => {
|
||||
const sampleDataConnectionInfo = userContext.sampleDataConnectionInfo;
|
||||
return Promise.resolve(sampleDataConnectionInfo.resourceToken);
|
||||
},
|
||||
connectionPolicy: {
|
||||
enableEndpointDiscovery: false,
|
||||
},
|
||||
userAgentSuffix: "Azure Portal",
|
||||
};
|
||||
|
||||
_sampleDataclient = new Cosmos.CosmosClient(options);
|
||||
return _sampleDataclient;
|
||||
}
|
||||
@@ -1,13 +1,39 @@
|
||||
import { CosmosClient } from "@azure/cosmos";
|
||||
import { sampleDataClient } from "Common/SampleDataClient";
|
||||
import { userContext } from "UserContext";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
|
||||
export async function readCollection(databaseId: string, collectionId: string): Promise<DataModels.Collection> {
|
||||
const cosmosClient = client();
|
||||
return await readCollectionInternal(cosmosClient, databaseId, collectionId);
|
||||
}
|
||||
|
||||
export async function readSampleCollection(): Promise<DataModels.Collection> {
|
||||
const cosmosClient = sampleDataClient();
|
||||
|
||||
const sampleDataConnectionInfo = userContext.sampleDataConnectionInfo;
|
||||
const databaseId = sampleDataConnectionInfo?.databaseId;
|
||||
const collectionId = sampleDataConnectionInfo?.collectionId;
|
||||
|
||||
if (!databaseId || !collectionId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return await readCollectionInternal(cosmosClient, databaseId, collectionId);
|
||||
}
|
||||
|
||||
export async function readCollectionInternal(
|
||||
cosmosClient: CosmosClient,
|
||||
databaseId: string,
|
||||
collectionId: string
|
||||
): Promise<DataModels.Collection> {
|
||||
let collection: DataModels.Collection;
|
||||
const clearMessage = logConsoleProgress(`Querying container ${collectionId}`);
|
||||
try {
|
||||
const response = await client().database(databaseId).container(collectionId).read();
|
||||
const response = await cosmosClient.database(databaseId).container(collectionId).read();
|
||||
collection = response.resource as DataModels.Collection;
|
||||
} catch (error) {
|
||||
handleError(error, "ReadCollection", `Error while querying container ${collectionId}`);
|
||||
|
||||
@@ -96,6 +96,14 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
||||
? parseInt(resource.minimumThroughput)
|
||||
: resource.minimumThroughput;
|
||||
const autoscaleSettings = resource.autoscaleSettings;
|
||||
const instantMaximumThroughput: number =
|
||||
typeof resource.instantMaximumThroughput === "string"
|
||||
? parseInt(resource.instantMaximumThroughput)
|
||||
: resource.instantMaximumThroughput;
|
||||
const softAllowedMaximumThroughput: number =
|
||||
typeof resource.softAllowedMaximumThroughput === "string"
|
||||
? parseInt(resource.softAllowedMaximumThroughput)
|
||||
: resource.softAllowedMaximumThroughput;
|
||||
|
||||
if (autoscaleSettings) {
|
||||
return {
|
||||
@@ -104,6 +112,8 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
||||
manualThroughput: undefined,
|
||||
minimumThroughput,
|
||||
offerReplacePending: resource.offerReplacePending === "true",
|
||||
instantMaximumThroughput,
|
||||
softAllowedMaximumThroughput,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -113,6 +123,8 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
||||
manualThroughput: resource.throughput,
|
||||
minimumThroughput,
|
||||
offerReplacePending: resource.offerReplacePending === "true",
|
||||
instantMaximumThroughput,
|
||||
softAllowedMaximumThroughput,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,14 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
|
||||
? parseInt(resource.minimumThroughput)
|
||||
: resource.minimumThroughput;
|
||||
const autoscaleSettings = resource.autoscaleSettings;
|
||||
const instantMaximumThroughput: number =
|
||||
typeof resource.instantMaximumThroughput === "string"
|
||||
? parseInt(resource.instantMaximumThroughput)
|
||||
: resource.instantMaximumThroughput;
|
||||
const softAllowedMaximumThroughput: number =
|
||||
typeof resource.softAllowedMaximumThroughput === "string"
|
||||
? parseInt(resource.softAllowedMaximumThroughput)
|
||||
: resource.softAllowedMaximumThroughput;
|
||||
|
||||
if (autoscaleSettings) {
|
||||
return {
|
||||
@@ -76,6 +84,8 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
|
||||
manualThroughput: undefined,
|
||||
minimumThroughput,
|
||||
offerReplacePending: resource.offerReplacePending === "true",
|
||||
instantMaximumThroughput,
|
||||
softAllowedMaximumThroughput,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -85,6 +95,8 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
|
||||
manualThroughput: resource.throughput,
|
||||
minimumThroughput,
|
||||
offerReplacePending: resource.offerReplacePending === "true",
|
||||
instantMaximumThroughput,
|
||||
softAllowedMaximumThroughput,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||
import { CloudError, SqlStoredProcedureListResult } from "Utils/arm/generatedClients/cosmos/types";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { listSqlStoredProcedures } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { listSqlStoredProcedures } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
@@ -24,7 +25,13 @@ export async function readStoredProcedures(
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
return rpResponse?.value?.map((sproc) => sproc.properties?.resource as StoredProcedureDefinition & Resource);
|
||||
const listResult = rpResponse as SqlStoredProcedureListResult;
|
||||
if (listResult) {
|
||||
return listResult?.value?.map((sproc) => sproc.properties?.resource as StoredProcedureDefinition & Resource);
|
||||
}
|
||||
|
||||
const cloudError = rpResponse as CloudError;
|
||||
throw new Error(cloudError?.error?.message);
|
||||
}
|
||||
|
||||
const response = await client()
|
||||
|
||||
@@ -242,6 +242,8 @@ export interface Offer {
|
||||
minimumThroughput: number | undefined;
|
||||
offerDefinition?: SDKOfferDefinition;
|
||||
offerReplacePending: boolean;
|
||||
instantMaximumThroughput?: number;
|
||||
softAllowedMaximumThroughput?: number;
|
||||
}
|
||||
|
||||
export interface SDKOfferDefinition extends Resource {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
||||
import React from "react";
|
||||
import AddCollectionIcon from "../../images/AddCollection.svg";
|
||||
import AddSqlQueryIcon from "../../images/AddSqlQuery_16x16.svg";
|
||||
@@ -11,9 +12,9 @@ import DeleteTriggerIcon from "../../images/DeleteTrigger.svg";
|
||||
import DeleteUDFIcon from "../../images/DeleteUDF.svg";
|
||||
import HostedTerminalIcon from "../../images/Hosted-Terminal.svg";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
|
||||
import Explorer from "./Explorer";
|
||||
import { useNotebook } from "./Notebook/useNotebook";
|
||||
@@ -61,6 +62,13 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
|
||||
return items;
|
||||
};
|
||||
|
||||
const isQueryCopilotMoreMenu = (selectedCollection: ViewModels.Collection): boolean => {
|
||||
return (
|
||||
selectedCollection.databaseId === QueryCopilotSampleDatabaseId &&
|
||||
selectedCollection.id() === QueryCopilotSampleContainerId
|
||||
);
|
||||
};
|
||||
|
||||
export const createCollectionContextMenuButton = (
|
||||
container: Explorer,
|
||||
selectedCollection: ViewModels.Collection
|
||||
@@ -95,48 +103,49 @@ export const createCollectionContextMenuButton = (
|
||||
});
|
||||
}
|
||||
|
||||
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
|
||||
items.push({
|
||||
iconSrc: AddStoredProcedureIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined);
|
||||
},
|
||||
label: "New Stored Procedure",
|
||||
});
|
||||
if (!isQueryCopilotMoreMenu(selectedCollection)) {
|
||||
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
|
||||
items.push({
|
||||
iconSrc: AddStoredProcedureIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined);
|
||||
},
|
||||
label: "New Stored Procedure",
|
||||
});
|
||||
|
||||
items.push({
|
||||
iconSrc: AddUdfIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
|
||||
},
|
||||
label: "New UDF",
|
||||
});
|
||||
|
||||
items.push({
|
||||
iconSrc: AddTriggerIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, undefined);
|
||||
},
|
||||
label: "New Trigger",
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
iconSrc: AddUdfIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
|
||||
},
|
||||
label: "New UDF",
|
||||
});
|
||||
|
||||
items.push({
|
||||
iconSrc: AddTriggerIcon,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, undefined);
|
||||
},
|
||||
label: "New Trigger",
|
||||
iconSrc: DeleteCollectionIcon,
|
||||
onClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getCollectionName(),
|
||||
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />
|
||||
),
|
||||
label: `Delete ${getCollectionName()}`,
|
||||
styleClass: "deleteCollectionMenuItem",
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
iconSrc: DeleteCollectionIcon,
|
||||
onClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getCollectionName(),
|
||||
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />
|
||||
),
|
||||
label: `Delete ${getCollectionName()}`,
|
||||
styleClass: "deleteCollectionMenuItem",
|
||||
});
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
@@ -152,7 +161,7 @@ export const createStoreProcedureContextMenuItems = (
|
||||
{
|
||||
iconSrc: DeleteSprocIcon,
|
||||
onClick: () => storedProcedure.delete(),
|
||||
label: "Delete Store Procedure",
|
||||
label: "Delete Stored Procedure",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -1,46 +1,29 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { IColumn, Text } from "@fluentui/react";
|
||||
import {
|
||||
getAutoPilotV3SpendElement,
|
||||
getEstimatedSpendingElement,
|
||||
manualToAutoscaleDisclaimerElement,
|
||||
ttlWarning,
|
||||
indexingPolicynUnsavedWarningMessage,
|
||||
updateThroughputBeyondLimitWarningMessage,
|
||||
updateThroughputDelayedApplyWarningMessage,
|
||||
getThroughputApplyDelayedMessage,
|
||||
getThroughputApplyShortDelayMessage,
|
||||
getThroughputApplyLongDelayMessage,
|
||||
getToolTipContainer,
|
||||
conflictResolutionCustomToolTip,
|
||||
changeFeedPolicyToolTip,
|
||||
conflictResolutionLwwTooltip,
|
||||
mongoIndexingPolicyDisclaimer,
|
||||
mongoIndexingPolicyAADError,
|
||||
mongoIndexTransformationRefreshingMessage,
|
||||
renderMongoIndexTransformationRefreshMessage,
|
||||
ManualEstimatedSpendingDisplayProps,
|
||||
PriceBreakdown,
|
||||
changeFeedPolicyToolTip,
|
||||
conflictResolutionCustomToolTip,
|
||||
conflictResolutionLwwTooltip,
|
||||
getEstimatedSpendingElement,
|
||||
getRuPriceBreakdown,
|
||||
getThroughputApplyDelayedMessage,
|
||||
getThroughputApplyLongDelayMessage,
|
||||
getThroughputApplyShortDelayMessage,
|
||||
getToolTipContainer,
|
||||
indexingPolicynUnsavedWarningMessage,
|
||||
manualToAutoscaleDisclaimerElement,
|
||||
mongoIndexTransformationRefreshingMessage,
|
||||
mongoIndexingPolicyAADError,
|
||||
mongoIndexingPolicyDisclaimer,
|
||||
renderMongoIndexTransformationRefreshMessage,
|
||||
ttlWarning,
|
||||
updateThroughputDelayedApplyWarningMessage,
|
||||
} from "./SettingsRenderUtils";
|
||||
|
||||
class SettingsRenderUtilsTestComponent extends React.Component {
|
||||
public render(): JSX.Element {
|
||||
const estimatedSpendingColumns: IColumn[] = [
|
||||
{ key: "costType", name: "", fieldName: "costType", minWidth: 100, maxWidth: 200, isResizable: true },
|
||||
{ key: "hourly", name: "Hourly", fieldName: "hourly", minWidth: 100, maxWidth: 200, isResizable: true },
|
||||
{ key: "daily", name: "Daily", fieldName: "daily", minWidth: 100, maxWidth: 200, isResizable: true },
|
||||
{ key: "monthly", name: "Monthly", fieldName: "monthly", minWidth: 100, maxWidth: 200, isResizable: true },
|
||||
];
|
||||
const estimatedSpendingItems: ManualEstimatedSpendingDisplayProps[] = [
|
||||
{
|
||||
costType: <Text>Current Cost</Text>,
|
||||
hourly: <Text>$ 1.02</Text>,
|
||||
daily: <Text>$ 24.48</Text>,
|
||||
monthly: <Text>$ 744.6</Text>,
|
||||
},
|
||||
];
|
||||
const costElement: JSX.Element = <></>;
|
||||
const priceBreakdown: PriceBreakdown = {
|
||||
hourlyPrice: 1.02,
|
||||
dailyPrice: 24.48,
|
||||
@@ -52,17 +35,11 @@ class SettingsRenderUtilsTestComponent extends React.Component {
|
||||
|
||||
return (
|
||||
<>
|
||||
{getAutoPilotV3SpendElement(1000, false)}
|
||||
{getAutoPilotV3SpendElement(undefined, false)}
|
||||
{getAutoPilotV3SpendElement(1000, true)}
|
||||
{getAutoPilotV3SpendElement(undefined, true)}
|
||||
|
||||
{getEstimatedSpendingElement(estimatedSpendingColumns, estimatedSpendingItems, 1000, 2, priceBreakdown, false)}
|
||||
{getEstimatedSpendingElement(costElement, 1000, 2, priceBreakdown, false)}
|
||||
|
||||
{manualToAutoscaleDisclaimerElement}
|
||||
{ttlWarning}
|
||||
{indexingPolicynUnsavedWarningMessage}
|
||||
{updateThroughputBeyondLimitWarningMessage}
|
||||
{updateThroughputDelayedApplyWarningMessage}
|
||||
|
||||
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import {
|
||||
DetailsList,
|
||||
DetailsListLayoutMode,
|
||||
DetailsRow,
|
||||
ICheckboxStyles,
|
||||
IChoiceGroupStyles,
|
||||
IColumn,
|
||||
IDetailsColumnStyles,
|
||||
IDetailsListStyles,
|
||||
IDetailsRowProps,
|
||||
@@ -20,7 +17,6 @@ import {
|
||||
Link,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
SelectionMode,
|
||||
Spinner,
|
||||
SpinnerSize,
|
||||
Stack,
|
||||
@@ -28,8 +24,7 @@ import {
|
||||
} from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { StyleConstants, Urls } from "../../../Common/Constants";
|
||||
import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants";
|
||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||
import { hoursInAMonth } from "../../../Shared/Constants";
|
||||
import {
|
||||
computeRUUsagePriceHourly,
|
||||
estimatedCostDisclaimer,
|
||||
@@ -103,6 +98,10 @@ export const checkBoxAndInputStackProps: Partial<IStackProps> = {
|
||||
tokens: { childrenGap: 10 },
|
||||
};
|
||||
|
||||
export const relaxedSpacingStackProps: Partial<IStackProps> = {
|
||||
tokens: { childrenGap: 20 },
|
||||
};
|
||||
|
||||
export const toolTipLabelStackTokens: IStackTokens = {
|
||||
childrenGap: 6,
|
||||
};
|
||||
@@ -174,41 +173,6 @@ export function onRenderRow(props: IDetailsRowProps): JSX.Element {
|
||||
return <DetailsRow {...props} styles={transparentDetailsRowStyles} />;
|
||||
}
|
||||
|
||||
export const getAutoPilotV3SpendElement = (
|
||||
maxAutoPilotThroughputSet: number,
|
||||
isDatabaseThroughput: boolean,
|
||||
requestUnitsUsageCostElement?: JSX.Element
|
||||
): JSX.Element => {
|
||||
if (!maxAutoPilotThroughputSet) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const resource: string = isDatabaseThroughput ? "database" : "container";
|
||||
return (
|
||||
<>
|
||||
<Text>
|
||||
Your {resource} throughput will automatically scale from{" "}
|
||||
<b>
|
||||
{AutoPilotUtils.getMinRUsBasedOnUserInput(maxAutoPilotThroughputSet)} RU/s (10% of max RU/s) -{" "}
|
||||
{maxAutoPilotThroughputSet} RU/s
|
||||
</b>{" "}
|
||||
based on usage.
|
||||
<br />
|
||||
</Text>
|
||||
{requestUnitsUsageCostElement}
|
||||
<Text>
|
||||
After the first {AutoPilotUtils.getStorageBasedOnUserInput(maxAutoPilotThroughputSet)} GB of data stored, the
|
||||
max RU/s will be automatically upgraded based on the new storage value.
|
||||
<Link href={AutopilotDocumentation.Url} target="_blank">
|
||||
{" "}
|
||||
Learn more
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const getRuPriceBreakdown = (
|
||||
throughput: number,
|
||||
serverId: string,
|
||||
@@ -238,8 +202,7 @@ export const getRuPriceBreakdown = (
|
||||
};
|
||||
|
||||
export const getEstimatedSpendingElement = (
|
||||
estimatedSpendingColumns: IColumn[],
|
||||
estimatedSpendingItems: EstimatedSpendingDisplayProps[],
|
||||
costElement: JSX.Element,
|
||||
throughput: number,
|
||||
numberOfRegions: number,
|
||||
priceBreakdown: PriceBreakdown,
|
||||
@@ -247,22 +210,25 @@ export const getEstimatedSpendingElement = (
|
||||
): JSX.Element => {
|
||||
const ruRange: string = isAutoscale ? throughput / 10 + " RU/s - " : "";
|
||||
return (
|
||||
<Stack {...addMongoIndexStackProps} styles={mediumWidthStackStyles}>
|
||||
<DetailsList
|
||||
disableSelectionZone
|
||||
items={estimatedSpendingItems}
|
||||
columns={estimatedSpendingColumns}
|
||||
selectionMode={SelectionMode.none}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
onRenderRow={onRenderRow}
|
||||
/>
|
||||
<Text id="throughputSpendElement">
|
||||
({"regions: "} {numberOfRegions}, {ruRange}
|
||||
{throughput} RU/s, {priceBreakdown.currencySign}
|
||||
{priceBreakdown.pricePerRu}/RU)
|
||||
</Text>
|
||||
<Text>
|
||||
<em>{estimatedCostDisclaimer}</em>
|
||||
<Stack>
|
||||
<Text style={{ fontWeight: 600 }}>Cost estimate*</Text>
|
||||
{costElement}
|
||||
<Text style={{ fontWeight: 600, marginTop: 15 }}>How we calculate this</Text>
|
||||
<Stack id="throughputSpendElement" style={{ marginTop: 5 }}>
|
||||
<span>
|
||||
{numberOfRegions} region{numberOfRegions > 1 && <span>s</span>}
|
||||
</span>
|
||||
<span>
|
||||
{ruRange}
|
||||
{throughput} RU/s
|
||||
</span>
|
||||
<span>
|
||||
{priceBreakdown.currencySign}
|
||||
{priceBreakdown.pricePerRu}/RU
|
||||
</span>
|
||||
</Stack>
|
||||
<Text style={{ marginTop: 15 }}>
|
||||
<em>*{estimatedCostDisclaimer}</em>
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
@@ -293,14 +259,6 @@ export const indexingPolicynUnsavedWarningMessage: JSX.Element = (
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const updateThroughputBeyondLimitWarningMessage: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle} id="updateThroughputBeyondLimitWarningMessage">
|
||||
You are about to request an increase in throughput beyond the pre-allocated capacity. The service will scale out and
|
||||
increase throughput for the selected container. This operation will take 1-3 business days to complete. You can
|
||||
track the status of this request in Notifications.
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const updateThroughputDelayedApplyWarningMessage: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
|
||||
You are about to request an increase in throughput beyond the pre-allocated capacity. This operation will take some
|
||||
@@ -308,6 +266,61 @@ export const updateThroughputDelayedApplyWarningMessage: JSX.Element = (
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const getUpdateThroughputBeyondInstantLimitMessage = (instantMaximumThroughput: number): JSX.Element => {
|
||||
return (
|
||||
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
|
||||
Scaling up will take 4-6 hours as it exceeds what Azure Cosmos DB can instantly support currently based on your
|
||||
number of physical partitions. You can increase your throughput to {instantMaximumThroughput} instantly or proceed
|
||||
with this value and wait until the scale-up is completed.
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export const getUpdateThroughputBeyondSupportLimitMessage = (
|
||||
instantMaximumThroughput: number,
|
||||
maximumThroughput: number
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
|
||||
Your request to increase throughput exceeds the pre-allocated capacity which may take longer than expected.
|
||||
There are three options you can choose from to proceed:
|
||||
</Text>
|
||||
<ol style={{ fontSize: 14, color: "windowtext", marginTop: "5px" }}>
|
||||
<li>You can instantly scale up to {instantMaximumThroughput} RU/s.</li>
|
||||
{instantMaximumThroughput < maximumThroughput && (
|
||||
<li>You can asynchronously scale up to any value under {maximumThroughput} RU/s in 4-6 hours.</li>
|
||||
)}
|
||||
<li>
|
||||
Your current quota max is {maximumThroughput} RU/s. To go over this limit, you must request a quota increase
|
||||
and the Azure Cosmos DB team will review.
|
||||
<Link
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</li>
|
||||
</ol>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const getUpdateThroughputBelowMinimumMessage = (minimum: number): JSX.Element => {
|
||||
return (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
You are not able to lower throughput below your current minimum of {minimum} RU/s. For more information on this
|
||||
limit, please refer to our service quote documentation.
|
||||
<Link
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export const saveThroughputWarningMessage: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below
|
||||
@@ -499,7 +512,11 @@ export const getTextFieldStyles = (current: isDirtyTypes, baseline: isDirtyTypes
|
||||
},
|
||||
});
|
||||
|
||||
export const getChoiceGroupStyles = (current: isDirtyTypes, baseline: isDirtyTypes): Partial<IChoiceGroupStyles> => ({
|
||||
export const getChoiceGroupStyles = (
|
||||
current: isDirtyTypes,
|
||||
baseline: isDirtyTypes,
|
||||
isHorizontal?: boolean
|
||||
): Partial<IChoiceGroupStyles> => ({
|
||||
flexContainer: [
|
||||
{
|
||||
selectors: {
|
||||
@@ -516,6 +533,8 @@ export const getChoiceGroupStyles = (current: isDirtyTypes, baseline: isDirtyTyp
|
||||
padding: "2px 5px",
|
||||
},
|
||||
},
|
||||
display: isHorizontal ? "inline-flex" : "default",
|
||||
columnGap: isHorizontal ? "30px" : "default",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ import ko from "knockout";
|
||||
import React from "react";
|
||||
import * as Constants from "../../../../Common/Constants";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import * as SharedConstants from "../../../../Shared/Constants";
|
||||
import { updateUserContext } from "../../../../UserContext";
|
||||
import Explorer from "../../../Explorer";
|
||||
import { throughputUnit } from "../SettingsRenderUtils";
|
||||
@@ -12,7 +11,6 @@ import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
|
||||
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
|
||||
|
||||
describe("ScaleComponent", () => {
|
||||
const nonNationalCloudContainer = new Explorer();
|
||||
const targetThroughput = 6000;
|
||||
|
||||
const baseProps: ScaleComponentProps = {
|
||||
@@ -125,11 +123,4 @@ describe("ScaleComponent", () => {
|
||||
scaleComponent = new ScaleComponent(newProps);
|
||||
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true);
|
||||
});
|
||||
|
||||
it("getThroughputWarningMessage", () => {
|
||||
const throughputBeyondLimit = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million + 1000;
|
||||
const newProps = { ...baseProps, container: nonNationalCloudContainer, throughput: throughputBeyondLimit };
|
||||
const scaleComponent = new ScaleComponent(newProps);
|
||||
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputBeyondLimitWarningMessage");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Label, Link, MessageBar, MessageBarType, Stack, Text, TextField } from "@fluentui/react";
|
||||
import { Link, MessageBar, MessageBarType, Stack, Text, TextField } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import * as Constants from "../../../../Common/Constants";
|
||||
import { configContext, Platform } from "../../../../ConfigContext";
|
||||
import { Platform, configContext } from "../../../../ConfigContext";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
import * as SharedConstants from "../../../../Shared/Constants";
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
subComponentStackProps,
|
||||
throughputUnit,
|
||||
titleAndInputStackProps,
|
||||
updateThroughputBeyondLimitWarningMessage,
|
||||
} from "../SettingsRenderUtils";
|
||||
import { hasDatabaseSharedThroughput } from "../SettingsUtils";
|
||||
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
|
||||
@@ -68,16 +67,6 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
return !!enableAutoScaleCapability;
|
||||
};
|
||||
|
||||
private getStorageCapacityTitle = (): JSX.Element => {
|
||||
const capacity: string = this.props.isFixedContainer ? "Fixed" : "Unlimited";
|
||||
return (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<Label>Storage capacity</Label>
|
||||
<Text>{capacity}</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
public getMaxRUs = (): number => {
|
||||
if (userContext.isTryCosmosDBSubscription) {
|
||||
return Constants.TryCosmosExperience.maxRU;
|
||||
@@ -131,18 +120,6 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
public getThroughputWarningMessage = (): JSX.Element => {
|
||||
const throughputExceedsBackendLimits: boolean =
|
||||
this.canThroughputExceedMaximumValue() &&
|
||||
this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||
|
||||
if (throughputExceedsBackendLimits && !this.props.isFixedContainer) {
|
||||
return updateThroughputBeyondLimitWarningMessage;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
public getLongDelayMessage = (): JSX.Element => {
|
||||
const matches: string[] = this.props.initialNotification?.description.match(
|
||||
`Throughput update for (.*) ${throughputUnit}`
|
||||
@@ -188,9 +165,10 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
spendAckChecked={false}
|
||||
onScaleSaveableChange={this.props.onScaleSaveableChange}
|
||||
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
|
||||
getThroughputWarningMessage={this.getThroughputWarningMessage}
|
||||
usageSizeInKB={this.props.collection?.usageSizeInKB()}
|
||||
throughputError={this.props.throughputError}
|
||||
instantMaximumThroughput={this.offer?.instantMaximumThroughput}
|
||||
softAllowedMaximumThroughput={this.offer?.softAllowedMaximumThroughput}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -229,12 +207,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
{this.getInitialNotificationElement() && (
|
||||
<MessageBar messageBarType={MessageBarType.warning}>{this.getInitialNotificationElement()}</MessageBar>
|
||||
)}
|
||||
{!this.isAutoScaleEnabled() && (
|
||||
<Stack {...subComponentStackProps}>
|
||||
{this.getThroughputInputComponent()}
|
||||
{!this.props.database && this.getStorageCapacityTitle()}
|
||||
</Stack>
|
||||
)}
|
||||
{!this.isAutoScaleEnabled() && <Stack {...subComponentStackProps}>{this.getThroughputInputComponent()}</Stack>}
|
||||
|
||||
{/* TODO: Replace link with call to the Azure Support blade */}
|
||||
{this.isAutoScaleEnabled() && (
|
||||
|
||||
@@ -311,8 +311,15 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
)}
|
||||
|
||||
{userContext.apiType === "SQL" && this.isLargePartitionKeyEnabled() && (
|
||||
<Text>Large {this.partitionKeyName.toLowerCase()} has been enabled</Text>
|
||||
<Text>Large {this.partitionKeyName.toLowerCase()} has been enabled.</Text>
|
||||
)}
|
||||
|
||||
{userContext.apiType === "SQL" &&
|
||||
(this.isHierarchicalPartitionedContainer() ? (
|
||||
<Text>Hierarchically partitioned container.</Text>
|
||||
) : (
|
||||
<Text>Non-hierarchically partitioned container.</Text>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -330,6 +337,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
};
|
||||
|
||||
public isLargePartitionKeyEnabled = (): boolean => this.props.collection.partitionKey?.version >= 2;
|
||||
public isHierarchicalPartitionedContainer = (): boolean => this.props.collection.partitionKey?.kind === "MultiHash";
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
|
||||
@@ -42,7 +42,8 @@ describe("ThroughputInputAutoPilotV3Component", () => {
|
||||
onScaleDiscardableChange: () => {
|
||||
return;
|
||||
},
|
||||
getThroughputWarningMessage: () => undefined,
|
||||
instantMaximumThroughput: 5000,
|
||||
softAllowedMaximumThroughput: 1000000,
|
||||
};
|
||||
|
||||
it("throughput input visible", () => {
|
||||
|
||||
@@ -3,10 +3,14 @@ import {
|
||||
ChoiceGroup,
|
||||
FontIcon,
|
||||
IChoiceGroupOption,
|
||||
IColumn,
|
||||
IProgressIndicatorStyles,
|
||||
ISeparatorStyles,
|
||||
Label,
|
||||
Link,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
ProgressIndicator,
|
||||
Separator,
|
||||
Stack,
|
||||
Text,
|
||||
TextField,
|
||||
@@ -23,24 +27,24 @@ import { autoPilotThroughput1K } from "../../../../../Utils/AutoPilotUtils";
|
||||
import { calculateEstimateNumber, usageInGB } from "../../../../../Utils/PricingUtils";
|
||||
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||
import {
|
||||
AutoscaleEstimatedSpendingDisplayProps,
|
||||
PriceBreakdown,
|
||||
checkBoxAndInputStackProps,
|
||||
getAutoPilotV3SpendElement,
|
||||
getChoiceGroupStyles,
|
||||
getEstimatedSpendingElement,
|
||||
getRuPriceBreakdown,
|
||||
getTextFieldStyles,
|
||||
getToolTipContainer,
|
||||
ManualEstimatedSpendingDisplayProps,
|
||||
getUpdateThroughputBelowMinimumMessage,
|
||||
getUpdateThroughputBeyondInstantLimitMessage,
|
||||
getUpdateThroughputBeyondSupportLimitMessage,
|
||||
manualToAutoscaleDisclaimerElement,
|
||||
messageBarStyles,
|
||||
noLeftPaddingCheckBoxStyle,
|
||||
PriceBreakdown,
|
||||
relaxedSpacingStackProps,
|
||||
saveThroughputWarningMessage,
|
||||
titleAndInputStackProps,
|
||||
transparentDetailsHeaderStyle,
|
||||
} from "../../SettingsRenderUtils";
|
||||
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
|
||||
import { IsComponentDirtyResult, getSanitizedInputValue, isDirty } from "../../SettingsUtils";
|
||||
import { ToolTipLabelComponent } from "../ToolTipLabelComponent";
|
||||
|
||||
export interface ThroughputInputAutoPilotV3Props {
|
||||
@@ -73,9 +77,10 @@ export interface ThroughputInputAutoPilotV3Props {
|
||||
onMaxAutoPilotThroughputChange: (newThroughput: number) => void;
|
||||
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
||||
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
||||
getThroughputWarningMessage: () => JSX.Element;
|
||||
usageSizeInKB: number;
|
||||
throughputError?: string;
|
||||
instantMaximumThroughput: number;
|
||||
softAllowedMaximumThroughput: number;
|
||||
}
|
||||
|
||||
interface ThroughputInputAutoPilotV3State {
|
||||
@@ -127,7 +132,12 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
} else if (this.props.isAutoPilotSelected) {
|
||||
if (isDirty(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)) {
|
||||
isDiscardable = true;
|
||||
if (AutoPilotUtils.isValidAutoPilotThroughput(this.props.maxAutoPilotThroughput)) {
|
||||
if (
|
||||
this.props.softAllowedMaximumThroughput
|
||||
? this.props.maxAutoPilotThroughput <= this.props.softAllowedMaximumThroughput &&
|
||||
AutoPilotUtils.isValidAutoPilotThroughput(this.props.maxAutoPilotThroughput)
|
||||
: AutoPilotUtils.isValidAutoPilotThroughput(this.props.maxAutoPilotThroughput)
|
||||
) {
|
||||
isSaveable = true;
|
||||
}
|
||||
}
|
||||
@@ -186,7 +196,15 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
|
||||
let estimatedSpend: JSX.Element;
|
||||
|
||||
if (!this.props.isAutoPilotSelected) {
|
||||
if (this.props.isAutoPilotSelected) {
|
||||
estimatedSpend = this.getEstimatedAutoscaleSpendElement(
|
||||
this.props.maxAutoPilotThroughputBaseline,
|
||||
userContext.portalEnv,
|
||||
regions,
|
||||
multimaster,
|
||||
isDirty ? this.props.maxAutoPilotThroughput : undefined
|
||||
);
|
||||
} else {
|
||||
estimatedSpend = this.getEstimatedManualSpendElement(
|
||||
// if migrating from autoscale to manual, we use the autoscale RUs value as that is what will be set...
|
||||
this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : this.props.throughputBaseline,
|
||||
@@ -195,14 +213,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
multimaster,
|
||||
isDirty ? this.props.throughput : undefined
|
||||
);
|
||||
} else {
|
||||
estimatedSpend = this.getEstimatedAutoscaleSpendElement(
|
||||
this.props.maxAutoPilotThroughputBaseline,
|
||||
userContext.portalEnv,
|
||||
regions,
|
||||
multimaster,
|
||||
isDirty ? this.props.maxAutoPilotThroughput : undefined
|
||||
);
|
||||
}
|
||||
return estimatedSpend;
|
||||
};
|
||||
@@ -215,52 +225,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
newThroughput?: number
|
||||
): JSX.Element => {
|
||||
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, true);
|
||||
const estimatedSpendingColumns: IColumn[] = [
|
||||
{
|
||||
key: "costType",
|
||||
name: "",
|
||||
fieldName: "costType",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle,
|
||||
},
|
||||
{
|
||||
key: "minPerMonth",
|
||||
name: "Min Per Month",
|
||||
fieldName: "minPerMonth",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle,
|
||||
},
|
||||
{
|
||||
key: "maxPerMonth",
|
||||
name: "Max Per Month",
|
||||
fieldName: "maxPerMonth",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle,
|
||||
},
|
||||
];
|
||||
const estimatedSpendingItems: AutoscaleEstimatedSpendingDisplayProps[] = [
|
||||
{
|
||||
costType: <Text>Current Cost</Text>,
|
||||
minPerMonth: (
|
||||
<Text>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)}
|
||||
</Text>
|
||||
),
|
||||
maxPerMonth: (
|
||||
<Text>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (newThroughput) {
|
||||
const newThroughputCostElement = (): JSX.Element => {
|
||||
const newPrices: PriceBreakdown = getRuPriceBreakdown(
|
||||
newThroughput,
|
||||
serverId,
|
||||
@@ -268,37 +234,40 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
isMultimaster,
|
||||
true
|
||||
);
|
||||
estimatedSpendingItems.unshift({
|
||||
costType: (
|
||||
<Text>
|
||||
<b>Updated Cost</b>
|
||||
</Text>
|
||||
),
|
||||
minPerMonth: (
|
||||
<Text>
|
||||
<b>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)}
|
||||
</b>
|
||||
</Text>
|
||||
),
|
||||
maxPerMonth: (
|
||||
<Text>
|
||||
<b>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}
|
||||
</b>
|
||||
</Text>
|
||||
),
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Text style={{ fontWeight: 600 }}>Updated cost per month</Text>
|
||||
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
|
||||
<Text style={{ width: "50%" }}>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)} min
|
||||
</Text>
|
||||
<Text style={{ width: "50%" }}>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)} max
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return getEstimatedSpendingElement(
|
||||
estimatedSpendingColumns,
|
||||
estimatedSpendingItems,
|
||||
newThroughput ?? throughput,
|
||||
numberOfRegions,
|
||||
prices,
|
||||
true
|
||||
);
|
||||
const costElement = (): JSX.Element => {
|
||||
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, true);
|
||||
return (
|
||||
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
|
||||
{newThroughput && newThroughputCostElement()}
|
||||
<Text style={{ fontWeight: 600 }}>Current cost per month</Text>
|
||||
<Stack horizontal style={{ marginTop: 5 }}>
|
||||
<Text style={{ width: "50%" }}>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)} min
|
||||
</Text>
|
||||
<Text style={{ width: "50%" }}>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)} max
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
return getEstimatedSpendingElement(costElement(), newThroughput ?? throughput, numberOfRegions, prices, true);
|
||||
};
|
||||
|
||||
private getEstimatedManualSpendElement = (
|
||||
@@ -309,122 +278,55 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
newThroughput?: number
|
||||
): JSX.Element => {
|
||||
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, false);
|
||||
const estimatedSpendingColumns: IColumn[] = [
|
||||
{
|
||||
key: "costType",
|
||||
name: "",
|
||||
fieldName: "costType",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle,
|
||||
},
|
||||
{
|
||||
key: "hourly",
|
||||
name: "Hourly",
|
||||
fieldName: "hourly",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle,
|
||||
},
|
||||
{
|
||||
key: "daily",
|
||||
name: "Daily",
|
||||
fieldName: "daily",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle,
|
||||
},
|
||||
{
|
||||
key: "monthly",
|
||||
name: "Monthly",
|
||||
fieldName: "monthly",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle,
|
||||
},
|
||||
];
|
||||
const estimatedSpendingItems: ManualEstimatedSpendingDisplayProps[] = [
|
||||
{
|
||||
costType: <Text>Current Cost</Text>,
|
||||
hourly: (
|
||||
<Text>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.hourlyPrice)}
|
||||
</Text>
|
||||
),
|
||||
daily: (
|
||||
<Text>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.dailyPrice)}
|
||||
</Text>
|
||||
),
|
||||
monthly: (
|
||||
<Text>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (newThroughput) {
|
||||
const newThroughputCostElement = (): JSX.Element => {
|
||||
const newPrices: PriceBreakdown = getRuPriceBreakdown(
|
||||
newThroughput,
|
||||
serverId,
|
||||
numberOfRegions,
|
||||
isMultimaster,
|
||||
false
|
||||
true
|
||||
);
|
||||
estimatedSpendingItems.unshift({
|
||||
costType: (
|
||||
<Text>
|
||||
<b>Updated Cost</b>
|
||||
</Text>
|
||||
),
|
||||
hourly: (
|
||||
<Text>
|
||||
<b>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.hourlyPrice)}
|
||||
</b>
|
||||
</Text>
|
||||
),
|
||||
daily: (
|
||||
<Text>
|
||||
<b>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.dailyPrice)}
|
||||
</b>
|
||||
</Text>
|
||||
),
|
||||
monthly: (
|
||||
<Text>
|
||||
<b>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}
|
||||
</b>
|
||||
</Text>
|
||||
),
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Text style={{ fontWeight: 600 }}>Updated cost per month</Text>
|
||||
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
|
||||
<Text style={{ width: "33%" }}>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.hourlyPrice)}/hr
|
||||
</Text>
|
||||
<Text style={{ width: "33%" }}>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.dailyPrice)}/day
|
||||
</Text>
|
||||
<Text style={{ width: "33%" }}>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}/mo
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return getEstimatedSpendingElement(
|
||||
estimatedSpendingColumns,
|
||||
estimatedSpendingItems,
|
||||
newThroughput ?? throughput,
|
||||
numberOfRegions,
|
||||
prices,
|
||||
false
|
||||
);
|
||||
};
|
||||
const costElement = (): JSX.Element => {
|
||||
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, true);
|
||||
return (
|
||||
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
|
||||
{newThroughput && newThroughputCostElement()}
|
||||
<Text style={{ fontWeight: 600 }}>Current cost per month</Text>
|
||||
<Stack horizontal style={{ marginTop: 5 }}>
|
||||
<Text style={{ width: "33%" }}>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.hourlyPrice)}/hr
|
||||
</Text>
|
||||
<Text style={{ width: "33%" }}>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.dailyPrice)}/day
|
||||
</Text>
|
||||
<Text style={{ width: "33%" }}>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}/mo
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
private getAutoPilotUsageCost = (): JSX.Element => {
|
||||
if (!this.props.maxAutoPilotThroughput) {
|
||||
return <></>;
|
||||
}
|
||||
return getAutoPilotV3SpendElement(
|
||||
this.props.maxAutoPilotThroughput,
|
||||
false /* isDatabaseThroughput */,
|
||||
!this.props.isEmulator ? this.getRequestUnitsUsageCost() : <></>
|
||||
);
|
||||
return getEstimatedSpendingElement(costElement(), newThroughput ?? throughput, numberOfRegions, prices, false);
|
||||
};
|
||||
|
||||
private onAutoPilotThroughputChange = (
|
||||
@@ -511,7 +413,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
onChange={this.onChoiceGroupChange}
|
||||
required={this.props.showAsMandatory}
|
||||
ariaLabelledBy={labelId}
|
||||
styles={getChoiceGroupStyles(this.props.wasAutopilotOriginallySet, this.props.isAutoPilotSelected)}
|
||||
styles={getChoiceGroupStyles(this.props.wasAutopilotOriginallySet, this.props.isAutoPilotSelected, true)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
@@ -520,97 +422,266 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
private onSpendAckChecked = (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean): void =>
|
||||
this.setState({ spendAckChecked: checked });
|
||||
|
||||
private renderAutoPilotInput = (): JSX.Element => (
|
||||
<>
|
||||
<Text>
|
||||
Provision maximum RU/s required by this resource. Estimate your required RU/s with
|
||||
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
|
||||
{` capacity calculator`}
|
||||
</Link>
|
||||
</Text>
|
||||
<TextField
|
||||
label="Max RU/s"
|
||||
required
|
||||
type="number"
|
||||
id="autopilotInput"
|
||||
key="auto pilot throughput input"
|
||||
styles={getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)}
|
||||
disabled={this.overrideWithProvisionedThroughputSettings()}
|
||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
|
||||
onChange={this.onAutoPilotThroughputChange}
|
||||
min={autoPilotThroughput1K}
|
||||
errorMessage={this.props.throughputError}
|
||||
private getStorageCapacityTitle = (): JSX.Element => {
|
||||
const capacity: string = this.props.isFixed ? "Fixed" : "Unlimited";
|
||||
return (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<Label>Storage capacity</Label>
|
||||
<Text>{capacity}</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
private thoughputRangeSeparatorStyles: Partial<ISeparatorStyles> = {
|
||||
root: [
|
||||
{
|
||||
selectors: {
|
||||
"::before": {
|
||||
backgroundColor: "rgb(200, 200, 200)",
|
||||
height: "3px",
|
||||
marginTop: "-1px",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
private currentThroughputValue = (): number => {
|
||||
return this.props.isAutoPilotSelected
|
||||
? this.props.maxAutoPilotThroughput
|
||||
: this.overrideWithAutoPilotSettings()
|
||||
? this.props.maxAutoPilotThroughputBaseline
|
||||
: this.props.throughput;
|
||||
};
|
||||
|
||||
private getCurrentRuRange = (): "below" | "instant" | "delayed" | "requireSupport" => {
|
||||
if (this.currentThroughputValue() < this.props.minimum) {
|
||||
return "below";
|
||||
}
|
||||
if (
|
||||
this.currentThroughputValue() >= this.props.minimum &&
|
||||
this.currentThroughputValue() <= this.props.instantMaximumThroughput
|
||||
) {
|
||||
return "instant";
|
||||
}
|
||||
if (this.currentThroughputValue() > this.props.softAllowedMaximumThroughput) {
|
||||
return "requireSupport";
|
||||
}
|
||||
|
||||
return "delayed";
|
||||
};
|
||||
|
||||
private getRuThermometerStyles = (): Partial<IProgressIndicatorStyles> => ({
|
||||
progressBar: [
|
||||
{
|
||||
backgroundColor:
|
||||
this.getCurrentRuRange() === "instant"
|
||||
? "rgb(0, 120, 212)"
|
||||
: this.getCurrentRuRange() === "delayed"
|
||||
? "rgb(255 216 109)"
|
||||
: "rgb(251, 217, 203)",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
private getRuThermometerPercentValue = (): number => {
|
||||
let percentValue: number;
|
||||
const currentRus = this.currentThroughputValue();
|
||||
|
||||
switch (this.getCurrentRuRange()) {
|
||||
case "below":
|
||||
percentValue = 0;
|
||||
break;
|
||||
case "instant": {
|
||||
const percentOfInstantRange: number = currentRus / this.props.instantMaximumThroughput;
|
||||
percentValue = percentOfInstantRange * 0.34;
|
||||
break;
|
||||
}
|
||||
case "delayed": {
|
||||
const adjustedMax = this.props.softAllowedMaximumThroughput - this.props.instantMaximumThroughput;
|
||||
const adjustedRus = currentRus - this.props.instantMaximumThroughput;
|
||||
const percentOfDelayedRange = adjustedRus / adjustedMax;
|
||||
const adjustedPercent = percentOfDelayedRange * 0.66;
|
||||
percentValue = adjustedPercent + 0.34;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// over maximum
|
||||
percentValue = 1;
|
||||
}
|
||||
return percentValue;
|
||||
};
|
||||
|
||||
private getRUThermometer = (): JSX.Element => (
|
||||
<Stack>
|
||||
<Stack horizontal>
|
||||
<Stack.Item style={{ width: "34%" }}>
|
||||
<span>{this.props.minimum.toLocaleString()}</span>
|
||||
</Stack.Item>
|
||||
<Stack.Item style={{ width: "66%" }}>
|
||||
<span style={{ float: "left", transform: "translateX(-50%)" }}>
|
||||
{this.props.instantMaximumThroughput.toLocaleString()}
|
||||
</span>
|
||||
<span style={{ float: "right" }}>{this.props.softAllowedMaximumThroughput.toLocaleString()}</span>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<ProgressIndicator
|
||||
barHeight={20}
|
||||
percentComplete={this.getRuThermometerPercentValue()}
|
||||
styles={this.getRuThermometerStyles()}
|
||||
/>
|
||||
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
|
||||
{this.minRUperGBSurvey()}
|
||||
{this.props.spendAckVisible && (
|
||||
<Checkbox
|
||||
id="spendAckCheckBox"
|
||||
styles={noLeftPaddingCheckBoxStyle}
|
||||
label={this.props.spendAckText}
|
||||
checked={this.state.spendAckChecked}
|
||||
onChange={this.onSpendAckChecked}
|
||||
<Stack horizontal>
|
||||
<Stack.Item style={{ width: "34%", paddingRight: "5px" }}>
|
||||
<Separator styles={this.thoughputRangeSeparatorStyles}>Instant</Separator>
|
||||
</Stack.Item>
|
||||
<Stack.Item style={{ width: "66%", paddingLeft: "5px" }}>
|
||||
<Separator styles={this.thoughputRangeSeparatorStyles}>4-6 hrs</Separator>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
private showThroughputWarning = (): boolean => {
|
||||
return (
|
||||
this.currentThroughputValue() > this.props.instantMaximumThroughput ||
|
||||
this.currentThroughputValue() < this.props.minimum
|
||||
);
|
||||
};
|
||||
|
||||
private getThroughputWarningMessageText = (): JSX.Element => {
|
||||
switch (this.getCurrentRuRange()) {
|
||||
case "below":
|
||||
return getUpdateThroughputBelowMinimumMessage(this.props.minimum);
|
||||
case "delayed":
|
||||
return getUpdateThroughputBeyondInstantLimitMessage(this.props.instantMaximumThroughput);
|
||||
case "requireSupport":
|
||||
return getUpdateThroughputBeyondSupportLimitMessage(
|
||||
this.props.instantMaximumThroughput,
|
||||
this.props.softAllowedMaximumThroughput
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
|
||||
private getThroughputWarningMessageBar = (): JSX.Element => {
|
||||
const isSevereWarning: boolean =
|
||||
this.currentThroughputValue() > this.props.softAllowedMaximumThroughput ||
|
||||
this.currentThroughputValue() < this.props.minimum;
|
||||
return (
|
||||
<MessageBar messageBarType={isSevereWarning ? MessageBarType.severeWarning : MessageBarType.warning}>
|
||||
{this.getThroughputWarningMessageText()}
|
||||
</MessageBar>
|
||||
);
|
||||
};
|
||||
|
||||
private getThroughputTextField = (): JSX.Element => (
|
||||
<>
|
||||
{this.props.isAutoPilotSelected ? (
|
||||
<TextField
|
||||
label="Maximum RU/s required by this resource"
|
||||
required
|
||||
type="number"
|
||||
id="autopilotInput"
|
||||
key="auto pilot throughput input"
|
||||
styles={getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)}
|
||||
disabled={this.overrideWithProvisionedThroughputSettings()}
|
||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
|
||||
onChange={this.onAutoPilotThroughputChange}
|
||||
min={autoPilotThroughput1K}
|
||||
onGetErrorMessage={(value: string) => {
|
||||
const sanitizedValue = getSanitizedInputValue(value);
|
||||
return sanitizedValue % 1000
|
||||
? "Throughput value must be in increments of 1000"
|
||||
: this.props.throughputError;
|
||||
}}
|
||||
validateOnLoad={false}
|
||||
/>
|
||||
) : (
|
||||
<TextField
|
||||
required
|
||||
type="number"
|
||||
id="throughputInput"
|
||||
key="provisioned throughput input"
|
||||
styles={getTextFieldStyles(this.props.throughput, this.props.throughputBaseline)}
|
||||
disabled={this.overrideWithAutoPilotSettings()}
|
||||
step={this.step}
|
||||
value={
|
||||
this.overrideWithAutoPilotSettings()
|
||||
? this.props.maxAutoPilotThroughputBaseline?.toString()
|
||||
: this.props.throughput?.toString()
|
||||
}
|
||||
onChange={this.onThroughputChange}
|
||||
min={this.props.minimum}
|
||||
errorMessage={this.props.throughputError}
|
||||
/>
|
||||
)}
|
||||
{this.props.isFixed && <p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>}
|
||||
</>
|
||||
);
|
||||
|
||||
private renderThroughputInput = (): JSX.Element => (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<Text>
|
||||
Estimate your required throughput with
|
||||
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
|
||||
{` capacity calculator`} <FontIcon iconName="NavigateExternalInline" />
|
||||
</Link>
|
||||
</Text>
|
||||
<TextField
|
||||
required
|
||||
type="number"
|
||||
id="throughputInput"
|
||||
key="provisioned throughput input"
|
||||
styles={getTextFieldStyles(this.props.throughput, this.props.throughputBaseline)}
|
||||
disabled={this.overrideWithAutoPilotSettings()}
|
||||
step={this.step}
|
||||
value={
|
||||
this.overrideWithAutoPilotSettings()
|
||||
? this.props.maxAutoPilotThroughputBaseline?.toString()
|
||||
: this.props.throughput?.toString()
|
||||
}
|
||||
onChange={this.onThroughputChange}
|
||||
min={this.props.minimum}
|
||||
errorMessage={this.props.throughputError}
|
||||
/>
|
||||
{this.state.exceedFreeTierThroughput && (
|
||||
<MessageBar
|
||||
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
|
||||
styles={messageBarStyles}
|
||||
>
|
||||
{`Billing will apply if you provision more than ${SharedConstants.FreeTierLimits.RU} RU/s of manual throughput, or if the resource scales beyond ${SharedConstants.FreeTierLimits.RU} RU/s with autoscale.`}
|
||||
</MessageBar>
|
||||
)}
|
||||
{this.props.getThroughputWarningMessage() && (
|
||||
<MessageBar
|
||||
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
|
||||
styles={messageBarStyles}
|
||||
>
|
||||
{this.props.getThroughputWarningMessage()}
|
||||
</MessageBar>
|
||||
)}
|
||||
{!this.props.isEmulator && this.getRequestUnitsUsageCost()}
|
||||
{this.minRUperGBSurvey()}
|
||||
{this.props.spendAckVisible && (
|
||||
<Checkbox
|
||||
id="spendAckCheckBox"
|
||||
styles={noLeftPaddingCheckBoxStyle}
|
||||
label={this.props.spendAckText}
|
||||
checked={this.state.spendAckChecked}
|
||||
onChange={this.onSpendAckChecked}
|
||||
/>
|
||||
)}
|
||||
<br />
|
||||
{this.props.isFixed && <p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>}
|
||||
private renderThroughputComponent = (): JSX.Element => (
|
||||
<Stack horizontal>
|
||||
<Stack.Item style={{ width: "70%", maxWidth: "700px" }}>
|
||||
<Stack {...relaxedSpacingStackProps} style={{ paddingRight: "50px" }}>
|
||||
{this.getThroughputTextField()}
|
||||
{this.props.instantMaximumThroughput && (
|
||||
<Stack>
|
||||
{this.getRUThermometer()}
|
||||
{this.showThroughputWarning() && this.getThroughputWarningMessageBar()}
|
||||
</Stack>
|
||||
)}
|
||||
{this.props.isAutoPilotSelected ? (
|
||||
<Text style={{ marginTop: "40px" }}>
|
||||
Based on usage, your {this.props.collectionName ? "container" : "database"} throughput will scale from{" "}
|
||||
<b>
|
||||
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.props.maxAutoPilotThroughput)} RU/s (10% of max RU/s) -{" "}
|
||||
{this.props.maxAutoPilotThroughput} RU/s
|
||||
</b>
|
||||
<br />
|
||||
</Text>
|
||||
) : (
|
||||
<>
|
||||
{this.state.exceedFreeTierThroughput && (
|
||||
<MessageBar
|
||||
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
|
||||
styles={messageBarStyles}
|
||||
style={{ marginTop: "40px" }}
|
||||
>
|
||||
{`Billing will apply if you provision more than ${SharedConstants.FreeTierLimits.RU} RU/s of manual throughput, or if the resource scales beyond ${SharedConstants.FreeTierLimits.RU} RU/s with autoscale.`}
|
||||
</MessageBar>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!this.overrideWithProvisionedThroughputSettings() && (
|
||||
<Text>
|
||||
Estimate your required RU/s with
|
||||
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
|
||||
{` capacity calculator`} <FontIcon iconName="NavigateExternalInline" />
|
||||
</Link>
|
||||
</Text>
|
||||
)}
|
||||
{this.minRUperGBSurvey()}
|
||||
{this.props.spendAckVisible && (
|
||||
<Checkbox
|
||||
id="spendAckCheckBox"
|
||||
styles={noLeftPaddingCheckBoxStyle}
|
||||
label={this.props.spendAckText}
|
||||
checked={this.state.spendAckChecked}
|
||||
onChange={this.onSpendAckChecked}
|
||||
/>
|
||||
)}
|
||||
{this.props.isFixed && (
|
||||
<p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>
|
||||
)}
|
||||
{this.props.collectionName && (
|
||||
<Stack.Item style={{ marginTop: "40px" }}>{this.getStorageCapacityTitle()}</Stack.Item>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<Stack.Item style={{ width: "30%", maxWidth: "300px" }}>
|
||||
{!this.props.isEmulator ? this.getRequestUnitsUsageCost() : <></>}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -640,7 +711,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
{this.renderWarningMessage()}
|
||||
{this.renderThroughputModeChoices()}
|
||||
|
||||
{this.props.isAutoPilotSelected ? this.renderAutoPilotInput() : this.renderThroughputInput()}
|
||||
{this.renderThroughputComponent()}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ exports[`ConflictResolutionComponent Path text field displayed 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": undefined,
|
||||
@@ -100,6 +102,8 @@ exports[`ConflictResolutionComponent Sproc text field displayed 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
|
||||
@@ -39,7 +39,6 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||
canExceedMaximumValue={true}
|
||||
collectionName="test"
|
||||
databaseName="test"
|
||||
getThroughputWarningMessage={[Function]}
|
||||
isAutoPilotSelected={false}
|
||||
isEmulator={false}
|
||||
isEnabled={true}
|
||||
@@ -60,20 +59,6 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||
usageSizeInKB={100}
|
||||
wasAutopilotOriginallySet={true}
|
||||
/>
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledLabelBase>
|
||||
Storage capacity
|
||||
</StyledLabelBase>
|
||||
<Text>
|
||||
Unlimited
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
@@ -40,6 +40,8 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -106,6 +108,8 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -168,6 +172,8 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -218,7 +224,10 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
||||
<Text>
|
||||
Large
|
||||
partition key
|
||||
has been enabled
|
||||
has been enabled.
|
||||
</Text>
|
||||
<Text>
|
||||
Non-hierarchically partitioned container.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -264,6 +273,8 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -330,6 +341,8 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -382,6 +395,8 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": undefined,
|
||||
@@ -445,6 +460,8 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -495,7 +512,10 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
||||
<Text>
|
||||
Large
|
||||
partition key
|
||||
has been enabled
|
||||
has been enabled.
|
||||
</Text>
|
||||
<Text>
|
||||
Non-hierarchically partitioned container.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -541,6 +561,8 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -607,6 +629,8 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -659,6 +683,8 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -734,7 +760,10 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
|
||||
<Text>
|
||||
Large
|
||||
partition key
|
||||
has been enabled
|
||||
has been enabled.
|
||||
</Text>
|
||||
<Text>
|
||||
Non-hierarchically partitioned container.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -780,6 +809,8 @@ exports[`SubSettingsComponent renders 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -846,6 +877,8 @@ exports[`SubSettingsComponent renders 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -898,6 +931,8 @@ exports[`SubSettingsComponent renders 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -986,6 +1021,8 @@ exports[`SubSettingsComponent renders 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -1036,7 +1073,10 @@ exports[`SubSettingsComponent renders 1`] = `
|
||||
<Text>
|
||||
Large
|
||||
partition key
|
||||
has been enabled
|
||||
has been enabled.
|
||||
</Text>
|
||||
<Text>
|
||||
Non-hierarchically partitioned container.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -1082,6 +1122,8 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": undefined,
|
||||
@@ -1123,6 +1165,8 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -1175,6 +1219,8 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -1263,6 +1309,8 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -1313,7 +1361,10 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
||||
<Text>
|
||||
Large
|
||||
partition key
|
||||
has been enabled
|
||||
has been enabled.
|
||||
</Text>
|
||||
<Text>
|
||||
Non-hierarchically partitioned container.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -2,154 +2,60 @@
|
||||
|
||||
exports[`SettingsUtils functions render 1`] = `
|
||||
<Fragment>
|
||||
<Text>
|
||||
Your
|
||||
container
|
||||
throughput will automatically scale from
|
||||
|
||||
<b>
|
||||
100
|
||||
RU/s (10% of max RU/s) -
|
||||
|
||||
1000
|
||||
RU/s
|
||||
</b>
|
||||
|
||||
based on usage.
|
||||
<br />
|
||||
</Text>
|
||||
<Text>
|
||||
After the first
|
||||
10
|
||||
GB of data stored, the max RU/s will be automatically upgraded based on the new storage value.
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/cosmos-autoscale-info"
|
||||
target="_blank"
|
||||
>
|
||||
|
||||
Learn more
|
||||
</StyledLinkBase>
|
||||
.
|
||||
</Text>
|
||||
<Text>
|
||||
Your
|
||||
database
|
||||
throughput will automatically scale from
|
||||
|
||||
<b>
|
||||
100
|
||||
RU/s (10% of max RU/s) -
|
||||
|
||||
1000
|
||||
RU/s
|
||||
</b>
|
||||
|
||||
based on usage.
|
||||
<br />
|
||||
</Text>
|
||||
<Text>
|
||||
After the first
|
||||
10
|
||||
GB of data stored, the max RU/s will be automatically upgraded based on the new storage value.
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/cosmos-autoscale-info"
|
||||
target="_blank"
|
||||
>
|
||||
|
||||
Learn more
|
||||
</StyledLinkBase>
|
||||
.
|
||||
</Text>
|
||||
<Stack
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"width": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledWithViewportComponent
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"fieldName": "costType",
|
||||
"isResizable": true,
|
||||
"key": "costType",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "",
|
||||
},
|
||||
Object {
|
||||
"fieldName": "hourly",
|
||||
"isResizable": true,
|
||||
"key": "hourly",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "Hourly",
|
||||
},
|
||||
Object {
|
||||
"fieldName": "daily",
|
||||
"isResizable": true,
|
||||
"key": "daily",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "Daily",
|
||||
},
|
||||
Object {
|
||||
"fieldName": "monthly",
|
||||
"isResizable": true,
|
||||
"key": "monthly",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "Monthly",
|
||||
},
|
||||
]
|
||||
}
|
||||
disableSelectionZone={true}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"costType": <Text>
|
||||
Current Cost
|
||||
</Text>,
|
||||
"daily": <Text>
|
||||
$ 24.48
|
||||
</Text>,
|
||||
"hourly": <Text>
|
||||
$ 1.02
|
||||
</Text>,
|
||||
"monthly": <Text>
|
||||
$ 744.6
|
||||
</Text>,
|
||||
},
|
||||
]
|
||||
}
|
||||
layoutMode={1}
|
||||
onRenderRow={[Function]}
|
||||
selectionMode={0}
|
||||
/>
|
||||
<Stack>
|
||||
<Text
|
||||
id="throughputSpendElement"
|
||||
style={
|
||||
Object {
|
||||
"fontWeight": 600,
|
||||
}
|
||||
}
|
||||
>
|
||||
(
|
||||
regions:
|
||||
|
||||
2
|
||||
,
|
||||
1000
|
||||
RU/s,
|
||||
¥
|
||||
0.00051
|
||||
/RU)
|
||||
Cost estimate*
|
||||
</Text>
|
||||
<Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontWeight": 600,
|
||||
"marginTop": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
How we calculate this
|
||||
</Text>
|
||||
<Stack
|
||||
id="throughputSpendElement"
|
||||
style={
|
||||
Object {
|
||||
"marginTop": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<span>
|
||||
2
|
||||
region
|
||||
<span>
|
||||
s
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
1000
|
||||
RU/s
|
||||
</span>
|
||||
<span>
|
||||
¥
|
||||
0.00051
|
||||
/RU
|
||||
</span>
|
||||
</Stack>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"marginTop": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
<em>
|
||||
*
|
||||
This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account
|
||||
</em>
|
||||
</Text>
|
||||
@@ -205,19 +111,6 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
>
|
||||
You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes.
|
||||
</Text>
|
||||
<Text
|
||||
id="updateThroughputBeyondLimitWarningMessage"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
You are about to request an increase in throughput beyond the pre-allocated capacity. The service will scale out and increase throughput for the selected container. This operation will take 1-3 business days to complete. You can track the status of this request in Notifications.
|
||||
</Text>
|
||||
<Text
|
||||
id="updateThroughputDelayedApplyWarningMessage"
|
||||
styles={
|
||||
|
||||
@@ -22,10 +22,17 @@ export class ContainerSampleGenerator {
|
||||
/**
|
||||
* Factory function to load the json data file
|
||||
*/
|
||||
public static async createSampleGeneratorAsync(container: Explorer): Promise<ContainerSampleGenerator> {
|
||||
public static async createSampleGeneratorAsync(
|
||||
container: Explorer,
|
||||
isCopilot?: boolean
|
||||
): Promise<ContainerSampleGenerator> {
|
||||
const generator = new ContainerSampleGenerator(container);
|
||||
let dataFileContent: any;
|
||||
if (userContext.apiType === "Gremlin") {
|
||||
if (isCopilot) {
|
||||
dataFileContent = await import(
|
||||
/* webpackChunkName: "queryCopilotSampleData" */ "../../../sampleData/queryCopilotSampleData.json"
|
||||
);
|
||||
} else if (userContext.apiType === "Gremlin") {
|
||||
dataFileContent = await import(
|
||||
/* webpackChunkName: "gremlinSampleJsonData" */ "../../../sampleData/gremlinSampleData.json"
|
||||
);
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
import { Link } from "@fluentui/react/lib/Link";
|
||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||
import { IGalleryItem } from "Juno/JunoClient";
|
||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
||||
import * as ko from "knockout";
|
||||
import React from "react";
|
||||
import _ from "underscore";
|
||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
||||
import shallow from "zustand/shallow";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
||||
import * as Constants from "../Common/Constants";
|
||||
import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook, PoolIdType } from "../Common/Constants";
|
||||
import { readCollection } from "../Common/dataAccess/readCollection";
|
||||
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
||||
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { QueriesClient } from "../Common/QueriesClient";
|
||||
import { readCollection, readSampleCollection } from "../Common/dataAccess/readCollection";
|
||||
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import { ContainerConnectionInfo, IPhoenixServiceInfo, IProvisionData, IResponse } from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { useTabs } from "../hooks/useTabs";
|
||||
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 { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
||||
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import { stringToBlob } from "../Utils/BlobUtils";
|
||||
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
||||
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
||||
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { useTabs } from "../hooks/useTabs";
|
||||
import "./ComponentRegisterer";
|
||||
import { DialogProps, useDialog } from "./Controls/Dialog";
|
||||
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
||||
@@ -1272,5 +1272,26 @@ export default class Explorer {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
await this.initNotebooks(userContext.databaseAccount);
|
||||
}
|
||||
|
||||
await this.refreshSampleData();
|
||||
}
|
||||
|
||||
public async refreshSampleData(): Promise<void> {
|
||||
if (!userContext.sampleDataConnectionInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const collection: DataModels.Collection = await readSampleCollection();
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const databaseId = userContext.sampleDataConnectionInfo?.databaseId;
|
||||
if (!databaseId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection);
|
||||
useDatabases.setState({ sampleDataResourceTokenCollection });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import * as React from "react";
|
||||
import LoadGraphIcon from "../../../../images/LoadGraph.png";
|
||||
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
|
||||
import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
|
||||
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
|
||||
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { InputProperty } from "../../../Contracts/ViewModels";
|
||||
@@ -182,7 +182,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
public static readonly WITHOUT_STEP_ARGS_MAX_CHARS = 10000; // maximums char size of the without() step parameter
|
||||
public static readonly ROOT_LIST_PAGE_SIZE = 100;
|
||||
private static readonly MAX_LATEST_QUERIES = 10;
|
||||
private static readonly MAX_RESULT_SIZE = 10000;
|
||||
// TODO This prevents loading too much data in GraphExplorer and d3. A better is to cap the size of the result, rather than the number of items in the array.
|
||||
private static readonly MAX_RESULT_SIZE = 100_000;
|
||||
|
||||
private static readonly TAB_INDEX_JSON = 0;
|
||||
private static readonly TAB_INDEX_GRAPH = 1;
|
||||
|
||||
@@ -31,7 +31,7 @@ export class GremlinClient {
|
||||
public client: GremlinSimpleClient;
|
||||
public pendingResults: Map<string, PendingResultData>; // public for testing purposes
|
||||
private maxResultSize: number;
|
||||
private static readonly PENDING_REQUEST_TIMEOUT_MS = 6 /* minutes */ * 60 /* seconds */ * 1000 /* ms */;
|
||||
private static readonly PENDING_REQUEST_TIMEOUT_MS = 1 /* hour */ * 3_600 /* seconds */ * 1_000 /* ms */;
|
||||
private static readonly TIMEOUT_ERROR_MSG = `Pending request timed out (${GremlinClient.PENDING_REQUEST_TIMEOUT_MS} ms)`;
|
||||
private static readonly LOG_AREA = "GremlinClient";
|
||||
|
||||
|
||||
@@ -205,6 +205,7 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
||||
role="button"
|
||||
onClick={onAddNewProperty}
|
||||
onKeyPress={(event: React.KeyboardEvent<HTMLSpanElement>) => onAddNewPropertyKeyPress(event)}
|
||||
aria-label="Add property"
|
||||
>
|
||||
<img className="refreshcol rightPaneAddPropertyImg" src={AddIcon} alt="Add property" /> Add Property
|
||||
</span>
|
||||
|
||||
@@ -57,7 +57,7 @@ const SharedDatabaseDefault: DataModels.IndexingPolicy = {
|
||||
],
|
||||
};
|
||||
|
||||
const AllPropertiesIndexed: DataModels.IndexingPolicy = {
|
||||
export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
|
||||
indexingMode: "consistent",
|
||||
automatic: true,
|
||||
includedPaths: [
|
||||
@@ -634,18 +634,18 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
{userContext.apiType === "SQL" && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<DefaultButton
|
||||
styles={{ root: { padding: 0, width: 250, height: 30 }, label: { fontSize: 12 } }}
|
||||
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
||||
hidden={this.state.useHashV1}
|
||||
disabled={this.state.subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
|
||||
onClick={() => this.setState({ subPartitionKeys: [...this.state.subPartitionKeys, ""] })}
|
||||
>
|
||||
Add hierarchical partition key (preview)
|
||||
Add hierarchical partition key
|
||||
</DefaultButton>
|
||||
{this.state.subPartitionKeys.length > 0 && (
|
||||
<Text variant="small">
|
||||
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to
|
||||
partition your data with up to three levels of keys for better data distribution. Requires preview
|
||||
version of .NET V3 or Java V4 SDK.{" "}
|
||||
partition your data with up to three levels of keys for better data distribution. Requires .NET
|
||||
V3, Java V4 SDK, or preview JavaScript V3 SDK.{" "}
|
||||
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
|
||||
Learn more
|
||||
</Link>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Text, TextField } from "@fluentui/react";
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import { Areas } from "Common/Constants";
|
||||
import { deleteDatabase } from "Common/dataAccess/deleteDatabase";
|
||||
import DeleteFeedback from "Common/DeleteFeedback";
|
||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||
import { deleteDatabase } from "Common/dataAccess/deleteDatabase";
|
||||
import { Collection, Database } 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 { logConsoleError } 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 { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";
|
||||
@@ -36,8 +36,13 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
|
||||
const submit = async (): Promise<void> => {
|
||||
if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) {
|
||||
setFormError("Input database name does not match the selected database");
|
||||
setFormError(
|
||||
`Input database name "${databaseInput}" does not match the selected database "${selectedDatabase.id()}"`
|
||||
);
|
||||
logConsoleError(`Error while deleting collection ${selectedDatabase && selectedDatabase.id()}`);
|
||||
logConsoleError(
|
||||
`Input database name "${databaseInput}" does not match the selected database "${selectedDatabase.id()}"`
|
||||
);
|
||||
return;
|
||||
}
|
||||
setFormError("");
|
||||
|
||||
@@ -4,11 +4,11 @@ import React, { FunctionComponent, useState } from "react";
|
||||
import { Areas, SavedQueries } from "../../../Common/Constants";
|
||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
import { Query } from "../../../Contracts/DataModels";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import Explorer from "../../Explorer";
|
||||
import { NewQueryTab } from "../../Tabs/QueryTab/QueryTab";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
@@ -16,9 +16,13 @@ import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneFor
|
||||
|
||||
interface SaveQueryPaneProps {
|
||||
explorer: Explorer;
|
||||
queryToSave?: string;
|
||||
}
|
||||
|
||||
export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({ explorer }: SaveQueryPaneProps): JSX.Element => {
|
||||
export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
|
||||
explorer,
|
||||
queryToSave,
|
||||
}: SaveQueryPaneProps): JSX.Element => {
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
|
||||
const [formError, setFormError] = useState<string>("");
|
||||
@@ -36,7 +40,7 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({ explorer
|
||||
}
|
||||
|
||||
const queryTab = useTabs.getState().activeTab as NewQueryTab;
|
||||
const query: string = queryTab && queryTab.iTabAccessor.onSaveClickEvent();
|
||||
const query: string = queryToSave || queryTab?.iTabAccessor.onSaveClickEvent();
|
||||
|
||||
if (!queryName || queryName.length === 0) {
|
||||
setFormError("No query name specified");
|
||||
@@ -62,8 +66,8 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({ explorer
|
||||
try {
|
||||
await explorer.queriesClient.saveQuery(queryParam);
|
||||
setLoadingFalse();
|
||||
queryTab.tabTitle(queryParam.queryName);
|
||||
queryTab.tabPath(`${queryTab.collection.databaseId}>${queryTab.collection.id()}>${queryParam.queryName}`);
|
||||
queryTab?.tabTitle(queryParam.queryName);
|
||||
queryTab?.tabPath(`${queryTab.collection.databaseId}>${queryTab.collection.id()}>${queryParam.queryName}`);
|
||||
traceSuccess(
|
||||
Action.SaveQuery,
|
||||
{
|
||||
|
||||
@@ -295,8 +295,9 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
||||
className="addButtonEntiy"
|
||||
tabIndex={0}
|
||||
onKeyPress={handlekeypressaddentity}
|
||||
aria-label="Add Property"
|
||||
>
|
||||
<Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" />
|
||||
<Image {...imageProps} src={AddPropertyIcon} alt="Add Property" />
|
||||
<Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
@@ -26,6 +26,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
|
||||
className="panelMainContent"
|
||||
>
|
||||
<Stack
|
||||
aria-label="Add Property"
|
||||
className="addButtonEntiy"
|
||||
horizontal={true}
|
||||
onClick={[Function]}
|
||||
@@ -33,20 +34,21 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
aria-label="Add Property"
|
||||
className="ms-Stack addButtonEntiy css-53"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<StyledImageBase
|
||||
alt="Add Entity"
|
||||
alt="Add Property"
|
||||
height={30}
|
||||
key=".0:$.0"
|
||||
src=""
|
||||
width={16}
|
||||
>
|
||||
<ImageBase
|
||||
alt="Add Entity"
|
||||
alt="Add Property"
|
||||
height={30}
|
||||
src=""
|
||||
styles={[Function]}
|
||||
@@ -335,7 +337,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
|
||||
}
|
||||
>
|
||||
<img
|
||||
alt="Add Entity"
|
||||
alt="Add Property"
|
||||
className="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-55"
|
||||
key="fabricImage"
|
||||
onError={[Function]}
|
||||
|
||||
@@ -254,12 +254,12 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
"root": Object {
|
||||
"height": 30,
|
||||
"padding": 0,
|
||||
"width": 250,
|
||||
"width": 200,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
Add hierarchical partition key (preview)
|
||||
Add hierarchical partition key
|
||||
</CustomizedDefaultButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
11
src/Explorer/QueryCopilot/CopilotCarousel.test.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import Explorer from "../Explorer";
|
||||
import { QueryCopilotCarousel } from "./CopilotCarousel";
|
||||
|
||||
describe("Query Copilot Carousel snapshot test", () => {
|
||||
it("should render when isOpen is true", () => {
|
||||
const wrapper = shallow(<QueryCopilotCarousel isOpen={true} explorer={new Explorer()} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
277
src/Explorer/QueryCopilot/CopilotCarousel.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
import {
|
||||
DefaultButton,
|
||||
ISeparatorStyles,
|
||||
IconButton,
|
||||
Image,
|
||||
Link,
|
||||
Modal,
|
||||
PrimaryButton,
|
||||
Separator,
|
||||
Spinner,
|
||||
Stack,
|
||||
Text,
|
||||
} from "@fluentui/react";
|
||||
import { QueryCopilotSampleDatabaseId, StyleConstants } from "Common/Constants";
|
||||
import { handleError } from "Common/ErrorHandlingUtils";
|
||||
import { createCollection } from "Common/dataAccess/createCollection";
|
||||
import * as DataModels from "Contracts/DataModels";
|
||||
import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel";
|
||||
import { PromptCard } from "Explorer/QueryCopilot/PromptCard";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { useCarousel } from "hooks/useCarousel";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
import React, { useState } from "react";
|
||||
import YoutubePlaceholder from "../../../images/YoutubePlaceholder.svg";
|
||||
|
||||
interface QueryCopilotCarouselProps {
|
||||
isOpen: boolean;
|
||||
explorer: Explorer;
|
||||
}
|
||||
|
||||
const separatorStyles: Partial<ISeparatorStyles> = {
|
||||
root: {
|
||||
selectors: {
|
||||
"::before": {
|
||||
background: StyleConstants.BaseMedium,
|
||||
},
|
||||
},
|
||||
padding: "16px 0",
|
||||
height: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export const QueryCopilotCarousel: React.FC<QueryCopilotCarouselProps> = ({
|
||||
isOpen,
|
||||
explorer,
|
||||
}: QueryCopilotCarouselProps): JSX.Element => {
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [isCreatingDatabase, setIsCreatingDatabase] = useState<boolean>(false);
|
||||
const [spinnerText, setSpinnerText] = useState<string>("");
|
||||
const [selectedPrompt, setSelectedPrompt] = useState<number>(1);
|
||||
|
||||
const getHeaderText = (): string => {
|
||||
switch (page) {
|
||||
case 1:
|
||||
return "What exactly is copilot?";
|
||||
case 2:
|
||||
return "Setting up your Sample database";
|
||||
case 3:
|
||||
return "Sample prompts to help you";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const getQueryCopilotInitialInput = (): string => {
|
||||
switch (selectedPrompt) {
|
||||
case 1:
|
||||
return "Write a query to return all records in this table";
|
||||
case 2:
|
||||
return "Write a query to return all records in this table created in the last thirty days";
|
||||
case 3:
|
||||
return 'Write a query to return all records in this table created in the last thirty days which also have the record owner as "Contoso"';
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const createSampleDatabase = async (): Promise<void> => {
|
||||
const database = useDatabases.getState().findDatabaseWithId(QueryCopilotSampleDatabaseId);
|
||||
if (database) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsCreatingDatabase(true);
|
||||
setSpinnerText("Setting up your database...");
|
||||
const params: DataModels.CreateCollectionParams = {
|
||||
createNewDatabase: true,
|
||||
collectionId: "SampleContainer",
|
||||
databaseId: QueryCopilotSampleDatabaseId,
|
||||
databaseLevelThroughput: true,
|
||||
autoPilotMaxThroughput: 1000,
|
||||
offerThroughput: undefined,
|
||||
indexingPolicy: AllPropertiesIndexed,
|
||||
partitionKey: {
|
||||
paths: ["/categoryId"],
|
||||
kind: "Hash",
|
||||
version: 2,
|
||||
},
|
||||
};
|
||||
await createCollection(params);
|
||||
await explorer.refreshAllDatabases();
|
||||
const database = useDatabases.getState().findDatabaseWithId(QueryCopilotSampleDatabaseId);
|
||||
await database.loadCollections();
|
||||
const collection = database.findCollectionWithId("SampleContainer");
|
||||
|
||||
setSpinnerText("Adding sample data set...");
|
||||
const sampleGenerator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorer, true);
|
||||
await sampleGenerator.populateContainerAsync(collection);
|
||||
await database.expandDatabase();
|
||||
collection.expandCollection();
|
||||
useDatabases.getState().updateDatabase(database);
|
||||
} catch (error) {
|
||||
//TODO: show error in UI
|
||||
handleError(error, "QueryCopilotCreateSampleDB");
|
||||
throw error;
|
||||
} finally {
|
||||
setIsCreatingDatabase(false);
|
||||
setSpinnerText("");
|
||||
}
|
||||
};
|
||||
|
||||
const getContent = (): JSX.Element => {
|
||||
switch (page) {
|
||||
case 1:
|
||||
return (
|
||||
<Stack style={{ marginTop: 8 }}>
|
||||
<Text style={{ fontSize: 13 }}>
|
||||
A couple of lines about copilot and the background about it. The idea is to have some text to give context
|
||||
to the user.
|
||||
</Text>
|
||||
<Text style={{ fontSize: 14, fontWeight: 600, marginTop: 16 }}>How do you use copilot</Text>
|
||||
<Text style={{ fontSize: 13, marginTop: 8 }}>
|
||||
To generate queries , just describe the query you want and copilot will generate the query for you.Watch
|
||||
this video to learn more about how to use copilot.
|
||||
</Text>
|
||||
<Image src={YoutubePlaceholder} style={{ margin: "16px auto" }} />
|
||||
<Text style={{ fontSize: 14, fontWeight: 600 }}>What is copilot good at</Text>
|
||||
<Text style={{ fontSize: 13, marginTop: 8 }}>
|
||||
A couple of lines about what copilot can do and its capablites with a link to{" "}
|
||||
<Link href="" target="_blank">
|
||||
documentation
|
||||
</Link>{" "}
|
||||
if possible.
|
||||
</Text>
|
||||
<Text style={{ fontSize: 14, fontWeight: 600, marginTop: 16 }}>What are its limitations</Text>
|
||||
<Text style={{ fontSize: 13, marginTop: 8 }}>
|
||||
A couple of lines about what copilot cant do and its limitations.{" "}
|
||||
<Link href="" target="_blank">
|
||||
Link to documentation
|
||||
</Link>
|
||||
</Text>
|
||||
<Text style={{ fontSize: 14, fontWeight: 600, marginTop: 16 }}>Disclaimer</Text>
|
||||
<Text style={{ fontSize: 13, marginTop: 8 }}>
|
||||
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>
|
||||
</Stack>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<Stack style={{ marginTop: 8 }}>
|
||||
<Text style={{ fontSize: 13 }}>
|
||||
Before you get started, we need to configure your sample database for you. Here is a summary of the
|
||||
database being created for your reference. Configuration values can be updated using the settings icon in
|
||||
the query builder.
|
||||
</Text>
|
||||
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 24 }}>Database Id</Text>
|
||||
<Text style={{ fontSize: 13 }}>CopilotSampleDb</Text>
|
||||
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database throughput (autoscale)</Text>
|
||||
<Text style={{ fontSize: 13 }}>Autoscale</Text>
|
||||
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database Max RU/s</Text>
|
||||
<Text>1000</Text>
|
||||
<Text style={{ fontSize: 10, marginTop: 8 }}>
|
||||
Your database throughput will automatically scale from{" "}
|
||||
<strong>100 RU/s (10% of max RU/s) - 1000 RU/s</strong> based on usage.
|
||||
</Text>
|
||||
<Text style={{ fontSize: 10, marginTop: 8 }}>
|
||||
Estimated monthly cost (USD): <strong>$8.76 - $87.60</strong> (1 region, 100 - 1000 RU/s, $0.00012/RU)
|
||||
</Text>
|
||||
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Container Id</Text>
|
||||
<Text style={{ fontSize: 13 }}>SampleContainer</Text>
|
||||
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Partition key</Text>
|
||||
<Text style={{ fontSize: 13 }}>categoryId</Text>
|
||||
</Stack>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<Stack>
|
||||
<Text>To help you get started, here are some sample prompts to get you started</Text>
|
||||
<Stack tokens={{ childrenGap: 12 }} style={{ marginTop: 16 }}>
|
||||
<PromptCard
|
||||
header="Write a query to return all records in this table"
|
||||
description="This is a basic query which returns all records in the table "
|
||||
onSelect={() => setSelectedPrompt(1)}
|
||||
isSelected={selectedPrompt === 1}
|
||||
/>
|
||||
<PromptCard
|
||||
header="Write a query to return all records in this table created in the last thirty days"
|
||||
description="This builds on the previous query which returns all records in the table which were inserted in the last thirty days. You can also modify this query to return records based upon creation date"
|
||||
onSelect={() => setSelectedPrompt(2)}
|
||||
isSelected={selectedPrompt === 2}
|
||||
/>
|
||||
<PromptCard
|
||||
header='Write a query to return all records in this table created in the last thirty days which also have the record owner as "Contoso"'
|
||||
description='This builds on the previous query which returns all records in the table which were inserted in the last thirty days but which has the record owner as "contoso"'
|
||||
onSelect={() => setSelectedPrompt(3)}
|
||||
isSelected={selectedPrompt === 3}
|
||||
/>
|
||||
</Stack>
|
||||
<Text style={{ fontSize: 13, marginTop: 32 }}>
|
||||
Interested in learning more about how to write effective prompts. Please read this article for more
|
||||
information.
|
||||
</Text>
|
||||
<Text style={{ fontSize: 13, marginTop: 16 }}>
|
||||
You can also access these prompts by selecting the Samples prompts button in the query builder page.
|
||||
</Text>
|
||||
<Text style={{ fontSize: 13, marginTop: 16 }}>
|
||||
Don't like any of the prompts? Just click Get Started and write your own prompt.
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal styles={{ main: { width: 880 } }} isOpen={isOpen && page < 4}>
|
||||
<Stack style={{ padding: 16 }}>
|
||||
<Stack horizontal horizontalAlign="space-between">
|
||||
<Text variant="xLarge">{getHeaderText()}</Text>
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Cancel" }}
|
||||
onClick={() => useCarousel.getState().setShowCopilotCarousel(false)}
|
||||
/>
|
||||
</Stack>
|
||||
{getContent()}
|
||||
<Separator styles={separatorStyles} />
|
||||
<Stack horizontal horizontalAlign="start" verticalAlign="center">
|
||||
{page !== 1 && (
|
||||
<DefaultButton
|
||||
text="Previous"
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={() => setPage(page - 1)}
|
||||
disabled={isCreatingDatabase}
|
||||
/>
|
||||
)}
|
||||
<PrimaryButton
|
||||
text={page === 3 ? "Get started" : "Next"}
|
||||
onClick={async () => {
|
||||
if (page === 3) {
|
||||
useCarousel.getState().setShowCopilotCarousel(false);
|
||||
useTabs.getState().setQueryCopilotTabInitialInput(getQueryCopilotInitialInput());
|
||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
|
||||
return;
|
||||
}
|
||||
|
||||
if (page === 2) {
|
||||
await createSampleDatabase();
|
||||
}
|
||||
|
||||
setPage(page + 1);
|
||||
}}
|
||||
disabled={isCreatingDatabase}
|
||||
/>
|
||||
{isCreatingDatabase && <Spinner style={{ marginLeft: 8 }} />}
|
||||
{isCreatingDatabase && <Text style={{ marginLeft: 8, color: "#0078D4" }}>{spinnerText}</Text>}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
11
src/Explorer/QueryCopilot/Popup/CopyPopup.test.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { any } from "underscore";
|
||||
import { CopyPopup } from "./CopyPopup";
|
||||
|
||||
describe("Copy Popup snapshot test", () => {
|
||||
it("should render when showCopyPopup is true", () => {
|
||||
const wrapper = shallow(<CopyPopup showCopyPopup={true} setShowCopyPopup={() => any} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
63
src/Explorer/QueryCopilot/Popup/CopyPopup.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { IconButton, Image, Stack, Text } from "@fluentui/react";
|
||||
import React, { Dispatch, SetStateAction } from "react";
|
||||
import Success from "../../../../images/successfulPopup.svg";
|
||||
|
||||
export const CopyPopup = ({
|
||||
showCopyPopup,
|
||||
setShowCopyPopup,
|
||||
}: {
|
||||
showCopyPopup: boolean;
|
||||
setShowCopyPopup: Dispatch<SetStateAction<boolean>>;
|
||||
}): JSX.Element => {
|
||||
const closePopup = () => {
|
||||
setShowCopyPopup(false);
|
||||
};
|
||||
|
||||
return showCopyPopup ? (
|
||||
<Stack
|
||||
style={{
|
||||
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)",
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
horizontal
|
||||
verticalAlign="center"
|
||||
style={{ display: "flex", justifyContent: "space-between", padding: "5px, 2px, 0px, 0px" }}
|
||||
>
|
||||
<Stack horizontal verticalAlign="center" style={{ display: "flex", gap: 10 }}>
|
||||
<Image style={{ width: 15, height: 15 }} src={Success} />
|
||||
<Text>
|
||||
<b>Code copied successfully</b>
|
||||
</Text>
|
||||
</Stack>
|
||||
<IconButton
|
||||
styles={{
|
||||
root: {
|
||||
border: "none",
|
||||
backgroundColor: "transparent",
|
||||
padding: 0,
|
||||
selectors: {
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
iconProps={{ iconName: "Cancel" }}
|
||||
onClick={closePopup}
|
||||
/>
|
||||
</Stack>
|
||||
<Text style={{ marginTop: -10 }}>The query has been copied to the clipboard</Text>
|
||||
</Stack>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
11
src/Explorer/QueryCopilot/Popup/DeletePopup.test.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { any } from "underscore";
|
||||
import { DeletePopup } from "./DeletePopup";
|
||||
|
||||
describe("Delete Popup snapshot test", () => {
|
||||
it("should render when showDeletePopup is true", () => {
|
||||
const wrapper = shallow(<DeletePopup showDeletePopup={true} setShowDeletePopup={() => any} setQuery={() => any} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
38
src/Explorer/QueryCopilot/Popup/DeletePopup.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { DefaultButton, Modal, PrimaryButton, Stack, Text } from "@fluentui/react";
|
||||
import React, { Dispatch, SetStateAction } from "react";
|
||||
|
||||
export const DeletePopup = ({
|
||||
showDeletePopup,
|
||||
setShowDeletePopup,
|
||||
setQuery,
|
||||
}: {
|
||||
showDeletePopup: boolean;
|
||||
setShowDeletePopup: Dispatch<SetStateAction<boolean>>;
|
||||
setQuery: Dispatch<SetStateAction<string>>;
|
||||
}): JSX.Element => {
|
||||
const deleteCode = () => {
|
||||
setQuery("");
|
||||
setShowDeletePopup(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={showDeletePopup} styles={{ main: { minHeight: "122px", minWidth: "880px" } }}>
|
||||
<Stack style={{ padding: "16px 24px", height: "auto" }}>
|
||||
<Text style={{ height: 24, fontSize: "18px" }}>
|
||||
<b>Delete code?</b>
|
||||
</Text>
|
||||
<Text style={{ marginTop: 10, marginBottom: 20 }}>
|
||||
This will clear the query from the query builder pane along with all comments and also reset the prompt pane
|
||||
</Text>
|
||||
<Stack horizontal tokens={{ childrenGap: 10 }} horizontalAlign="start">
|
||||
<PrimaryButton style={{ padding: "0px 20px", height: 24 }} onClick={deleteCode}>
|
||||
Delete
|
||||
</PrimaryButton>
|
||||
<DefaultButton style={{ padding: "0px 20px", height: 24 }} onClick={() => setShowDeletePopup(false)}>
|
||||
Close
|
||||
</DefaultButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Copy Popup snapshot test should render when showCopyPopup is true 1`] = `
|
||||
<Stack
|
||||
style={
|
||||
Object {
|
||||
"background": "#FFFFFF",
|
||||
"boxShadow": "0 2px 6px rgba(0, 0, 0, 0.16)",
|
||||
"gap": 5,
|
||||
"height": 66,
|
||||
"padding": 10,
|
||||
"position": "fixed",
|
||||
"right": 20,
|
||||
"top": 75,
|
||||
"width": 345,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
style={
|
||||
Object {
|
||||
"display": "flex",
|
||||
"justifyContent": "space-between",
|
||||
"padding": "5px, 2px, 0px, 0px",
|
||||
}
|
||||
}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
style={
|
||||
Object {
|
||||
"display": "flex",
|
||||
"gap": 10,
|
||||
}
|
||||
}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Image
|
||||
src=""
|
||||
style={
|
||||
Object {
|
||||
"height": 15,
|
||||
"width": 15,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text>
|
||||
<b>
|
||||
Code copied successfully
|
||||
</b>
|
||||
</Text>
|
||||
</Stack>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Cancel",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"backgroundColor": "transparent",
|
||||
"border": "none",
|
||||
"padding": 0,
|
||||
"selectors": Object {
|
||||
"&:focus": Object {
|
||||
"outline": "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"marginTop": -10,
|
||||
}
|
||||
}
|
||||
>
|
||||
The query has been copied to the clipboard
|
||||
</Text>
|
||||
</Stack>
|
||||
`;
|
||||
@@ -0,0 +1,79 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Delete Popup snapshot test should render when showDeletePopup is true 1`] = `
|
||||
<Modal
|
||||
isOpen={true}
|
||||
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>
|
||||
`;
|
||||
19
src/Explorer/QueryCopilot/PromptCard.test.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { PromptCard } from "./PromptCard";
|
||||
|
||||
describe("Prompt card snapshot test", () => {
|
||||
it("should render properly if isSelected is true", () => {
|
||||
const wrapper = shallow(
|
||||
<PromptCard header="TestHeader" description="TestDescription" isSelected={true} onSelect={() => undefined} />
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render properly if isSelected is false", () => {
|
||||
const wrapper = shallow(
|
||||
<PromptCard header="TestHeader" description="TestDescription" isSelected={false} onSelect={() => undefined} />
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
48
src/Explorer/QueryCopilot/PromptCard.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { ChoiceGroup, Stack, Text } from "@fluentui/react";
|
||||
import React from "react";
|
||||
|
||||
interface PromptCardProps {
|
||||
header: string;
|
||||
description: string;
|
||||
isSelected: boolean;
|
||||
onSelect: () => void;
|
||||
}
|
||||
|
||||
export const PromptCard: React.FC<PromptCardProps> = ({
|
||||
header,
|
||||
description,
|
||||
isSelected,
|
||||
onSelect,
|
||||
}: PromptCardProps): JSX.Element => {
|
||||
return (
|
||||
<Stack
|
||||
horizontal
|
||||
style={{
|
||||
padding: "16px 0 16px 16px ",
|
||||
boxSizing: "border-box",
|
||||
width: 650,
|
||||
height: 100,
|
||||
border: "1px solid #F3F2F1",
|
||||
boxShadow: "0px 1.6px 3.6px rgba(0, 0, 0, 0.132), 0px 0.3px 0.9px rgba(0, 0, 0, 0.108)",
|
||||
}}
|
||||
>
|
||||
<Stack.Item grow={1}>
|
||||
<Stack horizontal>
|
||||
<div>
|
||||
<Text style={{ fontSize: 13, color: "#00A2AD", background: "#F8FFF0" }}>Prompt</Text>
|
||||
</div>
|
||||
<Text style={{ fontSize: 13, marginLeft: 16 }}>{header}</Text>
|
||||
</Stack>
|
||||
<Text style={{ fontSize: 10, marginTop: 16 }}>{description}</Text>
|
||||
</Stack.Item>
|
||||
<Stack.Item style={{ marginLeft: 16 }}>
|
||||
<ChoiceGroup
|
||||
styles={{ flexContainer: { width: 36 } }}
|
||||
options={[{ key: "selected", text: "" }]}
|
||||
selectedKey={isSelected ? "selected" : ""}
|
||||
onChange={onSelect}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
105
src/Explorer/QueryCopilot/QueryCopilotFeedbackModal.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
Checkbox,
|
||||
ChoiceGroup,
|
||||
DefaultButton,
|
||||
IconButton,
|
||||
Link,
|
||||
Modal,
|
||||
PrimaryButton,
|
||||
Stack,
|
||||
Text,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import { submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import React from "react";
|
||||
|
||||
export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
|
||||
const {
|
||||
generatedQuery,
|
||||
userPrompt,
|
||||
likeQuery,
|
||||
showFeedbackModal,
|
||||
closeFeedbackModal,
|
||||
setHideFeedbackModalForLikedQueries,
|
||||
} = useQueryCopilot();
|
||||
const [isContactAllowed, setIsContactAllowed] = React.useState<boolean>(true);
|
||||
const [description, setDescription] = React.useState<string>("");
|
||||
const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false);
|
||||
return (
|
||||
<Modal isOpen={showFeedbackModal}>
|
||||
<Stack style={{ padding: 24 }}>
|
||||
<Stack horizontal horizontalAlign="space-between">
|
||||
<Text style={{ fontSize: 20, fontWeight: 600, marginBottom: 20 }}>Send feedback to Microsoft</Text>
|
||||
<IconButton iconProps={{ iconName: "Cancel" }} onClick={() => closeFeedbackModal()} />
|
||||
</Stack>
|
||||
<Text style={{ fontSize: 14, marginBottom: 14 }}>Your feedback will help improve the experience.</Text>
|
||||
<TextField
|
||||
styles={{ root: { marginBottom: 14 } }}
|
||||
label="Description"
|
||||
required
|
||||
placeholder="Provide more details"
|
||||
value={description}
|
||||
onChange={(_, newValue) => setDescription(newValue)}
|
||||
multiline
|
||||
rows={3}
|
||||
/>
|
||||
<TextField
|
||||
styles={{ root: { marginBottom: 14 } }}
|
||||
label="Query generated"
|
||||
defaultValue={generatedQuery}
|
||||
readOnly
|
||||
/>
|
||||
<ChoiceGroup
|
||||
styles={{
|
||||
root: {
|
||||
marginBottom: 14,
|
||||
},
|
||||
flexContainer: {
|
||||
selectors: {
|
||||
".ms-ChoiceField-field::before": { marginTop: 4 },
|
||||
".ms-ChoiceField-field::after": { marginTop: 4 },
|
||||
".ms-ChoiceFieldLabel": { paddingLeft: 6 },
|
||||
},
|
||||
},
|
||||
}}
|
||||
label="May we contact you about your feedback?"
|
||||
options={[
|
||||
{ key: "yes", text: "Yes, you may contact me." },
|
||||
{ key: "no", text: "No, do not contact me." },
|
||||
]}
|
||||
selectedKey={isContactAllowed ? "yes" : "no"}
|
||||
onChange={(_, option) => setIsContactAllowed(option.key === "yes")}
|
||||
></ChoiceGroup>
|
||||
<Text style={{ fontSize: 12, marginBottom: 14 }}>
|
||||
By pressing submit, your feedback will be used to improve Microsoft products and services. IT admins for your
|
||||
organization will be able to view and manage your feedback data.{" "}
|
||||
<Link href="" target="_blank">
|
||||
Privacy statement
|
||||
</Link>
|
||||
</Text>
|
||||
{likeQuery && (
|
||||
<Checkbox
|
||||
styles={{ label: { paddingLeft: 0 }, root: { marginBottom: 14 } }}
|
||||
label="Don't show me this next time"
|
||||
checked={doNotShowAgainChecked}
|
||||
onChange={(_, checked) => setDoNotShowAgainChecked(checked)}
|
||||
/>
|
||||
)}
|
||||
<Stack horizontal horizontalAlign="end">
|
||||
<PrimaryButton
|
||||
styles={{ root: { marginRight: 8 } }}
|
||||
onClick={() => {
|
||||
closeFeedbackModal();
|
||||
setHideFeedbackModalForLikedQueries(doNotShowAgainChecked);
|
||||
submitFeedback({ generatedQuery, likeQuery, description, userPrompt });
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</PrimaryButton>
|
||||
<DefaultButton onClick={() => closeFeedbackModal()}>Cancel</DefaultButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
13
src/Explorer/QueryCopilot/QueryCopilotTab.test.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import Explorer from "../Explorer";
|
||||
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()} />
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
459
src/Explorer/QueryCopilot/QueryCopilotTab.tsx
Normal file
@@ -0,0 +1,459 @@
|
||||
/* eslint-disable no-console */
|
||||
import { FeedOptions } from "@azure/cosmos";
|
||||
import {
|
||||
Callout,
|
||||
CommandBarButton,
|
||||
DefaultButton,
|
||||
DirectionalHint,
|
||||
IButtonStyles,
|
||||
IconButton,
|
||||
Image,
|
||||
Link,
|
||||
Separator,
|
||||
Spinner,
|
||||
Stack,
|
||||
Text,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import {
|
||||
QueryCopilotSampleContainerId,
|
||||
QueryCopilotSampleContainerSchema,
|
||||
QueryCopilotSampleDatabaseId,
|
||||
} 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 { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
||||
import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
|
||||
import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup";
|
||||
import { submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||
import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/SamplePrompts/SamplePrompts";
|
||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||
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 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 SaveQueryIcon from "../../../images/save-cosmos.svg";
|
||||
import { useTabs } from "../../hooks/useTabs";
|
||||
|
||||
interface QueryCopilotTabProps {
|
||||
initialInput: string;
|
||||
explorer: Explorer;
|
||||
}
|
||||
|
||||
interface GenerateSQLQueryResponse {
|
||||
apiVersion: string;
|
||||
sql: string;
|
||||
explanation: string;
|
||||
generateStart: string;
|
||||
generateEnd: string;
|
||||
}
|
||||
|
||||
const promptStyles: IButtonStyles = {
|
||||
root: { border: 0, selectors: { ":hover": { outline: "1px dashed #605e5c" } } },
|
||||
label: { fontWeight: 400, textAlign: "left", paddingLeft: 8 },
|
||||
};
|
||||
|
||||
export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
||||
initialInput,
|
||||
explorer,
|
||||
}: QueryCopilotTabProps): JSX.Element => {
|
||||
const hideFeedbackModalForLikedQueries = useQueryCopilot((state) => state.hideFeedbackModalForLikedQueries);
|
||||
const [userPrompt, setUserPrompt] = useState<string>(initialInput || "");
|
||||
const [generatedQuery, setGeneratedQuery] = useState<string>("");
|
||||
const [query, setQuery] = useState<string>("");
|
||||
const [selectedQuery, setSelectedQuery] = useState<string>("");
|
||||
const [isGeneratingQuery, setIsGeneratingQuery] = useState<boolean>(false);
|
||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||
const [likeQuery, setLikeQuery] = useState<boolean>();
|
||||
const [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);
|
||||
|
||||
const sampleProps: SamplePromptsProps = {
|
||||
isSamplePromptsOpen: isSamplePromptsOpen,
|
||||
setIsSamplePromptsOpen: setIsSamplePromptsOpen,
|
||||
setTextBox: setUserPrompt,
|
||||
};
|
||||
|
||||
const copyGeneratedCode = () => {
|
||||
if (!query) {
|
||||
return;
|
||||
}
|
||||
const queryElement = document.createElement("textarea");
|
||||
queryElement.value = query;
|
||||
document.body.appendChild(queryElement);
|
||||
queryElement.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(queryElement);
|
||||
|
||||
setshowCopyPopup(true);
|
||||
setTimeout(() => {
|
||||
setshowCopyPopup(false);
|
||||
}, 6000);
|
||||
};
|
||||
|
||||
const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`);
|
||||
const cachedHistories = cachedHistoriesString?.split(",");
|
||||
const [histories, setHistories] = useState<string[]>(cachedHistories || []);
|
||||
|
||||
const updateHistories = (): void => {
|
||||
const newHistories = histories.length < 3 ? [userPrompt, ...histories] : [userPrompt, histories[1], histories[2]];
|
||||
setHistories(newHistories);
|
||||
localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join(","));
|
||||
};
|
||||
const generateSQLQuery = async (): Promise<void> => {
|
||||
try {
|
||||
setIsGeneratingQuery(true);
|
||||
useTabs.getState().setIsTabExecuting(true);
|
||||
const payload = {
|
||||
containerSchema: QueryCopilotSampleContainerSchema,
|
||||
userPrompt: userPrompt,
|
||||
};
|
||||
setShowDeletePopup(false);
|
||||
const response = await fetch("https://copilotorchestrater.azurewebsites.net/generateSQLQuery", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
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`;
|
||||
}
|
||||
query += generateSQLQueryResponse.sql;
|
||||
setQuery(query);
|
||||
setGeneratedQuery(generateSQLQueryResponse.sql);
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, "executeNaturalLanguageQuery");
|
||||
throw error;
|
||||
} finally {
|
||||
setIsGeneratingQuery(false);
|
||||
useTabs.getState().setIsTabExecuting(false);
|
||||
setShowFeedbackBar(true);
|
||||
}
|
||||
};
|
||||
|
||||
const onExecuteQueryClick = async (): Promise<void> => {
|
||||
const queryToExecute = selectedQuery || query;
|
||||
const queryIterator = queryDocuments(QueryCopilotSampleDatabaseId, QueryCopilotSampleContainerId, queryToExecute, {
|
||||
enableCrossPartitionQuery: shouldEnableCrossPartitionKey(),
|
||||
} as FeedOptions);
|
||||
setQueryIterator(queryIterator);
|
||||
|
||||
setTimeout(async () => {
|
||||
await queryDocumentsPerPage(0, queryIterator);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const queryDocumentsPerPage = async (firstItemIndex: number, queryIterator: MinimalQueryIterator): Promise<void> => {
|
||||
try {
|
||||
setIsExecuting(true);
|
||||
useTabs.getState().setIsTabExecuting(true);
|
||||
const queryResults: QueryResults = await queryPagesUntilContentPresent(
|
||||
firstItemIndex,
|
||||
async (firstItemIndex: number) =>
|
||||
queryDocumentsPage(QueryCopilotSampleContainerId, queryIterator, firstItemIndex)
|
||||
);
|
||||
|
||||
setQueryResults(queryResults);
|
||||
setErrorMessage("");
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
setErrorMessage(errorMessage);
|
||||
handleError(errorMessage, "executeQueryCopilotTab");
|
||||
} finally {
|
||||
setIsExecuting(false);
|
||||
useTabs.getState().setIsTabExecuting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
||||
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
|
||||
const executeQueryBtn = {
|
||||
iconSrc: ExecuteQueryIcon,
|
||||
iconAlt: executeQueryBtnLabel,
|
||||
onCommandClick: () => onExecuteQueryClick(),
|
||||
commandButtonLabel: executeQueryBtnLabel,
|
||||
ariaLabel: executeQueryBtnLabel,
|
||||
hasPopup: false,
|
||||
};
|
||||
|
||||
const saveQueryBtn = {
|
||||
iconSrc: SaveQueryIcon,
|
||||
iconAlt: "Save Query",
|
||||
onCommandClick: () =>
|
||||
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={explorer} queryToSave={query} />),
|
||||
commandButtonLabel: "Save Query",
|
||||
ariaLabel: "Save Query",
|
||||
hasPopup: false,
|
||||
};
|
||||
|
||||
const samplePromptsBtn = {
|
||||
iconSrc: SamplePromptsIcon,
|
||||
iconAlt: "Sample Prompts",
|
||||
onCommandClick: () => setIsSamplePromptsOpen(true),
|
||||
commandButtonLabel: "Sample Prompts",
|
||||
ariaLabel: "Sample Prompts",
|
||||
hasPopup: false,
|
||||
};
|
||||
|
||||
return [executeQueryBtn, saveQueryBtn, samplePromptsBtn];
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
{showFeedbackBar ? (
|
||||
<Stack style={{ backgroundColor: "#FFF8F0", padding: "2px 8px" }} horizontal verticalAlign="center">
|
||||
<Text style={{ fontWeight: 600, fontSize: 12 }}>Provide feedback on the query generated</Text>
|
||||
{showCallout && !hideFeedbackModalForLikedQueries && (
|
||||
<Callout
|
||||
style={{ padding: 8 }}
|
||||
target="#likeBtn"
|
||||
onDismiss={() => {
|
||||
setShowCallout(false);
|
||||
submitFeedback({ generatedQuery, likeQuery, description: "", userPrompt: userPrompt });
|
||||
}}
|
||||
directionalHint={DirectionalHint.topCenter}
|
||||
>
|
||||
<Text>
|
||||
Thank you. Need to give{" "}
|
||||
<Link
|
||||
onClick={() => {
|
||||
setShowCallout(false);
|
||||
useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userPrompt);
|
||||
}}
|
||||
>
|
||||
more feedback?
|
||||
</Link>
|
||||
</Text>
|
||||
</Callout>
|
||||
)}
|
||||
<IconButton
|
||||
id="likeBtn"
|
||||
style={{ marginLeft: 20 }}
|
||||
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
|
||||
onClick={() => {
|
||||
setLikeQuery(true);
|
||||
setShowCallout(true);
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
</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>
|
||||
{isSamplePromptsOpen ? <SamplePrompts sampleProps={sampleProps} /> : <></>}
|
||||
{query !== "" && query.trim().length !== 0 ? (
|
||||
<DeletePopup showDeletePopup={showDeletePopup} setShowDeletePopup={setShowDeletePopup} setQuery={setQuery} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<CopyPopup showCopyPopup={showCopyPopup} setShowCopyPopup={setshowCopyPopup} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
36
src/Explorer/QueryCopilot/QueryCopilotUtilities.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { QueryCopilotSampleContainerSchema } from "Common/Constants";
|
||||
import { handleError } from "Common/ErrorHandlingUtils";
|
||||
|
||||
interface FeedbackParams {
|
||||
likeQuery: boolean;
|
||||
generatedQuery: string;
|
||||
userPrompt: string;
|
||||
description?: string;
|
||||
contact?: string;
|
||||
}
|
||||
|
||||
export const submitFeedback = async (params: FeedbackParams): Promise<void> => {
|
||||
try {
|
||||
const { likeQuery, generatedQuery, userPrompt, description, contact } = params;
|
||||
const payload = {
|
||||
containerSchema: QueryCopilotSampleContainerSchema,
|
||||
like: likeQuery ? "like" : "dislike",
|
||||
generatedSql: generatedQuery,
|
||||
userPrompt,
|
||||
description: description || "",
|
||||
contact: contact || "",
|
||||
};
|
||||
|
||||
const response = await fetch("https://copilotorchestrater.azurewebsites.net/feedback", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(response);
|
||||
} catch (error) {
|
||||
handleError(error, "copilotSubmitFeedback");
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { SamplePrompts, SamplePromptsProps } from "./SamplePrompts";
|
||||
|
||||
describe("Sample Prompts snapshot test", () => {
|
||||
it("should render properly if isSamplePromptsOpen is true", () => {
|
||||
const sampleProps: SamplePromptsProps = {
|
||||
isSamplePromptsOpen: true,
|
||||
setIsSamplePromptsOpen: () => undefined,
|
||||
setTextBox: () => undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render properly if isSamplePromptsOpen is false", () => {
|
||||
const sampleProps: SamplePromptsProps = {
|
||||
isSamplePromptsOpen: false,
|
||||
setIsSamplePromptsOpen: () => undefined,
|
||||
setTextBox: () => undefined,
|
||||
};
|
||||
const wrapper = shallow(<SamplePrompts sampleProps={sampleProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
138
src/Explorer/QueryCopilot/SamplePrompts/SamplePrompts.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { DefaultButton, FontIcon, IconButton, Image, Modal, Stack, Text } from "@fluentui/react";
|
||||
import React, { Dispatch, SetStateAction } from "react";
|
||||
import ComplexPrompts from "../../../../images/ComplexPrompts.svg";
|
||||
import IntermediatePrompts from "../../../../images/IntermediatePrompts.svg";
|
||||
import SimplePrompts from "../../../../images/SimplePrompts.svg";
|
||||
|
||||
export interface SamplePromptsProps {
|
||||
isSamplePromptsOpen: boolean;
|
||||
setIsSamplePromptsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setTextBox: Dispatch<SetStateAction<string>>;
|
||||
}
|
||||
|
||||
const SampleUserInputs: string[] = [
|
||||
"Show me products less than 100 dolars",
|
||||
"Show schema",
|
||||
"Show items with a description that contains a number between 0 and 99 inclusive.",
|
||||
"Write a query to return all records in this table created in the last thirty days",
|
||||
"Show all the products that customer Bob has reviewed.",
|
||||
"Which computers are more than 300 dollars and less than 400 dollars?",
|
||||
];
|
||||
|
||||
export const SamplePrompts = ({ sampleProps }: { sampleProps: SamplePromptsProps }): JSX.Element => {
|
||||
const updateTextBox = (userInput: string) => {
|
||||
sampleProps.setTextBox(userInput);
|
||||
sampleProps.setIsSamplePromptsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={sampleProps.isSamplePromptsOpen}>
|
||||
<Stack
|
||||
style={{ padding: "16px 24px", overflowY: "auto", maxHeight: "calc(100vh - 120px)" }}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<Stack>
|
||||
<Stack horizontal style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<Text style={{ fontSize: 24, fontWeight: 600 }}>Sample Prompts</Text>
|
||||
<IconButton
|
||||
styles={{
|
||||
root: {
|
||||
border: "none",
|
||||
backgroundColor: "transparent",
|
||||
padding: 0,
|
||||
selectors: {
|
||||
"&:hover": {
|
||||
backgroundColor: "transparent",
|
||||
color: "#000", // Set the desired color for the X button on hover
|
||||
},
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
iconProps={{ iconName: "Cancel" }}
|
||||
onClick={() => sampleProps.setIsSamplePromptsOpen(false)}
|
||||
/>
|
||||
</Stack>
|
||||
<Text style={{ fontWeight: 400, fontSize: 13, marginTop: 10 }}>
|
||||
Here are some sample prompts for writing queries in NoSQL, ranging from simple to complex
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack style={{ marginTop: 30, display: "flex" }}>
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<Image style={{ width: 25, height: 25 }} src={SimplePrompts} />
|
||||
<Text style={{ fontSize: 14, fontWeight: 600 }}>Simple Prompts</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Stack horizontal style={{ gap: 35 }}>
|
||||
<DefaultButton
|
||||
style={{ width: 352, height: 135, background: "#F6F6F7" }}
|
||||
onClick={() => updateTextBox(SampleUserInputs[0])}
|
||||
>
|
||||
<Text style={{ height: 80, fontSize: 13 }}>{SampleUserInputs[0]}</Text>
|
||||
<FontIcon style={{ position: "absolute", left: "92.61%" }} aria-label="Forward" iconName="Forward" />
|
||||
</DefaultButton>
|
||||
<DefaultButton
|
||||
style={{ width: 352, height: 135, background: "#F6F6F7" }}
|
||||
onClick={() => updateTextBox(SampleUserInputs[1])}
|
||||
>
|
||||
<Text style={{ height: 80, fontSize: 13 }}>{SampleUserInputs[1]}</Text>
|
||||
<FontIcon style={{ position: "absolute", left: "92.61%" }} aria-label="Forward" iconName="Forward" />
|
||||
</DefaultButton>
|
||||
</Stack>
|
||||
|
||||
<Stack style={{ marginTop: 30, display: "flex" }}>
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<Image style={{ width: 25, height: 25 }} src={IntermediatePrompts} />
|
||||
<Text style={{ fontSize: 14, fontWeight: 600 }}>Intermediate Prompts</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Stack horizontal style={{ gap: 35 }}>
|
||||
<DefaultButton
|
||||
style={{ width: 352, height: 135, background: "#F6F6F7" }}
|
||||
onClick={() => updateTextBox(SampleUserInputs[2])}
|
||||
>
|
||||
<Text style={{ height: 80, fontSize: 13 }}>{SampleUserInputs[2]}</Text>
|
||||
<FontIcon style={{ position: "absolute", left: "92.61%" }} aria-label="Forward" iconName="Forward" />
|
||||
</DefaultButton>
|
||||
<DefaultButton
|
||||
style={{ width: 352, height: 135, background: "#F6F6F7" }}
|
||||
onClick={() => updateTextBox(SampleUserInputs[3])}
|
||||
>
|
||||
<Text style={{ height: 80, fontSize: 13 }}>{SampleUserInputs[3]}</Text>
|
||||
<FontIcon style={{ position: "absolute", left: "92.61%" }} aria-label="Forward" iconName="Forward" />
|
||||
</DefaultButton>
|
||||
</Stack>
|
||||
|
||||
<Stack style={{ marginTop: 30, display: "flex" }}>
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<Image style={{ width: 25, height: 25 }} src={ComplexPrompts} />
|
||||
<Text style={{ fontSize: 14, fontWeight: 600 }}>Complex Prompts</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Stack horizontal style={{ gap: 35 }}>
|
||||
<DefaultButton
|
||||
style={{ width: 352, height: 135, background: "#F6F6F7" }}
|
||||
onClick={() => updateTextBox(SampleUserInputs[4])}
|
||||
>
|
||||
<Text style={{ height: 80, fontSize: 13 }}>{SampleUserInputs[4]}</Text>
|
||||
<FontIcon style={{ position: "absolute", left: "92.61%" }} aria-label="Forward" iconName="Forward" />
|
||||
</DefaultButton>
|
||||
<DefaultButton
|
||||
style={{ width: 352, height: 135, background: "#F6F6F7" }}
|
||||
onClick={() => updateTextBox(SampleUserInputs[5])}
|
||||
>
|
||||
<Text style={{ height: 80, fontSize: 13 }}>{SampleUserInputs[5]}</Text>
|
||||
<FontIcon style={{ position: "absolute", left: "92.61%" }} aria-label="Forward" iconName="Forward" />
|
||||
</DefaultButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,781 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Sample Prompts snapshot test should render properly if isSamplePromptsOpen is false 1`] = `
|
||||
<Modal
|
||||
isOpen={false}
|
||||
>
|
||||
<Stack
|
||||
aria-modal="true"
|
||||
role="dialog"
|
||||
style={
|
||||
Object {
|
||||
"maxHeight": "calc(100vh - 120px)",
|
||||
"overflowY": "auto",
|
||||
"padding": "16px 24px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
style={
|
||||
Object {
|
||||
"display": "flex",
|
||||
"justifyContent": "space-between",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 24,
|
||||
"fontWeight": 600,
|
||||
}
|
||||
}
|
||||
>
|
||||
Sample Prompts
|
||||
</Text>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Cancel",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"backgroundColor": "transparent",
|
||||
"border": "none",
|
||||
"padding": 0,
|
||||
"selectors": Object {
|
||||
"&:focus": Object {
|
||||
"outline": "none",
|
||||
},
|
||||
"&:hover": Object {
|
||||
"backgroundColor": "transparent",
|
||||
"color": "#000",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"fontWeight": 400,
|
||||
"marginTop": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
Here are some sample prompts for writing queries in NoSQL, ranging from simple to complex
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack
|
||||
style={
|
||||
Object {
|
||||
"display": "flex",
|
||||
"marginTop": 30,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Image
|
||||
src=""
|
||||
style={
|
||||
Object {
|
||||
"height": 25,
|
||||
"width": 25,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 14,
|
||||
"fontWeight": 600,
|
||||
}
|
||||
}
|
||||
>
|
||||
Simple Prompts
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
style={
|
||||
Object {
|
||||
"gap": 35,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedDefaultButton
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"background": "#F6F6F7",
|
||||
"height": 135,
|
||||
"width": 352,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"height": 80,
|
||||
}
|
||||
}
|
||||
>
|
||||
Show me products less than 100 dolars
|
||||
</Text>
|
||||
<FontIcon
|
||||
aria-label="Forward"
|
||||
iconName="Forward"
|
||||
style={
|
||||
Object {
|
||||
"left": "92.61%",
|
||||
"position": "absolute",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</CustomizedDefaultButton>
|
||||
<CustomizedDefaultButton
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"background": "#F6F6F7",
|
||||
"height": 135,
|
||||
"width": 352,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"height": 80,
|
||||
}
|
||||
}
|
||||
>
|
||||
Show schema
|
||||
</Text>
|
||||
<FontIcon
|
||||
aria-label="Forward"
|
||||
iconName="Forward"
|
||||
style={
|
||||
Object {
|
||||
"left": "92.61%",
|
||||
"position": "absolute",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</CustomizedDefaultButton>
|
||||
</Stack>
|
||||
<Stack
|
||||
style={
|
||||
Object {
|
||||
"display": "flex",
|
||||
"marginTop": 30,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Image
|
||||
src=""
|
||||
style={
|
||||
Object {
|
||||
"height": 25,
|
||||
"width": 25,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 14,
|
||||
"fontWeight": 600,
|
||||
}
|
||||
}
|
||||
>
|
||||
Intermediate Prompts
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
style={
|
||||
Object {
|
||||
"gap": 35,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedDefaultButton
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"background": "#F6F6F7",
|
||||
"height": 135,
|
||||
"width": 352,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"height": 80,
|
||||
}
|
||||
}
|
||||
>
|
||||
Show items with a description that contains a number between 0 and 99 inclusive.
|
||||
</Text>
|
||||
<FontIcon
|
||||
aria-label="Forward"
|
||||
iconName="Forward"
|
||||
style={
|
||||
Object {
|
||||
"left": "92.61%",
|
||||
"position": "absolute",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</CustomizedDefaultButton>
|
||||
<CustomizedDefaultButton
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"background": "#F6F6F7",
|
||||
"height": 135,
|
||||
"width": 352,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"height": 80,
|
||||
}
|
||||
}
|
||||
>
|
||||
Write a query to return all records in this table created in the last thirty days
|
||||
</Text>
|
||||
<FontIcon
|
||||
aria-label="Forward"
|
||||
iconName="Forward"
|
||||
style={
|
||||
Object {
|
||||
"left": "92.61%",
|
||||
"position": "absolute",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</CustomizedDefaultButton>
|
||||
</Stack>
|
||||
<Stack
|
||||
style={
|
||||
Object {
|
||||
"display": "flex",
|
||||
"marginTop": 30,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Image
|
||||
src=""
|
||||
style={
|
||||
Object {
|
||||
"height": 25,
|
||||
"width": 25,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 14,
|
||||
"fontWeight": 600,
|
||||
}
|
||||
}
|
||||
>
|
||||
Complex Prompts
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
style={
|
||||
Object {
|
||||
"gap": 35,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedDefaultButton
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"background": "#F6F6F7",
|
||||
"height": 135,
|
||||
"width": 352,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"height": 80,
|
||||
}
|
||||
}
|
||||
>
|
||||
Show all the products that customer Bob has reviewed.
|
||||
</Text>
|
||||
<FontIcon
|
||||
aria-label="Forward"
|
||||
iconName="Forward"
|
||||
style={
|
||||
Object {
|
||||
"left": "92.61%",
|
||||
"position": "absolute",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</CustomizedDefaultButton>
|
||||
<CustomizedDefaultButton
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"background": "#F6F6F7",
|
||||
"height": 135,
|
||||
"width": 352,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"height": 80,
|
||||
}
|
||||
}
|
||||
>
|
||||
Which computers are more than 300 dollars and less than 400 dollars?
|
||||
</Text>
|
||||
<FontIcon
|
||||
aria-label="Forward"
|
||||
iconName="Forward"
|
||||
style={
|
||||
Object {
|
||||
"left": "92.61%",
|
||||
"position": "absolute",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</CustomizedDefaultButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
`;
|
||||
|
||||
exports[`Sample Prompts snapshot test should render properly if isSamplePromptsOpen is true 1`] = `
|
||||
<Modal
|
||||
isOpen={true}
|
||||
>
|
||||
<Stack
|
||||
aria-modal="true"
|
||||
role="dialog"
|
||||
style={
|
||||
Object {
|
||||
"maxHeight": "calc(100vh - 120px)",
|
||||
"overflowY": "auto",
|
||||
"padding": "16px 24px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
style={
|
||||
Object {
|
||||
"display": "flex",
|
||||
"justifyContent": "space-between",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 24,
|
||||
"fontWeight": 600,
|
||||
}
|
||||
}
|
||||
>
|
||||
Sample Prompts
|
||||
</Text>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Cancel",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"backgroundColor": "transparent",
|
||||
"border": "none",
|
||||
"padding": 0,
|
||||
"selectors": Object {
|
||||
"&:focus": Object {
|
||||
"outline": "none",
|
||||
},
|
||||
"&:hover": Object {
|
||||
"backgroundColor": "transparent",
|
||||
"color": "#000",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"fontWeight": 400,
|
||||
"marginTop": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
Here are some sample prompts for writing queries in NoSQL, ranging from simple to complex
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack
|
||||
style={
|
||||
Object {
|
||||
"display": "flex",
|
||||
"marginTop": 30,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Image
|
||||
src=""
|
||||
style={
|
||||
Object {
|
||||
"height": 25,
|
||||
"width": 25,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 14,
|
||||
"fontWeight": 600,
|
||||
}
|
||||
}
|
||||
>
|
||||
Simple Prompts
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
style={
|
||||
Object {
|
||||
"gap": 35,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedDefaultButton
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"background": "#F6F6F7",
|
||||
"height": 135,
|
||||
"width": 352,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"height": 80,
|
||||
}
|
||||
}
|
||||
>
|
||||
Show me products less than 100 dolars
|
||||
</Text>
|
||||
<FontIcon
|
||||
aria-label="Forward"
|
||||
iconName="Forward"
|
||||
style={
|
||||
Object {
|
||||
"left": "92.61%",
|
||||
"position": "absolute",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</CustomizedDefaultButton>
|
||||
<CustomizedDefaultButton
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"background": "#F6F6F7",
|
||||
"height": 135,
|
||||
"width": 352,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"height": 80,
|
||||
}
|
||||
}
|
||||
>
|
||||
Show schema
|
||||
</Text>
|
||||
<FontIcon
|
||||
aria-label="Forward"
|
||||
iconName="Forward"
|
||||
style={
|
||||
Object {
|
||||
"left": "92.61%",
|
||||
"position": "absolute",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</CustomizedDefaultButton>
|
||||
</Stack>
|
||||
<Stack
|
||||
style={
|
||||
Object {
|
||||
"display": "flex",
|
||||
"marginTop": 30,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Image
|
||||
src=""
|
||||
style={
|
||||
Object {
|
||||
"height": 25,
|
||||
"width": 25,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 14,
|
||||
"fontWeight": 600,
|
||||
}
|
||||
}
|
||||
>
|
||||
Intermediate Prompts
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
style={
|
||||
Object {
|
||||
"gap": 35,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedDefaultButton
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"background": "#F6F6F7",
|
||||
"height": 135,
|
||||
"width": 352,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"height": 80,
|
||||
}
|
||||
}
|
||||
>
|
||||
Show items with a description that contains a number between 0 and 99 inclusive.
|
||||
</Text>
|
||||
<FontIcon
|
||||
aria-label="Forward"
|
||||
iconName="Forward"
|
||||
style={
|
||||
Object {
|
||||
"left": "92.61%",
|
||||
"position": "absolute",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</CustomizedDefaultButton>
|
||||
<CustomizedDefaultButton
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"background": "#F6F6F7",
|
||||
"height": 135,
|
||||
"width": 352,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"height": 80,
|
||||
}
|
||||
}
|
||||
>
|
||||
Write a query to return all records in this table created in the last thirty days
|
||||
</Text>
|
||||
<FontIcon
|
||||
aria-label="Forward"
|
||||
iconName="Forward"
|
||||
style={
|
||||
Object {
|
||||
"left": "92.61%",
|
||||
"position": "absolute",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</CustomizedDefaultButton>
|
||||
</Stack>
|
||||
<Stack
|
||||
style={
|
||||
Object {
|
||||
"display": "flex",
|
||||
"marginTop": 30,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Image
|
||||
src=""
|
||||
style={
|
||||
Object {
|
||||
"height": 25,
|
||||
"width": 25,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 14,
|
||||
"fontWeight": 600,
|
||||
}
|
||||
}
|
||||
>
|
||||
Complex Prompts
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
style={
|
||||
Object {
|
||||
"gap": 35,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedDefaultButton
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"background": "#F6F6F7",
|
||||
"height": 135,
|
||||
"width": 352,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"height": 80,
|
||||
}
|
||||
}
|
||||
>
|
||||
Show all the products that customer Bob has reviewed.
|
||||
</Text>
|
||||
<FontIcon
|
||||
aria-label="Forward"
|
||||
iconName="Forward"
|
||||
style={
|
||||
Object {
|
||||
"left": "92.61%",
|
||||
"position": "absolute",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</CustomizedDefaultButton>
|
||||
<CustomizedDefaultButton
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"background": "#F6F6F7",
|
||||
"height": 135,
|
||||
"width": 352,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"height": 80,
|
||||
}
|
||||
}
|
||||
>
|
||||
Which computers are more than 300 dollars and less than 400 dollars?
|
||||
</Text>
|
||||
<FontIcon
|
||||
aria-label="Forward"
|
||||
iconName="Forward"
|
||||
style={
|
||||
Object {
|
||||
"left": "92.61%",
|
||||
"position": "absolute",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</CustomizedDefaultButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
`;
|
||||
@@ -0,0 +1,198 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Query Copilot Carousel snapshot test should render when isOpen is true 1`] = `
|
||||
<Modal
|
||||
isOpen={true}
|
||||
styles={
|
||||
Object {
|
||||
"main": Object {
|
||||
"width": 880,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
style={
|
||||
Object {
|
||||
"padding": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
horizontalAlign="space-between"
|
||||
>
|
||||
<Text
|
||||
variant="xLarge"
|
||||
>
|
||||
What exactly is copilot?
|
||||
</Text>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Cancel",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
style={
|
||||
Object {
|
||||
"marginTop": 8,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
}
|
||||
}
|
||||
>
|
||||
A couple of lines about copilot and the background about it. The idea is to have some text to give context to the user.
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 14,
|
||||
"fontWeight": 600,
|
||||
"marginTop": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
How do you use copilot
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"marginTop": 8,
|
||||
}
|
||||
}
|
||||
>
|
||||
To generate queries , just describe the query you want and copilot will generate the query for you.Watch this video to learn more about how to use copilot.
|
||||
</Text>
|
||||
<Image
|
||||
src=""
|
||||
style={
|
||||
Object {
|
||||
"margin": "16px auto",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 14,
|
||||
"fontWeight": 600,
|
||||
}
|
||||
}
|
||||
>
|
||||
What is copilot good at
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"marginTop": 8,
|
||||
}
|
||||
}
|
||||
>
|
||||
A couple of lines about what copilot can do and its capablites with a link to
|
||||
|
||||
<StyledLinkBase
|
||||
href=""
|
||||
target="_blank"
|
||||
>
|
||||
documentation
|
||||
</StyledLinkBase>
|
||||
|
||||
if possible.
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 14,
|
||||
"fontWeight": 600,
|
||||
"marginTop": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
What are its limitations
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"marginTop": 8,
|
||||
}
|
||||
}
|
||||
>
|
||||
A couple of lines about what copilot cant do and its limitations.
|
||||
|
||||
<StyledLinkBase
|
||||
href=""
|
||||
target="_blank"
|
||||
>
|
||||
Link to documentation
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 14,
|
||||
"fontWeight": 600,
|
||||
"marginTop": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
Disclaimer
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"marginTop": 8,
|
||||
}
|
||||
}
|
||||
>
|
||||
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.
|
||||
|
||||
<StyledLinkBase
|
||||
href=""
|
||||
target="_blank"
|
||||
>
|
||||
Read preview terms
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
</Stack>
|
||||
<Separator
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"height": 1,
|
||||
"padding": "16px 0",
|
||||
"selectors": Object {
|
||||
"::before": Object {
|
||||
"background": undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
horizontalAlign="start"
|
||||
verticalAlign="center"
|
||||
>
|
||||
<CustomizedPrimaryButton
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
text="Next"
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
`;
|
||||
171
src/Explorer/QueryCopilot/__snapshots__/PromptCard.test.tsx.snap
Normal file
@@ -0,0 +1,171 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Prompt card snapshot test should render properly if isSelected is false 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
style={
|
||||
Object {
|
||||
"border": "1px solid #F3F2F1",
|
||||
"boxShadow": "0px 1.6px 3.6px rgba(0, 0, 0, 0.132), 0px 0.3px 0.9px rgba(0, 0, 0, 0.108)",
|
||||
"boxSizing": "border-box",
|
||||
"height": 100,
|
||||
"padding": "16px 0 16px 16px ",
|
||||
"width": 650,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StackItem
|
||||
grow={1}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<div>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"background": "#F8FFF0",
|
||||
"color": "#00A2AD",
|
||||
"fontSize": 13,
|
||||
}
|
||||
}
|
||||
>
|
||||
Prompt
|
||||
</Text>
|
||||
</div>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"marginLeft": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
TestHeader
|
||||
</Text>
|
||||
</Stack>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 10,
|
||||
"marginTop": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
TestDescription
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem
|
||||
style={
|
||||
Object {
|
||||
"marginLeft": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledChoiceGroup
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": "selected",
|
||||
"text": "",
|
||||
},
|
||||
]
|
||||
}
|
||||
selectedKey=""
|
||||
styles={
|
||||
Object {
|
||||
"flexContainer": Object {
|
||||
"width": 36,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
exports[`Prompt card snapshot test should render properly if isSelected is true 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
style={
|
||||
Object {
|
||||
"border": "1px solid #F3F2F1",
|
||||
"boxShadow": "0px 1.6px 3.6px rgba(0, 0, 0, 0.132), 0px 0.3px 0.9px rgba(0, 0, 0, 0.108)",
|
||||
"boxSizing": "border-box",
|
||||
"height": 100,
|
||||
"padding": "16px 0 16px 16px ",
|
||||
"width": 650,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StackItem
|
||||
grow={1}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<div>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"background": "#F8FFF0",
|
||||
"color": "#00A2AD",
|
||||
"fontSize": 13,
|
||||
}
|
||||
}
|
||||
>
|
||||
Prompt
|
||||
</Text>
|
||||
</div>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"marginLeft": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
TestHeader
|
||||
</Text>
|
||||
</Stack>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 10,
|
||||
"marginTop": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
TestDescription
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem
|
||||
style={
|
||||
Object {
|
||||
"marginLeft": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledChoiceGroup
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": "selected",
|
||||
"text": "",
|
||||
},
|
||||
]
|
||||
}
|
||||
selectedKey="selected"
|
||||
styles={
|
||||
Object {
|
||||
"flexContainer": Object {
|
||||
"width": 36,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
`;
|
||||
@@ -0,0 +1,132 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Query copilot tab snapshot test should render with initial input 1`] = `
|
||||
<Stack
|
||||
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}
|
||||
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,
|
||||
}
|
||||
}
|
||||
>
|
||||
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.
|
||||
|
||||
<StyledLinkBase
|
||||
href=""
|
||||
target="_blank"
|
||||
>
|
||||
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}
|
||||
>
|
||||
<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>
|
||||
<CopyPopup
|
||||
setShowCopyPopup={[Function]}
|
||||
showCopyPopup={false}
|
||||
/>
|
||||
</Stack>
|
||||
`;
|
||||
@@ -1,10 +1,10 @@
|
||||
import { DefaultButton, IconButton, Image, Modal, PrimaryButton, Stack, Text } from "@fluentui/react";
|
||||
import { useCarousel } from "hooks/useCarousel";
|
||||
import React, { useState } from "react";
|
||||
import Youtube from "react-youtube";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "UserContext";
|
||||
import { useCarousel } from "hooks/useCarousel";
|
||||
import React, { useState } from "react";
|
||||
import Youtube from "react-youtube";
|
||||
import Image1 from "../../../images/CarouselImage1.svg";
|
||||
import Image2 from "../../../images/CarouselImage2.svg";
|
||||
|
||||
@@ -25,7 +25,7 @@ export const QuickstartCarousel: React.FC<QuickstartCarouselProps> = ({
|
||||
<Stack>
|
||||
<Stack horizontal horizontalAlign="space-between" style={{ padding: 16 }}>
|
||||
<Text variant="xLarge">{getHeaderText(page)}</Text>
|
||||
<IconButton iconProps={{ iconName: "Cancel" }} onClick={() => setPage(4)} />
|
||||
<IconButton iconProps={{ iconName: "Cancel" }} onClick={() => setPage(4)} ariaLabel="Close" />
|
||||
</Stack>
|
||||
{getContent(page)}
|
||||
<Text variant="medium" style={{ padding: "0 16px" }}>
|
||||
|
||||
@@ -14,19 +14,21 @@ import {
|
||||
import { sendMessage } from "Common/MessageHandler";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import { TerminalKind } from "Contracts/ViewModels";
|
||||
import { SplashScreenButton } from "Explorer/SplashScreen/SplashScreenButton";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { useCarousel } from "hooks/useCarousel";
|
||||
import { usePostgres } from "hooks/usePostgres";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
import * as React from "react";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import ConnectIcon from "../../../images/Connect_color.svg";
|
||||
import ContainersIcon from "../../../images/Containers.svg";
|
||||
import LinkIcon from "../../../images/Link_blue.svg";
|
||||
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
|
||||
import NotebookColorIcon from "../../../images/Notebooks.svg";
|
||||
import PowerShellIcon from "../../../images/PowerShell.svg";
|
||||
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
||||
import QuickStartIcon from "../../../images/Quickstart_Lightning.svg";
|
||||
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
|
||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { userContext } from "../../UserContext";
|
||||
@@ -54,10 +56,6 @@ export interface SplashScreenProps {
|
||||
}
|
||||
|
||||
export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
private static readonly dataModelingUrl = "https://docs.microsoft.com/azure/cosmos-db/modeling-data";
|
||||
private static readonly throughputEstimatorUrl = "https://cosmos.azure.com/capacitycalculator";
|
||||
private static readonly failoverUrl = "https://docs.microsoft.com/azure/cosmos-db/high-availability";
|
||||
|
||||
private readonly container: Explorer;
|
||||
private subscriptions: Array<{ dispose: () => void }>;
|
||||
|
||||
@@ -108,9 +106,141 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
this.setState({});
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const mainItems = this.createMainItems();
|
||||
private getSplashScreenButtons = (): JSX.Element => {
|
||||
if (userContext.features.enableCopilot && userContext.apiType === "SQL") {
|
||||
return (
|
||||
<Stack style={{ width: "66%", cursor: "pointer", margin: "40px auto" }} tokens={{ childrenGap: 16 }}>
|
||||
<Stack horizontal tokens={{ childrenGap: 16 }}>
|
||||
<SplashScreenButton
|
||||
imgSrc={QuickStartIcon}
|
||||
title={"Launch quick start"}
|
||||
description={"Launch a quick start tutorial to get started with sample data"}
|
||||
onClick={() => {
|
||||
this.container.onNewCollectionClicked({ isQuickstart: true });
|
||||
traceOpen(Action.LaunchQuickstart, { apiType: userContext.apiType });
|
||||
}}
|
||||
/>
|
||||
<SplashScreenButton
|
||||
imgSrc={ContainersIcon}
|
||||
title={`New ${getCollectionName()}`}
|
||||
description={"Create a new container for storage and throughput"}
|
||||
onClick={() => {
|
||||
this.container.onNewCollectionClicked();
|
||||
traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType });
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack horizontal tokens={{ childrenGap: 16 }}>
|
||||
<SplashScreenButton
|
||||
imgSrc={CopilotIcon}
|
||||
title={"Query faster with Copilot"}
|
||||
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)}
|
||||
/>
|
||||
<SplashScreenButton
|
||||
imgSrc={ConnectIcon}
|
||||
title={"Connect"}
|
||||
description={"Prefer using your own choice of tooling? Find the connection string you need to connect"}
|
||||
onClick={() => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect)}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const mainItems = this.createMainItems();
|
||||
return (
|
||||
<div className="mainButtonsContainer">
|
||||
{userContext.apiType === "Postgres" &&
|
||||
usePostgres.getState().showPostgreTeachingBubble &&
|
||||
!usePostgres.getState().showResetPasswordBubble && (
|
||||
<TeachingBubble
|
||||
headline="New to Cosmos DB PGSQL?"
|
||||
target={"#mainButton-quickstartDescription"}
|
||||
hasCloseButton
|
||||
onDismiss={() => usePostgres.getState().setShowPostgreTeachingBubble(false)}
|
||||
calloutProps={{
|
||||
directionalHint: DirectionalHint.rightCenter,
|
||||
directionalHintFixed: true,
|
||||
preventDismissOnLostFocus: true,
|
||||
preventDismissOnResize: true,
|
||||
preventDismissOnScroll: true,
|
||||
}}
|
||||
primaryButtonProps={{
|
||||
text: "Get started",
|
||||
onClick: () => {
|
||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.Quickstart);
|
||||
usePostgres.getState().setShowPostgreTeachingBubble(false);
|
||||
},
|
||||
}}
|
||||
>
|
||||
Welcome! If you are new to Cosmos DB PGSQL and need help with getting started, here is where you can find
|
||||
sample data, query.
|
||||
</TeachingBubble>
|
||||
)}
|
||||
{mainItems.map((item) => (
|
||||
<Stack
|
||||
id={`mainButton-${item.id}`}
|
||||
horizontal
|
||||
className="mainButton focusable"
|
||||
key={`${item.title}`}
|
||||
onClick={item.onClick}
|
||||
onKeyPress={(event: React.KeyboardEvent) => this.onSplashScreenItemKeyPress(event, item.onClick)}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<div>
|
||||
<img src={item.iconSrc} alt="" />
|
||||
</div>
|
||||
<div className="legendContainer">
|
||||
<Stack horizontal verticalAlign="center" style={{ marginBottom: 8 }}>
|
||||
<div className="legend">{item.title}</div>
|
||||
{item.showLinkIcon && <Image style={{ marginLeft: 8, width: 16 }} src={LinkIcon} />}
|
||||
</Stack>
|
||||
|
||||
<div id={item.id} className="newDescription">
|
||||
{item.description}
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
))}
|
||||
{userContext.apiType === "Postgres" && usePostgres.getState().showResetPasswordBubble && (
|
||||
<TeachingBubble
|
||||
headline="Create your password"
|
||||
target={"#mainButton-quickstartDescription"}
|
||||
hasCloseButton
|
||||
onDismiss={() => {
|
||||
localStorage.setItem(userContext.databaseAccount.id, "true");
|
||||
usePostgres.getState().setShowResetPasswordBubble(false);
|
||||
}}
|
||||
calloutProps={{
|
||||
directionalHint: DirectionalHint.bottomRightEdge,
|
||||
directionalHintFixed: true,
|
||||
preventDismissOnLostFocus: true,
|
||||
preventDismissOnResize: true,
|
||||
preventDismissOnScroll: true,
|
||||
}}
|
||||
primaryButtonProps={{
|
||||
text: "Create",
|
||||
onClick: () => {
|
||||
localStorage.setItem(userContext.databaseAccount.id, "true");
|
||||
sendMessage({
|
||||
type: MessageTypes.OpenPostgreSQLPasswordReset,
|
||||
});
|
||||
usePostgres.getState().setShowResetPasswordBubble(false);
|
||||
},
|
||||
}}
|
||||
>
|
||||
If you haven't changed your password yet, change it now.
|
||||
</TeachingBubble>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<div className="connectExplorerContainer">
|
||||
<form className="connectExplorerFormContainer">
|
||||
@@ -134,91 +264,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
? "Get started with our sample datasets, documentation, and additional tools."
|
||||
: "Globally distributed, multi-model database service for any scale"}
|
||||
</div>
|
||||
<div className="mainButtonsContainer">
|
||||
{userContext.apiType === "Postgres" &&
|
||||
usePostgres.getState().showPostgreTeachingBubble &&
|
||||
!usePostgres.getState().showResetPasswordBubble && (
|
||||
<TeachingBubble
|
||||
headline="New to Cosmos DB PGSQL?"
|
||||
target={"#mainButton-quickstartDescription"}
|
||||
hasCloseButton
|
||||
onDismiss={() => usePostgres.getState().setShowPostgreTeachingBubble(false)}
|
||||
calloutProps={{
|
||||
directionalHint: DirectionalHint.rightCenter,
|
||||
directionalHintFixed: true,
|
||||
preventDismissOnLostFocus: true,
|
||||
preventDismissOnResize: true,
|
||||
preventDismissOnScroll: true,
|
||||
}}
|
||||
primaryButtonProps={{
|
||||
text: "Get started",
|
||||
onClick: () => {
|
||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.Quickstart);
|
||||
usePostgres.getState().setShowPostgreTeachingBubble(false);
|
||||
},
|
||||
}}
|
||||
>
|
||||
Welcome! If you are new to Cosmos DB PGSQL and need help with getting started, here is where you
|
||||
can find sample data, query.
|
||||
</TeachingBubble>
|
||||
)}
|
||||
{mainItems.map((item) => (
|
||||
<Stack
|
||||
id={`mainButton-${item.id}`}
|
||||
horizontal
|
||||
className="mainButton focusable"
|
||||
key={`${item.title}`}
|
||||
onClick={item.onClick}
|
||||
onKeyPress={(event: React.KeyboardEvent) => this.onSplashScreenItemKeyPress(event, item.onClick)}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<div>
|
||||
<img src={item.iconSrc} alt="" />
|
||||
</div>
|
||||
<div className="legendContainer">
|
||||
<Stack horizontal verticalAlign="center" style={{ marginBottom: 8 }}>
|
||||
<div className="legend">{item.title}</div>
|
||||
{item.showLinkIcon && <Image style={{ marginLeft: 8, width: 16 }} src={LinkIcon} />}
|
||||
</Stack>
|
||||
|
||||
<div id={item.id} className="newDescription">
|
||||
{item.description}
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
))}
|
||||
{userContext.apiType === "Postgres" && usePostgres.getState().showResetPasswordBubble && (
|
||||
<TeachingBubble
|
||||
headline="Create your password"
|
||||
target={"#mainButton-quickstartDescription"}
|
||||
hasCloseButton
|
||||
onDismiss={() => {
|
||||
localStorage.setItem(userContext.databaseAccount.id, "true");
|
||||
usePostgres.getState().setShowResetPasswordBubble(false);
|
||||
}}
|
||||
calloutProps={{
|
||||
directionalHint: DirectionalHint.bottomRightEdge,
|
||||
directionalHintFixed: true,
|
||||
preventDismissOnLostFocus: true,
|
||||
preventDismissOnResize: true,
|
||||
preventDismissOnScroll: true,
|
||||
}}
|
||||
primaryButtonProps={{
|
||||
text: "Create",
|
||||
onClick: () => {
|
||||
localStorage.setItem(userContext.databaseAccount.id, "true");
|
||||
sendMessage({
|
||||
type: MessageTypes.OpenPostgreSQLPasswordReset,
|
||||
});
|
||||
usePostgres.getState().setShowResetPasswordBubble(false);
|
||||
},
|
||||
}}
|
||||
>
|
||||
If you haven't changed your password yet, change it now.
|
||||
</TeachingBubble>
|
||||
)}
|
||||
</div>
|
||||
{this.getSplashScreenButtons()}
|
||||
{useCarousel.getState().showCoachMark && (
|
||||
<Coachmark
|
||||
target="#quickstartDescription"
|
||||
@@ -248,7 +294,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
</Coachmark>
|
||||
)}
|
||||
{userContext.apiType === "Postgres" ? (
|
||||
<Stack horizontal style={{ margin: "0 auto", width: "84%" }} tokens={{ childrenGap: 32 }}>
|
||||
<Stack horizontal style={{ margin: "0 auto", width: "84%" }} tokens={{ childrenGap: 16 }}>
|
||||
<Stack style={{ width: "33%" }}>
|
||||
<Text
|
||||
variant="large"
|
||||
|
||||
50
src/Explorer/SplashScreen/SplashScreenButton.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Stack, Text } from "@fluentui/react";
|
||||
import React from "react";
|
||||
import { KeyCodes } from "../../Common/Constants";
|
||||
|
||||
interface SplashScreenButtonProps {
|
||||
imgSrc: string;
|
||||
title: string;
|
||||
description: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
|
||||
imgSrc,
|
||||
title,
|
||||
description,
|
||||
onClick,
|
||||
}: SplashScreenButtonProps): JSX.Element => {
|
||||
return (
|
||||
<Stack
|
||||
horizontal
|
||||
style={{
|
||||
border: "1px solid #949494",
|
||||
boxSizing: "border-box",
|
||||
boxShadow: "0 4px 4px rgba(0, 0, 0, 0.25)",
|
||||
borderRadius: 4,
|
||||
padding: "32px 16px",
|
||||
backgroundColor: "#ffffff",
|
||||
width: "100%",
|
||||
minHeight: 150,
|
||||
}}
|
||||
onClick={onClick}
|
||||
onKeyPress={(event: React.KeyboardEvent) => {
|
||||
if (event.charCode === KeyCodes.Space || event.charCode === KeyCodes.Enter) {
|
||||
onClick();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<div>
|
||||
<img src={imgSrc} />
|
||||
</div>
|
||||
<Stack style={{ marginLeft: 16 }}>
|
||||
<Text style={{ fontSize: 18, fontWeight: 600 }}>{title}</Text>
|
||||
<Text style={{ fontSize: 13 }}>{description}</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
495
src/Explorer/Tabs/QueryTab/QueryResultSection.tsx
Normal file
@@ -0,0 +1,495 @@
|
||||
import {
|
||||
DetailsList,
|
||||
DetailsListLayoutMode,
|
||||
IColumn,
|
||||
Pivot,
|
||||
PivotItem,
|
||||
SelectionMode,
|
||||
Stack,
|
||||
Text,
|
||||
} from "@fluentui/react";
|
||||
import { HttpHeaders, NormalizedEventKey } from "Common/Constants";
|
||||
import MongoUtility from "Common/MongoUtility";
|
||||
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
||||
import { QueryMetrics } from "Contracts/DataModels";
|
||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||
import { IDocument } from "Explorer/Tabs/QueryTab/QueryTabComponent";
|
||||
import { userContext } from "UserContext";
|
||||
import { useNotificationConsole } from "hooks/useNotificationConsole";
|
||||
import React from "react";
|
||||
import DownloadQueryMetrics from "../../../../images/DownloadQuery.svg";
|
||||
import QueryEditorNext from "../../../../images/Query-Editor-Next.svg";
|
||||
import RunQuery from "../../../../images/RunQuery.png";
|
||||
import InfoColor from "../../../../images/info_color.svg";
|
||||
import { QueryResults } from "../../../Contracts/ViewModels";
|
||||
|
||||
interface QueryResultProps {
|
||||
isMongoDB: boolean;
|
||||
queryEditorContent: string;
|
||||
error: string;
|
||||
isExecuting: boolean;
|
||||
queryResults: QueryResults;
|
||||
executeQueryDocumentsPage: (firstItemIndex: number) => Promise<void>;
|
||||
}
|
||||
|
||||
export const QueryResultSection: React.FC<QueryResultProps> = ({
|
||||
isMongoDB,
|
||||
queryEditorContent,
|
||||
error,
|
||||
queryResults,
|
||||
isExecuting,
|
||||
executeQueryDocumentsPage,
|
||||
}: QueryResultProps): JSX.Element => {
|
||||
const queryMetrics = React.useRef(queryResults?.headers?.[HttpHeaders.queryMetrics]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const latestQueryMetrics = queryResults?.headers?.[HttpHeaders.queryMetrics];
|
||||
if (latestQueryMetrics && Object.keys(latestQueryMetrics).length > 0) {
|
||||
queryMetrics.current = latestQueryMetrics;
|
||||
}
|
||||
}, [queryResults]);
|
||||
|
||||
const onRender = (item: IDocument): JSX.Element => (
|
||||
<>
|
||||
<InfoTooltip>{`${item.toolTip}`}</InfoTooltip>
|
||||
<Text style={{ paddingLeft: 10, margin: 0 }}>{`${item.metric}`}</Text>
|
||||
</>
|
||||
);
|
||||
const columns: IColumn[] = [
|
||||
{
|
||||
key: "column2",
|
||||
name: "METRIC",
|
||||
minWidth: 200,
|
||||
data: String,
|
||||
fieldName: "metric",
|
||||
onRender,
|
||||
},
|
||||
{
|
||||
key: "column3",
|
||||
name: "VALUE",
|
||||
minWidth: 200,
|
||||
data: String,
|
||||
fieldName: "value",
|
||||
},
|
||||
];
|
||||
const maybeSubQuery = queryEditorContent && /.*\(.*SELECT.*\)/i.test(queryEditorContent);
|
||||
const queryResultsString = queryResults
|
||||
? isMongoDB
|
||||
? MongoUtility.tojson(queryResults.documents, undefined, false)
|
||||
: JSON.stringify(queryResults.documents, undefined, 4)
|
||||
: "";
|
||||
|
||||
const onErrorDetailsClick = (): boolean => {
|
||||
useNotificationConsole.getState().expandConsole();
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const onErrorDetailsKeyPress = (event: React.KeyboardEvent<HTMLAnchorElement>): boolean => {
|
||||
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
|
||||
onErrorDetailsClick();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const onDownloadQueryMetricsCsvClick = (): boolean => {
|
||||
downloadQueryMetricsCsvData();
|
||||
return false;
|
||||
};
|
||||
|
||||
const onDownloadQueryMetricsCsvKeyPress = (event: React.KeyboardEvent<HTMLAnchorElement>): boolean => {
|
||||
if (event.key === NormalizedEventKey.Space || NormalizedEventKey.Enter) {
|
||||
downloadQueryMetricsCsvData();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const downloadQueryMetricsCsvData = (): void => {
|
||||
const csvData: string = generateQueryMetricsCsvData();
|
||||
if (!csvData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (navigator.msSaveBlob) {
|
||||
// for IE and Edge
|
||||
navigator.msSaveBlob(
|
||||
new Blob([csvData], { type: "data:text/csv;charset=utf-8" }),
|
||||
"PerPartitionQueryMetrics.csv"
|
||||
);
|
||||
} else {
|
||||
const downloadLink: HTMLAnchorElement = document.createElement("a");
|
||||
downloadLink.href = "data:text/csv;charset=utf-8," + encodeURI(csvData);
|
||||
downloadLink.target = "_self";
|
||||
downloadLink.download = "QueryMetricsPerPartition.csv";
|
||||
|
||||
// for some reason, FF displays the download prompt only when
|
||||
// the link is added to the dom so we add and remove it
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
downloadLink.remove();
|
||||
}
|
||||
};
|
||||
|
||||
const getAggregatedQueryMetrics = (): QueryMetrics => {
|
||||
const aggregatedQueryMetrics = {
|
||||
documentLoadTime: 0,
|
||||
documentWriteTime: 0,
|
||||
indexHitDocumentCount: 0,
|
||||
outputDocumentCount: 0,
|
||||
outputDocumentSize: 0,
|
||||
indexLookupTime: 0,
|
||||
retrievedDocumentCount: 0,
|
||||
retrievedDocumentSize: 0,
|
||||
vmExecutionTime: 0,
|
||||
runtimeExecutionTimes: {
|
||||
queryEngineExecutionTime: 0,
|
||||
systemFunctionExecutionTime: 0,
|
||||
userDefinedFunctionExecutionTime: 0,
|
||||
},
|
||||
totalQueryExecutionTime: 0,
|
||||
} as QueryMetrics;
|
||||
|
||||
if (queryMetrics.current) {
|
||||
Object.keys(queryMetrics.current).forEach((partitionKeyRangeId) => {
|
||||
const queryMetricsPerPartition = queryMetrics.current[partitionKeyRangeId];
|
||||
if (!queryMetricsPerPartition) {
|
||||
return;
|
||||
}
|
||||
aggregatedQueryMetrics.documentLoadTime += queryMetricsPerPartition.documentLoadTime?.totalMilliseconds() || 0;
|
||||
aggregatedQueryMetrics.documentWriteTime +=
|
||||
queryMetricsPerPartition.documentWriteTime?.totalMilliseconds() || 0;
|
||||
aggregatedQueryMetrics.indexHitDocumentCount += queryMetricsPerPartition.indexHitDocumentCount || 0;
|
||||
aggregatedQueryMetrics.outputDocumentCount += queryMetricsPerPartition.outputDocumentCount || 0;
|
||||
aggregatedQueryMetrics.outputDocumentSize += queryMetricsPerPartition.outputDocumentSize || 0;
|
||||
aggregatedQueryMetrics.indexLookupTime += queryMetricsPerPartition.indexLookupTime?.totalMilliseconds() || 0;
|
||||
aggregatedQueryMetrics.retrievedDocumentCount += queryMetricsPerPartition.retrievedDocumentCount || 0;
|
||||
aggregatedQueryMetrics.retrievedDocumentSize += queryMetricsPerPartition.retrievedDocumentSize || 0;
|
||||
aggregatedQueryMetrics.vmExecutionTime += queryMetricsPerPartition.vmExecutionTime?.totalMilliseconds() || 0;
|
||||
aggregatedQueryMetrics.totalQueryExecutionTime +=
|
||||
queryMetricsPerPartition.totalQueryExecutionTime?.totalMilliseconds() || 0;
|
||||
aggregatedQueryMetrics.runtimeExecutionTimes.queryEngineExecutionTime +=
|
||||
queryMetricsPerPartition.runtimeExecutionTimes?.queryEngineExecutionTime?.totalMilliseconds() || 0;
|
||||
aggregatedQueryMetrics.runtimeExecutionTimes.systemFunctionExecutionTime +=
|
||||
queryMetricsPerPartition.runtimeExecutionTimes?.systemFunctionExecutionTime?.totalMilliseconds() || 0;
|
||||
aggregatedQueryMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime +=
|
||||
queryMetricsPerPartition.runtimeExecutionTimes?.userDefinedFunctionExecutionTime?.totalMilliseconds() || 0;
|
||||
});
|
||||
}
|
||||
|
||||
return aggregatedQueryMetrics;
|
||||
};
|
||||
|
||||
const generateQueryMetricsCsvData = (): string => {
|
||||
if (queryMetrics.current) {
|
||||
let csvData =
|
||||
[
|
||||
"Partition key range id",
|
||||
"Retrieved document count",
|
||||
"Retrieved document size (in bytes)",
|
||||
"Output document count",
|
||||
"Output document size (in bytes)",
|
||||
"Index hit document count",
|
||||
"Index lookup time (ms)",
|
||||
"Document load time (ms)",
|
||||
"Query engine execution time (ms)",
|
||||
"System function execution time (ms)",
|
||||
"User defined function execution time (ms)",
|
||||
"Document write time (ms)",
|
||||
].join(",") + "\n";
|
||||
|
||||
Object.keys(queryMetrics.current).forEach((partitionKeyRangeId) => {
|
||||
const queryMetricsPerPartition = queryMetrics.current[partitionKeyRangeId];
|
||||
csvData +=
|
||||
[
|
||||
partitionKeyRangeId,
|
||||
queryMetricsPerPartition.retrievedDocumentCount,
|
||||
queryMetricsPerPartition.retrievedDocumentSize,
|
||||
queryMetricsPerPartition.outputDocumentCount,
|
||||
queryMetricsPerPartition.outputDocumentSize,
|
||||
queryMetricsPerPartition.indexHitDocumentCount,
|
||||
queryMetricsPerPartition.indexLookupTime?.totalMilliseconds(),
|
||||
queryMetricsPerPartition.documentLoadTime?.totalMilliseconds(),
|
||||
queryMetricsPerPartition.runtimeExecutionTimes?.queryEngineExecutionTime?.totalMilliseconds(),
|
||||
queryMetricsPerPartition.runtimeExecutionTimes?.systemFunctionExecutionTime?.totalMilliseconds(),
|
||||
queryMetricsPerPartition.runtimeExecutionTimes?.userDefinedFunctionExecutionTime?.totalMilliseconds(),
|
||||
queryMetricsPerPartition.documentWriteTime?.totalMilliseconds(),
|
||||
].join(",") + "\n";
|
||||
});
|
||||
|
||||
return csvData;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const onFetchNextPageClick = async (): Promise<void> => {
|
||||
const { firstItemIndex, itemCount } = queryResults;
|
||||
await executeQueryDocumentsPage(firstItemIndex + itemCount - 1);
|
||||
};
|
||||
|
||||
const generateQueryStatsItems = (): IDocument[] => {
|
||||
const items: IDocument[] = [
|
||||
{
|
||||
metric: "Request Charge",
|
||||
value: `${queryResults.requestCharge} RUs`,
|
||||
toolTip: "Request Charge",
|
||||
},
|
||||
{
|
||||
metric: "Showing Results",
|
||||
value: queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`,
|
||||
toolTip: "Showing Results",
|
||||
},
|
||||
];
|
||||
|
||||
if (userContext.apiType === "SQL") {
|
||||
const aggregatedQueryMetrics = getAggregatedQueryMetrics();
|
||||
items.push(
|
||||
{
|
||||
metric: "Retrieved document count",
|
||||
value: aggregatedQueryMetrics.retrievedDocumentCount?.toString() || "",
|
||||
toolTip: "Total number of retrieved documents",
|
||||
},
|
||||
{
|
||||
metric: "Retrieved document size",
|
||||
value: `${aggregatedQueryMetrics.retrievedDocumentSize?.toString() || 0} bytes`,
|
||||
toolTip: "Total size of retrieved documents in bytes",
|
||||
},
|
||||
{
|
||||
metric: "Output document count",
|
||||
value: aggregatedQueryMetrics.outputDocumentCount?.toString() || "",
|
||||
toolTip: "Number of output documents",
|
||||
},
|
||||
{
|
||||
metric: "Output document size",
|
||||
value: `${aggregatedQueryMetrics.outputDocumentSize?.toString() || 0} bytes`,
|
||||
toolTip: "Total size of output documents in bytes",
|
||||
},
|
||||
{
|
||||
metric: "Index hit document count",
|
||||
value: aggregatedQueryMetrics.indexHitDocumentCount?.toString() || "",
|
||||
toolTip: "Total number of documents matched by the filter",
|
||||
},
|
||||
{
|
||||
metric: "Index lookup time",
|
||||
value: `${aggregatedQueryMetrics.indexLookupTime?.toString() || 0} ms`,
|
||||
toolTip: "Time spent in physical index layer",
|
||||
},
|
||||
{
|
||||
metric: "Document load time",
|
||||
value: `${aggregatedQueryMetrics.documentLoadTime?.toString() || 0} ms`,
|
||||
toolTip: "Time spent in loading documents",
|
||||
},
|
||||
{
|
||||
metric: "Query engine execution time",
|
||||
value: `${aggregatedQueryMetrics.runtimeExecutionTimes?.queryEngineExecutionTime?.toString() || 0} ms`,
|
||||
toolTip:
|
||||
"Time spent by the query engine to execute the query expression (excludes other execution times like load documents or write results)",
|
||||
},
|
||||
{
|
||||
metric: "System function execution time",
|
||||
value: `${aggregatedQueryMetrics.runtimeExecutionTimes?.systemFunctionExecutionTime?.toString() || 0} ms`,
|
||||
toolTip: "Total time spent executing system (built-in) functions",
|
||||
},
|
||||
{
|
||||
metric: "User defined function execution time",
|
||||
value: `${
|
||||
aggregatedQueryMetrics.runtimeExecutionTimes?.userDefinedFunctionExecutionTime?.toString() || 0
|
||||
} ms`,
|
||||
toolTip: "Total time spent executing user-defined functions",
|
||||
},
|
||||
{
|
||||
metric: "Document write time",
|
||||
value: `${aggregatedQueryMetrics.documentWriteTime.toString() || 0} ms`,
|
||||
toolTip: "Time spent to write query result set to response buffer",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (queryResults.roundTrips) {
|
||||
items.push({
|
||||
metric: "Round Trips",
|
||||
value: queryResults.roundTrips?.toString(),
|
||||
toolTip: "Number of round trips",
|
||||
});
|
||||
}
|
||||
|
||||
if (queryResults.activityId) {
|
||||
items.push({
|
||||
metric: "Activity id",
|
||||
value: queryResults.activityId,
|
||||
toolTip: "",
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack style={{ height: "100%" }}>
|
||||
{isMongoDB && queryEditorContent.length === 0 && (
|
||||
<div className="mongoQueryHelper">
|
||||
Start by writing a Mongo query, for example: <strong>{"{'id':'foo'}"}</strong> or{" "}
|
||||
<strong>
|
||||
{"{ "}
|
||||
{" }"}
|
||||
</strong>{" "}
|
||||
to get all the documents.
|
||||
</div>
|
||||
)}
|
||||
{maybeSubQuery && (
|
||||
<div className="warningErrorContainer" aria-live="assertive">
|
||||
<div className="warningErrorContent">
|
||||
<span>
|
||||
<img className="paneErrorIcon" src={InfoColor} alt="Error" />
|
||||
</span>
|
||||
<span className="warningErrorDetailsLinkContainer">
|
||||
We have detected you may be using a subquery. Non-correlated subqueries are not currently supported.
|
||||
<a href="https://docs.microsoft.com/en-us/azure/cosmos-db/sql-query-subquery">
|
||||
Please see Cosmos sub query documentation for further information
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* <!-- Query Errors Tab - Start--> */}
|
||||
{error && (
|
||||
<div className="active queryErrorsHeaderContainer">
|
||||
<span className="queryErrors" data-toggle="tab">
|
||||
Errors
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/* <!-- Query Errors Tab - End --> */}
|
||||
{/* <!-- Query Results & Errors Content Container - Start--> */}
|
||||
<div className="queryResultErrorContentContainer">
|
||||
{!queryResults && !error && !isExecuting && (
|
||||
<div className="queryEditorWatermark">
|
||||
<p>
|
||||
<img src={RunQuery} alt="Execute Query Watermark" />
|
||||
</p>
|
||||
<p className="queryEditorWatermarkText">Execute a query to see the results</p>
|
||||
</div>
|
||||
)}
|
||||
{(queryResults || !!error) && (
|
||||
<div className="queryResultsErrorsContent">
|
||||
{!error && (
|
||||
<Pivot aria-label="Successful execution" style={{ height: "100%" }}>
|
||||
<PivotItem
|
||||
headerText="Results"
|
||||
headerButtonProps={{
|
||||
"data-order": 1,
|
||||
"data-title": "Results",
|
||||
}}
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
<div className="result-metadata">
|
||||
<span>
|
||||
<span>
|
||||
{queryResults.itemCount > 0
|
||||
? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}`
|
||||
: `0 - 0`}
|
||||
</span>
|
||||
</span>
|
||||
{queryResults.hasMoreResults && (
|
||||
<>
|
||||
<span className="queryResultDivider">|</span>
|
||||
<span className="queryResultNextEnable">
|
||||
<a onClick={() => onFetchNextPageClick()}>
|
||||
<span>Load more</span>
|
||||
<img className="queryResultnextImg" src={QueryEditorNext} alt="Fetch next page" />
|
||||
</a>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{queryResults && queryResultsString?.length > 0 && !error && (
|
||||
<div
|
||||
style={{
|
||||
paddingBottom: "100px",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<EditorReact
|
||||
language={"json"}
|
||||
content={queryResultsString}
|
||||
isReadOnly={true}
|
||||
ariaLabel={"Query results"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="Query Stats"
|
||||
headerButtonProps={{
|
||||
"data-order": 2,
|
||||
"data-title": "Query Stats",
|
||||
}}
|
||||
style={{ height: "100%", overflowY: "scroll" }}
|
||||
>
|
||||
{queryResults && !error && (
|
||||
<div className="queryMetricsSummaryContainer">
|
||||
<div className="queryMetricsSummary">
|
||||
<h5>Query Statistics</h5>
|
||||
<DetailsList
|
||||
items={generateQueryStatsItems()}
|
||||
columns={columns}
|
||||
selectionMode={SelectionMode.none}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
compact={true}
|
||||
/>
|
||||
</div>
|
||||
{userContext.apiType === "SQL" && (
|
||||
<div className="downloadMetricsLinkContainer">
|
||||
<a
|
||||
id="downloadMetricsLink"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => onDownloadQueryMetricsCsvClick()}
|
||||
onKeyPress={(event: React.KeyboardEvent<HTMLAnchorElement>) =>
|
||||
onDownloadQueryMetricsCsvKeyPress(event)
|
||||
}
|
||||
>
|
||||
<img
|
||||
className="downloadCsvImg"
|
||||
src={DownloadQueryMetrics}
|
||||
alt="download query metrics csv"
|
||||
/>
|
||||
<span>Per-partition query metrics (CSV)</span>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
)}
|
||||
{/* <!-- Query Errors Content - Start--> */}
|
||||
{!!error && (
|
||||
<div className="tab-pane active">
|
||||
<div className="errorContent">
|
||||
<span className="errorMessage">{error}</span>
|
||||
<span className="errorDetailsLink">
|
||||
<a
|
||||
onClick={() => onErrorDetailsClick()}
|
||||
onKeyPress={(event: React.KeyboardEvent<HTMLAnchorElement>) => onErrorDetailsKeyPress(event)}
|
||||
id="error-display"
|
||||
tabIndex={0}
|
||||
aria-label="Error details link"
|
||||
>
|
||||
More details
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* <!-- Query Errors Content - End--> */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,30 +1,22 @@
|
||||
import { DetailsList, DetailsListLayoutMode, IColumn, Pivot, PivotItem, SelectionMode, Text } from "@fluentui/react";
|
||||
import { FeedOptions } from "@azure/cosmos";
|
||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||
import React, { Fragment } from "react";
|
||||
import SplitterLayout from "react-splitter-layout";
|
||||
import "react-splitter-layout/lib/index.css";
|
||||
import DownloadQueryMetrics from "../../../../images/DownloadQuery.svg";
|
||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||
import InfoColor from "../../../../images/info_color.svg";
|
||||
import QueryEditorNext from "../../../../images/Query-Editor-Next.svg";
|
||||
import RunQuery from "../../../../images/RunQuery.png";
|
||||
import SaveQueryIcon from "../../../../images/save-cosmos.svg";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { NormalizedEventKey } from "../../../Common/Constants";
|
||||
import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
|
||||
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
|
||||
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
|
||||
import * as HeadersUtility from "../../../Common/HeadersUtility";
|
||||
import { MinimalQueryIterator } from "../../../Common/IteratorUtilities";
|
||||
import { queryIterator } from "../../../Common/MongoProxyClient";
|
||||
import MongoUtility from "../../../Common/MongoUtility";
|
||||
import { Splitter } from "../../../Common/Splitter";
|
||||
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
|
||||
import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
|
||||
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import * as QueryUtils from "../../../Utils/QueryUtils";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
||||
import Explorer from "../../Explorer";
|
||||
@@ -43,7 +35,6 @@ export interface IDocument {
|
||||
metric: string;
|
||||
value: string;
|
||||
toolTip: string;
|
||||
isQueryMetricsEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface ITabAccessor {
|
||||
@@ -74,30 +65,13 @@ export interface IQueryTabComponentProps {
|
||||
}
|
||||
|
||||
interface IQueryTabStates {
|
||||
queryMetrics: Map<string, DataModels.QueryMetrics>;
|
||||
aggregatedQueryMetrics: DataModels.QueryMetrics;
|
||||
activityId: string;
|
||||
roundTrips: number;
|
||||
toggleState: ToggleState;
|
||||
isQueryMetricsEnabled: boolean;
|
||||
showingDocumentsDisplayText: string;
|
||||
requestChargeDisplayText: string;
|
||||
initialEditorContent: string;
|
||||
sqlQueryEditorContent: string;
|
||||
selectedContent: string;
|
||||
_executeQueryButtonTitle: string;
|
||||
sqlStatementToExecute: string;
|
||||
queryResults: string;
|
||||
statusMessge: string;
|
||||
statusIcon: string;
|
||||
allResultsMetadata: ViewModels.QueryResultsMetadata[];
|
||||
queryResults: ViewModels.QueryResults;
|
||||
error: string;
|
||||
isTemplateReady: boolean;
|
||||
_isSaveQueriesEnabled: boolean;
|
||||
isExecutionError: boolean;
|
||||
isExecuting: boolean;
|
||||
columns: IColumn[];
|
||||
items: IDocument[];
|
||||
}
|
||||
|
||||
export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> {
|
||||
@@ -105,90 +79,39 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
public executeQueryButton: Button;
|
||||
public saveQueryButton: Button;
|
||||
public splitterId: string;
|
||||
public splitter: Splitter;
|
||||
public isPreferredApiMongoDB: boolean;
|
||||
public resultsDisplay: string;
|
||||
protected monacoSettings: ViewModels.MonacoEditorSettings;
|
||||
protected _iterator: MinimalQueryIterator;
|
||||
private _resourceTokenPartitionKey: string;
|
||||
_partitionKey: DataModels.PartitionKey;
|
||||
public maybeSubQuery: boolean;
|
||||
public isCloseClicked: boolean;
|
||||
public allItems: IDocument[];
|
||||
public defaultQueryText: string;
|
||||
private _iterator: MinimalQueryIterator;
|
||||
|
||||
constructor(props: IQueryTabComponentProps) {
|
||||
super(props);
|
||||
const columns: IColumn[] = [
|
||||
{
|
||||
key: "column2",
|
||||
name: "METRIC",
|
||||
minWidth: 200,
|
||||
data: String,
|
||||
fieldName: "metric",
|
||||
onRender: this.onRenderColumnItem,
|
||||
},
|
||||
{
|
||||
key: "column3",
|
||||
name: "VALUE",
|
||||
minWidth: 200,
|
||||
data: String,
|
||||
fieldName: "value",
|
||||
},
|
||||
];
|
||||
|
||||
if (this.props.isPreferredApiMongoDB) {
|
||||
this.defaultQueryText = props.queryText;
|
||||
} else {
|
||||
this.defaultQueryText = props.queryText !== void 0 ? props.queryText : "SELECT * FROM c";
|
||||
}
|
||||
|
||||
this.state = {
|
||||
queryMetrics: new Map(),
|
||||
aggregatedQueryMetrics: undefined,
|
||||
activityId: "",
|
||||
roundTrips: undefined,
|
||||
toggleState: ToggleState.Result,
|
||||
isQueryMetricsEnabled: userContext.apiType === "SQL" || false,
|
||||
showingDocumentsDisplayText: this.resultsDisplay,
|
||||
requestChargeDisplayText: "",
|
||||
initialEditorContent: this.defaultQueryText,
|
||||
sqlQueryEditorContent: this.defaultQueryText,
|
||||
sqlQueryEditorContent: props.queryText || "SELECT * FROM c",
|
||||
selectedContent: "",
|
||||
_executeQueryButtonTitle: "Execute Query",
|
||||
sqlStatementToExecute: this.defaultQueryText,
|
||||
queryResults: "",
|
||||
statusMessge: "",
|
||||
statusIcon: "",
|
||||
allResultsMetadata: [],
|
||||
queryResults: undefined,
|
||||
error: "",
|
||||
isTemplateReady: false,
|
||||
_isSaveQueriesEnabled: userContext.apiType === "SQL" || userContext.apiType === "Gremlin",
|
||||
isExecutionError: this.props.isExecutionError,
|
||||
isExecuting: false,
|
||||
columns: columns,
|
||||
items: [],
|
||||
};
|
||||
this.isCloseClicked = false;
|
||||
this.splitterId = this.props.tabId + "_splitter";
|
||||
this.queryEditorId = `queryeditor${this.props.tabId}`;
|
||||
this._partitionKey = props.partitionKey;
|
||||
this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB;
|
||||
this.monacoSettings = new ViewModels.MonacoEditorSettings(this.props.monacoEditorSetting, false);
|
||||
|
||||
this.executeQueryButton = {
|
||||
enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0,
|
||||
visible: true,
|
||||
};
|
||||
const sql = this.state.sqlQueryEditorContent;
|
||||
this.maybeSubQuery = sql && /.*\(.*SELECT.*\)/i.test(sql);
|
||||
|
||||
const isSaveQueryBtnEnabled = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
||||
this.saveQueryButton = {
|
||||
enabled: this.state._isSaveQueriesEnabled,
|
||||
visible: this.state._isSaveQueriesEnabled,
|
||||
enabled: isSaveQueryBtnEnabled,
|
||||
visible: isSaveQueryBtnEnabled,
|
||||
};
|
||||
|
||||
this._buildCommandBarOptions();
|
||||
this.props.tabsBaseInstance.updateNavbarWithTabsButtons();
|
||||
props.onTabAccessor({
|
||||
onTabClickEvent: this.onTabClick.bind(this),
|
||||
onSaveClickEvent: this.getCurrentEditorQuery.bind(this),
|
||||
@@ -196,165 +119,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
});
|
||||
}
|
||||
|
||||
public onRenderColumnItem(item: IDocument): JSX.Element {
|
||||
if (item.toolTip !== "") {
|
||||
return (
|
||||
<>
|
||||
<InfoTooltip>{`${item.toolTip}`}</InfoTooltip>
|
||||
<Text style={{ paddingLeft: 10, margin: 0 }}>{`${item.metric}`}</Text>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public generateDetailsList(): IDocument[] {
|
||||
const items: IDocument[] = [];
|
||||
const allItems: IDocument[] = [
|
||||
{
|
||||
metric: "Request Charge",
|
||||
value: this.state.requestChargeDisplayText,
|
||||
toolTip: "Request Charge",
|
||||
isQueryMetricsEnabled: true,
|
||||
},
|
||||
{
|
||||
metric: "Showing Results",
|
||||
value: this.state.showingDocumentsDisplayText,
|
||||
toolTip: "Showing Results",
|
||||
isQueryMetricsEnabled: true,
|
||||
},
|
||||
{
|
||||
metric: "Retrieved document count",
|
||||
value:
|
||||
this.state.aggregatedQueryMetrics.retrievedDocumentCount !== undefined
|
||||
? this.state.aggregatedQueryMetrics.retrievedDocumentCount.toString()
|
||||
: "",
|
||||
toolTip: "Total number of retrieved documents",
|
||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
||||
},
|
||||
{
|
||||
metric: "Retrieved document size",
|
||||
value:
|
||||
this.state.aggregatedQueryMetrics.retrievedDocumentSize !== undefined
|
||||
? this.state.aggregatedQueryMetrics.retrievedDocumentSize.toString() + " bytes"
|
||||
: "",
|
||||
toolTip: "Total size of retrieved documents in bytes",
|
||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
||||
},
|
||||
{
|
||||
metric: "Output document count",
|
||||
value:
|
||||
this.state.aggregatedQueryMetrics.outputDocumentCount !== undefined
|
||||
? this.state.aggregatedQueryMetrics.outputDocumentCount.toString()
|
||||
: "",
|
||||
toolTip: "Number of output documents",
|
||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
||||
},
|
||||
{
|
||||
metric: "Output document size",
|
||||
value:
|
||||
this.state.aggregatedQueryMetrics.outputDocumentSize !== undefined
|
||||
? this.state.aggregatedQueryMetrics.outputDocumentSize.toString() + " bytes"
|
||||
: "",
|
||||
toolTip: "Total size of output documents in bytes",
|
||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
||||
},
|
||||
{
|
||||
metric: "Index hit document count",
|
||||
value:
|
||||
this.state.aggregatedQueryMetrics.indexHitDocumentCount !== undefined
|
||||
? this.state.aggregatedQueryMetrics.indexHitDocumentCount.toString()
|
||||
: "",
|
||||
toolTip: "Total number of documents matched by the filter",
|
||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
||||
},
|
||||
{
|
||||
metric: "Index lookup time",
|
||||
value:
|
||||
this.state.aggregatedQueryMetrics.indexLookupTime !== undefined
|
||||
? this.state.aggregatedQueryMetrics.indexLookupTime.toString() + " ms"
|
||||
: "",
|
||||
toolTip: "Time spent in physical index layer",
|
||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
||||
},
|
||||
{
|
||||
metric: "Document load time",
|
||||
value:
|
||||
this.state.aggregatedQueryMetrics.documentLoadTime !== undefined
|
||||
? this.state.aggregatedQueryMetrics.documentLoadTime.toString() + " ms"
|
||||
: "",
|
||||
toolTip: "Time spent in loading documents",
|
||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
||||
},
|
||||
{
|
||||
metric: "Query engine execution time",
|
||||
value:
|
||||
this.state.aggregatedQueryMetrics.runtimeExecutionTimes.queryEngineExecutionTime !== undefined
|
||||
? this.state.aggregatedQueryMetrics.runtimeExecutionTimes.queryEngineExecutionTime.toString() + " ms"
|
||||
: "",
|
||||
toolTip:
|
||||
"Time spent by the query engine to execute the query expression (excludes other execution times like load documents or write results)",
|
||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
||||
},
|
||||
{
|
||||
metric: "System function execution time",
|
||||
value:
|
||||
this.state.aggregatedQueryMetrics.runtimeExecutionTimes.systemFunctionExecutionTime !== undefined
|
||||
? this.state.aggregatedQueryMetrics.runtimeExecutionTimes.systemFunctionExecutionTime.toString() + " ms"
|
||||
: "",
|
||||
toolTip: "Total time spent executing system (built-in) functions",
|
||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
||||
},
|
||||
{
|
||||
metric: "User defined function execution time",
|
||||
value:
|
||||
this.state.aggregatedQueryMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime !== undefined
|
||||
? this.state.aggregatedQueryMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime.toString() +
|
||||
" ms"
|
||||
: "",
|
||||
toolTip: "Total time spent executing user-defined functions",
|
||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
||||
},
|
||||
{
|
||||
metric: "Document write time",
|
||||
value:
|
||||
this.state.aggregatedQueryMetrics.documentWriteTime !== undefined
|
||||
? this.state.aggregatedQueryMetrics.documentWriteTime.toString() + " ms"
|
||||
: "",
|
||||
toolTip: "Time spent to write query result set to response buffer",
|
||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
||||
},
|
||||
{
|
||||
metric: "Round Trips",
|
||||
value: this.state.roundTrips ? this.state.roundTrips.toString() : "",
|
||||
toolTip: "",
|
||||
isQueryMetricsEnabled: true,
|
||||
},
|
||||
{
|
||||
metric: "Activity id",
|
||||
value: this.state.activityId ? this.state.activityId : "",
|
||||
toolTip: "",
|
||||
isQueryMetricsEnabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
allItems.forEach((item) => {
|
||||
if (item.metric === "Round Trips" || item.metric === "Activity id") {
|
||||
if (item.metric === "Round Trips" && this.state.roundTrips !== undefined) {
|
||||
items.push(item);
|
||||
} else if (item.metric === "Activity id" && this.state.activityId !== undefined) {
|
||||
items.push(item);
|
||||
}
|
||||
} else {
|
||||
if (item.isQueryMetricsEnabled) {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
public onCloseClick(isClicked: boolean): void {
|
||||
this.isCloseClicked = isClicked;
|
||||
}
|
||||
@@ -372,14 +136,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
}
|
||||
|
||||
public onExecuteQueryClick = async (): Promise<void> => {
|
||||
const sqlStatement = this.state.selectedContent || this.state.sqlQueryEditorContent;
|
||||
|
||||
this.setState({
|
||||
sqlStatementToExecute: sqlStatement,
|
||||
allResultsMetadata: [],
|
||||
queryResults: "",
|
||||
});
|
||||
|
||||
this._iterator = undefined;
|
||||
setTimeout(async () => {
|
||||
await this._executeQueryDocumentsPage(0);
|
||||
@@ -396,31 +152,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
.openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={this.props.collection.container} />);
|
||||
};
|
||||
|
||||
public async onFetchNextPageClick(): Promise<void> {
|
||||
const allResultsMetadata = (this.state.allResultsMetadata && this.state.allResultsMetadata) || [];
|
||||
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
|
||||
const firstResultIndex: number = (metadata && Number(metadata.firstItemIndex)) || 1;
|
||||
const itemCount: number = (metadata && Number(metadata.itemCount)) || 0;
|
||||
|
||||
await this._executeQueryDocumentsPage(firstResultIndex + itemCount - 1);
|
||||
}
|
||||
|
||||
//eslint-disable-next-line
|
||||
public onErrorDetailsClick = (): boolean => {
|
||||
useNotificationConsole.getState().expandConsole();
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
public onErrorDetailsKeyPress = (event: React.KeyboardEvent<HTMLAnchorElement>): boolean => {
|
||||
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
|
||||
this.onErrorDetailsClick();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
public toggleResult(): void {
|
||||
this.setState({
|
||||
toggleState: ToggleState.Result,
|
||||
@@ -452,54 +183,30 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
focusElement && focusElement.focus();
|
||||
}
|
||||
|
||||
public isResultToggled(): boolean {
|
||||
return this.state.toggleState === ToggleState.Result;
|
||||
}
|
||||
|
||||
public isMetricsToggled(): boolean {
|
||||
return this.state.toggleState === ToggleState.QueryMetrics;
|
||||
}
|
||||
|
||||
public onDownloadQueryMetricsCsvClick = (): boolean => {
|
||||
this._downloadQueryMetricsCsvData();
|
||||
return false;
|
||||
};
|
||||
|
||||
public onDownloadQueryMetricsCsvKeyPress = (event: React.KeyboardEvent<HTMLAnchorElement>): boolean => {
|
||||
if (event.key === NormalizedEventKey.Space || NormalizedEventKey.Enter) {
|
||||
this._downloadQueryMetricsCsvData();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
//eslint-disable-next-line
|
||||
private async _executeQueryDocumentsPage(firstItemIndex: number): Promise<any> {
|
||||
this.setState({
|
||||
error: "",
|
||||
roundTrips: undefined,
|
||||
});
|
||||
|
||||
private async _executeQueryDocumentsPage(firstItemIndex: number): Promise<void> {
|
||||
if (this._iterator === undefined) {
|
||||
if (this.isPreferredApiMongoDB) {
|
||||
this._initIteratorMongo();
|
||||
} else {
|
||||
this._initIterator();
|
||||
}
|
||||
this._iterator = this.props.isPreferredApiMongoDB
|
||||
? queryIterator(
|
||||
this.props.collection.databaseId,
|
||||
this.props.viewModelcollection,
|
||||
this.state.selectedContent || this.state.sqlQueryEditorContent
|
||||
)
|
||||
: queryDocuments(
|
||||
this.props.collection.databaseId,
|
||||
this.props.collection.id(),
|
||||
this.state.selectedContent || this.state.sqlQueryEditorContent,
|
||||
{ enableCrossPartitionQuery: HeadersUtility.shouldEnableCrossPartitionKey() } as FeedOptions
|
||||
);
|
||||
}
|
||||
|
||||
await this._queryDocumentsPage(firstItemIndex);
|
||||
}
|
||||
|
||||
private async _queryDocumentsPage(firstItemIndex: number): Promise<void> {
|
||||
let results: string;
|
||||
|
||||
this.props.tabsBaseInstance.isExecutionError(false);
|
||||
this.setState({
|
||||
isExecutionError: false,
|
||||
});
|
||||
this._resetAggregateQueryMetrics();
|
||||
|
||||
const queryDocuments = async (firstItemIndex: number) =>
|
||||
await queryDocumentsPage(this.props.collection && this.props.collection.id(), this._iterator, firstItemIndex);
|
||||
@@ -513,44 +220,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
firstItemIndex,
|
||||
queryDocuments
|
||||
);
|
||||
const allResultsMetadata = (this.state.allResultsMetadata && this.state.allResultsMetadata) || [];
|
||||
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
|
||||
const resultsMetadata: ViewModels.QueryResultsMetadata = {
|
||||
hasMoreResults: queryResults.hasMoreResults,
|
||||
itemCount: queryResults.itemCount,
|
||||
firstItemIndex: queryResults.firstItemIndex,
|
||||
lastItemIndex: queryResults.lastItemIndex,
|
||||
};
|
||||
this.state.allResultsMetadata.push(resultsMetadata);
|
||||
|
||||
this.setState({
|
||||
activityId: queryResults.activityId,
|
||||
roundTrips: queryResults.roundTrips,
|
||||
});
|
||||
|
||||
const documents = queryResults.documents;
|
||||
if (this.isPreferredApiMongoDB) {
|
||||
results = MongoUtility.tojson(documents, undefined, false);
|
||||
} else {
|
||||
results = this.props.tabsBaseInstance.renderObjectForEditor(documents, undefined, 4);
|
||||
}
|
||||
|
||||
const resultsDisplay: string =
|
||||
queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`;
|
||||
|
||||
this.setState({
|
||||
showingDocumentsDisplayText: resultsDisplay,
|
||||
requestChargeDisplayText: `${queryResults.requestCharge} RUs`,
|
||||
queryResults: results,
|
||||
});
|
||||
|
||||
this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]);
|
||||
|
||||
if (queryResults.itemCount === 0 && metadata !== undefined && metadata.itemCount >= 0) {
|
||||
// we let users query for the next page because the SDK sometimes specifies there are more elements
|
||||
// even though there aren't any so we should not update the prior query results.
|
||||
return;
|
||||
}
|
||||
this.setState({ queryResults, error: "" });
|
||||
} catch (error) {
|
||||
this.props.tabsBaseInstance.isExecutionError(true);
|
||||
this.setState({
|
||||
@@ -571,228 +241,10 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
}
|
||||
}
|
||||
|
||||
private _updateQueryMetricsMap(metricsMap: { [partitionKeyRange: string]: DataModels.QueryMetrics }): void {
|
||||
if (!metricsMap) {
|
||||
this.allItems = this.generateDetailsList();
|
||||
this.setState({
|
||||
items: this.allItems,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(metricsMap).forEach((key: string) => {
|
||||
this.state.queryMetrics.set(key, metricsMap[key]);
|
||||
});
|
||||
|
||||
this._aggregateQueryMetrics(this.state.queryMetrics);
|
||||
this.allItems = this.generateDetailsList();
|
||||
this.setState({
|
||||
items: this.allItems,
|
||||
});
|
||||
}
|
||||
|
||||
private _aggregateQueryMetrics(metricsMap: Map<string, DataModels.QueryMetrics>): DataModels.QueryMetrics {
|
||||
if (!metricsMap) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const aggregatedMetrics: DataModels.QueryMetrics = this.state.aggregatedQueryMetrics;
|
||||
metricsMap.forEach((queryMetrics) => {
|
||||
if (queryMetrics) {
|
||||
aggregatedMetrics.documentLoadTime =
|
||||
this._normalize(queryMetrics.documentLoadTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.documentLoadTime);
|
||||
aggregatedMetrics.documentWriteTime =
|
||||
this._normalize(queryMetrics.documentWriteTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.documentWriteTime);
|
||||
aggregatedMetrics.indexHitDocumentCount =
|
||||
this._normalize(queryMetrics.indexHitDocumentCount) +
|
||||
this._normalize(aggregatedMetrics.indexHitDocumentCount);
|
||||
aggregatedMetrics.outputDocumentCount =
|
||||
this._normalize(queryMetrics.outputDocumentCount) + this._normalize(aggregatedMetrics.outputDocumentCount);
|
||||
aggregatedMetrics.outputDocumentSize =
|
||||
this._normalize(queryMetrics.outputDocumentSize) + this._normalize(aggregatedMetrics.outputDocumentSize);
|
||||
aggregatedMetrics.indexLookupTime =
|
||||
this._normalize(queryMetrics.indexLookupTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.indexLookupTime);
|
||||
aggregatedMetrics.retrievedDocumentCount =
|
||||
this._normalize(queryMetrics.retrievedDocumentCount) +
|
||||
this._normalize(aggregatedMetrics.retrievedDocumentCount);
|
||||
aggregatedMetrics.retrievedDocumentSize =
|
||||
this._normalize(queryMetrics.retrievedDocumentSize) +
|
||||
this._normalize(aggregatedMetrics.retrievedDocumentSize);
|
||||
aggregatedMetrics.vmExecutionTime =
|
||||
this._normalize(queryMetrics.vmExecutionTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.vmExecutionTime);
|
||||
aggregatedMetrics.totalQueryExecutionTime =
|
||||
this._normalize(queryMetrics.totalQueryExecutionTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.totalQueryExecutionTime);
|
||||
|
||||
aggregatedMetrics.runtimeExecutionTimes.queryEngineExecutionTime =
|
||||
this._normalize(queryMetrics.runtimeExecutionTimes.queryEngineExecutionTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.runtimeExecutionTimes.queryEngineExecutionTime);
|
||||
aggregatedMetrics.runtimeExecutionTimes.systemFunctionExecutionTime =
|
||||
this._normalize(queryMetrics.runtimeExecutionTimes.systemFunctionExecutionTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.runtimeExecutionTimes.systemFunctionExecutionTime);
|
||||
aggregatedMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime =
|
||||
this._normalize(queryMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime);
|
||||
}
|
||||
});
|
||||
|
||||
return aggregatedMetrics;
|
||||
}
|
||||
|
||||
public _downloadQueryMetricsCsvData(): void {
|
||||
const csvData: string = this._generateQueryMetricsCsvData();
|
||||
if (!csvData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (navigator.msSaveBlob) {
|
||||
// for IE and Edge
|
||||
navigator.msSaveBlob(
|
||||
new Blob([csvData], { type: "data:text/csv;charset=utf-8" }),
|
||||
"PerPartitionQueryMetrics.csv"
|
||||
);
|
||||
} else {
|
||||
const downloadLink: HTMLAnchorElement = document.createElement("a");
|
||||
downloadLink.href = "data:text/csv;charset=utf-8," + encodeURI(csvData);
|
||||
downloadLink.target = "_self";
|
||||
downloadLink.download = "QueryMetricsPerPartition.csv";
|
||||
|
||||
// for some reason, FF displays the download prompt only when
|
||||
// the link is added to the dom so we add and remove it
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
downloadLink.remove();
|
||||
}
|
||||
}
|
||||
|
||||
protected _initIterator(): void {
|
||||
const options = QueryTabComponent.getIteratorOptions();
|
||||
if (this._resourceTokenPartitionKey) {
|
||||
options.partitionKey = this._resourceTokenPartitionKey;
|
||||
}
|
||||
|
||||
this._iterator = queryDocuments(
|
||||
this.props.collection.databaseId,
|
||||
this.props.collection.id(),
|
||||
this.state.sqlStatementToExecute,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
protected _initIteratorMongo(): Promise<MinimalQueryIterator> {
|
||||
//eslint-disable-next-line
|
||||
const options: any = {};
|
||||
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
|
||||
this._iterator = queryIterator(
|
||||
this.props.collection.databaseId,
|
||||
this.props.viewModelcollection,
|
||||
this.state.sqlStatementToExecute
|
||||
);
|
||||
const mongoPromise: Promise<MinimalQueryIterator> = new Promise((resolve) => {
|
||||
resolve(this._iterator);
|
||||
});
|
||||
return mongoPromise;
|
||||
}
|
||||
|
||||
//eslint-disable-next-line
|
||||
public static getIteratorOptions(collection?: ViewModels.Collection): any {
|
||||
//eslint-disable-next-line
|
||||
const options: any = {};
|
||||
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
|
||||
return options;
|
||||
}
|
||||
|
||||
private _normalize(value: number): number {
|
||||
if (!value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private _resetAggregateQueryMetrics(): void {
|
||||
this.setState({
|
||||
aggregatedQueryMetrics: {
|
||||
clientSideMetrics: {},
|
||||
documentLoadTime: undefined,
|
||||
documentWriteTime: undefined,
|
||||
indexHitDocumentCount: undefined,
|
||||
outputDocumentCount: undefined,
|
||||
outputDocumentSize: undefined,
|
||||
indexLookupTime: undefined,
|
||||
retrievedDocumentCount: undefined,
|
||||
retrievedDocumentSize: undefined,
|
||||
vmExecutionTime: undefined,
|
||||
queryPreparationTimes: undefined,
|
||||
runtimeExecutionTimes: {
|
||||
queryEngineExecutionTime: undefined,
|
||||
systemFunctionExecutionTime: undefined,
|
||||
userDefinedFunctionExecutionTime: undefined,
|
||||
},
|
||||
totalQueryExecutionTime: undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _generateQueryMetricsCsvData(): string {
|
||||
if (!this.state.queryMetrics) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const queryMetrics = this.state.queryMetrics;
|
||||
let csvData = "";
|
||||
const columnHeaders: string =
|
||||
[
|
||||
"Partition key range id",
|
||||
"Retrieved document count",
|
||||
"Retrieved document size (in bytes)",
|
||||
"Output document count",
|
||||
"Output document size (in bytes)",
|
||||
"Index hit document count",
|
||||
"Index lookup time (ms)",
|
||||
"Document load time (ms)",
|
||||
"Query engine execution time (ms)",
|
||||
"System function execution time (ms)",
|
||||
"User defined function execution time (ms)",
|
||||
"Document write time (ms)",
|
||||
].join(",") + "\n";
|
||||
csvData = csvData + columnHeaders;
|
||||
queryMetrics.forEach((queryMetric, partitionKeyRangeId) => {
|
||||
const partitionKeyRangeData: string =
|
||||
[
|
||||
partitionKeyRangeId,
|
||||
queryMetric.retrievedDocumentCount,
|
||||
queryMetric.retrievedDocumentSize,
|
||||
queryMetric.outputDocumentCount,
|
||||
queryMetric.outputDocumentSize,
|
||||
queryMetric.indexHitDocumentCount,
|
||||
queryMetric.indexLookupTime && queryMetric.indexLookupTime.totalMilliseconds(),
|
||||
queryMetric.documentLoadTime && queryMetric.documentLoadTime.totalMilliseconds(),
|
||||
queryMetric.runtimeExecutionTimes &&
|
||||
queryMetric.runtimeExecutionTimes.queryEngineExecutionTime &&
|
||||
queryMetric.runtimeExecutionTimes.queryEngineExecutionTime.totalMilliseconds(),
|
||||
queryMetric.runtimeExecutionTimes &&
|
||||
queryMetric.runtimeExecutionTimes.systemFunctionExecutionTime &&
|
||||
queryMetric.runtimeExecutionTimes.systemFunctionExecutionTime.totalMilliseconds(),
|
||||
queryMetric.runtimeExecutionTimes &&
|
||||
queryMetric.runtimeExecutionTimes.userDefinedFunctionExecutionTime &&
|
||||
queryMetric.runtimeExecutionTimes.userDefinedFunctionExecutionTime.totalMilliseconds(),
|
||||
queryMetric.documentWriteTime && queryMetric.documentWriteTime.totalMilliseconds(),
|
||||
].join(",") + "\n";
|
||||
csvData = csvData + partitionKeyRangeData;
|
||||
});
|
||||
|
||||
return csvData;
|
||||
}
|
||||
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
if (this.executeQueryButton.visible) {
|
||||
const label = this.state._executeQueryButtonTitle;
|
||||
const label = this.state.selectedContent?.length > 0 ? "Execute Selection" : "Execute Query";
|
||||
buttons.push({
|
||||
iconSrc: ExecuteQueryIcon,
|
||||
iconAlt: label,
|
||||
@@ -820,10 +272,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
return buttons;
|
||||
}
|
||||
|
||||
private _buildCommandBarOptions(): void {
|
||||
this.props.tabsBaseInstance.updateNavbarWithTabsButtons();
|
||||
}
|
||||
|
||||
public onChangeContent(newContent: string): void {
|
||||
this.setState({
|
||||
sqlQueryEditorContent: newContent,
|
||||
@@ -848,13 +296,11 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
public onSelectedContent(selectedContent: string): void {
|
||||
if (selectedContent.trim().length > 0) {
|
||||
this.setState({
|
||||
selectedContent: selectedContent,
|
||||
_executeQueryButtonTitle: "Execute Selection",
|
||||
selectedContent,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
selectedContent: "",
|
||||
_executeQueryButtonTitle: "Execute Query",
|
||||
});
|
||||
}
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
@@ -874,7 +320,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
<div className="queryEditor" style={{ height: "100%" }}>
|
||||
<EditorReact
|
||||
language={"sql"}
|
||||
content={this.state.initialEditorContent}
|
||||
content={this.state.sqlQueryEditorContent}
|
||||
isReadOnly={false}
|
||||
ariaLabel={"Editing Query"}
|
||||
lineNumbers={"on"}
|
||||
@@ -883,174 +329,14 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
<Fragment>
|
||||
{this.isPreferredApiMongoDB && this.state.sqlQueryEditorContent.length === 0 && (
|
||||
<div className="mongoQueryHelper">
|
||||
Start by writing a Mongo query, for example: <strong>{"{'id':'foo'}"}</strong> or{" "}
|
||||
<strong>
|
||||
{"{ "}
|
||||
{" }"}
|
||||
</strong>{" "}
|
||||
to get all the documents.
|
||||
</div>
|
||||
)}
|
||||
{this.maybeSubQuery && (
|
||||
<div className="warningErrorContainer" aria-live="assertive">
|
||||
<div className="warningErrorContent">
|
||||
<span>
|
||||
<img className="paneErrorIcon" src={InfoColor} alt="Error" />
|
||||
</span>
|
||||
<span className="warningErrorDetailsLinkContainer">
|
||||
We have detected you may be using a subquery. Non-correlated subqueries are not currently
|
||||
supported.
|
||||
<a href="https://docs.microsoft.com/en-us/azure/cosmos-db/sql-query-subquery">
|
||||
Please see Cosmos sub query documentation for further information
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* <!-- Query Errors Tab - Start--> */}
|
||||
{!!this.state.error && (
|
||||
<div className="active queryErrorsHeaderContainer">
|
||||
<span className="queryErrors" data-toggle="tab">
|
||||
Errors
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/* <!-- Query Errors Tab - End --> */}
|
||||
{/* <!-- Query Results & Errors Content Container - Start--> */}
|
||||
<div className="queryResultErrorContentContainer">
|
||||
{this.state.allResultsMetadata.length === 0 &&
|
||||
!this.state.error &&
|
||||
!this.state.queryResults &&
|
||||
!this.props.tabsBaseInstance.isExecuting() && (
|
||||
<div className="queryEditorWatermark">
|
||||
<p>
|
||||
<img src={RunQuery} alt="Execute Query Watermark" />
|
||||
</p>
|
||||
<p className="queryEditorWatermarkText">Execute a query to see the results</p>
|
||||
</div>
|
||||
)}
|
||||
{(this.state.allResultsMetadata.length > 0 || !!this.state.error || this.state.queryResults) && (
|
||||
<div className="queryResultsErrorsContent">
|
||||
{!this.state.error && (
|
||||
<Pivot aria-label="Successful execution" style={{ height: "100%" }}>
|
||||
<PivotItem
|
||||
headerText="Results"
|
||||
headerButtonProps={{
|
||||
"data-order": 1,
|
||||
"data-title": "Results",
|
||||
}}
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
<div className="result-metadata">
|
||||
<span>
|
||||
<span>{this.state.showingDocumentsDisplayText}</span>
|
||||
</span>
|
||||
{this.state.allResultsMetadata[this.state.allResultsMetadata.length - 1]
|
||||
.hasMoreResults && (
|
||||
<>
|
||||
<span className="queryResultDivider">|</span>
|
||||
<span className="queryResultNextEnable">
|
||||
<a onClick={this.onFetchNextPageClick.bind(this)}>
|
||||
<span>Load more</span>
|
||||
<img className="queryResultnextImg" src={QueryEditorNext} alt="Fetch next page" />
|
||||
</a>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{this.state.queryResults &&
|
||||
this.state.queryResults.length > 0 &&
|
||||
this.state.allResultsMetadata.length > 0 &&
|
||||
!this.state.error && (
|
||||
<div
|
||||
style={{
|
||||
paddingBottom: "100px",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<EditorReact
|
||||
language={"json"}
|
||||
content={this.state.queryResults}
|
||||
isReadOnly={true}
|
||||
ariaLabel={"Query results"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="Query Stats"
|
||||
headerButtonProps={{
|
||||
"data-order": 2,
|
||||
"data-title": "Query Stats",
|
||||
}}
|
||||
style={{ height: "100%", overflowY: "scroll" }}
|
||||
>
|
||||
{this.state.allResultsMetadata.length > 0 && !this.state.error && (
|
||||
<div className="queryMetricsSummaryContainer">
|
||||
<div className="queryMetricsSummary">
|
||||
<h5>Query Statistics</h5>
|
||||
<DetailsList
|
||||
items={this.state.items}
|
||||
columns={this.state.columns}
|
||||
selectionMode={SelectionMode.none}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
compact={true}
|
||||
/>
|
||||
</div>
|
||||
{this.state.isQueryMetricsEnabled && (
|
||||
<div className="downloadMetricsLinkContainer">
|
||||
<a
|
||||
id="downloadMetricsLink"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => this.onDownloadQueryMetricsCsvClick()}
|
||||
onKeyPress={(event: React.KeyboardEvent<HTMLAnchorElement>) =>
|
||||
this.onDownloadQueryMetricsCsvKeyPress(event)
|
||||
}
|
||||
>
|
||||
<img
|
||||
className="downloadCsvImg"
|
||||
src={DownloadQueryMetrics}
|
||||
alt="download query metrics csv"
|
||||
/>
|
||||
<span>Per-partition query metrics (CSV)</span>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
)}
|
||||
{/* <!-- Query Errors Content - Start--> */}
|
||||
{!!this.state.error && (
|
||||
<div className="tab-pane active">
|
||||
<div className="errorContent">
|
||||
<span className="errorMessage">{this.state.error}</span>
|
||||
<span className="errorDetailsLink">
|
||||
<a
|
||||
onClick={() => this.onErrorDetailsClick()}
|
||||
onKeyPress={(event: React.KeyboardEvent<HTMLAnchorElement>) =>
|
||||
this.onErrorDetailsKeyPress(event)
|
||||
}
|
||||
id="error-display"
|
||||
tabIndex={0}
|
||||
aria-label="Error details link"
|
||||
>
|
||||
More details
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* <!-- Query Errors Content - End--> */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
<QueryResultSection
|
||||
isMongoDB={this.props.isPreferredApiMongoDB}
|
||||
queryEditorContent={this.state.sqlQueryEditorContent}
|
||||
error={this.state.error}
|
||||
queryResults={this.state.queryResults}
|
||||
isExecuting={this.state.isExecuting}
|
||||
executeQueryDocumentsPage={(firstItemIndex: number) => this._executeQueryDocumentsPage(firstItemIndex)}
|
||||
/>
|
||||
</SplitterLayout>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -77,7 +77,13 @@
|
||||
<div class="addClause" data-bind=" ">
|
||||
<div class="addClause-heading">
|
||||
<span class="clause-table addClause-title">
|
||||
<img class="addclauseProperty-Img" style="margin-bottom: 5px" src="/Add-property.svg" />
|
||||
<img
|
||||
class="addclauseProperty-Img"
|
||||
style="margin-bottom: 5px"
|
||||
src="/Add-property.svg"
|
||||
alt="Add new Clause"
|
||||
role="presentation"
|
||||
/>
|
||||
<span style="margin-left: 5px" data-bind="text: addNewClauseLine"></span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -119,7 +125,12 @@
|
||||
aria-label="Show top results"
|
||||
/>
|
||||
<div role="alert" aria-atomic="true" class="inline-div" data-bind="visible: isExceedingLimit">
|
||||
<img class="advanced-options-icon" src="/QueryBuilder/StatusWarning_16x.png" />
|
||||
<img
|
||||
class="advanced-options-icon"
|
||||
src="/QueryBuilder/StatusWarning_16x.png"
|
||||
alt="Warning"
|
||||
role="presentation"
|
||||
/>
|
||||
<span data-bind="text: topValueLimitMessage"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { sendMessage } from "Common/MessageHandler";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab";
|
||||
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
|
||||
import { ConnectTab } from "Explorer/Tabs/ConnectTab";
|
||||
import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
|
||||
@@ -68,6 +69,13 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
|
||||
const focusTab = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
|
||||
const tabId = tab ? tab.tabId : "connect";
|
||||
|
||||
const getReactTabTitle = (): ko.Observable<string> => {
|
||||
if (tabKind === ReactTabKind.QueryCopilot) {
|
||||
return ko.observable("Query");
|
||||
}
|
||||
return ko.observable(ReactTabKind[tabKind]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (active && focusTab.current) {
|
||||
focusTab.current.focus();
|
||||
@@ -105,11 +113,11 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
|
||||
<div className="tab_Content">
|
||||
<span className="statusIconContainer" style={{ width: tabKind === ReactTabKind.Home ? 0 : 18 }}>
|
||||
{useObservable(tab?.isExecutionError || ko.observable(false)) && <ErrorIcon tab={tab} active={active} />}
|
||||
{useObservable(tab?.isExecuting || ko.observable(false)) && (
|
||||
{isTabExecuting(tab, tabKind) && (
|
||||
<img className="loadingIcon" title="Loading" src={loadingIcon} alt="Loading" />
|
||||
)}
|
||||
</span>
|
||||
<span className="tabNavText">{useObservable(tab?.tabTitle || ko.observable(ReactTabKind[tabKind]))}</span>
|
||||
<span className="tabNavText">{useObservable(tab?.tabTitle || getReactTabTitle())}</span>
|
||||
{tabKind !== ReactTabKind.Home && (
|
||||
<span className="tabIconSection">
|
||||
<CloseButton tab={tab} active={active} hovering={hovering} tabKind={tabKind} />
|
||||
@@ -203,6 +211,15 @@ const onKeyPressReactTab = (e: KeyboardEvent, tabKind: ReactTabKind): void => {
|
||||
}
|
||||
};
|
||||
|
||||
const isTabExecuting = (tab?: Tab, tabKind?: ReactTabKind): boolean => {
|
||||
if (useObservable(tab?.isExecuting || ko.observable(false))) {
|
||||
return true;
|
||||
} else if (tabKind !== undefined && tabKind !== ReactTabKind.Home && useTabs.getState()?.isTabExecuting) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => {
|
||||
switch (activeReactTab) {
|
||||
case ReactTabKind.Connect:
|
||||
@@ -211,6 +228,8 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
||||
return <SplashScreen explorer={explorer} />;
|
||||
case ReactTabKind.Quickstart:
|
||||
return <QuickstartTab explorer={explorer} />;
|
||||
case ReactTabKind.QueryCopilot:
|
||||
return <QueryCopilotTab initialInput={useTabs.getState().queryCopilotTabInitialInput} explorer={explorer} />;
|
||||
default:
|
||||
throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { SampleDataTree } from "Explorer/Tree/SampleDataTree";
|
||||
import { getItemName } from "Utils/APITypeUtils";
|
||||
import * as React from "react";
|
||||
import shallow from "zustand/shallow";
|
||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||
import DeleteIcon from "../../../images/delete.svg";
|
||||
import GalleryIcon from "../../../images/GalleryIcon.svg";
|
||||
import FileIcon from "../../../images/notebook/file-cosmos.svg";
|
||||
import DeleteIcon from "../../../images/delete.svg";
|
||||
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
|
||||
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
|
||||
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
|
||||
import FileIcon from "../../../images/notebook/file-cosmos.svg";
|
||||
import PublishIcon from "../../../images/notebook/publish_content.svg";
|
||||
import RefreshIcon from "../../../images/refresh-cosmos.svg";
|
||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||
@@ -16,14 +17,14 @@ import { Areas, ConnectionStatusType, Notebook } from "../../Common/Constants";
|
||||
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||
import { useTabs } from "../../hooks/useTabs";
|
||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { isServerlessAccount } from "../../Utils/CapabilityUtils";
|
||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||
import { useTabs } from "../../hooks/useTabs";
|
||||
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
|
||||
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
|
||||
import { useDialog } from "../Controls/Dialog";
|
||||
@@ -764,23 +765,59 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
};
|
||||
|
||||
const dataRootNode = buildDataTree();
|
||||
const isSampleDataEnabled = userContext.sampleDataConnectionInfo && userContext.apiType === "SQL";
|
||||
const sampleDataResourceTokenCollection = useDatabases((state) => state.sampleDataResourceTokenCollection);
|
||||
|
||||
if (isNotebookEnabled) {
|
||||
return (
|
||||
<>
|
||||
<AccordionComponent>
|
||||
<AccordionItemComponent title={"DATA"} isExpanded={!gitHubNotebooksContentRoot}>
|
||||
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||
</AccordionItemComponent>
|
||||
<AccordionItemComponent title={"NOTEBOOKS"}>
|
||||
<TreeComponent className="notebookResourceTree" rootNode={buildNotebooksTree()} />
|
||||
</AccordionItemComponent>
|
||||
</AccordionComponent>
|
||||
return (
|
||||
<>
|
||||
{!isNotebookEnabled && !isSampleDataEnabled && (
|
||||
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||
)}
|
||||
{isNotebookEnabled && !isSampleDataEnabled && (
|
||||
<>
|
||||
<AccordionComponent>
|
||||
<AccordionItemComponent title={"DATA"} isExpanded={!gitHubNotebooksContentRoot}>
|
||||
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||
</AccordionItemComponent>
|
||||
<AccordionItemComponent title={"NOTEBOOKS"}>
|
||||
<TreeComponent className="notebookResourceTree" rootNode={buildNotebooksTree()} />
|
||||
</AccordionItemComponent>
|
||||
</AccordionComponent>
|
||||
|
||||
{buildGalleryCallout()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
{buildGalleryCallout()}
|
||||
</>
|
||||
)}
|
||||
{!isNotebookEnabled && isSampleDataEnabled && (
|
||||
<>
|
||||
<AccordionComponent>
|
||||
<AccordionItemComponent title={"MY DATA"} isExpanded={!gitHubNotebooksContentRoot}>
|
||||
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||
</AccordionItemComponent>
|
||||
<AccordionItemComponent title={"SAMPLE DATA"}>
|
||||
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
|
||||
</AccordionItemComponent>
|
||||
</AccordionComponent>
|
||||
|
||||
return <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />;
|
||||
{buildGalleryCallout()}
|
||||
</>
|
||||
)}
|
||||
{isNotebookEnabled && isSampleDataEnabled && (
|
||||
<>
|
||||
<AccordionComponent>
|
||||
<AccordionItemComponent title={"MY DATA"} isExpanded={!gitHubNotebooksContentRoot}>
|
||||
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||
</AccordionItemComponent>
|
||||
<AccordionItemComponent title={"SAMPLE DATA"}>
|
||||
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
|
||||
</AccordionItemComponent>
|
||||
<AccordionItemComponent title={"NOTEBOOKS"}>
|
||||
<TreeComponent className="notebookResourceTree" rootNode={buildNotebooksTree()} />
|
||||
</AccordionItemComponent>
|
||||
</AccordionComponent>
|
||||
|
||||
{buildGalleryCallout()}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
41
src/Explorer/Tree/SampleDataTree.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent";
|
||||
|
||||
export const SampleDataTree = ({
|
||||
sampleDataResourceTokenCollection,
|
||||
}: {
|
||||
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",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
setRoot(updatedSampleTree);
|
||||
}
|
||||
}, [sampleDataResourceTokenCollection]);
|
||||
|
||||
return (
|
||||
<TreeComponent className="sampleDataResourceTree" rootNode={root || { label: "Sample data not initialized." }} />
|
||||
);
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import { useSelectedNode } from "./useSelectedNode";
|
||||
interface DatabasesState {
|
||||
databases: ViewModels.Database[];
|
||||
resourceTokenCollection: ViewModels.CollectionBase;
|
||||
sampleDataResourceTokenCollection: ViewModels.CollectionBase;
|
||||
updateDatabase: (database: ViewModels.Database) => void;
|
||||
addDatabases: (databases: ViewModels.Database[]) => void;
|
||||
deleteDatabase: (database: ViewModels.Database) => void;
|
||||
@@ -28,6 +29,7 @@ interface DatabasesState {
|
||||
export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
|
||||
databases: [],
|
||||
resourceTokenCollection: undefined,
|
||||
sampleDataResourceTokenCollection: undefined,
|
||||
updateDatabase: (updatedDatabase: ViewModels.Database) =>
|
||||
set((state) => {
|
||||
const updatedDatabases = state.databases.map((database: ViewModels.Database) => {
|
||||
|
||||
23
src/Main.tsx
@@ -1,13 +1,13 @@
|
||||
// CSS Dependencies
|
||||
import { initializeIcons } from "@fluentui/react";
|
||||
import "bootstrap/dist/css/bootstrap.css";
|
||||
import { QuickstartCarousel } from "Explorer/Quickstart/QuickstartCarousel";
|
||||
import { MongoQuickstartTutorial } from "Explorer/Quickstart/Tutorials/MongoQuickstartTutorial";
|
||||
import { SQLQuickstartTutorial } from "Explorer/Quickstart/Tutorials/SQLQuickstartTutorial";
|
||||
import { userContext } from "UserContext";
|
||||
import "bootstrap/dist/css/bootstrap.css";
|
||||
import { useCarousel } from "hooks/useCarousel";
|
||||
import React, { useState } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { userContext } from "UserContext";
|
||||
import "../externals/jquery-ui.min.css";
|
||||
import "../externals/jquery-ui.min.js";
|
||||
import "../externals/jquery-ui.structure.min.css";
|
||||
@@ -16,19 +16,21 @@ import "../externals/jquery.dataTables.min.css";
|
||||
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 "../images/CosmosDB_rgb_ui_lighttheme.ico";
|
||||
import "../images/favicon.ico";
|
||||
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
||||
import "../images/favicon.ico";
|
||||
import "../less/TableStyles/CustomizeColumns.less";
|
||||
import "../less/TableStyles/EntityEditor.less";
|
||||
import "../less/TableStyles/fulldatatables.less";
|
||||
import "../less/TableStyles/queryBuilder.less";
|
||||
import "../less/documentDB.less";
|
||||
import "../less/forms.less";
|
||||
import "../less/infobox.less";
|
||||
import "../less/menus.less";
|
||||
import "../less/messagebox.less";
|
||||
import "../less/resourceTree.less";
|
||||
import "../less/TableStyles/CustomizeColumns.less";
|
||||
import "../less/TableStyles/EntityEditor.less";
|
||||
import "../less/TableStyles/fulldatatables.less";
|
||||
import "../less/TableStyles/queryBuilder.less";
|
||||
import "../less/tree.less";
|
||||
import { CollapsedResourceTree } from "./Common/CollapsedResourceTree";
|
||||
import { ResourceTreeContainer } from "./Common/ResourceTreeContainer";
|
||||
@@ -50,16 +52,17 @@ import "./Explorer/Panes/PanelComponent.less";
|
||||
import { SidePanel } from "./Explorer/Panes/PanelContainerComponent";
|
||||
import "./Explorer/SplashScreen/SplashScreen.less";
|
||||
import { Tabs } from "./Explorer/Tabs/Tabs";
|
||||
import { useConfig } from "./hooks/useConfig";
|
||||
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
||||
import "./Libs/jquery";
|
||||
import "./Shared/appInsights";
|
||||
import { useConfig } from "./hooks/useConfig";
|
||||
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
||||
|
||||
initializeIcons();
|
||||
|
||||
const App: React.FunctionComponent = () => {
|
||||
const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState<boolean>(true);
|
||||
const isCarouselOpen = useCarousel((state) => state.shouldOpen);
|
||||
const isCopilotCarouselOpen = useCarousel((state) => state.showCopilotCarousel);
|
||||
|
||||
const config = useConfig();
|
||||
const explorer = useKnockoutExplorer(config?.platform);
|
||||
@@ -122,6 +125,8 @@ const App: React.FunctionComponent = () => {
|
||||
{<QuickstartCarousel isOpen={isCarouselOpen} />}
|
||||
{<SQLQuickstartTutorial />}
|
||||
{<MongoQuickstartTutorial />}
|
||||
{<QueryCopilotCarousel isOpen={isCopilotCarouselOpen} explorer={explorer} />}
|
||||
{<QueryCopilotFeedbackModal />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
export interface ParsedResourceTokenConnectionString {
|
||||
accountEndpoint: string;
|
||||
collectionId: string;
|
||||
databaseId: string;
|
||||
accountEndpoint?: string;
|
||||
collectionId?: string;
|
||||
databaseId?: string;
|
||||
partitionKey?: string;
|
||||
resourceToken: string;
|
||||
resourceToken?: string;
|
||||
}
|
||||
|
||||
export function parseResourceTokenConnectionString(connectionString: string): ParsedResourceTokenConnectionString {
|
||||
let accountEndpoint: string;
|
||||
let collectionId: string;
|
||||
let databaseId: string;
|
||||
let partitionKey: string;
|
||||
let resourceToken: string;
|
||||
let accountEndpoint: string | undefined;
|
||||
let collectionId: string | undefined;
|
||||
let databaseId: string | undefined;
|
||||
let partitionKey: string | undefined;
|
||||
let resourceToken: string | undefined;
|
||||
|
||||
const connectionStringParts = connectionString.split(";");
|
||||
connectionStringParts.forEach((part: string) => {
|
||||
if (part.startsWith("type=resource")) {
|
||||
@@ -39,5 +40,5 @@ export function parseResourceTokenConnectionString(connectionString: string): Pa
|
||||
}
|
||||
|
||||
export function isResourceTokenConnectionString(connectionString: string): boolean {
|
||||
return connectionString && connectionString.includes("type=resource");
|
||||
return !!connectionString && connectionString.includes("type=resource");
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export type Features = {
|
||||
readonly enableLegacyMongoShellV2: boolean;
|
||||
readonly enableLegacyMongoShellV2Debug: boolean;
|
||||
readonly loadLegacyMongoShellFromBE: boolean;
|
||||
readonly enableCopilot: boolean;
|
||||
|
||||
// can be set via both flight and feature flag
|
||||
autoscaleDefault: boolean;
|
||||
@@ -102,6 +103,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
||||
enableLegacyMongoShellV2: "true" === get("enablelegacymongoshellv2"),
|
||||
enableLegacyMongoShellV2Debug: "true" === get("enablelegacymongoshellv2debug"),
|
||||
loadLegacyMongoShellFromBE: "true" === get("loadlegacymongoshellfrombe"),
|
||||
enableCopilot: "true" === get("enablecopilot"),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCarousel } from "hooks/useCarousel";
|
||||
import { usePostgres } from "hooks/usePostgres";
|
||||
import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { AuthType } from "./AuthType";
|
||||
@@ -69,6 +70,7 @@ interface UserContext {
|
||||
readonly postgresConnectionStrParams?: PostgresConnectionStrParams;
|
||||
readonly isReplica?: boolean;
|
||||
collectionCreationDefaults: CollectionCreationDefaults;
|
||||
sampleDataConnectionInfo?: ParsedResourceTokenConnectionString;
|
||||
}
|
||||
|
||||
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres";
|
||||
@@ -157,4 +159,4 @@ function apiType(account: DatabaseAccount | undefined): ApiType {
|
||||
return "SQL";
|
||||
}
|
||||
|
||||
export { userContext, updateUserContext };
|
||||
export { updateUserContext, userContext };
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2023-04-15/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2021-04-15";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-04-15";
|
||||
|
||||
/* Lists the Cassandra keyspaces under an existing Azure Cosmos DB database account. */
|
||||
export async function listCassandraKeyspaces(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2023-04-15/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2021-04-15";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-04-15";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given database account and collection. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2023-04-15/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2021-04-15";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-04-15";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given collection, split by partition. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2023-04-15/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2021-04-15";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-04-15";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given collection and region, split by partition. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2023-04-15/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2021-04-15";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-04-15";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given database account, collection and region. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2023-04-15/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2021-04-15";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-04-15";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given database account and database. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2023-04-15/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2021-04-15";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-04-15";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given database account and region. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2023-04-15/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2021-04-15";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-04-15";
|
||||
|
||||
/* Retrieves the properties of an existing Azure Cosmos DB database account. */
|
||||
export async function get(
|
||||
@@ -29,13 +29,7 @@ export async function update(
|
||||
body: Types.DatabaseAccountUpdateParameters
|
||||
): Promise<Types.DatabaseAccountGetResults> {
|
||||
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`;
|
||||
return armRequest({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path,
|
||||
method: "PATCH",
|
||||
apiVersion,
|
||||
body,
|
||||
});
|
||||
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PATCH", apiVersion, body });
|
||||
}
|
||||
|
||||
/* Creates or updates an Azure Cosmos DB database account. The "Update" method is preferred when performing updates on an account. */
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2023-04-15/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2021-04-15";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-04-15";
|
||||
|
||||
/* Lists the Gremlin databases under an existing Azure Cosmos DB database account. */
|
||||
export async function listGremlinDatabases(
|
||||
|
||||
27
src/Utils/arm/generatedClients/cosmos/locations.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
AUTOGENERATED FILE
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2023-04-15/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-04-15";
|
||||
|
||||
/* List Cosmos DB locations and their properties */
|
||||
export async function list(subscriptionId: string): Promise<Types.LocationListResult | Types.CloudError> {
|
||||
const path = `/subscriptions/${subscriptionId}/providers/Microsoft.DocumentDB/locations`;
|
||||
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion });
|
||||
}
|
||||
|
||||
/* Get the properties of an existing Cosmos DB location */
|
||||
export async function get(
|
||||
subscriptionId: string,
|
||||
location: string
|
||||
): Promise<Types.LocationGetResult | Types.CloudError> {
|
||||
const path = `/subscriptions/${subscriptionId}/providers/Microsoft.DocumentDB/locations/${location}`;
|
||||
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion });
|
||||
}
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2023-04-15/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2021-04-15";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-04-15";
|
||||
|
||||
/* Lists the MongoDB databases under an existing Azure Cosmos DB database account. */
|
||||
export async function listMongoDBDatabases(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2023-04-15/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2021-04-15";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-04-15";
|
||||
|
||||
/* Lists all of the available Cosmos DB Resource Provider operations. */
|
||||
export async function list(): Promise<Types.OperationListResult> {
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2023-04-15/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2021-04-15";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-04-15";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given partition key range id. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2023-04-15/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2021-04-15";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-04-15";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given partition key range id and region. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2023-04-15/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2021-04-15";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-04-15";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given database account. This url is only for PBS and Replication Latency data */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2023-04-15/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2021-04-15";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-04-15";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given account, source and target region. This url is only for PBS and Replication Latency data */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2023-04-15/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2021-04-15";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-04-15";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given account target region. This url is only for PBS and Replication Latency data */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2023-04-15/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2021-04-15";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-04-15";
|
||||
|
||||
/* Lists the SQL databases under an existing Azure Cosmos DB database account. */
|
||||
export async function listSqlDatabases(
|
||||
@@ -197,6 +197,42 @@ export async function migrateSqlContainerToManualThroughput(
|
||||
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion });
|
||||
}
|
||||
|
||||
/* Lists the ClientEncryptionKeys under an existing Azure Cosmos DB SQL database. */
|
||||
export async function listClientEncryptionKeys(
|
||||
subscriptionId: string,
|
||||
resourceGroupName: string,
|
||||
accountName: string,
|
||||
databaseName: string
|
||||
): Promise<Types.ClientEncryptionKeysListResult> {
|
||||
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases/${databaseName}/clientEncryptionKeys`;
|
||||
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion });
|
||||
}
|
||||
|
||||
/* Gets the ClientEncryptionKey under an existing Azure Cosmos DB SQL database. */
|
||||
export async function getClientEncryptionKey(
|
||||
subscriptionId: string,
|
||||
resourceGroupName: string,
|
||||
accountName: string,
|
||||
databaseName: string,
|
||||
clientEncryptionKeyName: string
|
||||
): Promise<Types.ClientEncryptionKeyGetResults> {
|
||||
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases/${databaseName}/clientEncryptionKeys/${clientEncryptionKeyName}`;
|
||||
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion });
|
||||
}
|
||||
|
||||
/* Create or update a ClientEncryptionKey. This API is meant to be invoked via tools such as the Azure Powershell (instead of directly). */
|
||||
export async function createUpdateClientEncryptionKey(
|
||||
subscriptionId: string,
|
||||
resourceGroupName: string,
|
||||
accountName: string,
|
||||
databaseName: string,
|
||||
clientEncryptionKeyName: string,
|
||||
body: Types.ClientEncryptionKeyCreateUpdateParameters
|
||||
): Promise<Types.ClientEncryptionKeyGetResults | void> {
|
||||
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases/${databaseName}/clientEncryptionKeys/${clientEncryptionKeyName}`;
|
||||
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
|
||||
}
|
||||
|
||||
/* Lists the SQL storedProcedure under an existing Azure Cosmos DB database account. */
|
||||
export async function listSqlStoredProcedures(
|
||||
subscriptionId: string,
|
||||
@@ -204,7 +240,7 @@ export async function listSqlStoredProcedures(
|
||||
accountName: string,
|
||||
databaseName: string,
|
||||
containerName: string
|
||||
): Promise<Types.SqlStoredProcedureListResult> {
|
||||
): Promise<Types.SqlStoredProcedureListResult | Types.CloudError> {
|
||||
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases/${databaseName}/containers/${containerName}/storedProcedures`;
|
||||
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion });
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2023-04-15/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2021-04-15";
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
const apiVersion = "2023-04-15";
|
||||
|
||||
/* Lists the Tables under an existing Azure Cosmos DB database account. */
|
||||
export async function listTables(
|
||||
|
||||
@@ -3,9 +3,15 @@
|
||||
Run "npm run generateARMClients" to regenerate
|
||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
||||
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2023-04-15/cosmos-db.json
|
||||
*/
|
||||
|
||||
/* The List operation response, that contains the client encryption keys and their properties. */
|
||||
export interface ClientEncryptionKeysListResult {
|
||||
/* List of client encryption keys and their properties. */
|
||||
readonly value?: ClientEncryptionKeyGetResults[];
|
||||
}
|
||||
|
||||
/* The List operation response, that contains the database accounts and their properties. */
|
||||
export interface DatabaseAccountsListResult {
|
||||
/* List of database account and their properties. */
|
||||
@@ -161,6 +167,30 @@ export interface ARMProxyResource {
|
||||
readonly type?: string;
|
||||
}
|
||||
|
||||
/* Parameters to create and update ClientEncryptionKey. */
|
||||
export interface ClientEncryptionKeyCreateUpdateParameters {
|
||||
/* Properties to create and update ClientEncryptionKey. */
|
||||
properties: ClientEncryptionKeyCreateUpdateProperties;
|
||||
}
|
||||
|
||||
/* Properties to create and update ClientEncryptionKey. */
|
||||
export interface ClientEncryptionKeyCreateUpdateProperties {
|
||||
/* The standard JSON format of a ClientEncryptionKey */
|
||||
resource: ClientEncryptionKeyResource;
|
||||
}
|
||||
|
||||
/* Client Encryption Key. */
|
||||
export type ClientEncryptionKeyGetResults = ARMProxyResource & {
|
||||
/* The properties of a ClientEncryptionKey */
|
||||
properties?: ClientEncryptionKeyGetProperties;
|
||||
};
|
||||
|
||||
/* The properties of a ClientEncryptionKey resource */
|
||||
export interface ClientEncryptionKeyGetProperties {
|
||||
/* undocumented */
|
||||
resource?: ClientEncryptionKeyResource & ExtendedResourceProperties;
|
||||
}
|
||||
|
||||
/* An Azure Cosmos DB database account. */
|
||||
export type DatabaseAccountGetResults = ARMResourceProperties & {
|
||||
/* Indicates the type of database account. This can only be set at database account creation. */
|
||||
@@ -171,6 +201,9 @@ export type DatabaseAccountGetResults = ARMResourceProperties & {
|
||||
|
||||
/* undocumented */
|
||||
properties?: DatabaseAccountGetProperties;
|
||||
|
||||
/* The system meta data relating to this resource. */
|
||||
readonly systemData?: unknown;
|
||||
};
|
||||
|
||||
/* The system generated resource properties associated with SQL databases, SQL containers, Gremlin databases and Gremlin graphs. */
|
||||
@@ -446,6 +479,17 @@ export interface DatabaseAccountGetProperties {
|
||||
|
||||
/* Flag to indicate whether to enable storage analytics. */
|
||||
enableAnalyticalStorage?: boolean;
|
||||
/* Analytical storage specific properties. */
|
||||
analyticalStorageConfiguration?: AnalyticalStorageConfiguration;
|
||||
|
||||
/* A unique identifier assigned to the database account */
|
||||
readonly instanceId?: string;
|
||||
/* Enum to indicate the mode of account creation. */
|
||||
createMode?: CreateMode;
|
||||
|
||||
/* Parameters to indicate the information about the restore. */
|
||||
restoreParameters?: RestoreParameters;
|
||||
|
||||
/* The object representing the policy for taking backups on an account. */
|
||||
backupPolicy?: BackupPolicy;
|
||||
|
||||
@@ -457,6 +501,19 @@ export interface DatabaseAccountGetProperties {
|
||||
|
||||
/* An array that contains the Resource Ids for Network Acl Bypass for the Cosmos DB account. */
|
||||
networkAclBypassResourceIds?: unknown[];
|
||||
|
||||
/* Opt-out of local authentication and ensure only MSI and AAD can be used exclusively for authentication. */
|
||||
disableLocalAuth?: boolean;
|
||||
/* The object that represents all properties related to capacity enforcement on an account. */
|
||||
capacity?: Capacity;
|
||||
|
||||
/* The object that represents the metadata for the Account Keys of the Cosmos DB account. */
|
||||
keysMetadata?: DatabaseAccountKeysMetadata;
|
||||
|
||||
/* Flag to indicate enabling/disabling of Partition Merge feature on the account */
|
||||
enablePartitionMerge?: boolean;
|
||||
/* Indicates the minimum allowed Tls version. The default is Tls 1.0, except for Cassandra and Mongo API's, which only work with Tls 1.2. */
|
||||
minimalTlsVersion?: MinimalTlsVersion;
|
||||
}
|
||||
|
||||
/* Properties to create and update Azure Cosmos DB database accounts. */
|
||||
@@ -506,6 +563,12 @@ export interface DatabaseAccountCreateUpdateProperties {
|
||||
|
||||
/* Flag to indicate whether to enable storage analytics. */
|
||||
enableAnalyticalStorage?: boolean;
|
||||
/* Analytical storage specific properties. */
|
||||
analyticalStorageConfiguration?: AnalyticalStorageConfiguration;
|
||||
|
||||
/* Enum to indicate the mode of account creation. */
|
||||
createMode?: CreateMode;
|
||||
|
||||
/* The object representing the policy for taking backups on an account. */
|
||||
backupPolicy?: BackupPolicy;
|
||||
|
||||
@@ -517,6 +580,22 @@ export interface DatabaseAccountCreateUpdateProperties {
|
||||
|
||||
/* An array that contains the Resource Ids for Network Acl Bypass for the Cosmos DB account. */
|
||||
networkAclBypassResourceIds?: unknown[];
|
||||
|
||||
/* Opt-out of local authentication and ensure only MSI and AAD can be used exclusively for authentication. */
|
||||
disableLocalAuth?: boolean;
|
||||
/* Parameters to indicate the information about the restore. */
|
||||
restoreParameters?: RestoreParameters;
|
||||
|
||||
/* The object that represents all properties related to capacity enforcement on an account. */
|
||||
capacity?: Capacity;
|
||||
|
||||
/* This property is ignored during the update/create operation, as the metadata is read-only. The object represents the metadata for the Account Keys of the Cosmos DB account. */
|
||||
keysMetadata?: DatabaseAccountKeysMetadata;
|
||||
|
||||
/* Flag to indicate enabling/disabling of Partition Merge feature on the account */
|
||||
enablePartitionMerge?: boolean;
|
||||
/* Indicates the minimum allowed Tls version. The default is Tls 1.0, except for Cassandra and Mongo API's, which only work with Tls 1.2. */
|
||||
minimalTlsVersion?: MinimalTlsVersion;
|
||||
}
|
||||
|
||||
/* Parameters to create and update Cosmos DB database accounts. */
|
||||
@@ -575,6 +654,9 @@ export interface DatabaseAccountUpdateProperties {
|
||||
|
||||
/* Flag to indicate whether to enable storage analytics. */
|
||||
enableAnalyticalStorage?: boolean;
|
||||
/* Analytical storage specific properties. */
|
||||
analyticalStorageConfiguration?: AnalyticalStorageConfiguration;
|
||||
|
||||
/* The object representing the policy for taking backups on an account. */
|
||||
backupPolicy?: BackupPolicy;
|
||||
|
||||
@@ -586,6 +668,19 @@ export interface DatabaseAccountUpdateProperties {
|
||||
|
||||
/* An array that contains the Resource Ids for Network Acl Bypass for the Cosmos DB account. */
|
||||
networkAclBypassResourceIds?: unknown[];
|
||||
|
||||
/* Opt-out of local authentication and ensure only MSI and AAD can be used exclusively for authentication. */
|
||||
disableLocalAuth?: boolean;
|
||||
/* The object that represents all properties related to capacity enforcement on an account. */
|
||||
capacity?: Capacity;
|
||||
|
||||
/* This property is ignored during the update operation, as the metadata is read-only. The object represents the metadata for the Account Keys of the Cosmos DB account. */
|
||||
keysMetadata?: DatabaseAccountKeysMetadata;
|
||||
|
||||
/* Flag to indicate enabling/disabling of Partition Merge feature on the account */
|
||||
enablePartitionMerge?: boolean;
|
||||
/* Indicates the minimum allowed Tls version. The default is Tls 1.0, except for Cassandra and Mongo API's, which only work with Tls 1.2. */
|
||||
minimalTlsVersion?: MinimalTlsVersion;
|
||||
}
|
||||
|
||||
/* Parameters for patching Azure Cosmos DB database account properties. */
|
||||
@@ -624,6 +719,20 @@ export interface DatabaseAccountConnectionString {
|
||||
readonly connectionString?: string;
|
||||
/* Description of the connection string */
|
||||
readonly description?: string;
|
||||
/* Kind of the connection string key */
|
||||
readonly keyKind?: "Primary" | "Secondary" | "PrimaryReadonly" | "SecondaryReadonly";
|
||||
|
||||
/* Type of the connection string */
|
||||
readonly type?:
|
||||
| "Sql"
|
||||
| "Table"
|
||||
| "MongoDB"
|
||||
| "Cassandra"
|
||||
| "CassandraConnectorMetadata"
|
||||
| "Gremlin"
|
||||
| "SqlDedicatedGateway"
|
||||
| "GremlinV2"
|
||||
| "Undefined";
|
||||
}
|
||||
|
||||
/* The connection strings for the given database account. */
|
||||
@@ -844,6 +953,10 @@ export interface ThroughputSettingsResource {
|
||||
readonly minimumThroughput?: string;
|
||||
/* The throughput replace is pending */
|
||||
readonly offerReplacePending?: string;
|
||||
/* The offer throughput value to instantly scale up without triggering splits */
|
||||
readonly instantMaximumThroughput?: string;
|
||||
/* The maximum throughput value or the maximum maxThroughput value (for autoscale) that can be specified */
|
||||
readonly softAllowedMaximumThroughput?: string;
|
||||
}
|
||||
|
||||
/* Cosmos DB provisioned throughput settings object */
|
||||
@@ -879,6 +992,30 @@ export interface OptionsResource {
|
||||
autoscaleSettings?: AutoscaleSettings;
|
||||
}
|
||||
|
||||
/* Cosmos DB client encryption key resource object. */
|
||||
export interface ClientEncryptionKeyResource {
|
||||
/* Name of the ClientEncryptionKey */
|
||||
id?: string;
|
||||
/* Encryption algorithm that will be used along with this client encryption key to encrypt/decrypt data. */
|
||||
encryptionAlgorithm?: string;
|
||||
/* Wrapped (encrypted) form of the key represented as a byte array. */
|
||||
wrappedDataEncryptionKey?: string;
|
||||
/* Metadata for the wrapping provider that can be used to unwrap the wrapped client encryption key. */
|
||||
keyWrapMetadata?: KeyWrapMetadata;
|
||||
}
|
||||
|
||||
/* Represents key wrap metadata that a key wrapping provider can use to wrap/unwrap a client encryption key. */
|
||||
export interface KeyWrapMetadata {
|
||||
/* The name of associated KeyEncryptionKey (aka CustomerManagedKey). */
|
||||
name?: string;
|
||||
/* ProviderName of KeyStoreProvider. */
|
||||
type?: string;
|
||||
/* Reference / link to the KeyEncryptionKey. */
|
||||
value?: string;
|
||||
/* Algorithm used in wrapping and unwrapping of the data encryption key. */
|
||||
algorithm?: string;
|
||||
}
|
||||
|
||||
/* Cosmos DB SQL database resource object */
|
||||
export interface SqlDatabaseResource {
|
||||
/* Name of the Cosmos DB SQL database */
|
||||
@@ -903,6 +1040,9 @@ export interface SqlContainerResource {
|
||||
/* The conflict resolution policy for the container. */
|
||||
conflictResolutionPolicy?: ConflictResolutionPolicy;
|
||||
|
||||
/* The client encryption policy for the container. */
|
||||
clientEncryptionPolicy?: ClientEncryptionPolicy;
|
||||
|
||||
/* Analytical TTL. */
|
||||
analyticalStorageTtl?: number;
|
||||
}
|
||||
@@ -1014,6 +1154,27 @@ export interface ConflictResolutionPolicy {
|
||||
conflictResolutionProcedure?: string;
|
||||
}
|
||||
|
||||
/* Cosmos DB client encryption policy. */
|
||||
export interface ClientEncryptionPolicy {
|
||||
/* Paths of the item that need encryption along with path-specific settings. */
|
||||
includedPaths: ClientEncryptionIncludedPath[];
|
||||
|
||||
/* Version of the client encryption policy definition. Supported versions are 1 and 2. Version 2 supports id and partition key path encryption. */
|
||||
policyFormatVersion: number;
|
||||
}
|
||||
|
||||
/* . */
|
||||
export interface ClientEncryptionIncludedPath {
|
||||
/* Path that needs to be encrypted. */
|
||||
path: string;
|
||||
/* The identifier of the Client Encryption Key to be used to encrypt the path. */
|
||||
clientEncryptionKeyId: string;
|
||||
/* The type of encryption to be performed. Eg - Deterministic, Randomized. */
|
||||
encryptionType: string;
|
||||
/* The encryption algorithm which will be used. Eg - AEAD_AES_256_CBC_HMAC_SHA256. */
|
||||
encryptionAlgorithm: string;
|
||||
}
|
||||
|
||||
/* Cosmos DB SQL storedProcedure resource object */
|
||||
export interface SqlStoredProcedureResource {
|
||||
/* Name of the Cosmos DB SQL storedProcedure */
|
||||
@@ -1035,7 +1196,7 @@ export interface SqlTriggerResource {
|
||||
/* Name of the Cosmos DB SQL trigger */
|
||||
id: string;
|
||||
/* Body of the Trigger */
|
||||
body: string;
|
||||
body?: string;
|
||||
/* Type of the Trigger */
|
||||
triggerType?: "Pre" | "Post";
|
||||
|
||||
@@ -1174,6 +1335,9 @@ export interface GremlinGraphResource {
|
||||
|
||||
/* The conflict resolution policy for the graph. */
|
||||
conflictResolutionPolicy?: ConflictResolutionPolicy;
|
||||
|
||||
/* Analytical TTL. */
|
||||
analyticalStorageTtl?: number;
|
||||
}
|
||||
|
||||
/* CreateUpdateOptions are a list of key-value pairs that describe the resource. Supported keys are "If-Match", "If-None-Match", "Session-Token" and "Throughput" */
|
||||
@@ -1196,6 +1360,12 @@ export interface Capability {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
/* The object that represents all properties related to capacity enforcement on an account. */
|
||||
export interface Capacity {
|
||||
/* The total throughput limit imposed on the account. A totalThroughputLimit of 2000 imposes a strict limit of max throughput that can be provisioned on that account to be 2000. A totalThroughputLimit of -1 indicates no limits on provisioning of throughput. */
|
||||
totalThroughputLimit?: number;
|
||||
}
|
||||
|
||||
/* Tags are a list of key-value pairs that describe the resource. These tags can be used in viewing and grouping this resource (across resource groups). A maximum of 15 tags can be provided for a resource. Each tag must have a key no greater than 128 characters and value no greater than 256 characters. For example, the default experience for a template type is set with "defaultExperience": "Cassandra". Current "defaultExperience" values also include "Table", "Graph", "DocumentDB", and "MongoDB". */
|
||||
export type Tags = { [key: string]: string };
|
||||
|
||||
@@ -1235,6 +1405,27 @@ export interface VirtualNetworkRule {
|
||||
/* Indicates what services are allowed to bypass firewall checks. */
|
||||
export type NetworkAclBypass = "None" | "AzureServices";
|
||||
|
||||
/* The metadata related to an access key for a given database account. */
|
||||
export interface AccountKeyMetadata {
|
||||
/* Generation time in UTC of the key in ISO-8601 format. If the value is missing from the object, it means that the last key regeneration was triggered before 2022-06-18. */
|
||||
readonly generationTime?: string;
|
||||
}
|
||||
|
||||
/* The metadata related to each access key for the given Cosmos DB database account. */
|
||||
export interface DatabaseAccountKeysMetadata {
|
||||
/* The metadata related to the Primary Read-Write Key for the given Cosmos DB database account. */
|
||||
readonly primaryMasterKey?: AccountKeyMetadata;
|
||||
|
||||
/* The metadata related to the Secondary Read-Write Key for the given Cosmos DB database account. */
|
||||
readonly secondaryMasterKey?: AccountKeyMetadata;
|
||||
|
||||
/* The metadata related to the Primary Read-Only Key for the given Cosmos DB database account. */
|
||||
readonly primaryReadonlyMasterKey?: AccountKeyMetadata;
|
||||
|
||||
/* The metadata related to the Secondary Read-Only Key for the given Cosmos DB database account. */
|
||||
readonly secondaryReadonlyMasterKey?: AccountKeyMetadata;
|
||||
}
|
||||
|
||||
/* REST API operation */
|
||||
export interface Operation {
|
||||
/* Operation name: {provider}/{resource}/{operation} */
|
||||
@@ -1430,23 +1621,97 @@ export type UnitType = "Count" | "Bytes" | "Seconds" | "Percent" | "CountPerSeco
|
||||
export type ConnectorOffer = "Small";
|
||||
|
||||
/* Whether requests from Public Network are allowed */
|
||||
export type PublicNetworkAccess = "Enabled" | "Disabled";
|
||||
export type PublicNetworkAccess = "Enabled" | "Disabled" | "SecuredByPerimeter";
|
||||
|
||||
/* undocumented */
|
||||
export interface ApiProperties {
|
||||
/* Describes the ServerVersion of an a MongoDB account. */
|
||||
serverVersion?: "3.2" | "3.6" | "4.0";
|
||||
serverVersion?: "3.2" | "3.6" | "4.0" | "4.2";
|
||||
}
|
||||
|
||||
/* Analytical storage specific properties. */
|
||||
export interface AnalyticalStorageConfiguration {
|
||||
/* undocumented */
|
||||
schemaType?: AnalyticalStorageSchemaType;
|
||||
}
|
||||
|
||||
/* Describes the types of schema for analytical storage. */
|
||||
export type AnalyticalStorageSchemaType = "WellDefined" | "FullFidelity";
|
||||
|
||||
/* Enum to indicate the mode of account creation. */
|
||||
export type CreateMode = "Default" | "Restore";
|
||||
|
||||
/* Parameters to indicate the information about the restore. */
|
||||
export interface RestoreParameters {
|
||||
/* Describes the mode of the restore. */
|
||||
restoreMode?: "PointInTime";
|
||||
|
||||
/* The id of the restorable database account from which the restore has to be initiated. For example: /subscriptions/{subscriptionId}/providers/Microsoft.DocumentDB/locations/{location}/restorableDatabaseAccounts/{restorableDatabaseAccountName} */
|
||||
restoreSource?: string;
|
||||
/* Time to which the account has to be restored (ISO-8601 format). */
|
||||
restoreTimestampInUtc?: string;
|
||||
/* List of specific databases available for restore. */
|
||||
databasesToRestore?: DatabaseRestoreResource[];
|
||||
|
||||
/* List of specific gremlin databases available for restore. */
|
||||
gremlinDatabasesToRestore?: GremlinDatabaseRestoreResource[];
|
||||
|
||||
/* List of specific tables available for restore. */
|
||||
tablesToRestore?: TableName[];
|
||||
}
|
||||
|
||||
/* Specific Databases to restore. */
|
||||
export interface DatabaseRestoreResource {
|
||||
/* The name of the database available for restore. */
|
||||
databaseName?: string;
|
||||
/* The names of the collections available for restore. */
|
||||
collectionNames?: CollectionName[];
|
||||
}
|
||||
|
||||
/* Specific Gremlin Databases to restore. */
|
||||
export interface GremlinDatabaseRestoreResource {
|
||||
/* The name of the gremlin database available for restore. */
|
||||
databaseName?: string;
|
||||
/* The names of the graphs available for restore. */
|
||||
graphNames?: GraphName[];
|
||||
}
|
||||
|
||||
/* The name of the collection. */
|
||||
export type CollectionName = string;
|
||||
|
||||
/* The name of the graph. */
|
||||
export type GraphName = string;
|
||||
|
||||
/* The name of the table. */
|
||||
export type TableName = string;
|
||||
|
||||
/* The object representing the policy for taking backups on an account. */
|
||||
export interface BackupPolicy {
|
||||
/* undocumented */
|
||||
/* Describes the mode of backups. */
|
||||
type: BackupPolicyType;
|
||||
|
||||
/* The object representing the state of the migration between the backup policies. */
|
||||
migrationState?: BackupPolicyMigrationState;
|
||||
}
|
||||
|
||||
/* Describes the mode of backups. */
|
||||
export type BackupPolicyType = "Periodic" | "Continuous";
|
||||
|
||||
/* The object representing the state of the migration between the backup policies. */
|
||||
export interface BackupPolicyMigrationState {
|
||||
/* Describes the status of migration between backup policy types. */
|
||||
status?: BackupPolicyMigrationStatus;
|
||||
|
||||
/* Describes the target backup policy type of the backup policy migration. */
|
||||
targetType?: BackupPolicyType;
|
||||
|
||||
/* Time at which the backup policy migration started (ISO-8601 format). */
|
||||
startTime?: string;
|
||||
}
|
||||
|
||||
/* Describes the status of migration between backup policy types. */
|
||||
export type BackupPolicyMigrationStatus = "Invalid" | "InProgress" | "Completed" | "Failed";
|
||||
|
||||
/* The object representing periodic mode backup policy. */
|
||||
export type PeriodicModeBackupPolicy = BackupPolicy & {
|
||||
/* Configuration values for periodic mode backup */
|
||||
@@ -1454,7 +1719,10 @@ export type PeriodicModeBackupPolicy = BackupPolicy & {
|
||||
};
|
||||
|
||||
/* The object representing continuous mode backup policy. */
|
||||
export type ContinuousModeBackupPolicy = BackupPolicy;
|
||||
export type ContinuousModeBackupPolicy = BackupPolicy & {
|
||||
/* Configuration values for continuous mode backup */
|
||||
continuousModeProperties?: ContinuousModeProperties;
|
||||
};
|
||||
|
||||
/* Configuration values for periodic mode backup */
|
||||
export interface PeriodicModeProperties {
|
||||
@@ -1462,4 +1730,50 @@ export interface PeriodicModeProperties {
|
||||
backupIntervalInMinutes?: number;
|
||||
/* An integer representing the time (in hours) that each backup is retained */
|
||||
backupRetentionIntervalInHours?: number;
|
||||
/* Enum to indicate type of backup residency */
|
||||
backupStorageRedundancy?: BackupStorageRedundancy;
|
||||
}
|
||||
|
||||
/* Configuration values for periodic mode backup */
|
||||
export interface ContinuousModeProperties {
|
||||
/* Enum to indicate type of Continuous backup mode */
|
||||
tier?: ContinuousTier;
|
||||
}
|
||||
|
||||
/* The List operation response, that contains Cosmos DB locations and their properties. */
|
||||
export interface LocationListResult {
|
||||
/* List of Cosmos DB locations and their properties. */
|
||||
readonly value?: LocationGetResult[];
|
||||
}
|
||||
|
||||
/* Cosmos DB location get result */
|
||||
export type LocationGetResult = ARMProxyResource & {
|
||||
/* Cosmos DB location metadata */
|
||||
properties?: LocationProperties;
|
||||
};
|
||||
|
||||
/* Cosmos DB location metadata */
|
||||
export interface LocationProperties {
|
||||
/* Flag indicating whether the location supports availability zones or not. */
|
||||
readonly supportsAvailabilityZone?: boolean;
|
||||
/* Flag indicating whether the location is residency sensitive. */
|
||||
readonly isResidencyRestricted?: boolean;
|
||||
/* The properties of available backup storage redundancies. */
|
||||
readonly backupStorageRedundancies?: BackupStorageRedundancy[];
|
||||
|
||||
/* Flag indicating whether the subscription have access in region for Non-Availability Zones. */
|
||||
readonly isSubscriptionRegionAccessAllowedForRegular?: boolean;
|
||||
/* Flag indicating whether the subscription have access in region for Availability Zones(Az). */
|
||||
readonly isSubscriptionRegionAccessAllowedForAz?: boolean;
|
||||
/* Enum to indicate current buildout status of the region. */
|
||||
readonly status?: "Uninitialized" | "Initializing" | "InternallyReady" | "Online" | "Deleting";
|
||||
}
|
||||
|
||||
/* Enum to indicate type of backup storage redundancy. */
|
||||
export type BackupStorageRedundancy = "Geo" | "Local" | "Zone";
|
||||
|
||||
/* Enum to indicate type of Continuous backup tier. */
|
||||
export type ContinuousTier = "Continuous7Days" | "Continuous30Days";
|
||||
|
||||
/* Indicates the minimum allowed Tls version. The default is Tls 1.0, except for Cassandra and Mongo API's, which only work with Tls 1.2. */
|
||||
export type MinimalTlsVersion = "Tls" | "Tls11" | "Tls12";
|
||||
|
||||