Merge branch 'master' of https://github.com/Azure/cosmos-explorer into user/balalakshmin/chatbot
6
.vscode/launch.json
vendored
@ -12,7 +12,8 @@
|
||||
"--inspect-brk",
|
||||
"${workspaceRoot}/node_modules/jest/bin/jest.js",
|
||||
"--runInBand",
|
||||
"--coverage", "false"
|
||||
"--coverage",
|
||||
"false"
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
@ -26,7 +27,8 @@
|
||||
"--inspect-brk",
|
||||
"${workspaceRoot}/node_modules/jest/bin/jest.js",
|
||||
"${fileBasenameNoExtension}",
|
||||
"--coverage", "false",
|
||||
"--coverage",
|
||||
"false",
|
||||
// "--watch",
|
||||
// // --no-cache only used to make --watch work. Otherwise jest ignores the breakpoints.
|
||||
// // https://github.com/facebook/jest/issues/6683
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com",
|
||||
"isTerminalEnabled" : true
|
||||
"isTerminalEnabled" : true,
|
||||
"isPhoenixEnabled" : true
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
||||
"isTerminalEnabled" : false
|
||||
"isTerminalEnabled" : false,
|
||||
"isPhoenixEnabled" : false
|
||||
}
|
||||
|
23
images/Connect_color.svg
Normal file
@ -0,0 +1,23 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_480_185430)">
|
||||
<path d="M35.4911 6.39638L33.4106 4.31591L37.559 0.167553C37.6611 0.0654497 37.7996 0.00808785 37.944 0.00808785C38.0884 0.00808801 38.2269 0.0654482 38.329 0.167551L39.641 1.47963C39.7431 1.58173 39.8005 1.72021 39.8005 1.86461C39.8005 2.009 39.7431 2.14749 39.641 2.24959L35.4927 6.39795L35.4911 6.39638Z" fill="#32BEDD"/>
|
||||
<path d="M4.45313 33.6455L6.53988 35.7323L2.44494 39.8272C2.34367 39.9285 2.20632 39.9853 2.06311 39.9853C1.91989 39.9853 1.78254 39.9285 1.68127 39.8272L0.364478 38.5104C0.312974 38.4606 0.271905 38.401 0.243665 38.3351C0.215426 38.2692 0.20058 38.1984 0.199995 38.1267C0.19941 38.0551 0.213098 37.984 0.240258 37.9177C0.267419 37.8514 0.307509 37.7911 0.358193 37.7404L4.45313 33.6455Z" fill="#0078D4"/>
|
||||
<path d="M5.09381 25.0287C3.78099 26.3415 3.04346 28.1221 3.04346 29.9787C3.04346 31.8353 3.78099 33.6159 5.09381 34.9287C6.40664 36.2415 8.1872 36.9791 10.0438 36.9791C11.9004 36.9791 13.681 36.2415 14.9938 34.9287L18.2027 31.7176L8.30492 21.8198L5.09381 25.0287Z" fill="url(#paint0_linear_480_185430)"/>
|
||||
<path d="M17.4209 18.1581L17.6157 18.353C17.8133 18.5505 17.9242 18.8185 17.9242 19.0978C17.9242 19.3772 17.8133 19.6451 17.6157 19.8426L13.6009 23.8574L11.918 22.1745L15.9344 18.1581C16.1319 17.9606 16.3998 17.8496 16.6792 17.8496C16.9586 17.8496 17.2265 17.9606 17.424 18.1581L17.4209 18.1581Z" fill="#C3F1FF"/>
|
||||
<path d="M21.5835 22.32L21.7783 22.5149C21.9759 22.7124 22.0868 22.9803 22.0868 23.2597C22.0868 23.539 21.9759 23.807 21.7783 24.0045L17.7588 28.024L16.0759 26.3411L20.097 22.32C20.2945 22.1225 20.5624 22.0115 20.8418 22.0115C21.1212 22.0115 21.3891 22.1225 21.5866 22.32L21.5835 22.32Z" fill="#C3F1FF"/>
|
||||
<path d="M20.9363 30.0618L9.87241 18.9979C9.66673 18.7922 9.33327 18.7922 9.12759 18.9979L7.67566 20.4498C7.46999 20.6555 7.46999 20.989 7.67566 21.1946L18.7395 32.2585C18.9452 32.4642 19.2787 32.4642 19.4843 32.2585L20.9363 30.8066C21.1419 30.6009 21.1419 30.2674 20.9363 30.0618Z" fill="#5EA0EF"/>
|
||||
<path d="M34.9067 14.9711C36.2196 13.6583 36.9571 11.8777 36.9571 10.0211C36.9571 8.1645 36.2196 6.38393 34.9067 5.07111C33.5939 3.75829 31.8134 3.02075 29.9567 3.02075C28.1001 3.02075 26.3196 3.75829 25.0067 5.07111L21.7979 8.28222L31.6956 18.18L34.9067 14.9711Z" fill="#ECF4FD"/>
|
||||
<path d="M22.5828 21.8375L22.388 21.6426C22.1904 21.4451 22.0795 21.1772 22.0795 20.8978C22.0795 20.6184 22.1904 20.3505 22.388 20.153L26.4075 16.1335L28.092 17.8179L24.0677 21.8422C23.8698 22.0376 23.6025 22.1469 23.3243 22.146C23.0461 22.1451 22.7795 22.0342 22.5828 21.8375Z" fill="#ECF4FD"/>
|
||||
<path d="M18.4178 17.6802L18.2229 17.4854C18.0254 17.2878 17.9144 17.0199 17.9144 16.7406C17.9144 16.4612 18.0254 16.1933 18.2229 15.9957L22.2409 11.9778L23.9254 13.6623L19.909 17.6787C19.7115 17.8762 19.4435 17.9872 19.1642 17.9872C18.8848 17.9872 18.6169 17.8762 18.4194 17.6787L18.4178 17.6802Z" fill="#ECF4FD"/>
|
||||
<path d="M19.0642 9.93799L30.1281 21.0019C30.3338 21.2075 30.6672 21.2075 30.8729 21.0019L32.3248 19.5499C32.5305 19.3443 32.5305 19.0108 32.3248 18.8051L21.261 7.74125C21.0553 7.53557 20.7218 7.53557 20.5161 7.74125L19.0642 9.19317C18.8585 9.39885 18.8585 9.73232 19.0642 9.93799Z" fill="#ECF4FD"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_480_185430" x1="10.6227" y1="21.8179" x2="10.6227" y2="36.9783" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5EA0EF"/>
|
||||
<stop offset="0.997" stop-color="#0078D4"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_480_185430">
|
||||
<rect width="40" height="40" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 3.5 KiB |
8
images/Containers.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24 34.635C24 34.8143 23.9647 34.9918 23.8961 35.1574C23.8275 35.323 23.727 35.4734 23.6002 35.6002C23.4734 35.727 23.323 35.8275 23.1574 35.8961C22.9918 35.9647 22.8143 36 22.635 36H1.365C1.18575 36 1.00825 35.9647 0.842637 35.8961C0.677028 35.8275 0.526551 35.727 0.399799 35.6002C0.273047 35.4734 0.172502 35.323 0.103904 35.1574C0.0353068 34.9918 0 34.8143 0 34.635L0 13.365C0 13.003 0.143812 12.6558 0.399799 12.3998C0.655786 12.1438 1.00298 12 1.365 12H22.635C22.997 12 23.3442 12.1438 23.6002 12.3998C23.8562 12.6558 24 13.003 24 13.365V34.635Z" fill="#005BA1"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M30 28.635C30 28.997 29.8562 29.3442 29.6002 29.6002C29.3442 29.8562 28.997 30 28.635 30H7.365C7.00298 30 6.65579 29.8562 6.3998 29.6002C6.14381 29.3442 6 28.997 6 28.635V7.365C6 7.00298 6.14381 6.65579 6.3998 6.3998C6.65579 6.14381 7.00298 6 7.365 6H28.635C28.997 6 29.3442 6.14381 29.6002 6.3998C29.8562 6.65579 30 7.00298 30 7.365V28.635Z" fill="#5EA0EF"/>
|
||||
<path d="M22.635 12H6V28.635C6 28.997 6.14381 29.3442 6.3998 29.6002C6.65579 29.8562 7.00298 30 7.365 30H24V13.365C24 13.003 23.8562 12.6558 23.6002 12.3998C23.3442 12.1438 22.997 12 22.635 12Z" fill="#0078D4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36 22.635C36 22.8143 35.9647 22.9918 35.8961 23.1574C35.8275 23.323 35.727 23.4734 35.6002 23.6002C35.4734 23.727 35.323 23.8275 35.1574 23.8961C34.9918 23.9647 34.8143 24 34.635 24H13.365C13.003 24 12.6558 23.8562 12.3998 23.6002C12.1438 23.3442 12 22.997 12 22.635V1.365C12 1.00298 12.1438 0.655786 12.3998 0.399799C12.6558 0.143812 13.003 0 13.365 0L34.635 0C34.8143 0 34.9918 0.0353068 35.1574 0.103904C35.323 0.172502 35.4734 0.273047 35.6002 0.399799C35.727 0.526551 35.8275 0.677028 35.8961 0.842637C35.9647 1.00825 36 1.18575 36 1.365V22.635Z" fill="#E6E7E8"/>
|
||||
<path d="M22.635 12H12V22.635C12 22.997 12.1438 23.3442 12.3998 23.6002C12.6558 23.8562 13.003 24 13.365 24H24V13.365C24 13.003 23.8562 12.6558 23.6002 12.3998C23.3442 12.1438 22.997 12 22.635 12Z" fill="#BCBEC0"/>
|
||||
<path d="M28.635 6H12V12H22.635C22.997 12 23.3442 12.1438 23.6002 12.3998C23.8562 12.6558 24 13.003 24 13.365V24H30V7.365C30 7.00298 29.8562 6.65579 29.6002 6.3998C29.3442 6.14381 28.997 6 28.635 6Z" fill="#D1D3D4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
16
images/Cost.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.3328 13.6532C13.3454 13.8483 13.3149 14.0438 13.2435 14.2259C13.172 14.4079 13.0614 14.572 12.9195 14.7065C12.5895 14.9979 12.1721 15.1714 11.7328 15.1998V15.9998H11.1462V15.1732C10.6342 15.1699 10.1307 15.0418 9.67951 14.7998V13.7198C9.88302 13.865 10.108 13.9775 10.3462 14.0532C10.6041 14.1483 10.8728 14.211 11.1462 14.2398V12.8532C10.6948 12.7095 10.2805 12.4683 9.93285 12.1465C9.68588 11.8542 9.55715 11.4801 9.57195 11.0977C9.58675 10.7153 9.74402 10.3522 10.0128 10.0798C10.3224 9.7875 10.7219 9.60886 11.1462 9.57318V8.83984H11.7195V9.55984C12.1428 9.55959 12.5612 9.65055 12.9462 9.82651V10.8932C12.5717 10.6689 12.1533 10.528 11.7195 10.4798V11.9998C12.1784 12.1422 12.5982 12.3886 12.9462 12.7198C13.1925 12.9682 13.3314 13.3034 13.3328 13.6532ZM11.1462 11.7465V10.5065C11.0094 10.52 10.8832 10.5858 10.7938 10.6902C10.7043 10.7945 10.6586 10.9293 10.6662 11.0665C10.6662 11.3465 10.8395 11.5732 11.1995 11.7465H11.1462ZM12.2795 13.7065C12.2795 13.4532 12.0928 13.2532 11.7195 13.0798V14.2798C12.0928 14.2132 12.2795 14.0265 12.2795 13.7065Z" fill="#258277"/>
|
||||
<path d="M22.0531 12.6532H17.2531C17.2522 13.4842 17.076 14.3056 16.7359 15.0638C16.3959 15.822 15.8997 16.4999 15.2798 17.0532L18.4665 20.6399C19.597 19.6367 20.5014 18.4046 21.1197 17.0254C21.738 15.6462 22.0562 14.1514 22.0531 12.6399" fill="#FFCA00"/>
|
||||
<path d="M11.3595 18.573C9.79299 18.573 8.29061 17.9507 7.18289 16.843C6.07518 15.7353 5.45287 14.2329 5.45287 12.6663C5.45287 11.0998 6.07518 9.5974 7.18289 8.48968C8.29061 7.38197 9.79299 6.75966 11.3595 6.75966V1.98633C8.53056 1.98633 5.81745 3.11013 3.81707 5.11052C1.81668 7.11091 0.692871 9.82402 0.692871 12.653C0.692871 15.482 1.81668 18.1951 3.81707 20.1955C5.81745 22.1959 8.53056 23.3197 11.3595 23.3197C13.9799 23.3355 16.5143 22.3863 18.4795 20.653L15.2795 17.0663C14.2049 18.0372 12.8078 18.5742 11.3595 18.573Z" fill="url(#paint0_radial_669_8617)"/>
|
||||
<path d="M10.8662 0.6399V6.18657C11.7766 6.17211 12.6805 6.34098 13.5243 6.68313C14.368 7.02527 15.1343 7.53369 15.7775 8.17812C16.4207 8.82254 16.9276 9.5898 17.2681 10.4342C17.6086 11.2786 17.7758 12.1829 17.7595 13.0932H23.3329C23.3329 11.4567 23.0103 9.83624 22.3836 8.32447C21.757 6.81269 20.8385 5.43925 19.6807 4.28268C18.5229 3.12611 17.1484 2.20908 15.636 1.58402C14.1235 0.95897 12.5027 0.638148 10.8662 0.6399Z" fill="#CCCCCC"/>
|
||||
<path d="M17.7862 13.0934C17.804 11.2839 17.104 9.54104 15.8395 8.24653C14.575 6.95203 12.849 6.21137 11.0395 6.18677H10.8662L11.3062 7.01343C12.7784 7.09978 14.1672 7.72528 15.2075 8.77052C16.2479 9.81576 16.8668 11.2075 16.9462 12.6801L17.7862 13.0934Z" fill="#999999"/>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_669_8617" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(9.57287 12.6663) scale(9.84)">
|
||||
<stop stop-color="#76BC2D"/>
|
||||
<stop offset="0.41" stop-color="#74B92C"/>
|
||||
<stop offset="0.66" stop-color="#6FB12A"/>
|
||||
<stop offset="0.88" stop-color="#66A227"/>
|
||||
<stop offset="1" stop-color="#5E9624"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 3.0 KiB |
4
images/Green_check.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 24C18.6274 24 24 18.6274 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24Z" fill="#57A300"/>
|
||||
<path d="M5.32942 12.4366C5.21842 12.3181 5.16142 12.1621 5.16742 12.0001C5.17342 11.8366 5.24242 11.6866 5.36242 11.5741L6.61492 10.4161C6.72892 10.3126 6.87442 10.2556 7.02592 10.2556C7.19392 10.2556 7.35592 10.3261 7.46992 10.4491L10.6739 13.8871L16.3844 6.57461C16.4999 6.42611 16.6739 6.33911 16.8629 6.33911C16.9979 6.33911 17.1254 6.38261 17.2334 6.46511L18.5924 7.51361C18.8519 7.70561 18.9074 8.07911 18.7124 8.34461L11.4104 17.6941C11.1269 18.0571 10.5854 18.0811 10.2704 17.7436L5.32942 12.4366Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 767 B |
3
images/Link_blue.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 6H11V11H0V0H5V1H1V10H10V6ZM11 0V5H10V1.71094L5.35156 6.35156L4.64844 5.64844L9.28906 1H6V0H11Z" fill="#0078D4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 229 B |
28
images/Notebooks.svg
Normal file
@ -0,0 +1,28 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_669_8707)">
|
||||
<path d="M20.7873 0H4.403C4.30483 -1.37536e-07 4.20762 0.0193565 4.11694 0.0569623C4.02626 0.0945681 3.94389 0.149685 3.87453 0.21916C3.80517 0.288634 3.75019 0.371103 3.71273 0.461847C3.67528 0.552592 3.65609 0.64983 3.65625 0.748V21.1432C3.65625 21.3414 3.73489 21.5314 3.87491 21.6716C4.01492 21.8118 4.20486 21.8907 4.403 21.891H20.7873C20.9854 21.8908 21.1754 21.8119 21.3154 21.6717C21.4554 21.5315 21.5341 21.3414 21.534 21.1432V0.748C21.534 0.549834 21.4554 0.359767 21.3154 0.219526C21.1754 0.0792842 20.9854 0.000331159 20.7873 0V0Z" fill="url(#paint0_linear_669_8707)"/>
|
||||
<path d="M11.433 2.1499H2.11601C1.92247 2.1499 1.73685 2.22679 1.6 2.36364C1.46314 2.5005 1.38626 2.68611 1.38626 2.87965V22.9767C1.38543 23.0725 1.4035 23.1675 1.43941 23.2564C1.47533 23.3452 1.5284 23.4261 1.59558 23.4944C1.66277 23.5627 1.74276 23.6172 1.83098 23.6546C1.91921 23.692 2.01394 23.7116 2.10976 23.7124H18.2618C18.4553 23.7124 18.6409 23.6355 18.7778 23.4987C18.9146 23.3618 18.9915 23.1762 18.9915 22.9827V9.68015C18.9916 9.58428 18.9727 9.48933 18.9361 9.40074C18.8995 9.31214 18.8457 9.23163 18.7779 9.16382C18.7102 9.096 18.6297 9.0422 18.5411 9.0055C18.4526 8.96879 18.3576 8.9499 18.2618 8.9499H12.9033C12.7095 8.94799 12.5243 8.86969 12.3879 8.73202C12.2516 8.59435 12.1751 8.40843 12.175 8.21465V2.8879C12.1752 2.69332 12.0985 2.50655 11.9616 2.3683C11.8246 2.23006 11.6386 2.15155 11.444 2.1499H11.433Z" fill="white"/>
|
||||
<path d="M11.45 2.10889H1.99675C1.79861 2.10915 1.60867 2.18805 1.46866 2.32825C1.32864 2.46845 1.25 2.65849 1.25 2.85664V23.2519C1.25 23.4501 1.32864 23.6401 1.46864 23.7804C1.60865 23.9206 1.79858 23.9996 1.99675 23.9999H18.381C18.5792 23.9996 18.7691 23.9206 18.9091 23.7804C19.0491 23.6401 19.1278 23.4501 19.1278 23.2519V9.75364C19.1278 9.55559 19.0491 9.36565 18.909 9.22561C18.769 9.08556 18.5791 9.00689 18.381 9.00689H12.9423C12.7442 9.00689 12.5543 8.92821 12.4142 8.78817C12.2742 8.64813 12.1955 8.45819 12.1955 8.26014V8.26014V2.85664C12.1949 2.65889 12.1162 2.46938 11.9766 2.32934C11.837 2.1893 11.6477 2.11007 11.45 2.10889Z" fill="url(#paint1_linear_669_8707)"/>
|
||||
<path d="M18.8418 9.15547L11.9375 2.27197V7.88072C11.9355 8.21667 12.067 8.53967 12.303 8.77874C12.539 9.01782 12.8603 9.15341 13.1963 9.15572L18.8418 9.15547Z" fill="#83B9F9"/>
|
||||
<path d="M12.496 10.9395H3.84072C3.64797 10.9395 3.49072 11.0362 3.49072 11.1555V11.6882C3.49072 11.8075 3.64697 11.904 3.84072 11.904H12.496C12.6887 11.904 12.846 11.8075 12.846 11.6882V11.1555C12.845 11.0362 12.6887 10.9395 12.496 10.9395Z" fill="#83B9F9"/>
|
||||
<path d="M12.496 13.8333H3.84072C3.64797 13.8333 3.49072 13.9298 3.49072 14.049V14.5818C3.49072 14.701 3.64697 14.7978 3.84072 14.7978H12.496C12.6887 14.7978 12.846 14.701 12.846 14.5818V14.05C12.845 13.9298 12.6887 13.8333 12.496 13.8333Z" fill="#83B9F9"/>
|
||||
<path d="M12.496 16.7271H3.84072C3.64797 16.7271 3.49072 16.8236 3.49072 16.9428V17.4751C3.49072 17.5943 3.64697 17.6911 3.84072 17.6911H12.496C12.6887 17.6911 12.846 17.5943 12.846 17.4751V16.9428C12.845 16.8236 12.6887 16.7271 12.496 16.7271Z" fill="#83B9F9"/>
|
||||
<path d="M8.98195 19.6208H3.93445C3.68995 19.6208 3.4917 19.7173 3.4917 19.8366V20.3693C3.4917 20.4886 3.68995 20.5854 3.93445 20.5854H8.98195C9.22645 20.5854 9.4247 20.4886 9.4247 20.3693V19.8366C9.42495 19.7173 9.22645 19.6208 8.98195 19.6208Z" fill="#83B9F9"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_669_8707" x1="12.595" y1="1.4615" x2="12.595" y2="23.415" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DCDCDC"/>
|
||||
<stop offset="1" stop-color="#AAAAAA"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_669_8707" x1="10.1888" y1="3.57039" x2="10.1888" y2="25.5236" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0078D7"/>
|
||||
<stop offset="0.327" stop-color="#0076D4"/>
|
||||
<stop offset="0.576" stop-color="#0071CA"/>
|
||||
<stop offset="0.799" stop-color="#0068BA"/>
|
||||
<stop offset="1" stop-color="#005BA4"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_669_8707">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 4.0 KiB |
11
images/Quickstart_Lightning.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg width="30" height="34" viewBox="0 0 30 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.6457 18.9089H0.67782C0.520018 18.9215 0.361625 18.8893 0.235445 18.819C0.109264 18.7487 0.0249634 18.6456 0 18.5312C0.000766863 18.4748 0.0212497 18.4196 0.0595033 18.3706L14.4179 0.229509C14.4859 0.156313 14.5784 0.0970502 14.6866 0.0573734C14.7949 0.0176966 14.9152 -0.00107025 15.0362 0.00286318H29.1877C29.3456 -0.0102086 29.5043 0.0218054 29.6306 0.0922084C29.757 0.162611 29.8411 0.26595 29.8655 0.380607C29.8655 0.463721 29.823 0.54385 29.7465 0.605364L12.8941 14.7991H29.3222C29.48 14.7865 29.6384 14.8187 29.7646 14.889C29.8907 14.9593 29.975 15.0624 30 15.1768C29.998 15.2265 29.9814 15.2752 29.9516 15.3198C29.9217 15.3645 29.8791 15.4039 29.8267 15.4356L2.51725 33.5994C2.25854 33.6957 0.447568 34.6608 1.33236 33.1952L12.6457 18.9089Z" fill="url(#paint0_radial_480_182017)"/>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_480_182017" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(15.0039 17.0718) scale(23.6442 13.4446)">
|
||||
<stop offset="0.196" stop-color="#FFD70F"/>
|
||||
<stop offset="0.438" stop-color="#FFCB12"/>
|
||||
<stop offset="0.873" stop-color="#FEAC19"/>
|
||||
<stop offset="1" stop-color="#FEA11B"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
35053
package-lock.json
generated
@ -96,8 +96,10 @@ export class Flights {
|
||||
public static readonly AutoscaleTest = "autoscaletest";
|
||||
public static readonly PartitionKeyTest = "partitionkeytest";
|
||||
public static readonly PKPartitionKeyTest = "pkpartitionkeytest";
|
||||
public static readonly Phoenix = "phoenix";
|
||||
public static readonly PhoenixNotebooks = "phoenixnotebooks";
|
||||
public static readonly PhoenixFeatures = "phoenixfeatures";
|
||||
public static readonly NotebooksDownBanner = "notebooksdownbanner";
|
||||
public static readonly PublicGallery = "publicgallery";
|
||||
}
|
||||
|
||||
export class AfecFeatures {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import * as Cosmos from "@azure/cosmos";
|
||||
import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos";
|
||||
import { CosmosHeaders } from "@azure/cosmos/dist-esm";
|
||||
import { configContext, Platform } from "../ConfigContext";
|
||||
import { userContext } from "../UserContext";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
@ -77,10 +78,21 @@ export async function getTokenFromAuthService(verb: string, resourceType: string
|
||||
}
|
||||
}
|
||||
|
||||
// The Capability is a bitmap, which cosmosdb backend decodes as per the below enum
|
||||
enum SDKSupportedCapabilities {
|
||||
None = 0,
|
||||
PartitionMerge = 1 << 0,
|
||||
}
|
||||
|
||||
let _client: Cosmos.CosmosClient;
|
||||
|
||||
export function client(): Cosmos.CosmosClient {
|
||||
if (_client) return _client;
|
||||
|
||||
let _defaultHeaders: CosmosHeaders = {};
|
||||
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
|
||||
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
||||
|
||||
const options: Cosmos.CosmosClientOptions = {
|
||||
endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
|
||||
key: userContext.masterKey,
|
||||
@ -89,6 +101,7 @@ export function client(): Cosmos.CosmosClient {
|
||||
enableEndpointDiscovery: false,
|
||||
},
|
||||
userAgentSuffix: "Azure Portal",
|
||||
defaultHeaders: _defaultHeaders,
|
||||
};
|
||||
|
||||
if (configContext.PROXY_PATH !== undefined) {
|
||||
|
@ -25,12 +25,12 @@ const fetchMock = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const partitionKeyProperty = "pk";
|
||||
const partitionKeyProperties = ["pk"];
|
||||
|
||||
const collection = {
|
||||
id: () => "testCollection",
|
||||
rid: "testCollectionrid",
|
||||
partitionKeyProperty,
|
||||
partitionKeyProperties,
|
||||
partitionKey: {
|
||||
paths: ["/pk"],
|
||||
kind: "Hash",
|
||||
@ -41,7 +41,7 @@ const collection = {
|
||||
const documentId = ({
|
||||
partitionKeyHeader: () => "[]",
|
||||
self: "db/testDB/db/testCollection/docs/testId",
|
||||
partitionKeyProperty,
|
||||
partitionKeyProperties,
|
||||
partitionKey: {
|
||||
paths: ["/pk"],
|
||||
kind: "Hash",
|
||||
@ -236,13 +236,12 @@ describe("MongoProxyClient", () => {
|
||||
});
|
||||
|
||||
it("returns a production endpoint", () => {
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
|
||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
|
||||
});
|
||||
|
||||
it("returns a development endpoint", () => {
|
||||
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getEndpoint("https://localhost:1234");
|
||||
expect(endpoint).toEqual("https://localhost:1234/api/mongo/explorer");
|
||||
});
|
||||
|
||||
@ -250,7 +249,7 @@ describe("MongoProxyClient", () => {
|
||||
updateUserContext({
|
||||
authType: AuthType.EncryptedToken,
|
||||
});
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
|
||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||
import queryString from "querystring";
|
||||
import { allowedMongoProxyEndpoints, validateEndpoint } from "Utils/EndpointValidation";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
@ -75,7 +76,7 @@ export function queryDocuments(
|
||||
dba: databaseAccount.name,
|
||||
pk:
|
||||
collection && collection.partitionKey && !collection.partitionKey.systemKey
|
||||
? collection.partitionKeyProperty
|
||||
? collection.partitionKeyProperties?.[0]
|
||||
: "",
|
||||
};
|
||||
|
||||
@ -138,7 +139,7 @@ export function readDocument(
|
||||
dba: databaseAccount.name,
|
||||
pk:
|
||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||
? documentId.partitionKeyProperty
|
||||
? documentId.partitionKeyProperties?.[0]
|
||||
: "",
|
||||
};
|
||||
|
||||
@ -224,7 +225,7 @@ export function updateDocument(
|
||||
dba: databaseAccount.name,
|
||||
pk:
|
||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||
? documentId.partitionKeyProperty
|
||||
? documentId.partitionKeyProperties?.[0]
|
||||
: "",
|
||||
};
|
||||
const endpoint = getFeatureEndpointOrDefault("updateDocument");
|
||||
@ -265,7 +266,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
|
||||
dba: databaseAccount.name,
|
||||
pk:
|
||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||
? documentId.partitionKeyProperty
|
||||
? documentId.partitionKeyProperties?.[0]
|
||||
: "",
|
||||
};
|
||||
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
||||
@ -336,14 +337,17 @@ export function createMongoCollectionWithProxy(
|
||||
}
|
||||
|
||||
export function getFeatureEndpointOrDefault(feature: string): string {
|
||||
return hasFlag(userContext.features.mongoProxyAPIs, feature)
|
||||
? getEndpoint(userContext.features.mongoProxyEndpoint)
|
||||
: getEndpoint();
|
||||
const endpoint =
|
||||
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
||||
validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints)
|
||||
? userContext.features.mongoProxyEndpoint
|
||||
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||
|
||||
return getEndpoint(endpoint);
|
||||
}
|
||||
|
||||
export function getEndpoint(customEndpoint?: string): string {
|
||||
let url = customEndpoint ? customEndpoint : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||
url += "/api/mongo/explorer";
|
||||
export function getEndpoint(endpoint: string): string {
|
||||
let url = endpoint + "/api/mongo/explorer";
|
||||
|
||||
if (userContext.authType === AuthType.EncryptedToken) {
|
||||
url = url.replace("api/mongo", "api/guest/mongo");
|
||||
|
@ -149,10 +149,10 @@ export class QueriesClient {
|
||||
const documentId = new DocumentId(
|
||||
{
|
||||
partitionKey: QueriesClient.PartitionKey,
|
||||
partitionKeyProperty: "id",
|
||||
partitionKeyProperties: ["id"],
|
||||
} as DocumentsTab,
|
||||
query,
|
||||
query.queryName
|
||||
[query.queryName]
|
||||
); // TODO: Remove DocumentId's dependency on DocumentsTab
|
||||
const options: any = { partitionKey: query.resourceId };
|
||||
return deleteDocument(queriesCollection, documentId)
|
||||
|
@ -1,21 +1,29 @@
|
||||
import { Item } from "@azure/cosmos";
|
||||
import { Item, RequestOptions } from "@azure/cosmos";
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { client } from "../CosmosClient";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||
|
||||
export const readDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<Item> => {
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Reading ${entityName} ${documentId.id()}`);
|
||||
|
||||
try {
|
||||
const options: RequestOptions =
|
||||
documentId.partitionKey.kind === "MultiHash"
|
||||
? {
|
||||
[HttpHeaders.partitionKey]: documentId.partitionKeyValue,
|
||||
}
|
||||
: {};
|
||||
const response = await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.item(documentId.id(), documentId.partitionKeyValue)
|
||||
.read();
|
||||
// use undefined if the partitionKeyValue is empty
|
||||
.item(documentId.id(), documentId.partitionKeyValue?.length === 0 ? undefined : documentId.partitionKeyValue)
|
||||
.read(options);
|
||||
|
||||
return response?.resource;
|
||||
} catch (error) {
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Item, RequestOptions } from "@azure/cosmos";
|
||||
import { HttpHeaders } from "Common/Constants";
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import { Item } from "@azure/cosmos";
|
||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||
|
||||
export const updateDocument = async (
|
||||
collection: CollectionBase,
|
||||
@ -15,11 +16,17 @@ export const updateDocument = async (
|
||||
const clearMessage = logConsoleProgress(`Updating ${entityName} ${documentId.id()}`);
|
||||
|
||||
try {
|
||||
const options: RequestOptions =
|
||||
documentId.partitionKey.kind === "MultiHash"
|
||||
? {
|
||||
[HttpHeaders.partitionKey]: documentId.partitionKeyValue,
|
||||
}
|
||||
: {};
|
||||
const response = await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.item(documentId.id(), documentId.partitionKeyValue)
|
||||
.replace(newDocument);
|
||||
.replace(newDocument, options);
|
||||
|
||||
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);
|
||||
return response?.resource;
|
||||
|
@ -1,4 +1,16 @@
|
||||
import { JunoEndpoints } from "Common/Constants";
|
||||
import {
|
||||
allowedAadEndpoints,
|
||||
allowedArcadiaEndpoints,
|
||||
allowedArmEndpoints,
|
||||
allowedBackendEndpoints,
|
||||
allowedEmulatorEndpoints,
|
||||
allowedGraphEndpoints,
|
||||
allowedHostedExplorerEndpoints,
|
||||
allowedJunoOrigins,
|
||||
allowedMongoBackendEndpoints,
|
||||
allowedMsalRedirectEndpoints,
|
||||
validateEndpoint,
|
||||
} from "Utils/EndpointValidation";
|
||||
|
||||
export enum Platform {
|
||||
Portal = "Portal",
|
||||
@ -8,7 +20,7 @@ export enum Platform {
|
||||
|
||||
export interface ConfigContext {
|
||||
platform: Platform;
|
||||
allowedParentFrameOrigins: string[];
|
||||
allowedParentFrameOrigins: ReadonlyArray<string>;
|
||||
gitSha?: string;
|
||||
proxyPath?: string;
|
||||
AAD_ENDPOINT: string;
|
||||
@ -28,9 +40,9 @@ export interface ConfigContext {
|
||||
GITHUB_TEST_ENV_CLIENT_ID: string;
|
||||
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
|
||||
isTerminalEnabled: boolean;
|
||||
isPhoenixEnabled: boolean;
|
||||
hostedExplorerURL: string;
|
||||
armAPIVersion?: string;
|
||||
allowedJunoOrigins: string[];
|
||||
msalRedirectURI?: string;
|
||||
}
|
||||
|
||||
@ -40,12 +52,11 @@ let configContext: Readonly<ConfigContext> = {
|
||||
allowedParentFrameOrigins: [
|
||||
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*portal\\.microsoftazure.de$`,
|
||||
`^https:\\/\\/[\\.\\w]*portal\\.microsoftazure\\.de$`,
|
||||
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
|
||||
`^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$`,
|
||||
],
|
||||
// Webpack injects this at build time
|
||||
`^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`,
|
||||
], // Webpack injects this at build time
|
||||
gitSha: process.env.GIT_SHA,
|
||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||
AAD_ENDPOINT: "https://login.microsoftonline.com/",
|
||||
@ -61,14 +72,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||
JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
isTerminalEnabled: false,
|
||||
allowedJunoOrigins: [
|
||||
JunoEndpoints.Test,
|
||||
JunoEndpoints.Test2,
|
||||
JunoEndpoints.Test3,
|
||||
JunoEndpoints.Prod,
|
||||
JunoEndpoints.Stage,
|
||||
"https://localhost",
|
||||
],
|
||||
isPhoenixEnabled: false,
|
||||
};
|
||||
|
||||
export function resetConfigContext(): void {
|
||||
@ -79,6 +83,50 @@ export function resetConfigContext(): void {
|
||||
}
|
||||
|
||||
export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
if (!newContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.ARM_ENDPOINT, allowedArmEndpoints)) {
|
||||
delete newContext.ARM_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.AAD_ENDPOINT, allowedAadEndpoints)) {
|
||||
delete newContext.AAD_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.EMULATOR_ENDPOINT, allowedEmulatorEndpoints)) {
|
||||
delete newContext.EMULATOR_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.GRAPH_ENDPOINT, allowedGraphEndpoints)) {
|
||||
delete newContext.GRAPH_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.ARCADIA_ENDPOINT, allowedArcadiaEndpoints)) {
|
||||
delete newContext.ARCADIA_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.BACKEND_ENDPOINT, allowedBackendEndpoints)) {
|
||||
delete newContext.BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) {
|
||||
delete newContext.MONGO_BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.JUNO_ENDPOINT, allowedJunoOrigins)) {
|
||||
delete newContext.JUNO_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.hostedExplorerURL, allowedHostedExplorerEndpoints)) {
|
||||
delete newContext.hostedExplorerURL;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.msalRedirectURI, allowedMsalRedirectEndpoints)) {
|
||||
delete newContext.msalRedirectURI;
|
||||
}
|
||||
|
||||
Object.assign(configContext, newContext);
|
||||
}
|
||||
|
||||
@ -102,18 +150,8 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
|
||||
});
|
||||
if (response.status === 200) {
|
||||
try {
|
||||
const { allowedParentFrameOrigins, allowedJunoOrigins, ...externalConfig } = await response.json();
|
||||
Object.assign(configContext, externalConfig);
|
||||
if (allowedParentFrameOrigins && allowedParentFrameOrigins.length > 0) {
|
||||
updateConfigContext({
|
||||
allowedParentFrameOrigins: [...configContext.allowedParentFrameOrigins, ...allowedParentFrameOrigins],
|
||||
});
|
||||
}
|
||||
if (allowedJunoOrigins && allowedJunoOrigins.length > 0) {
|
||||
updateConfigContext({
|
||||
allowedJunoOrigins: [...configContext.allowedJunoOrigins, ...allowedJunoOrigins],
|
||||
});
|
||||
}
|
||||
const { ...externalConfig } = await response.json();
|
||||
updateConfigContext(externalConfig);
|
||||
} catch (error) {
|
||||
console.error("Unable to parse json in config file");
|
||||
console.error(error);
|
||||
|
@ -450,6 +450,24 @@ export interface IResponse<T> {
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface IValidationError {
|
||||
message: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface IMaxAllocationTimeExceeded extends IValidationError {
|
||||
earliestAllocationTimestamp: string;
|
||||
maxAllocationTimePerDayPerUserInMinutes: string;
|
||||
}
|
||||
|
||||
export interface IMaxDbAccountsPerUserExceeded extends IValidationError {
|
||||
maxSimultaneousConnectionsPerUser: string;
|
||||
}
|
||||
|
||||
export interface IMaxUsersPerDbAccountExceeded extends IValidationError {
|
||||
maxSimultaneousUsersPerDbAccount: string;
|
||||
}
|
||||
|
||||
export interface IPhoenixConnectionInfoResult {
|
||||
readonly notebookAuthToken?: string;
|
||||
readonly notebookServerUrl?: string;
|
||||
@ -531,3 +549,12 @@ export interface ContainerConnectionInfo {
|
||||
status: ConnectionStatusType;
|
||||
//need to add ram and rom info
|
||||
}
|
||||
|
||||
export enum PhoenixErrorType {
|
||||
MaxAllocationTimeExceeded = "MaxAllocationTimeExceeded",
|
||||
MaxDbAccountsPerUserExceeded = "MaxDbAccountsPerUserExceeded",
|
||||
MaxUsersPerDbAccountExceeded = "MaxUsersPerDbAccountExceeded",
|
||||
AllocationValidationResult = "AllocationValidationResult",
|
||||
RegionNotServicable = "RegionNotServicable",
|
||||
SubscriptionNotAllowed = "SubscriptionNotAllowed",
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ export enum MessageTypes {
|
||||
CreateWorkspace,
|
||||
CreateSparkPool,
|
||||
RefreshDatabaseAccount,
|
||||
CloseTab,
|
||||
}
|
||||
|
||||
export { Versions, ActionContracts, Diagnostics };
|
||||
|
@ -106,8 +106,8 @@ export interface CollectionBase extends TreeNode {
|
||||
self: string;
|
||||
rawDataModel: DataModels.Collection;
|
||||
partitionKey: DataModels.PartitionKey;
|
||||
partitionKeyProperty: string;
|
||||
partitionKeyPropertyHeader: string;
|
||||
partitionKeyProperties: string[];
|
||||
partitionKeyPropertyHeaders: string[];
|
||||
id: ko.Observable<string>;
|
||||
selectedSubnodeKind: ko.Observable<CollectionTabKind>;
|
||||
children: ko.ObservableArray<TreeNode>;
|
||||
|
@ -55,6 +55,7 @@ describe("NotebookTerminalComponent", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testAccount,
|
||||
notebookServerInfo: testNotebookServerInfo,
|
||||
tabId: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
@ -65,6 +66,7 @@ describe("NotebookTerminalComponent", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testMongo32Account,
|
||||
notebookServerInfo: testMongoNotebookServerInfo,
|
||||
tabId: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
@ -75,6 +77,7 @@ describe("NotebookTerminalComponent", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testMongo36Account,
|
||||
notebookServerInfo: testMongoNotebookServerInfo,
|
||||
tabId: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
@ -85,6 +88,7 @@ describe("NotebookTerminalComponent", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testCassandraAccount,
|
||||
notebookServerInfo: testCassandraNotebookServerInfo,
|
||||
tabId: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
|
@ -12,6 +12,7 @@ import * as StringUtils from "../../../Utils/StringUtils";
|
||||
export interface NotebookTerminalComponentProps {
|
||||
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
||||
databaseAccount: DataModels.DatabaseAccount;
|
||||
tabId: string;
|
||||
}
|
||||
|
||||
export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> {
|
||||
@ -55,6 +56,7 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
||||
apiType: userContext.apiType,
|
||||
authType: userContext.authType,
|
||||
databaseAccount: userContext.databaseAccount,
|
||||
tabId: this.props.tabId,
|
||||
};
|
||||
|
||||
postRobot.send(this.terminalWindow, "props", props, {
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
Text,
|
||||
} from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { userContext } from "UserContext";
|
||||
import { HttpStatusCodes } from "../../../Common/Constants";
|
||||
import { handleError } from "../../../Common/ErrorHandlingUtils";
|
||||
import { IGalleryItem, IJunoResponse, IPublicGalleryData, JunoClient } from "../../../Juno/JunoClient";
|
||||
@ -148,18 +149,23 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
public render(): JSX.Element {
|
||||
this.traceViewGallery();
|
||||
|
||||
const tabs: GalleryTabInfo[] = [
|
||||
this.createPublicGalleryTab(
|
||||
GalleryTab.PublicGallery,
|
||||
this.state.publicNotebooks,
|
||||
this.state.isCodeOfConductAccepted
|
||||
),
|
||||
this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks),
|
||||
];
|
||||
const tabs: GalleryTabInfo[] = [];
|
||||
if (userContext.features.publicGallery) {
|
||||
tabs.push(
|
||||
this.createPublicGalleryTab(
|
||||
GalleryTab.PublicGallery,
|
||||
this.state.publicNotebooks,
|
||||
this.state.isCodeOfConductAccepted
|
||||
)
|
||||
);
|
||||
}
|
||||
tabs.push(this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks));
|
||||
|
||||
if (this.props.container) {
|
||||
tabs.push(this.createFavoritesTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
||||
tabs.push(this.createPublishedNotebooksTab(GalleryTab.Published, this.state.publishedNotebooks));
|
||||
if (userContext.features.publicGallery) {
|
||||
tabs.push(this.createPublishedNotebooksTab(GalleryTab.Published, this.state.publishedNotebooks));
|
||||
}
|
||||
}
|
||||
|
||||
const pivotProps: IPivotProps = {
|
||||
|
@ -8,95 +8,6 @@ exports[`GalleryViewerComponent renders 1`] = `
|
||||
onLinkClick={[Function]}
|
||||
selectedKey="OfficialSamples"
|
||||
>
|
||||
<PivotItem
|
||||
headerText="Public gallery"
|
||||
itemKey="PublicGallery"
|
||||
key="PublicGallery"
|
||||
style={
|
||||
Object {
|
||||
"marginTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="publicGalleryTabContainer"
|
||||
>
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 20,
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
wrap={true}
|
||||
>
|
||||
<StackItem
|
||||
grow={true}
|
||||
>
|
||||
<StyledSearchBox
|
||||
onChange={[Function]}
|
||||
placeholder="Search"
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<StyledLabelBase>
|
||||
Sort by
|
||||
</StyledLabelBase>
|
||||
</StackItem>
|
||||
<StackItem
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"minWidth": 200,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<Dropdown
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": 0,
|
||||
"text": "Most viewed",
|
||||
},
|
||||
Object {
|
||||
"key": 1,
|
||||
"text": "Most downloaded",
|
||||
},
|
||||
Object {
|
||||
"key": 3,
|
||||
"text": "Most recent",
|
||||
},
|
||||
Object {
|
||||
"key": 2,
|
||||
"text": "Most favorited",
|
||||
},
|
||||
]
|
||||
}
|
||||
selectedKey={0}
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<InfoComponent />
|
||||
</StackItem>
|
||||
</Stack>
|
||||
<StackItem>
|
||||
<StyledSpinnerBase
|
||||
size={3}
|
||||
/>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</div>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="Official samples"
|
||||
itemKey="OfficialSamples"
|
||||
|
@ -149,7 +149,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.offer = this.database?.offer();
|
||||
}
|
||||
|
||||
this.state = {
|
||||
const initialState: SettingsComponentState = {
|
||||
throughput: undefined,
|
||||
throughputBaseline: undefined,
|
||||
autoPilotThroughput: undefined,
|
||||
@ -199,6 +199,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
selectedTab: SettingsV2TabTypes.ScaleTab,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
...initialState,
|
||||
...this.getBaselineValues(),
|
||||
...this.getAutoscaleBaselineValues(),
|
||||
};
|
||||
|
||||
this.saveSettingsButton = {
|
||||
isEnabled: this.isSaveSettingsButtonEnabled,
|
||||
isVisible: () => {
|
||||
@ -225,7 +231,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.loadMongoIndexes();
|
||||
}
|
||||
|
||||
this.setAutoPilotStates();
|
||||
this.setBaseline();
|
||||
if (this.props.settingsTab.isActive()) {
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
@ -286,17 +291,24 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
);
|
||||
};
|
||||
|
||||
private setAutoPilotStates = (): void => {
|
||||
private getAutoscaleBaselineValues = (): Partial<SettingsComponentState> => {
|
||||
const autoscaleMaxThroughput = this.offer?.autoscaleMaxThroughput;
|
||||
|
||||
if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
|
||||
this.setState({
|
||||
return {
|
||||
isAutoPilotSelected: true,
|
||||
wasAutopilotOriginallySet: true,
|
||||
autoPilotThroughput: autoscaleMaxThroughput,
|
||||
autoPilotThroughputBaseline: autoscaleMaxThroughput,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isAutoPilotSelected: false,
|
||||
wasAutopilotOriginallySet: false,
|
||||
autoPilotThroughput: undefined,
|
||||
autoPilotThroughputBaseline: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
public hasProvisioningTypeChanged = (): boolean =>
|
||||
@ -561,21 +573,25 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
};
|
||||
|
||||
public setBaseline = (): void => {
|
||||
const baselineValues = this.getBaselineValues();
|
||||
const autoscaleBaselineValues = this.getAutoscaleBaselineValues();
|
||||
this.setState({ ...baselineValues, ...autoscaleBaselineValues } as SettingsComponentState);
|
||||
};
|
||||
|
||||
private getBaselineValues = (): Partial<SettingsComponentState> => {
|
||||
const offerThroughput = this.offer?.manualThroughput;
|
||||
|
||||
if (!this.isCollectionSettingsTab) {
|
||||
this.setState({
|
||||
return {
|
||||
throughput: offerThroughput,
|
||||
throughputBaseline: offerThroughput,
|
||||
});
|
||||
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
const defaultTtl = this.collection.defaultTtl();
|
||||
|
||||
let timeToLive: TtlType = this.state.timeToLive;
|
||||
let timeToLiveSeconds = this.state.timeToLiveSeconds;
|
||||
let timeToLive: TtlType;
|
||||
let timeToLiveSeconds: number;
|
||||
switch (defaultTtl) {
|
||||
case undefined:
|
||||
case 0:
|
||||
@ -620,7 +636,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
(this.collection.geospatialConfig && this.collection.geospatialConfig()?.type) || GeospatialConfigType.Geometry;
|
||||
const geoSpatialConfigType = GeospatialConfigType[geospatialConfigTypeString as keyof typeof GeospatialConfigType];
|
||||
|
||||
this.setState({
|
||||
return {
|
||||
throughput: offerThroughput,
|
||||
throughputBaseline: offerThroughput,
|
||||
changeFeedPolicy: changeFeedPolicy,
|
||||
@ -643,7 +659,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
conflictResolutionPolicyProcedureBaseline: conflictResolutionPolicyProcedure,
|
||||
geospatialConfigType: geoSpatialConfigType,
|
||||
geospatialConfigTypeBaseline: geoSpatialConfigType,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
private getTabsButtons = (): CommandButtonComponentProps[] => {
|
||||
|
@ -65,8 +65,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
constructor(props: SubSettingsComponentProps) {
|
||||
super(props);
|
||||
this.geospatialVisible = userContext.apiType === "SQL";
|
||||
this.partitionKeyValue = "/" + this.props.collection.partitionKeyProperty;
|
||||
this.partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
|
||||
this.partitionKeyValue = this.getPartitionKeyValue();
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
@ -291,6 +291,14 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
);
|
||||
};
|
||||
|
||||
private getPartitionKeyValue = (): string => {
|
||||
if (userContext.apiType === "Mongo") {
|
||||
return this.props.collection.partitionKeyProperties?.[0] || "";
|
||||
}
|
||||
|
||||
return (this.props.collection.partitionKeyProperties || []).map((property) => "/" + property).join(", ");
|
||||
};
|
||||
|
||||
private getPartitionKeyComponent = (): JSX.Element => (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
{this.getPartitionKeyVisible() && (
|
||||
@ -310,7 +318,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
if (
|
||||
userContext.apiType === "Cassandra" ||
|
||||
userContext.apiType === "Tables" ||
|
||||
!this.props.collection.partitionKeyProperty ||
|
||||
!this.props.collection.partitionKeyProperties ||
|
||||
this.props.collection.partitionKeyProperties.length === 0 ||
|
||||
(userContext.apiType === "Mongo" && this.props.collection.partitionKey.systemKey)
|
||||
) {
|
||||
return false;
|
||||
|
@ -19,7 +19,7 @@ import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/Telemet
|
||||
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../../../../UserContext";
|
||||
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
|
||||
import { minAutoPilotThroughput } from "../../../../../Utils/AutoPilotUtils";
|
||||
import { autoPilotThroughput1K } from "../../../../../Utils/AutoPilotUtils";
|
||||
import { calculateEstimateNumber, usageInGB } from "../../../../../Utils/PricingUtils";
|
||||
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||
import {
|
||||
@ -540,7 +540,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
|
||||
onChange={this.onAutoPilotThroughputChange}
|
||||
min={minAutoPilotThroughput}
|
||||
min={autoPilotThroughput1K}
|
||||
errorMessage={this.props.throughputError}
|
||||
/>
|
||||
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
|
||||
|
@ -145,7 +145,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
id="autopilotInput"
|
||||
key="auto pilot throughput input"
|
||||
label="Max RU/s"
|
||||
min={4000}
|
||||
min={1000}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
step={1000}
|
||||
|
@ -39,7 +39,7 @@ export const collection = ({
|
||||
kind: "hash",
|
||||
version: 2,
|
||||
},
|
||||
partitionKeyProperty: "partitionKey",
|
||||
partitionKeyProperties: ["partitionKey"],
|
||||
readSettings: () => {
|
||||
return;
|
||||
},
|
||||
|
@ -5,6 +5,7 @@ const props = {
|
||||
isDatabase: false,
|
||||
showFreeTierExceedThroughputTooltip: true,
|
||||
isSharded: true,
|
||||
isFreeTier: false,
|
||||
setThroughputValue: () => jest.fn(),
|
||||
setIsAutoscale: () => jest.fn(),
|
||||
setIsThroughputCapExceeded: () => jest.fn(),
|
||||
|
@ -14,6 +14,7 @@ import "./ThroughputInput.less";
|
||||
export interface ThroughputInputProps {
|
||||
isDatabase: boolean;
|
||||
isSharded: boolean;
|
||||
isFreeTier: boolean;
|
||||
showFreeTierExceedThroughputTooltip: boolean;
|
||||
setThroughputValue: (throughput: number) => void;
|
||||
setIsAutoscale: (isAutoscale: boolean) => void;
|
||||
@ -23,15 +24,18 @@ export interface ThroughputInputProps {
|
||||
|
||||
export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
isDatabase,
|
||||
isSharded,
|
||||
isFreeTier,
|
||||
showFreeTierExceedThroughputTooltip,
|
||||
setThroughputValue,
|
||||
setIsAutoscale,
|
||||
setIsThroughputCapExceeded,
|
||||
isSharded,
|
||||
onCostAcknowledgeChange,
|
||||
}: ThroughputInputProps) => {
|
||||
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
|
||||
const [throughput, setThroughput] = useState<number>(AutoPilotUtils.minAutoPilotThroughput);
|
||||
const [throughput, setThroughput] = useState<number>(
|
||||
isFreeTier ? AutoPilotUtils.autoPilotThroughput1K : AutoPilotUtils.autoPilotThroughput4K
|
||||
);
|
||||
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
|
||||
const [throughputError, setThroughputError] = useState<string>("");
|
||||
const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0);
|
||||
@ -151,11 +155,14 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
|
||||
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
|
||||
if (mode === "Autoscale") {
|
||||
setThroughput(AutoPilotUtils.minAutoPilotThroughput);
|
||||
const defaultThroughput = isFreeTier
|
||||
? AutoPilotUtils.autoPilotThroughput1K
|
||||
: AutoPilotUtils.autoPilotThroughput4K;
|
||||
setThroughput(defaultThroughput);
|
||||
setIsAutoScaleSelected(true);
|
||||
setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
|
||||
setThroughputValue(defaultThroughput);
|
||||
setIsAutoscale(true);
|
||||
checkThroughputCap(AutoPilotUtils.minAutoPilotThroughput);
|
||||
checkThroughputCap(defaultThroughput);
|
||||
} else {
|
||||
setThroughput(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
|
||||
setIsAutoScaleSelected(false);
|
||||
@ -226,7 +233,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
}}
|
||||
onChange={(event, newInput?: string) => onThroughputValueChange(newInput)}
|
||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||
min={AutoPilotUtils.minAutoPilotThroughput}
|
||||
min={AutoPilotUtils.autoPilotThroughput1K}
|
||||
value={throughput.toString()}
|
||||
aria-label="Max request units per second"
|
||||
required={true}
|
||||
|
@ -3,6 +3,7 @@
|
||||
exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
<ThroughputInput
|
||||
isDatabase={false}
|
||||
isFreeTier={false}
|
||||
isSharded={true}
|
||||
onCostAcknowledgeChange={[Function]}
|
||||
setIsAutoscale={[Function]}
|
||||
@ -1637,7 +1638,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
aria-label="Max request units per second"
|
||||
errorMessage=""
|
||||
key=".0:$.2"
|
||||
min={4000}
|
||||
min={1000}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
step={1000}
|
||||
@ -1659,7 +1660,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
aria-label="Max request units per second"
|
||||
deferredValidationTime={200}
|
||||
errorMessage=""
|
||||
min={4000}
|
||||
min={1000}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
resizable={true}
|
||||
@ -1955,7 +1956,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
aria-invalid={false}
|
||||
className="ms-TextField-field field-64"
|
||||
id="TextField2"
|
||||
min={4000}
|
||||
min={1000}
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { Link } from "@fluentui/react/lib/Link";
|
||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||
import { IGalleryItem } from "Juno/JunoClient";
|
||||
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";
|
||||
@ -24,7 +26,6 @@ import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { useTabs } from "../hooks/useTabs";
|
||||
import { IGalleryItem } from "../Juno/JunoClient";
|
||||
import { PhoenixClient } from "../Phoenix/PhoenixClient";
|
||||
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||
@ -32,11 +33,7 @@ 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 {
|
||||
get as getWorkspace,
|
||||
listByDatabaseAccount, start
|
||||
} from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import { decryptJWTToken } from "../Utils/AuthorizationUtils";
|
||||
import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import { stringToBlob } from "../Utils/BlobUtils";
|
||||
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
||||
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
||||
@ -50,13 +47,12 @@ import * as FileSystemUtil from "./Notebook/FileSystemUtil";
|
||||
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
||||
import type NotebookManager from "./Notebook/NotebookManager";
|
||||
import type { NotebookPaneContent } from "./Notebook/NotebookManager";
|
||||
import { NotebookPaneContent } from "./Notebook/NotebookManager";
|
||||
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
||||
import { useNotebook } from "./Notebook/useNotebook";
|
||||
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
|
||||
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
||||
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
||||
import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel";
|
||||
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
||||
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
|
||||
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
|
||||
@ -188,7 +184,11 @@ export default class Explorer {
|
||||
this.resourceTree = new ResourceTreeAdapter(this);
|
||||
|
||||
// Override notebook server parameters from URL parameters
|
||||
if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) {
|
||||
if (
|
||||
userContext.features.notebookServerUrl &&
|
||||
validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
|
||||
userContext.features.notebookServerToken
|
||||
) {
|
||||
useNotebook.getState().setNotebookServerInfo({
|
||||
notebookServerEndpoint: userContext.features.notebookServerUrl,
|
||||
authToken: userContext.features.notebookServerToken,
|
||||
@ -200,19 +200,6 @@ export default class Explorer {
|
||||
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
|
||||
}
|
||||
|
||||
if (userContext.features.livyEndpoint) {
|
||||
useNotebook.getState().setSparkClusterConnectionInfo({
|
||||
userName: undefined,
|
||||
password: undefined,
|
||||
endpoints: [
|
||||
{
|
||||
endpoint: userContext.features.livyEndpoint,
|
||||
kind: DataModels.SparkClusterEndpointKind.Livy,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
this.refreshExplorer();
|
||||
}
|
||||
|
||||
@ -385,12 +372,13 @@ export default class Explorer {
|
||||
status: ConnectionStatusType.Connecting,
|
||||
};
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
let connectionInfo;
|
||||
try {
|
||||
TelemetryProcessor.traceStart(Action.PhoenixConnection, {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
});
|
||||
useNotebook.getState().setIsAllocating(true);
|
||||
const connectionInfo = await this.phoenixClient.allocateContainer(provisionData);
|
||||
connectionInfo = await this.phoenixClient.allocateContainer(provisionData);
|
||||
if (connectionInfo.status !== HttpStatusCodes.OK) {
|
||||
throw new Error(`Received status code: ${connectionInfo?.status}`);
|
||||
}
|
||||
@ -409,6 +397,16 @@ export default class Explorer {
|
||||
});
|
||||
connectionStatus.status = ConnectionStatusType.Failed;
|
||||
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||
if (error?.status === HttpStatusCodes.Forbidden && error.message) {
|
||||
useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`);
|
||||
} else {
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkModalDialog(
|
||||
"Connection Failed",
|
||||
"We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket."
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
useNotebook.getState().setIsAllocating(false);
|
||||
@ -432,7 +430,10 @@ export default class Explorer {
|
||||
connectionStatus.status = ConnectionStatusType.Connected;
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
useNotebook.getState().setNotebookServerInfo({
|
||||
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl,
|
||||
notebookServerEndpoint:
|
||||
(validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
|
||||
userContext.features.notebookServerUrl) ||
|
||||
connectionInfo.data.notebookServerUrl,
|
||||
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
|
||||
forwardingId: connectionInfo.data.forwardingId,
|
||||
});
|
||||
@ -449,7 +450,7 @@ export default class Explorer {
|
||||
);
|
||||
return;
|
||||
}
|
||||
const dialogContent = useNotebook.getState().isPhoenix
|
||||
const dialogContent = useNotebook.getState().isPhoenixNotebooks
|
||||
? "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?"
|
||||
: "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?";
|
||||
|
||||
@ -528,35 +529,6 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureNotebookWorkspaceRunning() {
|
||||
if (!userContext.databaseAccount) {
|
||||
return;
|
||||
}
|
||||
|
||||
let clearMessage;
|
||||
try {
|
||||
const notebookWorkspace = await getWorkspace(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
"default"
|
||||
);
|
||||
if (
|
||||
notebookWorkspace &&
|
||||
notebookWorkspace.properties &&
|
||||
notebookWorkspace.properties.status &&
|
||||
notebookWorkspace.properties.status.toLowerCase() === "stopped"
|
||||
) {
|
||||
clearMessage = NotificationConsoleUtils.logConsoleProgress("Initializing notebook workspace");
|
||||
await start(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, "default");
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, "Explorer/ensureNotebookWorkspaceRunning", "Failed to initialize notebook workspace");
|
||||
} finally {
|
||||
clearMessage && clearMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private _resetNotebookWorkspace = async () => {
|
||||
useDialog.getState().closeDialog();
|
||||
const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace");
|
||||
@ -572,7 +544,7 @@ export default class Explorer {
|
||||
TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
});
|
||||
if (useNotebook.getState().isPhoenix) {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
useTabs.getState().closeAllNotebookTabs(true);
|
||||
connectionStatus = {
|
||||
status: ConnectionStatusType.Connecting,
|
||||
@ -586,7 +558,7 @@ export default class Explorer {
|
||||
if (!connectionInfo?.data?.notebookServerUrl) {
|
||||
throw new Error(`Reset Workspace: NotebookServerUrl is invalid!`);
|
||||
}
|
||||
if (useNotebook.getState().isPhoenix) {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
await this.setNotebookInfo(connectionInfo, connectionStatus);
|
||||
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||
}
|
||||
@ -601,7 +573,7 @@ export default class Explorer {
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error),
|
||||
});
|
||||
if (useNotebook.getState().isPhoenix) {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
connectionStatus = {
|
||||
status: ConnectionStatusType.Failed,
|
||||
};
|
||||
@ -799,7 +771,7 @@ export default class Explorer {
|
||||
if (!notebookContentItem || !notebookContentItem.path) {
|
||||
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
|
||||
}
|
||||
if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenix) {
|
||||
if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenixNotebooks) {
|
||||
await this.allocateContainer();
|
||||
}
|
||||
|
||||
@ -1023,7 +995,7 @@ export default class Explorer {
|
||||
handleError(error, "Explorer/onNewNotebookClicked");
|
||||
throw new Error(error);
|
||||
}
|
||||
if (useNotebook.getState().isPhoenix) {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
if (isGithubTree) {
|
||||
await this.allocateContainer();
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
@ -1112,7 +1084,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
|
||||
if (useNotebook.getState().isPhoenix) {
|
||||
if (useNotebook.getState().isPhoenixFeatures) {
|
||||
await this.allocateContainer();
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
|
||||
@ -1152,7 +1124,7 @@ export default class Explorer {
|
||||
|
||||
const terminalTabs: TerminalTab[] = useTabs
|
||||
.getState()
|
||||
.getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => tab.tabTitle() === title) as TerminalTab[];
|
||||
.getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => tab.tabTitle().startsWith(title)) as TerminalTab[];
|
||||
|
||||
let index = 1;
|
||||
if (terminalTabs.length > 0) {
|
||||
@ -1243,20 +1215,12 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
private _openSetupNotebooksPaneForQuickstart(): void {
|
||||
const title = "Enable Notebooks (Preview)";
|
||||
const description =
|
||||
"You have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account.";
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(title, <SetupNoteBooksPanel explorer={this} panelTitle={title} panelDescription={description} />);
|
||||
}
|
||||
|
||||
public async handleOpenFileAction(path: string): Promise<void> {
|
||||
if (useNotebook.getState().isPhoenix) {
|
||||
if (useNotebook.getState().isPhoenixNotebooks === undefined) {
|
||||
await useNotebook.getState().getPhoenixStatus();
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
await this.allocateContainer();
|
||||
} else if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) {
|
||||
this._openSetupNotebooksPaneForQuickstart();
|
||||
}
|
||||
|
||||
// We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb
|
||||
@ -1287,7 +1251,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public openUploadFilePanel(parent?: NotebookContentItem): void {
|
||||
if (useNotebook.getState().isPhoenix) {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
Notebook.newNotebookUploadModalTitle,
|
||||
undefined,
|
||||
@ -1317,7 +1281,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public getDownloadModalConent(fileName: string): JSX.Element {
|
||||
if (useNotebook.getState().isPhoenix) {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
return (
|
||||
<>
|
||||
<p>{Notebook.galleryNotebookDownloadContent1}</p>
|
||||
@ -1341,16 +1305,21 @@ export default class Explorer {
|
||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||
|
||||
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
|
||||
const isNotebookEnabled = userContext.features.notebooksDownBanner || useNotebook.getState().isPhoenix;
|
||||
const isNotebookEnabled =
|
||||
userContext.features.notebooksDownBanner ||
|
||||
useNotebook.getState().isPhoenixNotebooks ||
|
||||
useNotebook.getState().isPhoenixFeatures;
|
||||
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
|
||||
useNotebook.getState().setIsShellEnabled(useNotebook.getState().isPhoenix && isPublicInternetAccessAllowed());
|
||||
useNotebook
|
||||
.getState()
|
||||
.setIsShellEnabled(useNotebook.getState().isPhoenixFeatures && isPublicInternetAccessAllowed());
|
||||
|
||||
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
|
||||
isNotebookEnabled,
|
||||
dataExplorerArea: Constants.Areas.Notebook,
|
||||
});
|
||||
|
||||
if (useNotebook.getState().isPhoenix) {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
await this.initNotebooks(userContext.databaseAccount);
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
|
||||
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||
|
||||
if (useNotebook.getState().isPhoenix) {
|
||||
if (useNotebook.getState().isPhoenixNotebooks || useNotebook.getState().isPhoenixFeatures) {
|
||||
uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus(container, "connectionStatus"));
|
||||
}
|
||||
|
||||
|
@ -31,28 +31,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("Account is not serverless - button should be visible", () => {
|
||||
it("Button should be visible", () => {
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableAzureSynapseLinkBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
|
||||
);
|
||||
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
||||
});
|
||||
|
||||
it("Account is serverless - button should be hidden", () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableServerless" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableAzureSynapseLinkBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
|
||||
);
|
||||
expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Enable notebook button", () => {
|
||||
|
@ -10,7 +10,6 @@ import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
|
||||
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
|
||||
import GitHubIcon from "../../../../images/github.svg";
|
||||
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
|
||||
import EnableNotebooksIcon from "../../../../images/notebook/Notebook-enable.svg";
|
||||
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
|
||||
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
|
||||
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
|
||||
@ -25,7 +24,6 @@ import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { JunoClient } from "../../../Juno/JunoClient";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
|
||||
import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
|
||||
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import { SupportPaneComponent } from "../../Controls/SupportPaneComponent/SupportPaneComponent";
|
||||
@ -37,7 +35,6 @@ import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPa
|
||||
import { GitHubReposPanel } from "../../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
|
||||
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
|
||||
import { SetupNoteBooksPanel } from "../../Panes/SetupNotebooksPanel/SetupNotebooksPanel";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
import { SelectedNodeState } from "../../useSelectedNode";
|
||||
|
||||
@ -79,10 +76,10 @@ export function createStaticCommandBarButtons(
|
||||
if (container.notebookManager?.gitHubOAuthService) {
|
||||
notebookButtons.push(createManageGitHubAccountButton(container));
|
||||
}
|
||||
if (useNotebook.getState().isPhoenix && configContext.isTerminalEnabled) {
|
||||
if (useNotebook.getState().isPhoenixFeatures && configContext.isTerminalEnabled) {
|
||||
notebookButtons.push(createOpenTerminalButton(container));
|
||||
}
|
||||
if (selectedNodeState.isConnectedToContainer()) {
|
||||
if (useNotebook.getState().isPhoenixNotebooks && selectedNodeState.isConnectedToContainer()) {
|
||||
notebookButtons.push(createNotebookWorkspaceResetButton(container));
|
||||
}
|
||||
if (
|
||||
@ -100,22 +97,19 @@ export function createStaticCommandBarButtons(
|
||||
}
|
||||
|
||||
notebookButtons.forEach((btn) => {
|
||||
if (!useNotebook.getState().isPhoenix) {
|
||||
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
|
||||
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
|
||||
if (!useNotebook.getState().isPhoenixFeatures) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
|
||||
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
|
||||
} else {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
|
||||
}
|
||||
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
|
||||
if (!useNotebook.getState().isPhoenixFeatures) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
|
||||
}
|
||||
} else if (!useNotebook.getState().isPhoenixNotebooks) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
|
||||
}
|
||||
buttons.push(btn);
|
||||
});
|
||||
} else {
|
||||
if (!isRunningOnNationalCloud() && useNotebook.getState().isPhoenix) {
|
||||
buttons.push(createDivider());
|
||||
buttons.push(createEnableNotebooksButton(container));
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
|
||||
@ -318,10 +312,6 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isServerlessAccount()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (userContext?.databaseAccount?.properties?.enableAnalyticalStorage) {
|
||||
return undefined;
|
||||
}
|
||||
@ -515,33 +505,6 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
|
||||
};
|
||||
}
|
||||
|
||||
function createEnableNotebooksButton(container: Explorer): CommandButtonComponentProps {
|
||||
if (configContext.platform === Platform.Emulator) {
|
||||
return undefined;
|
||||
}
|
||||
const label = "Enable Notebooks (Preview)";
|
||||
const tooltip =
|
||||
"Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
||||
const description =
|
||||
"Looks like you have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account.";
|
||||
return {
|
||||
iconSrc: EnableNotebooksIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
label,
|
||||
<SetupNoteBooksPanel explorer={container} panelTitle={label} panelDescription={description} />
|
||||
),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !useNotebook.getState().isNotebooksEnabledForAccount,
|
||||
ariaLabel: label,
|
||||
tooltipText: useNotebook.getState().isNotebooksEnabledForAccount ? "" : tooltip,
|
||||
};
|
||||
}
|
||||
|
||||
function createOpenTerminalButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Open Terminal";
|
||||
return {
|
||||
@ -559,9 +522,6 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
|
||||
const label = "Open Mongo Shell";
|
||||
const tooltip =
|
||||
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
||||
const title = "Set up workspace";
|
||||
const description =
|
||||
"Looks like you have not created a workspace for this account. To proceed and start using features including mongo shell and notebook, we will need to create a default workspace in this account.";
|
||||
const disableButton =
|
||||
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
|
||||
return {
|
||||
@ -570,13 +530,6 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
|
||||
onCommandClick: () => {
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
} else {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
title,
|
||||
<SetupNoteBooksPanel explorer={container} panelTitle={title} panelDescription={description} />
|
||||
);
|
||||
}
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
@ -591,9 +544,6 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
|
||||
const label = "Open Cassandra Shell";
|
||||
const tooltip =
|
||||
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
||||
const title = "Set up workspace";
|
||||
const description =
|
||||
"Looks like you have not created a workspace for this account. To proceed and start using features including cassandra shell and notebook, we will need to create a default workspace in this account.";
|
||||
const disableButton =
|
||||
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
|
||||
return {
|
||||
@ -602,13 +552,6 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
|
||||
onCommandClick: () => {
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
||||
} else {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
title,
|
||||
<SetupNoteBooksPanel explorer={container} panelTitle={title} panelDescription={description} />
|
||||
);
|
||||
}
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
|
@ -12,11 +12,12 @@ import {
|
||||
ServerConfig as JupyterServerConfig,
|
||||
} from "@nteract/core";
|
||||
import { Channels, childOf, createMessage, JupyterMessage, message, ofMessageType } from "@nteract/messaging";
|
||||
import { defineConfigOption } from "@nteract/mythic-configuration";
|
||||
import { RecordOf } from "immutable";
|
||||
import { AnyAction } from "redux";
|
||||
import { Action, AnyAction } from "redux";
|
||||
import { ofType, StateObservable } from "redux-observable";
|
||||
import { kernels, sessions } from "rx-jupyter";
|
||||
import { concat, EMPTY, from, merge, Observable, Observer, of, Subject, Subscriber, timer } from "rxjs";
|
||||
import { concat, EMPTY, from, interval, merge, Observable, Observer, of, Subject, Subscriber, timer } from "rxjs";
|
||||
import {
|
||||
catchError,
|
||||
concatMap,
|
||||
@ -41,7 +42,7 @@ import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationCons
|
||||
import { useDialog } from "../../Controls/Dialog";
|
||||
import * as FileSystemUtil from "../FileSystemUtil";
|
||||
import * as cdbActions from "../NotebookComponent/actions";
|
||||
import { NotebookUtil } from "../NotebookUtil";
|
||||
import { NotebookContentProviderType, NotebookUtil } from "../NotebookUtil";
|
||||
import * as CdbActions from "./actions";
|
||||
import * as TextFile from "./contents/file/text-file";
|
||||
import { CdbAppState } from "./types";
|
||||
@ -948,6 +949,54 @@ const resetCellStatusOnExecuteCanceledEpic = (
|
||||
);
|
||||
};
|
||||
|
||||
const { selector: autoSaveInterval } = defineConfigOption({
|
||||
key: "autoSaveInterval",
|
||||
label: "Auto-save interval",
|
||||
defaultValue: 120_000,
|
||||
});
|
||||
|
||||
/**
|
||||
* Override autoSaveCurrentContentEpic to disable auto save for notebooks under temporary workspace.
|
||||
* @param action$
|
||||
*/
|
||||
export function autoSaveCurrentContentEpic(
|
||||
action$: Observable<Action>,
|
||||
state$: StateObservable<AppState>
|
||||
): Observable<actions.Save> {
|
||||
return state$.pipe(
|
||||
map((state) => autoSaveInterval(state)),
|
||||
switchMap((time) => interval(time)),
|
||||
mergeMap(() => {
|
||||
const state = state$.value;
|
||||
return from(
|
||||
selectors
|
||||
.contentByRef(state)
|
||||
.filter(
|
||||
/*
|
||||
* Only save contents that are files or notebooks with
|
||||
* a filepath already set.
|
||||
*/
|
||||
(content) => (content.type === "file" || content.type === "notebook") && content.filepath !== ""
|
||||
)
|
||||
.keys()
|
||||
);
|
||||
}),
|
||||
filter((contentRef: ContentRef) => {
|
||||
const model = selectors.model(state$.value, { contentRef });
|
||||
const content = selectors.content(state$.value, { contentRef });
|
||||
if (
|
||||
model &&
|
||||
model.type === "notebook" &&
|
||||
NotebookUtil.getContentProviderType(content.filepath) !== NotebookContentProviderType.JupyterContentProviderType
|
||||
) {
|
||||
return selectors.notebook.isDirty(model);
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
map((contentRef: ContentRef) => actions.save({ contentRef }))
|
||||
);
|
||||
}
|
||||
|
||||
export const allEpics = [
|
||||
addInitialCodeCellEpic,
|
||||
focusInitialCodeCellEpic,
|
||||
@ -965,4 +1014,5 @@ export const allEpics = [
|
||||
traceNotebookInfoEpic,
|
||||
traceNotebookKernelEpic,
|
||||
resetCellStatusOnExecuteCanceledEpic,
|
||||
autoSaveCurrentContentEpic,
|
||||
];
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { AppState, epics as coreEpics, reducers, IContentProvider } from "@nteract/core";
|
||||
import { compose, Store, AnyAction, Middleware, Dispatch, MiddlewareAPI } from "redux";
|
||||
import { Epic } from "redux-observable";
|
||||
import { allEpics } from "./epics";
|
||||
import { coreReducer, cdbReducer } from "./reducers";
|
||||
import { catchError } from "rxjs/operators";
|
||||
import { Observable } from "rxjs";
|
||||
import { AppState, epics as coreEpics, IContentProvider, reducers } from "@nteract/core";
|
||||
import { configuration } from "@nteract/mythic-configuration";
|
||||
import { makeConfigureStore } from "@nteract/myths";
|
||||
import { AnyAction, compose, Dispatch, Middleware, MiddlewareAPI, Store } from "redux";
|
||||
import { Epic } from "redux-observable";
|
||||
import { Observable } from "rxjs";
|
||||
import { catchError } from "rxjs/operators";
|
||||
import { allEpics } from "./epics";
|
||||
import { cdbReducer, coreReducer } from "./reducers";
|
||||
import { CdbAppState } from "./types";
|
||||
|
||||
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
@ -81,7 +81,6 @@ export const getCoreEpics = (autoStartKernelOnNotebookOpen: boolean): Epic[] =>
|
||||
// This list needs to be consistent and in sync with core.allEpics until we figure
|
||||
// out how to safely filter out the ones we are overriding here.
|
||||
const filteredCoreEpics = [
|
||||
coreEpics.autoSaveCurrentContentEpic,
|
||||
coreEpics.executeCellEpic,
|
||||
coreEpics.executeFocusedCellEpic,
|
||||
coreEpics.executeCellAfterKernelLaunchEpic,
|
||||
|
@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Notebook container related stuff
|
||||
*/
|
||||
import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
@ -19,6 +20,7 @@ export class NotebookContainerClient {
|
||||
private isResettingWorkspace: boolean;
|
||||
private phoenixClient: PhoenixClient;
|
||||
private retryOptions: promiseRetry.Options;
|
||||
private scheduleTimerId: NodeJS.Timeout;
|
||||
|
||||
constructor(private onConnectionLost: () => void) {
|
||||
this.phoenixClient = new PhoenixClient();
|
||||
@ -27,34 +29,34 @@ export class NotebookContainerClient {
|
||||
maxTimeout: Notebook.retryAttemptDelayMs,
|
||||
minTimeout: Notebook.retryAttemptDelayMs,
|
||||
};
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (notebookServerInfo?.notebookServerEndpoint) {
|
||||
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||
} else {
|
||||
const unsub = useNotebook.subscribe(
|
||||
(newServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => {
|
||||
if (newServerInfo?.notebookServerEndpoint) {
|
||||
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||
}
|
||||
unsub();
|
||||
},
|
||||
(state) => state.notebookServerInfo
|
||||
);
|
||||
}
|
||||
|
||||
this.initHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Heartbeat: each ping schedules another ping
|
||||
*/
|
||||
private scheduleHeartbeat(delayMs: number): void {
|
||||
setTimeout(async () => {
|
||||
const memoryUsageInfo = await this.getMemoryUsage();
|
||||
useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo);
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (notebookServerInfo?.notebookServerEndpoint) {
|
||||
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||
}
|
||||
}, delayMs);
|
||||
private initHeartbeat(delayMs: number): void {
|
||||
this.scheduleHeartbeat(delayMs);
|
||||
|
||||
useNotebook.subscribe(
|
||||
() => this.scheduleHeartbeat(delayMs),
|
||||
(state) => state.notebookServerInfo
|
||||
);
|
||||
}
|
||||
|
||||
private scheduleHeartbeat(delayMs: number) {
|
||||
if (this.scheduleTimerId) {
|
||||
clearInterval(this.scheduleTimerId);
|
||||
}
|
||||
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (notebookServerInfo?.notebookServerEndpoint) {
|
||||
this.scheduleTimerId = setInterval(async () => {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (notebookServerInfo?.notebookServerEndpoint) {
|
||||
const memoryUsageInfo = await this.getMemoryUsage();
|
||||
useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo);
|
||||
}
|
||||
}, delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
public async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
|
||||
@ -149,7 +151,7 @@ export class NotebookContainerClient {
|
||||
}
|
||||
|
||||
try {
|
||||
if (useNotebook.getState().isPhoenix) {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
const provisionData: IProvisionData = {
|
||||
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
|
||||
};
|
||||
@ -158,6 +160,16 @@ export class NotebookContainerClient {
|
||||
return null;
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "NotebookContainerClient/resetWorkspace");
|
||||
if (error?.status === HttpStatusCodes.Forbidden && error.message) {
|
||||
useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`);
|
||||
} else {
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkModalDialog(
|
||||
"Connection Failed",
|
||||
"We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket."
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
@ -303,8 +303,8 @@ export class NotebookContentClient {
|
||||
private getServerConfig(): ServerConfig {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
return {
|
||||
endpoint: notebookServerInfo.notebookServerEndpoint,
|
||||
token: notebookServerInfo.authToken,
|
||||
endpoint: notebookServerInfo?.notebookServerEndpoint,
|
||||
token: notebookServerInfo?.authToken,
|
||||
crossDomain: true,
|
||||
};
|
||||
}
|
||||
|
@ -38,7 +38,8 @@ interface NotebookState {
|
||||
isAllocating: boolean;
|
||||
isRefreshed: boolean;
|
||||
containerStatus: ContainerInfo;
|
||||
isPhoenix: boolean;
|
||||
isPhoenixNotebooks: boolean;
|
||||
isPhoenixFeatures: boolean;
|
||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
|
||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
|
||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
|
||||
@ -61,7 +62,8 @@ interface NotebookState {
|
||||
setIsRefreshed: (isAllocating: boolean) => void;
|
||||
setContainerStatus: (containerStatus: ContainerInfo) => void;
|
||||
getPhoenixStatus: () => Promise<void>;
|
||||
setIsPhoenix: (isPhoenix: boolean) => void;
|
||||
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => void;
|
||||
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => void;
|
||||
}
|
||||
|
||||
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
@ -96,7 +98,8 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
durationLeftInMinutes: undefined,
|
||||
notebookServerInfo: undefined,
|
||||
},
|
||||
isPhoenix: undefined,
|
||||
isPhoenixNotebooks: undefined,
|
||||
isPhoenixFeatures: undefined,
|
||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
|
||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
|
||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
|
||||
@ -202,7 +205,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
|
||||
},
|
||||
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
|
||||
const notebookFolderName = get().isPhoenix ? "Temporary Notebooks" : "My Notebooks";
|
||||
const notebookFolderName = get().isPhoenixNotebooks ? "Temporary Notebooks" : "My Notebooks";
|
||||
set({ notebookFolderName });
|
||||
const myNotebooksContentRoot = {
|
||||
name: get().notebookFolderName,
|
||||
@ -299,14 +302,20 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }),
|
||||
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
|
||||
getPhoenixStatus: async () => {
|
||||
if (get().isPhoenix === undefined) {
|
||||
if (get().isPhoenixNotebooks === undefined || get().isPhoenixFeatures === undefined) {
|
||||
let isPhoenix = false;
|
||||
if (userContext.features.phoenix) {
|
||||
if (userContext.features.phoenixNotebooks || userContext.features.phoenixFeatures) {
|
||||
const phoenixClient = new PhoenixClient();
|
||||
isPhoenix = isPublicInternetAccessAllowed() && (await phoenixClient.isDbAcountWhitelisted());
|
||||
}
|
||||
set({ isPhoenix });
|
||||
|
||||
const isPhoenixNotebooks = userContext.features.phoenixNotebooks && isPhoenix;
|
||||
const isPhoenixFeatures = userContext.features.phoenixFeatures && isPhoenix;
|
||||
|
||||
set({ isPhoenixNotebooks: isPhoenixNotebooks });
|
||||
set({ isPhoenixFeatures: isPhoenixFeatures });
|
||||
}
|
||||
},
|
||||
setIsPhoenix: (isPhoenix: boolean) => set({ isPhoenix }),
|
||||
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }),
|
||||
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => set({ isPhoenixFeatures: isPhoenixFeatures }),
|
||||
}));
|
||||
|
@ -8,23 +8,23 @@ import { CassandraAddCollectionPane } from "../Panes/CassandraAddCollectionPane/
|
||||
import { SettingsPane } from "../Panes/SettingsPane/SettingsPane";
|
||||
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
|
||||
|
||||
function generateQueryText(action: ActionContracts.OpenQueryTab, partitionKeyProperty: string): string {
|
||||
function generateQueryText(action: ActionContracts.OpenQueryTab, partitionKeyProperties: string[]): string {
|
||||
if (!action.query) {
|
||||
return "SELECT * FROM c";
|
||||
} else if (action.query.text) {
|
||||
return action.query.text;
|
||||
} else if (!!action.query.partitionKeys && action.query.partitionKeys.length > 0) {
|
||||
} else if (action.query.partitionKeys?.length > 0 && partitionKeyProperties?.length > 0) {
|
||||
let query = "SELECT * FROM c WHERE";
|
||||
for (let i = 0; i < action.query.partitionKeys.length; i++) {
|
||||
const partitionKey = action.query.partitionKeys[i];
|
||||
if (!partitionKey) {
|
||||
// null partition key case
|
||||
query = query.concat(` c.${partitionKeyProperty} = ${action.query.partitionKeys[i]}`);
|
||||
query = query.concat(` c.${partitionKeyProperties[i]} = ${action.query.partitionKeys[i]}`);
|
||||
} else if (typeof partitionKey !== "string") {
|
||||
// Undefined partition key case
|
||||
query = query.concat(` NOT IS_DEFINED(c.${partitionKeyProperty})`);
|
||||
query = query.concat(` NOT IS_DEFINED(c.${partitionKeyProperties[i]})`);
|
||||
} else {
|
||||
query = query.concat(` c.${partitionKeyProperty} = "${action.query.partitionKeys[i]}"`);
|
||||
query = query.concat(` c.${partitionKeyProperties[i]} = "${action.query.partitionKeys[i]}"`);
|
||||
}
|
||||
if (i !== action.query.partitionKeys.length - 1) {
|
||||
query = query.concat(" OR");
|
||||
@ -109,7 +109,7 @@ function openCollectionTab(
|
||||
collection.onNewQueryClick(
|
||||
collection,
|
||||
undefined,
|
||||
generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperty)
|
||||
generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperties)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
@ -249,6 +249,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
|
||||
isDatabase={true}
|
||||
isSharded={this.state.isSharded}
|
||||
isFreeTier={this.isFreeTierAccount()}
|
||||
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
|
||||
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
|
||||
@ -483,6 +484,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
|
||||
isDatabase={false}
|
||||
isSharded={this.state.isSharded}
|
||||
isFreeTier={this.isFreeTierAccount()}
|
||||
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
|
||||
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
|
||||
@ -667,7 +669,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
{userContext.apiType === "SQL" && (
|
||||
<Checkbox
|
||||
label="My partition key is larger than 100 bytes"
|
||||
label="My partition key is larger than 101 bytes"
|
||||
checked={this.state.useHashV2}
|
||||
styles={{
|
||||
text: { fontSize: 12 },
|
||||
@ -887,10 +889,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isServerlessAccount()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (userContext.apiType) {
|
||||
case "SQL":
|
||||
case "Mongo":
|
||||
|
@ -147,7 +147,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||
if (isAutoscaleSelected) {
|
||||
if (!AutoPilotUtils.isValidAutoPilotThroughput(throughput)) {
|
||||
setFormErrors(
|
||||
`Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
|
||||
`Please enter a value greater than ${AutoPilotUtils.autoPilotThroughput1K} for autopilot throughput`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@ -241,6 +241,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
|
||||
isDatabase={true}
|
||||
isSharded={databaseCreateNewShared}
|
||||
isFreeTier={isFreeTierAccount}
|
||||
setThroughputValue={(newThroughput: number) => (throughput = newThroughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (isAutoscaleSelected = isAutoscale)}
|
||||
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
|
||||
|
@ -262,6 +262,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
}
|
||||
isDatabase
|
||||
isSharded
|
||||
isFreeTier={isFreeTierAccount}
|
||||
setThroughputValue={(throughput: number) => (newKeySpaceThroughput = throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (isNewKeySpaceAutoscale = isAutoscale)}
|
||||
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
|
||||
@ -335,6 +336,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
|
||||
isDatabase={false}
|
||||
isSharded
|
||||
isFreeTier={isFreeTierAccount}
|
||||
setThroughputValue={(throughput: number) => (tableThroughput = throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (isTableAutoscale = isAutoscale)}
|
||||
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
|
||||
|
@ -10,7 +10,6 @@ import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUti
|
||||
import Explorer from "../../Explorer";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem";
|
||||
import { useNotebook } from "../../Notebook/useNotebook";
|
||||
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
|
||||
|
||||
@ -75,7 +74,7 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
|
||||
selectedLocation.owner,
|
||||
selectedLocation.repo
|
||||
)} - ${selectedLocation.branch}`;
|
||||
} else if (selectedLocation.type === "MyNotebooks" && useNotebook.getState().isPhoenix) {
|
||||
} else if (selectedLocation.type === "MyNotebooks" && useNotebook.getState().isPhoenixNotebooks) {
|
||||
destination = useNotebook.getState().notebookFolderName;
|
||||
}
|
||||
|
||||
@ -104,11 +103,14 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
|
||||
switch (location.type) {
|
||||
case "MyNotebooks":
|
||||
parent = {
|
||||
name: ResourceTreeAdapter.MyNotebooksTitle,
|
||||
name: useNotebook.getState().notebookFolderName,
|
||||
path: useNotebook.getState().notebookBasePath,
|
||||
type: NotebookContentItemType.Directory,
|
||||
};
|
||||
isGithubTree = false;
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
await container.allocateContainer();
|
||||
}
|
||||
break;
|
||||
|
||||
case "GitHub":
|
||||
|
@ -1,50 +0,0 @@
|
||||
import { PrimaryButton } from "@fluentui/react";
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import Explorer from "../../Explorer";
|
||||
import { SetupNoteBooksPanel } from "./SetupNotebooksPanel";
|
||||
|
||||
describe("Setup Notebooks Panel", () => {
|
||||
it("should render Default properly", () => {
|
||||
const fakeExplorer = {} as Explorer;
|
||||
const props = {
|
||||
explorer: fakeExplorer,
|
||||
closePanel: (): void => undefined,
|
||||
openNotificationConsole: (): void => undefined,
|
||||
panelTitle: "",
|
||||
panelDescription: "",
|
||||
};
|
||||
const wrapper = mount(<SetupNoteBooksPanel {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render button", () => {
|
||||
const fakeExplorer = {} as Explorer;
|
||||
const props = {
|
||||
explorer: fakeExplorer,
|
||||
closePanel: (): void => undefined,
|
||||
openNotificationConsole: (): void => undefined,
|
||||
panelTitle: "",
|
||||
panelDescription: "",
|
||||
};
|
||||
const wrapper = mount(<SetupNoteBooksPanel {...props} />);
|
||||
const button = wrapper.find("PrimaryButton").first();
|
||||
expect(button).toBeDefined();
|
||||
});
|
||||
|
||||
it("Button onClick should call onCompleteSetup", () => {
|
||||
const onCompleteSetupClick = jest.fn();
|
||||
const wrapper = mount(<PrimaryButton onClick={onCompleteSetupClick} />);
|
||||
wrapper.find("button").simulate("click");
|
||||
|
||||
expect(onCompleteSetupClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("Button onKeyPress should call onCompleteSetupKeyPress", () => {
|
||||
const onCompleteSetupKeyPress = jest.fn();
|
||||
const wrapper = mount(<PrimaryButton onKeyPress={onCompleteSetupKeyPress} />);
|
||||
wrapper.find("button").simulate("keypress");
|
||||
|
||||
expect(onCompleteSetupKeyPress).toHaveBeenCalled();
|
||||
});
|
||||
});
|
@ -1,121 +0,0 @@
|
||||
import { PrimaryButton } from "@fluentui/react";
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import React, { FunctionComponent, KeyboardEvent, useState } from "react";
|
||||
import { Areas, NormalizedEventKey } from "../../../Common/Constants";
|
||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { createOrUpdate } from "../../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
|
||||
import { PanelLoadingScreen } from "../PanelLoadingScreen";
|
||||
interface SetupNoteBooksPanelProps {
|
||||
explorer: Explorer;
|
||||
panelTitle: string;
|
||||
panelDescription: string;
|
||||
}
|
||||
|
||||
export const SetupNoteBooksPanel: FunctionComponent<SetupNoteBooksPanelProps> = ({
|
||||
explorer,
|
||||
panelTitle,
|
||||
panelDescription,
|
||||
}: SetupNoteBooksPanelProps): JSX.Element => {
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
|
||||
const description = panelDescription;
|
||||
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||
const [showErrorDetails, setShowErrorDetails] = useState<boolean>(false);
|
||||
|
||||
const onCompleteSetupClick = async () => {
|
||||
await setupNotebookWorkspace();
|
||||
};
|
||||
|
||||
const onCompleteSetupKeyPress = async (event: KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (event.key === " " || event.key === NormalizedEventKey.Enter) {
|
||||
await setupNotebookWorkspace();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const setupNotebookWorkspace = async (): Promise<void> => {
|
||||
if (!explorer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNotebookWorkspace, {
|
||||
dataExplorerArea: Areas.ContextualPane,
|
||||
paneTitle: panelTitle,
|
||||
});
|
||||
|
||||
const clear = NotificationConsoleUtils.logConsoleProgress("Creating a new default notebook workspace");
|
||||
|
||||
try {
|
||||
setLoadingTrue();
|
||||
await createOrUpdate(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
"default"
|
||||
);
|
||||
explorer.refreshExplorer();
|
||||
|
||||
closeSidePanel();
|
||||
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.CreateNotebookWorkspace,
|
||||
{
|
||||
dataExplorerArea: Areas.ContextualPane,
|
||||
paneTitle: panelTitle,
|
||||
},
|
||||
startKey
|
||||
);
|
||||
NotificationConsoleUtils.logConsoleInfo("Successfully created a default notebook workspace for the account");
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.CreateNotebookWorkspace,
|
||||
{
|
||||
dataExplorerArea: Areas.ContextualPane,
|
||||
paneTitle: panelTitle,
|
||||
error: errorMessage,
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
setErrorMessage(`Failed to setup a default notebook workspace: ${errorMessage}`);
|
||||
setShowErrorDetails(true);
|
||||
NotificationConsoleUtils.logConsoleError(`Failed to create a default notebook workspace: ${errorMessage}`);
|
||||
} finally {
|
||||
setLoadingFalse();
|
||||
clear();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="panelFormWrapper">
|
||||
{errorMessage && (
|
||||
<PanelInfoErrorComponent message={errorMessage} messageType="error" showErrorDetails={showErrorDetails} />
|
||||
)}
|
||||
<div className="panelMainContent">
|
||||
<div className="pkPadding">
|
||||
<div>{description}</div>
|
||||
<PrimaryButton
|
||||
id="completeSetupBtn"
|
||||
className="btncreatecoll1 btnSetupQueries"
|
||||
text="Complete Setup"
|
||||
onClick={onCompleteSetupClick}
|
||||
onKeyPress={onCompleteSetupKeyPress}
|
||||
aria-label="Complete setup"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isLoading && <PanelLoadingScreen />}
|
||||
</form>
|
||||
);
|
||||
};
|
@ -35,6 +35,60 @@ const {
|
||||
Smallint,
|
||||
Tinyint,
|
||||
Timestamp,
|
||||
// List
|
||||
List_Ascii,
|
||||
List_Bigint,
|
||||
List_Blob,
|
||||
List_Boolean,
|
||||
List_Date,
|
||||
List_Decimal,
|
||||
List_Double,
|
||||
List_Float,
|
||||
List_Int,
|
||||
List_Text,
|
||||
List_Timestamp,
|
||||
List_Uuid,
|
||||
List_Varchar,
|
||||
List_Varint,
|
||||
List_Inet,
|
||||
List_Smallint,
|
||||
List_Tinyint,
|
||||
// Map
|
||||
Map_Ascii,
|
||||
Map_Bigint,
|
||||
Map_Blob,
|
||||
Map_Boolean,
|
||||
Map_Date,
|
||||
Map_Decimal,
|
||||
Map_Double,
|
||||
Map_Float,
|
||||
Map_Int,
|
||||
Map_Text,
|
||||
Map_Timestamp,
|
||||
Map_Uuid,
|
||||
Map_Varchar,
|
||||
Map_Varint,
|
||||
Map_Inet,
|
||||
Map_Smallint,
|
||||
Map_Tinyint,
|
||||
// Set
|
||||
Set_Ascii,
|
||||
Set_Bigint,
|
||||
Set_Blob,
|
||||
Set_Boolean,
|
||||
Set_Date,
|
||||
Set_Decimal,
|
||||
Set_Double,
|
||||
Set_Float,
|
||||
Set_Int,
|
||||
Set_Text,
|
||||
Set_Timestamp,
|
||||
Set_Uuid,
|
||||
Set_Varchar,
|
||||
Set_Varint,
|
||||
Set_Inet,
|
||||
Set_Smallint,
|
||||
Set_Tinyint,
|
||||
} = TableConstants.CassandraType;
|
||||
export const cassandraOptions = [
|
||||
{ key: Text, text: Text },
|
||||
@ -54,6 +108,60 @@ export const cassandraOptions = [
|
||||
{ key: Smallint, text: Smallint },
|
||||
{ key: Tinyint, text: Tinyint },
|
||||
{ key: Timestamp, text: Timestamp },
|
||||
// List
|
||||
{ key: List_Ascii, text: List_Ascii },
|
||||
{ key: List_Bigint, text: List_Bigint },
|
||||
{ key: List_Blob, text: List_Blob },
|
||||
{ key: List_Boolean, text: List_Boolean },
|
||||
{ key: List_Date, text: List_Date },
|
||||
{ key: List_Decimal, text: List_Decimal },
|
||||
{ key: List_Double, text: List_Double },
|
||||
{ key: List_Float, text: List_Float },
|
||||
{ key: List_Int, text: List_Int },
|
||||
{ key: List_Text, text: List_Text },
|
||||
{ key: List_Timestamp, text: List_Timestamp },
|
||||
{ key: List_Uuid, text: List_Uuid },
|
||||
{ key: List_Varchar, text: List_Varchar },
|
||||
{ key: List_Varint, text: List_Varint },
|
||||
{ key: List_Inet, text: List_Inet },
|
||||
{ key: List_Smallint, text: List_Smallint },
|
||||
{ key: List_Tinyint, text: List_Tinyint },
|
||||
// Map
|
||||
{ key: Map_Ascii, text: Map_Ascii },
|
||||
{ key: Map_Bigint, text: Map_Bigint },
|
||||
{ key: Map_Blob, text: Map_Blob },
|
||||
{ key: Map_Boolean, text: Map_Boolean },
|
||||
{ key: Map_Date, text: Map_Date },
|
||||
{ key: Map_Decimal, text: Map_Decimal },
|
||||
{ key: Map_Double, text: Map_Double },
|
||||
{ key: Map_Float, text: Map_Float },
|
||||
{ key: Map_Int, text: Map_Int },
|
||||
{ key: Map_Text, text: Map_Text },
|
||||
{ key: Map_Timestamp, text: Map_Timestamp },
|
||||
{ key: Map_Uuid, text: Map_Uuid },
|
||||
{ key: Map_Varchar, text: Map_Varchar },
|
||||
{ key: Map_Varint, text: Map_Varint },
|
||||
{ key: Map_Inet, text: Map_Inet },
|
||||
{ key: Map_Smallint, text: Map_Smallint },
|
||||
{ key: Map_Tinyint, text: Map_Tinyint },
|
||||
// Set
|
||||
{ key: Set_Ascii, text: Set_Ascii },
|
||||
{ key: Set_Bigint, text: Set_Bigint },
|
||||
{ key: Set_Blob, text: Set_Blob },
|
||||
{ key: Set_Boolean, text: Set_Boolean },
|
||||
{ key: Set_Date, text: Set_Date },
|
||||
{ key: Set_Decimal, text: Set_Decimal },
|
||||
{ key: Set_Double, text: Set_Double },
|
||||
{ key: Set_Float, text: Set_Float },
|
||||
{ key: Set_Int, text: Set_Int },
|
||||
{ key: Set_Text, text: Set_Text },
|
||||
{ key: Set_Timestamp, text: Set_Timestamp },
|
||||
{ key: Set_Uuid, text: Set_Uuid },
|
||||
{ key: Set_Varchar, text: Set_Varchar },
|
||||
{ key: Set_Varint, text: Set_Varint },
|
||||
{ key: Set_Inet, text: Set_Inet },
|
||||
{ key: Set_Smallint, text: Set_Smallint },
|
||||
{ key: Set_Tinyint, text: Set_Tinyint },
|
||||
];
|
||||
|
||||
export const imageProps: IImageProps = {
|
||||
|
@ -12,13 +12,13 @@
|
||||
margin: auto;
|
||||
padding-left: 21px;
|
||||
padding-right: 16px;
|
||||
max-width: 1168px;;
|
||||
max-width: 1168px;
|
||||
|
||||
>* {
|
||||
> * {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
>.title {
|
||||
> .title {
|
||||
position: relative; // To attach FeaturePanelLauncher as absolute
|
||||
color: @BaseHigh;
|
||||
font-size: 48px;
|
||||
@ -27,7 +27,7 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
>.subtitle {
|
||||
> .subtitle {
|
||||
color: @BaseHigh;
|
||||
font-size: 18px;
|
||||
padding-left: 0px;
|
||||
@ -41,18 +41,17 @@
|
||||
cursor: pointer;
|
||||
margin: 40px auto;
|
||||
|
||||
>.mainButton {
|
||||
> .mainButton {
|
||||
min-width: 124px;
|
||||
max-width: 296px;
|
||||
padding: 32px 16px;
|
||||
display: flex;
|
||||
background-color: @BaseLight;
|
||||
border: 1px solid #949494;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 4px;
|
||||
|
||||
>.legendContainer {
|
||||
> .legendContainer {
|
||||
margin-left: 16px;
|
||||
text-align: left;
|
||||
|
||||
@ -65,10 +64,14 @@
|
||||
.description {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.newDescription {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
>:nth-child(n+2) {
|
||||
> :nth-child(n + 2) {
|
||||
margin-left: 32px;
|
||||
}
|
||||
}
|
||||
@ -83,7 +86,7 @@
|
||||
min-width: 124px;
|
||||
max-width: 296px;
|
||||
|
||||
>.title {
|
||||
> .title {
|
||||
font-size: 18px;
|
||||
font-family: @SemiboldFont;
|
||||
color: @BaseDark;
|
||||
@ -91,7 +94,7 @@
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
>ul {
|
||||
> ul {
|
||||
list-style: none;
|
||||
padding-left: 0px;
|
||||
margin-bottom: 0px;
|
||||
@ -101,7 +104,7 @@
|
||||
.flex-display();
|
||||
align-items: flex-start;
|
||||
|
||||
>img {
|
||||
> img {
|
||||
margin-right: @DefaultSpace;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
@ -133,12 +136,12 @@
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
|
||||
>.title {
|
||||
> .title {
|
||||
color: @BaseDark;
|
||||
padding: 0px;
|
||||
font-size: 12px;
|
||||
}
|
||||
>.description {
|
||||
> .description {
|
||||
color: @BaseDark;
|
||||
}
|
||||
|
||||
@ -167,12 +170,21 @@
|
||||
}
|
||||
|
||||
&:focus {
|
||||
.focus();
|
||||
.focus();
|
||||
}
|
||||
|
||||
&:active {
|
||||
.active();
|
||||
.active();
|
||||
}
|
||||
}
|
||||
|
||||
.notebookSplashScreenItem {
|
||||
padding: 12px 0 12px 12px;
|
||||
|
||||
.itemText {
|
||||
margin-left: 12px;
|
||||
font-family: @SemiboldFont;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,23 @@
|
||||
/**
|
||||
* Accordion top class
|
||||
*/
|
||||
import { Link } from "@fluentui/react";
|
||||
import { Image, Link, Stack, Text } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import AddDatabaseIcon from "../../../images/AddDatabase.svg";
|
||||
import NewQueryIcon from "../../../images/AddSqlQuery_16x16.svg";
|
||||
import NewStoredProcedureIcon from "../../../images/AddStoredProcedure.svg";
|
||||
import OpenQueryIcon from "../../../images/BrowseQuery.svg";
|
||||
import ConnectIcon from "../../../images/Connect_color.svg";
|
||||
import ContainersIcon from "../../../images/Containers.svg";
|
||||
import CostIcon from "../../../images/Cost.svg";
|
||||
import GreenCheckIcon from "../../../images/Green_check.svg";
|
||||
import NewContainerIcon from "../../../images/Hero-new-container.svg";
|
||||
import NewNotebookIcon from "../../../images/Hero-new-notebook.svg";
|
||||
import SampleIcon from "../../../images/Hero-sample.svg";
|
||||
import LinkIcon from "../../../images/Link_blue.svg";
|
||||
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
|
||||
import NotebooksIcon from "../../../images/Notebooks.svg";
|
||||
import QuickStartIcon from "../../../images/Quickstart_Lightning.svg";
|
||||
import ScaleAndSettingsIcon from "../../../images/Scale_15x15.svg";
|
||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||
import { AuthType } from "../../AuthType";
|
||||
@ -82,110 +89,63 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
|
||||
public render(): JSX.Element {
|
||||
const mainItems = this.createMainItems();
|
||||
const commonTaskItems = this.createCommonTaskItems();
|
||||
let recentItems = this.createRecentItems();
|
||||
recentItems = recentItems.filter((item) => item.description !== "Notebook");
|
||||
|
||||
const tipsItems = this.createTipsItems();
|
||||
const onClearRecent = this.clearMostRecent;
|
||||
|
||||
const formContainer = (jsx: JSX.Element) => (
|
||||
return (
|
||||
<div className="connectExplorerContainer">
|
||||
<form className="connectExplorerFormContainer">{jsx}</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
return formContainer(
|
||||
<div className="splashScreenContainer">
|
||||
<div className="splashScreen">
|
||||
<div className="title">
|
||||
Welcome to Cosmos DB
|
||||
<FeaturePanelLauncher />
|
||||
</div>
|
||||
<div className="subtitle">Globally distributed, multi-model database service for any scale</div>
|
||||
<div className="mainButtonsContainer">
|
||||
{mainItems.map((item) => (
|
||||
<div
|
||||
className="mainButton focusable"
|
||||
key={`${item.title}`}
|
||||
onClick={item.onClick}
|
||||
onKeyPress={(event: React.KeyboardEvent) => this.onSplashScreenItemKeyPress(event, item.onClick)}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<img src={item.iconSrc} alt="" />
|
||||
<div className="legendContainer">
|
||||
<div className="legend">{item.title}</div>
|
||||
<div className="description">{item.description}</div>
|
||||
</div>
|
||||
<form className="connectExplorerFormContainer">
|
||||
<div className="splashScreenContainer">
|
||||
<div className="splashScreen">
|
||||
<div className="title">
|
||||
Welcome to Cosmos DB
|
||||
<FeaturePanelLauncher />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="moreStuffContainer">
|
||||
<div className="moreStuffColumn commonTasks">
|
||||
<div className="title">Common Tasks</div>
|
||||
<ul>
|
||||
{commonTaskItems.map((item) => (
|
||||
<li
|
||||
className="focusable"
|
||||
key={`${item.title}${item.description}`}
|
||||
<div className="subtitle">Globally distributed, multi-model database service for any scale</div>
|
||||
<div className="mainButtonsContainer">
|
||||
{mainItems.map((item) => (
|
||||
<Stack
|
||||
horizontal
|
||||
className="mainButton focusable"
|
||||
key={`${item.title}`}
|
||||
onClick={item.onClick}
|
||||
onKeyPress={(event: React.KeyboardEvent) => this.onSplashScreenItemKeyPress(event, item.onClick)}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<img src={item.iconSrc} alt="" />
|
||||
<span className="oneLineContent" title={item.info}>
|
||||
{item.title}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="moreStuffColumn">
|
||||
<div className="title">Recents</div>
|
||||
<ul>
|
||||
{recentItems.map((item, index) => (
|
||||
<li key={`${item.title}${item.description}${index}`}>
|
||||
<img src={item.iconSrc} alt="" />
|
||||
<span className="twoLineContent">
|
||||
<Link onClick={item.onClick} title={item.info}>
|
||||
{item.title}
|
||||
</Link>
|
||||
<div className="description">{item.description}</div>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{recentItems.length > 0 && <Link onClick={() => onClearRecent()}>Clear Recents</Link>}
|
||||
</div>
|
||||
<div className="moreStuffColumn tipsContainer">
|
||||
<div className="title">Tips</div>
|
||||
<ul>
|
||||
{tipsItems.map((item) => (
|
||||
<li
|
||||
className="tipContainer focusable"
|
||||
key={`${item.title}${item.description}`}
|
||||
onClick={item.onClick}
|
||||
onKeyPress={(event: React.KeyboardEvent) => this.onSplashScreenItemKeyPress(event, item.onClick)}
|
||||
tabIndex={0}
|
||||
role="link"
|
||||
>
|
||||
<div className="title" title={item.info}>
|
||||
{item.title}
|
||||
<div>
|
||||
<img src={item.iconSrc} alt="" />
|
||||
</div>
|
||||
<div className="description">{item.description}</div>
|
||||
</li>
|
||||
<div className="legendContainer">
|
||||
<div className="legend">{item.title}</div>
|
||||
<div className={userContext.features.enableNewQuickstart ? "newDescription" : "description"}>
|
||||
{item.description}
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
))}
|
||||
<li>
|
||||
<a role="link" href={SplashScreen.seeMoreItemUrl} rel="noreferrer" target="_blank" tabIndex={0}>
|
||||
{SplashScreen.seeMoreItemTitle}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="moreStuffContainer">
|
||||
<div className="moreStuffColumn commonTasks">
|
||||
<div className="title">
|
||||
{userContext.features.enableNewQuickstart ? "Why Cosmos DB" : "Common Tasks"}
|
||||
</div>
|
||||
{userContext.features.enableNewQuickstart ? this.getNotebookItems() : this.getCommonTasksItems()}
|
||||
</div>
|
||||
<div className="moreStuffColumn">
|
||||
<div className="title">
|
||||
{userContext.features.enableNewQuickstart ? "Top 3 things you need to know" : "Recents"}
|
||||
</div>
|
||||
{userContext.features.enableNewQuickstart ? this.top3Items() : this.getRecentItems()}
|
||||
</div>
|
||||
<div className="moreStuffColumn tipsContainer">
|
||||
<div className="title">
|
||||
{userContext.features.enableNewQuickstart ? "Learning Resources" : "Tips"}
|
||||
</div>
|
||||
{userContext.features.enableNewQuickstart ? this.getLearningResourceItems() : this.getTipItems()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -202,35 +162,62 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
*/
|
||||
public createMainItems(): SplashScreenItem[] {
|
||||
const dataSampleUtil = this.createDataSampleUtil();
|
||||
const heroes: SplashScreenItem[] = [
|
||||
{
|
||||
|
||||
if (userContext.features.enableNewQuickstart) {
|
||||
const launchQuickstartBtn = {
|
||||
iconSrc: QuickStartIcon,
|
||||
title: "Launch quick start",
|
||||
description: "Launch a quick start tutorial to get started with sample data",
|
||||
// TODO: replace onClick function
|
||||
onClick: () => 1,
|
||||
};
|
||||
|
||||
const newContainerBtn = {
|
||||
iconSrc: ContainersIcon,
|
||||
title: `New ${getCollectionName()}`,
|
||||
description: "Create a new container for storage and throughput",
|
||||
onClick: () => this.container.onNewCollectionClicked(),
|
||||
};
|
||||
|
||||
const connectBtn = {
|
||||
iconSrc: ConnectIcon,
|
||||
title: "Connect",
|
||||
description: "Prefer using your own choice of tooling? Find the connection string you need to connect",
|
||||
// TODO: replace onClick function
|
||||
onClick: () => 2,
|
||||
};
|
||||
|
||||
return [launchQuickstartBtn, newContainerBtn, connectBtn];
|
||||
} else {
|
||||
const heroes: SplashScreenItem[] = [];
|
||||
|
||||
if (dataSampleUtil.isSampleContainerCreationSupported()) {
|
||||
heroes.push({
|
||||
iconSrc: SampleIcon,
|
||||
title: "Start with Sample",
|
||||
description: "Get started with a sample provided by Cosmos DB",
|
||||
onClick: () => dataSampleUtil.createSampleContainerAsync(),
|
||||
});
|
||||
}
|
||||
|
||||
heroes.push({
|
||||
iconSrc: NewContainerIcon,
|
||||
title: `New ${getCollectionName()}`,
|
||||
description: "Create a new container for storage and throughput",
|
||||
onClick: () => this.container.onNewCollectionClicked(),
|
||||
},
|
||||
];
|
||||
|
||||
if (dataSampleUtil.isSampleContainerCreationSupported()) {
|
||||
// Insert at the front
|
||||
heroes.unshift({
|
||||
iconSrc: SampleIcon,
|
||||
title: "Start with Sample",
|
||||
description: "Get started with a sample provided by Cosmos DB",
|
||||
onClick: () => dataSampleUtil.createSampleContainerAsync(),
|
||||
});
|
||||
}
|
||||
|
||||
if (useNotebook.getState().isPhoenix) {
|
||||
heroes.push({
|
||||
iconSrc: NewNotebookIcon,
|
||||
title: "New Notebook",
|
||||
description: "Create a notebook to start querying, visualizing, and modeling your data",
|
||||
onClick: () => this.container.onNewNotebookClicked(),
|
||||
});
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
heroes.push({
|
||||
iconSrc: NewNotebookIcon,
|
||||
title: "New Notebook",
|
||||
description: "Create a notebook to start querying, visualizing, and modeling your data",
|
||||
onClick: () => this.container.onNewNotebookClicked(),
|
||||
});
|
||||
}
|
||||
|
||||
return heroes;
|
||||
return heroes;
|
||||
}
|
||||
}
|
||||
|
||||
private createCommonTaskItems(): SplashScreenItem[] {
|
||||
@ -393,4 +380,174 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
private getNotebookItems(): JSX.Element {
|
||||
return (
|
||||
<Stack>
|
||||
<Stack className="notebookSplashScreenItem" horizontal style={{ marginBottom: 14 }}>
|
||||
<Image src={NotebooksIcon} />
|
||||
<Text className="itemText">Notebook - Easy to develop</Text>
|
||||
</Stack>
|
||||
<Stack className="notebookSplashScreenItem" horizontal style={{ marginBottom: 14 }}>
|
||||
<Image src={GreenCheckIcon} />
|
||||
<Text className="itemText">Notebook - Enterprise ready</Text>
|
||||
</Stack>
|
||||
<Stack className="notebookSplashScreenItem" horizontal style={{ marginBottom: 14 }}>
|
||||
<Image src={CostIcon} />
|
||||
<Text className="itemText">Notebook - Cost effective</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
private getCommonTasksItems(): JSX.Element {
|
||||
const commonTaskItems = this.createCommonTaskItems();
|
||||
return (
|
||||
<ul>
|
||||
{commonTaskItems.map((item) => (
|
||||
<li
|
||||
className="focusable"
|
||||
key={`${item.title}${item.description}`}
|
||||
onClick={item.onClick}
|
||||
onKeyPress={(event: React.KeyboardEvent) => this.onSplashScreenItemKeyPress(event, item.onClick)}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<img src={item.iconSrc} alt="" />
|
||||
<span className="oneLineContent" title={item.info}>
|
||||
{item.title}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
private top3Items(): JSX.Element {
|
||||
return (
|
||||
<Stack>
|
||||
<Stack style={{ marginBottom: 26 }}>
|
||||
<Stack horizontal verticalAlign="center" style={{ fontSize: 14 }}>
|
||||
<Link href="https://aka.ms/msl-modeling-partitioning-2" target="_blank" style={{ marginRight: 5 }}>
|
||||
Advanced Modeling Patterns
|
||||
</Link>
|
||||
<Image src={LinkIcon} />
|
||||
</Stack>
|
||||
<Text>
|
||||
Learn advanced strategies for managing relationships between data entities to optimize your database.
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack style={{ marginBottom: 26 }}>
|
||||
<Stack horizontal verticalAlign="center" style={{ fontSize: 14 }}>
|
||||
<Link href="https://aka.ms/msl-modeling-partitioning-1" target="_blank" style={{ marginRight: 5 }}>
|
||||
Partitioning Best Practices
|
||||
</Link>
|
||||
<Image src={LinkIcon} />
|
||||
</Stack>
|
||||
<Text>
|
||||
Learn to apply data model and partitioning strategies to support an efficient and scalable NoSQL database.
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Stack horizontal verticalAlign="center" style={{ fontSize: 14 }}>
|
||||
<Link href="https://aka.ms/msl-resource-planning" target="_blank" style={{ marginRight: 5 }}>
|
||||
Plan Your Resource Requirements
|
||||
</Link>
|
||||
<Image src={LinkIcon} />
|
||||
</Stack>
|
||||
<Text>
|
||||
Familiarize yourself with the various configuration options for a new Azure Cosmos DB SQL API account.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
private getRecentItems(): JSX.Element {
|
||||
const recentItems = this.createRecentItems()?.filter((item) => item.description !== "Notebook");
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<ul>
|
||||
{recentItems.map((item, index) => (
|
||||
<li key={`${item.title}${item.description}${index}`}>
|
||||
<img src={item.iconSrc} alt="" />
|
||||
<span className="twoLineContent">
|
||||
<Link onClick={item.onClick} title={item.info}>
|
||||
{item.title}
|
||||
</Link>
|
||||
<div className="description">{item.description}</div>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{recentItems.length > 0 && <Link onClick={() => this.clearMostRecent()}>Clear Recents</Link>}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
private getLearningResourceItems(): JSX.Element {
|
||||
return (
|
||||
<Stack>
|
||||
<Stack style={{ marginBottom: 26 }}>
|
||||
<Stack horizontal verticalAlign="center" style={{ fontSize: 14 }}>
|
||||
<Link href="https://aka.ms/msl-sdk-connect" target="_blank" style={{ marginRight: 5 }}>
|
||||
Get Started using th SQL API with the SDK
|
||||
</Link>
|
||||
<Image src={LinkIcon} />
|
||||
</Stack>
|
||||
<Text>Learn about the Azure Cosmos DB SDK, then download and use in a .NET application.</Text>
|
||||
</Stack>
|
||||
<Stack style={{ marginBottom: 26 }}>
|
||||
<Stack horizontal verticalAlign="center" style={{ fontSize: 14 }}>
|
||||
<Link href="https://aka.ms/msl-complex-queries" target="_blank" style={{ marginRight: 5 }}>
|
||||
Master Complex Queries
|
||||
</Link>
|
||||
<Image src={LinkIcon} />
|
||||
</Stack>
|
||||
<Text>Learn how to author complex queries using cross-products and correlated subqueries.</Text>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Stack horizontal verticalAlign="center" style={{ fontSize: 14 }}>
|
||||
<Link href="https://aka.ms/msl-move-data" target="_blank" style={{ marginRight: 5 }}>
|
||||
Migrate Your Data
|
||||
</Link>
|
||||
<Image src={LinkIcon} />
|
||||
</Stack>
|
||||
<Text>
|
||||
Migrate data into and out of Azure Cosmos DB SQL API using Azure services and open-source solutions.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
private getTipItems(): JSX.Element {
|
||||
const tipsItems = this.createTipsItems();
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{tipsItems.map((item) => (
|
||||
<li
|
||||
className="tipContainer focusable"
|
||||
key={`${item.title}${item.description}`}
|
||||
onClick={item.onClick}
|
||||
onKeyPress={(event: React.KeyboardEvent) => this.onSplashScreenItemKeyPress(event, item.onClick)}
|
||||
tabIndex={0}
|
||||
role="link"
|
||||
>
|
||||
<div className="title" title={item.info}>
|
||||
{item.title}
|
||||
</div>
|
||||
<div className="description">{item.description}</div>
|
||||
</li>
|
||||
))}
|
||||
<li>
|
||||
<a role="link" href={SplashScreen.seeMoreItemUrl} rel="noreferrer" target="_blank" tabIndex={0}>
|
||||
{SplashScreen.seeMoreItemTitle}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,60 @@ export const CassandraType = {
|
||||
Inet: "Inet",
|
||||
Smallint: "Smallint",
|
||||
Tinyint: "Tinyint",
|
||||
|
||||
List_Ascii: "List<Ascii>",
|
||||
List_Bigint: "List<Bigint>",
|
||||
List_Blob: "List<Blob>",
|
||||
List_Boolean: "List<Boolean>",
|
||||
List_Date: "List<Date>",
|
||||
List_Decimal: "List<Decimal>",
|
||||
List_Double: "List<Double>",
|
||||
List_Float: "List<Float>",
|
||||
List_Int: "List<Int>",
|
||||
List_Text: "List<Text>",
|
||||
List_Timestamp: "List<Timestamp>",
|
||||
List_Uuid: "List<Uuid>",
|
||||
List_Varchar: "List<Varchar>",
|
||||
List_Varint: "List<Varint>",
|
||||
List_Inet: "List<Inet>",
|
||||
List_Smallint: "List<Smallint>",
|
||||
List_Tinyint: "List<Tinyint>",
|
||||
|
||||
Map_Ascii: "Map<Ascii>",
|
||||
Map_Bigint: "Map<Bigint>",
|
||||
Map_Blob: "Map<Blob>",
|
||||
Map_Boolean: "Map<Boolean>",
|
||||
Map_Date: "Map<Date>",
|
||||
Map_Decimal: "Map<Decimal>",
|
||||
Map_Double: "Map<Double>",
|
||||
Map_Float: "Map<Float>",
|
||||
Map_Int: "Map<Int>",
|
||||
Map_Text: "Map<Text>",
|
||||
Map_Timestamp: "Map<Timestamp>",
|
||||
Map_Uuid: "Map<Uuid>",
|
||||
Map_Varchar: "Map<Varchar>",
|
||||
Map_Varint: "Map<Varint>",
|
||||
Map_Inet: "Map<Inet>",
|
||||
Map_Smallint: "Map<Smallint>",
|
||||
Map_Tinyint: "Map<Tinyint>",
|
||||
|
||||
Set_Ascii: "Set<Ascii>",
|
||||
Set_Bigint: "Set<Bigint>",
|
||||
Set_Blob: "Set<Blob>",
|
||||
Set_Boolean: "Set<Boolean>",
|
||||
Set_Date: "Set<Date>",
|
||||
Set_Decimal: "Set<Decimal>",
|
||||
Set_Double: "Set<Double>",
|
||||
Set_Float: "Set<Float>",
|
||||
Set_Int: "Set<Int>",
|
||||
Set_Text: "Set<Text>",
|
||||
Set_Timestamp: "Set<Timestamp>",
|
||||
Set_Uuid: "Set<Uuid>",
|
||||
Set_Varchar: "Set<Varchar>",
|
||||
Set_Varint: "Set<Varint>",
|
||||
Set_Inet: "Set<Inet>",
|
||||
Set_Smallint: "Set<Smallint>",
|
||||
Set_Tinyint: "Set<Tinyint>",
|
||||
};
|
||||
|
||||
export const ClauseRule = {
|
||||
|
@ -126,7 +126,7 @@ export function convertEntitiesToDocuments(
|
||||
};
|
||||
if (collection.partitionKey) {
|
||||
document["partitionKey"] = collection.partitionKey;
|
||||
document[collection.partitionKeyProperty] = entity.PartitionKey._;
|
||||
document[collection.partitionKeyProperties[0]] = entity.PartitionKey._;
|
||||
document["partitionKeyValue"] = entity.PartitionKey._;
|
||||
}
|
||||
for (var property in entity) {
|
||||
|
@ -74,7 +74,7 @@ export default class ConflictsTab extends TabsBase {
|
||||
this.partitionKey = options.partitionKey || (this.collection && this.collection.partitionKey);
|
||||
this.conflictIds = options.conflictIds;
|
||||
this.partitionKeyPropertyHeader =
|
||||
(this.collection && this.collection.partitionKeyPropertyHeader) || this._getPartitionKeyPropertyHeader();
|
||||
this.collection?.partitionKeyPropertyHeaders?.[0] || this._getPartitionKeyPropertyHeader();
|
||||
this.partitionKeyProperty = !!this.partitionKeyPropertyHeader
|
||||
? this.partitionKeyPropertyHeader.replace(/[/]+/g, ".").substr(1).replace(/[']+/g, "")
|
||||
: null;
|
||||
|
@ -143,16 +143,18 @@
|
||||
<tr>
|
||||
<th class="documentsGridHeader" data-bind="text: idHeader" tabindex="0"></th>
|
||||
<!-- ko if: showPartitionKey -->
|
||||
<!-- ko foreach: partitionKeyPropertyHeaders -->
|
||||
<th
|
||||
class="documentsGridHeader documentsGridPartition evenlySpacedHeader"
|
||||
data-bind="
|
||||
attr: {
|
||||
title: partitionKeyPropertyHeader
|
||||
title: $data
|
||||
},
|
||||
text: partitionKeyPropertyHeader"
|
||||
text: $data"
|
||||
tabindex="0"
|
||||
></th>
|
||||
<!-- /ko -->
|
||||
<!-- /ko -->
|
||||
<th
|
||||
class="refreshColHeader"
|
||||
role="button"
|
||||
@ -182,13 +184,13 @@
|
||||
tabindex="0"
|
||||
>
|
||||
<td class="tabdocumentsGridElement"><a data-bind="text: $data.id, attr: { title: $data.id }"></a></td>
|
||||
<!-- ko if: $data.partitionKeyProperty -->
|
||||
<td class="tabdocumentsGridElement" colspan="2">
|
||||
<a
|
||||
data-bind="text: $data.stringPartitionKeyValue, attr: { title: $data.stringPartitionKeyValue }"
|
||||
></a>
|
||||
<!-- ko if: $data.partitionKeyProperties -->
|
||||
<!-- ko foreach: $data.stringPartitionKeyValues -->
|
||||
<td class="tabdocumentsGridElement" data-bind="colspan: $parent.stringPartitionKeyValues.length + 1">
|
||||
<a data-bind="text: $data, attr: { title: $data }"></a>
|
||||
</td>
|
||||
<!-- /ko -->
|
||||
<!-- /ko -->
|
||||
</tr>
|
||||
<!-- /ko -->
|
||||
</tbody>
|
||||
|
@ -50,7 +50,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
public editorState: ko.Observable<ViewModels.DocumentExplorerState>;
|
||||
public newDocumentButton: ViewModels.Button;
|
||||
public saveNewDocumentButton: ViewModels.Button;
|
||||
public saveExisitingDocumentButton: ViewModels.Button;
|
||||
public saveExistingDocumentButton: ViewModels.Button;
|
||||
public discardNewDocumentChangesButton: ViewModels.Button;
|
||||
public discardExisitingDocumentChangesButton: ViewModels.Button;
|
||||
public deleteExisitingDocumentButton: ViewModels.Button;
|
||||
@ -65,8 +65,8 @@ export default class DocumentsTab extends TabsBase {
|
||||
|
||||
// TODO need to refactor
|
||||
public partitionKey: DataModels.PartitionKey;
|
||||
public partitionKeyPropertyHeader: string;
|
||||
public partitionKeyProperty: string;
|
||||
public partitionKeyPropertyHeaders: string[];
|
||||
public partitionKeyProperties: string[];
|
||||
public documentIds: ko.ObservableArray<DocumentId>;
|
||||
|
||||
private _documentsIterator: QueryIterator<ItemDefinition & Resource>;
|
||||
@ -90,11 +90,10 @@ export default class DocumentsTab extends TabsBase {
|
||||
this._resourceTokenPartitionKey = options.resourceTokenPartitionKey;
|
||||
this.documentIds = options.documentIds;
|
||||
|
||||
this.partitionKeyPropertyHeader =
|
||||
(this.collection && this.collection.partitionKeyPropertyHeader) || this._getPartitionKeyPropertyHeader();
|
||||
this.partitionKeyProperty = !!this.partitionKeyPropertyHeader
|
||||
? this.partitionKeyPropertyHeader.replace(/[/]+/g, ".").substr(1).replace(/[']+/g, "")
|
||||
: null;
|
||||
this.partitionKeyPropertyHeaders = this.collection?.partitionKeyPropertyHeaders || this.partitionKey?.paths;
|
||||
this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) =>
|
||||
partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, "")
|
||||
);
|
||||
|
||||
this.isFilterExpanded = ko.observable<boolean>(false);
|
||||
this.isFilterCreated = ko.observable<boolean>(true);
|
||||
@ -227,7 +226,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
}),
|
||||
};
|
||||
|
||||
this.saveExisitingDocumentButton = {
|
||||
this.saveExistingDocumentButton = {
|
||||
enabled: ko.computed<boolean>(() => {
|
||||
switch (this.editorState()) {
|
||||
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
|
||||
@ -445,8 +444,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
savedDocument,
|
||||
this.partitionKey as PartitionKeyDefinition
|
||||
);
|
||||
const partitionKeyValue = partitionKeyValueArray && partitionKeyValueArray[0];
|
||||
let id = new DocumentId(this, savedDocument, partitionKeyValue);
|
||||
let id = new DocumentId(this, savedDocument, partitionKeyValueArray);
|
||||
let ids = this.documentIds();
|
||||
ids.push(id);
|
||||
|
||||
@ -489,14 +487,12 @@ export default class DocumentsTab extends TabsBase {
|
||||
return Q();
|
||||
};
|
||||
|
||||
public onSaveExisitingDocumentClick = (): Promise<any> => {
|
||||
public onSaveExistingDocumentClick = (): Promise<any> => {
|
||||
const selectedDocumentId = this.selectedDocumentId();
|
||||
const documentContent = JSON.parse(this.selectedDocumentContent());
|
||||
|
||||
const partitionKeyValueArray = extractPartitionKey(documentContent, this.partitionKey as PartitionKeyDefinition);
|
||||
const partitionKeyValue = partitionKeyValueArray && partitionKeyValueArray[0];
|
||||
|
||||
selectedDocumentId.partitionKeyValue = partitionKeyValue;
|
||||
selectedDocumentId.partitionKeyValue = partitionKeyValueArray;
|
||||
|
||||
this.isExecutionError(false);
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.UpdateDocument, {
|
||||
@ -800,7 +796,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
}
|
||||
|
||||
public buildQuery(filter: string): string {
|
||||
return QueryUtils.buildDocumentsQuery(filter, this.partitionKeyProperty, this.partitionKey);
|
||||
return QueryUtils.buildDocumentsQuery(filter, this.partitionKeyProperties, this.partitionKey);
|
||||
}
|
||||
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
@ -844,16 +840,16 @@ export default class DocumentsTab extends TabsBase {
|
||||
});
|
||||
}
|
||||
|
||||
if (this.saveExisitingDocumentButton.visible()) {
|
||||
if (this.saveExistingDocumentButton.visible()) {
|
||||
const label = "Update";
|
||||
buttons.push({
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: this.onSaveExisitingDocumentClick,
|
||||
onCommandClick: this.onSaveExistingDocumentClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.saveExisitingDocumentButton.enabled(),
|
||||
disabled: !this.saveExistingDocumentButton.enabled(),
|
||||
});
|
||||
}
|
||||
|
||||
@ -899,8 +895,8 @@ export default class DocumentsTab extends TabsBase {
|
||||
this.saveNewDocumentButton.enabled,
|
||||
this.discardNewDocumentChangesButton.visible,
|
||||
this.discardNewDocumentChangesButton.enabled,
|
||||
this.saveExisitingDocumentButton.visible,
|
||||
this.saveExisitingDocumentButton.enabled,
|
||||
this.saveExistingDocumentButton.visible,
|
||||
this.saveExistingDocumentButton.enabled,
|
||||
this.discardExisitingDocumentChangesButton.visible,
|
||||
this.discardExisitingDocumentChangesButton.enabled,
|
||||
this.deleteExisitingDocumentButton.visible,
|
||||
@ -910,16 +906,6 @@ export default class DocumentsTab extends TabsBase {
|
||||
this.updateNavbarWithTabsButtons();
|
||||
}
|
||||
|
||||
private _getPartitionKeyPropertyHeader(): string {
|
||||
return (
|
||||
(this.partitionKey &&
|
||||
this.partitionKey.paths &&
|
||||
this.partitionKey.paths.length > 0 &&
|
||||
this.partitionKey.paths[0]) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
public static _createUploadButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Upload Item";
|
||||
return {
|
||||
|
@ -29,15 +29,19 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
||||
super(options);
|
||||
this.lastFilterContents = ko.observableArray<string>(['{"id":"foo"}', "{ qty: { $gte: 20 } }"]);
|
||||
|
||||
if (this.partitionKeyProperty && ~this.partitionKeyProperty.indexOf(`"`)) {
|
||||
this.partitionKeyProperty = this.partitionKeyProperty.replace(/["]+/g, "");
|
||||
}
|
||||
this.partitionKeyProperties = this.partitionKeyProperties?.map((partitionKeyProperty, i) => {
|
||||
if (partitionKeyProperty && ~partitionKeyProperty.indexOf(`"`)) {
|
||||
partitionKeyProperty = partitionKeyProperty.replace(/["]+/g, "");
|
||||
}
|
||||
|
||||
if (this.partitionKeyProperty && this.partitionKeyProperty.indexOf("$v") > -1) {
|
||||
// From $v.shard.$v.key.$v > shard.key
|
||||
this.partitionKeyProperty = this.partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, "");
|
||||
this.partitionKeyPropertyHeader = "/" + this.partitionKeyProperty;
|
||||
}
|
||||
if (partitionKeyProperty && partitionKeyProperty.indexOf("$v") > -1) {
|
||||
// From $v.shard.$v.key.$v > shard.key
|
||||
partitionKeyProperty = partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, "");
|
||||
this.partitionKeyPropertyHeaders[i] = "/" + partitionKeyProperty;
|
||||
}
|
||||
|
||||
return partitionKeyProperty;
|
||||
});
|
||||
|
||||
this.isFilterExpanded = ko.observable<boolean>(true);
|
||||
super.buildCommandBarOptions.bind(this);
|
||||
@ -52,12 +56,9 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
||||
tabTitle: this.tabTitle(),
|
||||
});
|
||||
|
||||
if (
|
||||
this.partitionKeyProperty &&
|
||||
this.partitionKeyProperty !== "_id" &&
|
||||
!this._hasShardKeySpecified(documentContent)
|
||||
) {
|
||||
const message = `The document is lacking the shard property: ${this.partitionKeyProperty}`;
|
||||
const partitionKeyProperty = this.partitionKeyProperties?.[0];
|
||||
if (partitionKeyProperty !== "_id" && !this._hasShardKeySpecified(documentContent)) {
|
||||
const message = `The document is lacking the shard property: ${partitionKeyProperty}`;
|
||||
this.displayedError(message);
|
||||
let that = this;
|
||||
setTimeout(() => {
|
||||
@ -79,7 +80,12 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
||||
|
||||
this.isExecutionError(false);
|
||||
this.isExecuting(true);
|
||||
return createDocument(this.collection.databaseId, this.collection, this.partitionKeyProperty, documentContent)
|
||||
return createDocument(
|
||||
this.collection.databaseId,
|
||||
this.collection,
|
||||
this.partitionKeyProperties?.[0],
|
||||
documentContent
|
||||
)
|
||||
.then(
|
||||
(savedDocument: any) => {
|
||||
let partitionKeyArray = extractPartitionKey(
|
||||
@ -87,9 +93,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
||||
this._getPartitionKeyDefinition() as PartitionKeyDefinition
|
||||
);
|
||||
|
||||
let partitionKeyValue = partitionKeyArray && partitionKeyArray[0];
|
||||
|
||||
let id = new ObjectId(this, savedDocument, partitionKeyValue);
|
||||
let id = new ObjectId(this, savedDocument, partitionKeyArray);
|
||||
let ids = this.documentIds();
|
||||
ids.push(id);
|
||||
delete savedDocument._self;
|
||||
@ -128,7 +132,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
||||
.finally(() => this.isExecuting(false));
|
||||
};
|
||||
|
||||
public onSaveExisitingDocumentClick = (): Promise<any> => {
|
||||
public onSaveExistingDocumentClick = (): Promise<any> => {
|
||||
const selectedDocumentId = this.selectedDocumentId();
|
||||
const documentContent = this.selectedDocumentContent();
|
||||
this.isExecutionError(false);
|
||||
@ -151,9 +155,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
||||
this._getPartitionKeyDefinition() as PartitionKeyDefinition
|
||||
);
|
||||
|
||||
let partitionKeyValue = partitionKeyArray && partitionKeyArray[0];
|
||||
|
||||
const id = new ObjectId(this, updatedDocument, partitionKeyValue);
|
||||
const id = new ObjectId(this, updatedDocument, partitionKeyArray);
|
||||
documentId.id(id.id());
|
||||
}
|
||||
});
|
||||
@ -214,7 +216,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
||||
})
|
||||
.map((rawDocument: any) => {
|
||||
const partitionKeyValue = rawDocument._partitionKeyValue;
|
||||
return new DocumentId(this, rawDocument, partitionKeyValue);
|
||||
return new DocumentId(this, rawDocument, [partitionKeyValue]);
|
||||
});
|
||||
|
||||
const merged = currentDocuments.concat(nextDocumentIds);
|
||||
@ -303,7 +305,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
||||
// Convert BsonSchema2 to /path format
|
||||
partitionKey = {
|
||||
kind: partitionKey.kind,
|
||||
paths: ["/" + this.partitionKeyProperty.replace(/\./g, "/")],
|
||||
paths: ["/" + this.partitionKeyProperties?.[0].replace(/\./g, "/")],
|
||||
version: partitionKey.version,
|
||||
};
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { stringifyNotebook, toJS } from "@nteract/commutable";
|
||||
import * as ko from "knockout";
|
||||
import * as Q from "q";
|
||||
import { userContext } from "UserContext";
|
||||
import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg";
|
||||
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
|
||||
import CutIcon from "../../../images/notebook/Notebook-cut.svg";
|
||||
@ -129,14 +130,16 @@ export default class NotebookTabV2 extends NotebookTabBase {
|
||||
});
|
||||
}
|
||||
|
||||
saveButtonChildren.push({
|
||||
iconName: "PublishContent",
|
||||
onCommandClick: async () => await this.publishToGallery(),
|
||||
commandButtonLabel: publishLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: publishLabel,
|
||||
});
|
||||
if (userContext.features.publicGallery) {
|
||||
saveButtonChildren.push({
|
||||
iconName: "PublishContent",
|
||||
onCommandClick: async () => await this.publishToGallery(),
|
||||
commandButtonLabel: publishLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: publishLabel,
|
||||
});
|
||||
}
|
||||
|
||||
let buttons: CommandButtonComponentProps[] = [
|
||||
{
|
||||
|
@ -215,13 +215,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
{
|
||||
metric: "Request Charge",
|
||||
value: this.state.requestChargeDisplayText,
|
||||
toolTip: "",
|
||||
toolTip: "Request Charge",
|
||||
isQueryMetricsEnabled: true,
|
||||
},
|
||||
{
|
||||
metric: "Showing Results",
|
||||
value: this.state.showingDocumentsDisplayText,
|
||||
toolTip: "",
|
||||
toolTip: "Showing Results",
|
||||
isQueryMetricsEnabled: true,
|
||||
},
|
||||
{
|
||||
|
@ -25,7 +25,8 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Computed<boolean>;
|
||||
constructor(
|
||||
private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo,
|
||||
private getDatabaseAccount: () => DataModels.DatabaseAccount
|
||||
private getDatabaseAccount: () => DataModels.DatabaseAccount,
|
||||
private getTabId: () => string
|
||||
) {}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
@ -33,6 +34,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
||||
<NotebookTerminalComponent
|
||||
notebookServerInfo={this.getNotebookServerInfo()}
|
||||
databaseAccount={this.getDatabaseAccount()}
|
||||
tabId={this.getTabId()}
|
||||
/>
|
||||
) : (
|
||||
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner>
|
||||
@ -50,7 +52,8 @@ export default class TerminalTab extends TabsBase {
|
||||
this.container = options.container;
|
||||
this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter(
|
||||
() => this.getNotebookServerInfo(options),
|
||||
() => userContext?.databaseAccount
|
||||
() => userContext?.databaseAccount,
|
||||
() => this.tabId
|
||||
);
|
||||
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||
if (
|
||||
|
@ -37,7 +37,8 @@ describe("Collection", () => {
|
||||
version: 2,
|
||||
});
|
||||
collection = generateMockCollectionWithDataModel(collectionsDataModel);
|
||||
expect(collection.partitionKeyProperty).toBe("somePartitionKey.anotherPartitionKey");
|
||||
expect(collection.partitionKeyProperties.length).toBe(1);
|
||||
expect(collection.partitionKeyProperties[0]).toBe("somePartitionKey.anotherPartitionKey");
|
||||
});
|
||||
|
||||
it("should strip out forward slashes from single partition key paths", () => {
|
||||
@ -47,7 +48,8 @@ describe("Collection", () => {
|
||||
version: 2,
|
||||
});
|
||||
collection = generateMockCollectionWithDataModel(collectionsDataModel);
|
||||
expect(collection.partitionKeyProperty).toBe("somePartitionKey");
|
||||
expect(collection.partitionKeyProperties.length).toBe(1);
|
||||
expect(collection.partitionKeyProperties[0]).toBe("somePartitionKey");
|
||||
});
|
||||
});
|
||||
|
||||
@ -61,7 +63,8 @@ describe("Collection", () => {
|
||||
version: 2,
|
||||
});
|
||||
collection = generateMockCollectionWithDataModel(collectionsDataModel);
|
||||
expect(collection.partitionKeyPropertyHeader).toBe("/somePartitionKey/anotherPartitionKey");
|
||||
expect(collection.partitionKeyPropertyHeaders.length).toBe(1);
|
||||
expect(collection.partitionKeyPropertyHeaders[0]).toBe("/somePartitionKey/anotherPartitionKey");
|
||||
});
|
||||
|
||||
it("should preserve forward slash on a single partition key", () => {
|
||||
@ -71,7 +74,8 @@ describe("Collection", () => {
|
||||
version: 2,
|
||||
});
|
||||
collection = generateMockCollectionWithDataModel(collectionsDataModel);
|
||||
expect(collection.partitionKeyPropertyHeader).toBe("/somePartitionKey");
|
||||
expect(collection.partitionKeyPropertyHeaders.length).toBe(1);
|
||||
expect(collection.partitionKeyPropertyHeaders[0]).toBe("/somePartitionKey");
|
||||
});
|
||||
|
||||
it("should be null if there is no partition key", () => {
|
||||
@ -81,7 +85,7 @@ describe("Collection", () => {
|
||||
kind: "Hash",
|
||||
});
|
||||
collection = generateMockCollectionWithDataModel(collectionsDataModel);
|
||||
expect(collection.partitionKeyPropertyHeader).toBeNull();
|
||||
expect(collection.partitionKeyPropertyHeaders.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -50,8 +50,8 @@ export default class Collection implements ViewModels.Collection {
|
||||
public rid: string;
|
||||
public databaseId: string;
|
||||
public partitionKey: DataModels.PartitionKey;
|
||||
public partitionKeyPropertyHeader: string;
|
||||
public partitionKeyProperty: string;
|
||||
public partitionKeyPropertyHeaders: string[];
|
||||
public partitionKeyProperties: string[];
|
||||
public id: ko.Observable<string>;
|
||||
public defaultTtl: ko.Observable<number>;
|
||||
public indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
||||
@ -120,31 +120,25 @@ export default class Collection implements ViewModels.Collection {
|
||||
this.requestSchema = data.requestSchema;
|
||||
this.geospatialConfig = ko.observable(data.geospatialConfig);
|
||||
|
||||
// TODO fix this to only replace non-excaped single quotes
|
||||
this.partitionKeyProperty =
|
||||
(this.partitionKey &&
|
||||
this.partitionKey.paths &&
|
||||
this.partitionKey.paths.length &&
|
||||
this.partitionKey.paths.length > 0 &&
|
||||
this.partitionKey.paths[0].replace(/[/]+/g, ".").substr(1).replace(/[']+/g, "")) ||
|
||||
null;
|
||||
this.partitionKeyPropertyHeader =
|
||||
(this.partitionKey &&
|
||||
this.partitionKey.paths &&
|
||||
this.partitionKey.paths.length > 0 &&
|
||||
this.partitionKey.paths[0]) ||
|
||||
null;
|
||||
this.partitionKeyPropertyHeaders = this.partitionKey?.paths;
|
||||
this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader, i) => {
|
||||
// TODO fix this to only replace non-excaped single quotes
|
||||
let partitionKeyProperty = partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, "");
|
||||
|
||||
if (userContext.apiType === "Mongo" && this.partitionKeyProperty && ~this.partitionKeyProperty.indexOf(`"`)) {
|
||||
this.partitionKeyProperty = this.partitionKeyProperty.replace(/["]+/g, "");
|
||||
}
|
||||
if (userContext.apiType === "Mongo" && partitionKeyProperty) {
|
||||
if (~partitionKeyProperty.indexOf(`"`)) {
|
||||
partitionKeyProperty = partitionKeyProperty.replace(/["]+/g, "");
|
||||
}
|
||||
// TODO #10738269 : Add this logic in a derived class for Mongo
|
||||
if (partitionKeyProperty.indexOf("$v") > -1) {
|
||||
// From $v.shard.$v.key.$v > shard.key
|
||||
partitionKeyProperty = partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, "");
|
||||
this.partitionKeyPropertyHeaders[i] = partitionKeyProperty;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO #10738269 : Add this logic in a derived class for Mongo
|
||||
if (userContext.apiType === "Mongo" && this.partitionKeyProperty && this.partitionKeyProperty.indexOf("$v") > -1) {
|
||||
// From $v.shard.$v.key.$v > shard.key
|
||||
this.partitionKeyProperty = this.partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, "");
|
||||
this.partitionKeyPropertyHeader = "/" + this.partitionKeyProperty;
|
||||
}
|
||||
return partitionKeyProperty;
|
||||
});
|
||||
|
||||
this.documentIds = ko.observableArray<DocumentId>([]);
|
||||
this.isCollectionExpanded = ko.observable<boolean>(false);
|
||||
@ -308,7 +302,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
collectionName: this.id(),
|
||||
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: "Items",
|
||||
tabTitle: this.rawDataModel.id + " - Items",
|
||||
});
|
||||
this.documentIds([]);
|
||||
|
||||
@ -316,7 +310,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
partitionKey: this.partitionKey,
|
||||
documentIds: ko.observableArray<DocumentId>([]),
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "Items",
|
||||
title: this.rawDataModel.id + " - Items",
|
||||
collection: this,
|
||||
node: this,
|
||||
tabPath: `${this.databaseId}>${this.id()}>Documents`,
|
||||
@ -471,7 +465,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
|
||||
collection: this,
|
||||
masterKey: userContext.masterKey || "",
|
||||
collectionPartitionKeyProperty: this.partitionKeyProperty,
|
||||
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
|
||||
collectionId: this.id(),
|
||||
databaseId: this.databaseId,
|
||||
isTabsContentExpanded: this.container.isTabsContentExpanded,
|
||||
@ -529,7 +523,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
};
|
||||
|
||||
public onSchemaAnalyzerClick = async () => {
|
||||
if (useNotebook.getState().isPhoenix) {
|
||||
if (useNotebook.getState().isPhoenixFeatures) {
|
||||
await this.container.allocateContainer();
|
||||
}
|
||||
useSelectedNode.getState().setSelectedNode(this);
|
||||
@ -710,7 +704,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
tabPath: "",
|
||||
collection: this,
|
||||
masterKey: userContext.masterKey || "",
|
||||
collectionPartitionKeyProperty: this.partitionKeyProperty,
|
||||
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
|
||||
collectionId: this.id(),
|
||||
databaseId: this.databaseId,
|
||||
isTabsContentExpanded: this.container.isTabsContentExpanded,
|
||||
|
@ -150,7 +150,7 @@ export default class ConflictId {
|
||||
partitionKeyValueResolved
|
||||
);
|
||||
|
||||
documentId.partitionKeyProperty = this.partitionKeyProperty;
|
||||
documentId.partitionKeyProperties = [this.partitionKeyProperty];
|
||||
documentId.partitionKey = this.partitionKey;
|
||||
|
||||
return documentId;
|
||||
|
@ -9,21 +9,21 @@ export default class DocumentId {
|
||||
public self: string;
|
||||
public ts: string;
|
||||
public id: ko.Observable<string>;
|
||||
public partitionKeyProperty: string;
|
||||
public partitionKeyProperties: string[];
|
||||
public partitionKey: DataModels.PartitionKey;
|
||||
public partitionKeyValue: any;
|
||||
public stringPartitionKeyValue: string;
|
||||
public partitionKeyValue: any[];
|
||||
public stringPartitionKeyValues: string[];
|
||||
public isDirty: ko.Observable<boolean>;
|
||||
|
||||
constructor(container: DocumentsTab, data: any, partitionKeyValue: any) {
|
||||
constructor(container: DocumentsTab, data: any, partitionKeyValue: any[]) {
|
||||
this.container = container;
|
||||
this.self = data._self;
|
||||
this.rid = data._rid;
|
||||
this.ts = data._ts;
|
||||
this.partitionKeyValue = partitionKeyValue;
|
||||
this.partitionKeyProperty = container && container.partitionKeyProperty;
|
||||
this.partitionKeyProperties = container?.partitionKeyProperties;
|
||||
this.partitionKey = container && container.partitionKey;
|
||||
this.stringPartitionKeyValue = this.getPartitionKeyValueAsString();
|
||||
this.stringPartitionKeyValues = this.getPartitionKeyValueAsString();
|
||||
this.id = ko.observable(data.id);
|
||||
this.isDirty = ko.observable(false);
|
||||
}
|
||||
@ -46,34 +46,35 @@ export default class DocumentId {
|
||||
}
|
||||
|
||||
public partitionKeyHeader(): Object {
|
||||
if (!this.partitionKeyProperty) {
|
||||
if (!this.partitionKeyProperties || this.partitionKeyProperties.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (this.partitionKeyValue === undefined) {
|
||||
if (!this.partitionKeyValue || this.partitionKeyValue.length === 0) {
|
||||
return [{}];
|
||||
}
|
||||
|
||||
return [this.partitionKeyValue];
|
||||
}
|
||||
|
||||
public getPartitionKeyValueAsString(): string {
|
||||
const partitionKeyValue: any = this.partitionKeyValue;
|
||||
const typeOfPartitionKeyValue: string = typeof partitionKeyValue;
|
||||
public getPartitionKeyValueAsString(): string[] {
|
||||
return this.partitionKeyValue?.map((partitionKeyValue) => {
|
||||
const typeOfPartitionKeyValue: string = typeof partitionKeyValue;
|
||||
|
||||
if (
|
||||
typeOfPartitionKeyValue === "undefined" ||
|
||||
typeOfPartitionKeyValue === "null" ||
|
||||
typeOfPartitionKeyValue === "object"
|
||||
) {
|
||||
return "";
|
||||
}
|
||||
if (
|
||||
typeOfPartitionKeyValue === "undefined" ||
|
||||
typeOfPartitionKeyValue === "null" ||
|
||||
typeOfPartitionKeyValue === "object"
|
||||
) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (typeOfPartitionKeyValue === "string") {
|
||||
return partitionKeyValue;
|
||||
}
|
||||
if (typeOfPartitionKeyValue === "string") {
|
||||
return partitionKeyValue;
|
||||
}
|
||||
|
||||
return JSON.stringify(partitionKeyValue);
|
||||
return JSON.stringify(partitionKeyValue);
|
||||
});
|
||||
}
|
||||
|
||||
public async loadDocument(): Promise<void> {
|
||||
|
@ -22,8 +22,8 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
||||
public rid: string;
|
||||
public rawDataModel: DataModels.Collection;
|
||||
public partitionKey: DataModels.PartitionKey;
|
||||
public partitionKeyProperty: string;
|
||||
public partitionKeyPropertyHeader: string;
|
||||
public partitionKeyProperties: string[];
|
||||
public partitionKeyPropertyHeaders: string[];
|
||||
public id: ko.Observable<string>;
|
||||
public children: ko.ObservableArray<ViewModels.TreeNode>;
|
||||
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
|
||||
|
@ -121,7 +121,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
children: [],
|
||||
};
|
||||
|
||||
if (!useNotebook.getState().isPhoenix) {
|
||||
if (!useNotebook.getState().isPhoenixNotebooks) {
|
||||
notebooksTree.children.push(buildNotebooksTemporarilyDownTree());
|
||||
} else {
|
||||
if (galleryContentRoot) {
|
||||
@ -130,7 +130,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
|
||||
if (
|
||||
myNotebooksContentRoot &&
|
||||
useNotebook.getState().isPhoenix &&
|
||||
useNotebook.getState().isPhoenixNotebooks &&
|
||||
useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected
|
||||
) {
|
||||
notebooksTree.children.push(buildMyNotebooksTree());
|
||||
@ -299,7 +299,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
},
|
||||
];
|
||||
|
||||
if (item.type === NotebookContentItemType.Notebook) {
|
||||
if (item.type === NotebookContentItemType.Notebook && userContext.features.publicGallery) {
|
||||
items.push({
|
||||
label: "Publish to gallery",
|
||||
iconSrc: PublishIcon,
|
||||
@ -516,7 +516,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
isNotebookEnabled &&
|
||||
userContext.apiType === "Mongo" &&
|
||||
isPublicInternetAccessAllowed() &&
|
||||
useNotebook.getState().isPhoenix
|
||||
useNotebook.getState().isPhoenixFeatures
|
||||
) {
|
||||
children.push({
|
||||
label: "Schema (Preview)",
|
||||
|
@ -1,4 +1,5 @@
|
||||
import ko from "knockout";
|
||||
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation";
|
||||
import { GetGithubClientId } from "Utils/GitHubUtils";
|
||||
import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
|
||||
import { configContext } from "../ConfigContext";
|
||||
@ -484,7 +485,7 @@ export class JunoClient {
|
||||
// public for tests
|
||||
public static getJunoEndpoint(): string {
|
||||
const junoEndpoint = userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
|
||||
if (configContext.allowedJunoOrigins.indexOf(new URL(junoEndpoint).origin) === -1) {
|
||||
if (!validateEndpoint(junoEndpoint, allowedJunoOrigins)) {
|
||||
const error = `${junoEndpoint} not allowed as juno endpoint`;
|
||||
console.error(error);
|
||||
throw new Error(error);
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation";
|
||||
import {
|
||||
Areas,
|
||||
ConnectionStatusType,
|
||||
@ -8,16 +10,21 @@ import {
|
||||
HttpStatusCodes,
|
||||
Notebook,
|
||||
} from "../Common/Constants";
|
||||
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
|
||||
import { getErrorMessage, getErrorStack } from "../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import {
|
||||
ContainerConnectionInfo,
|
||||
ContainerInfo,
|
||||
IContainerData,
|
||||
IMaxAllocationTimeExceeded,
|
||||
IMaxDbAccountsPerUserExceeded,
|
||||
IMaxUsersPerDbAccountExceeded,
|
||||
IPhoenixConnectionInfoResult,
|
||||
IProvisionData,
|
||||
IResponse,
|
||||
IValidationError,
|
||||
PhoenixErrorType,
|
||||
} from "../Contracts/DataModels";
|
||||
import { useNotebook } from "../Explorer/Notebook/useNotebook";
|
||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||
@ -44,23 +51,25 @@ export class PhoenixClient {
|
||||
provisionData: IProvisionData,
|
||||
operation: string
|
||||
): Promise<IResponse<IPhoenixConnectionInfoResult>> {
|
||||
let response;
|
||||
try {
|
||||
const response = await fetch(`${this.getPhoenixControlPlanePathPrefix()}/containerconnections`, {
|
||||
response = await fetch(`${this.getPhoenixControlPlanePathPrefix()}/containerconnections`, {
|
||||
method: operation === "allocate" ? "POST" : "PATCH",
|
||||
headers: PhoenixClient.getHeaders(),
|
||||
body: JSON.stringify(provisionData),
|
||||
});
|
||||
|
||||
let data: IPhoenixConnectionInfoResult;
|
||||
if (response.status === HttpStatusCodes.OK) {
|
||||
data = await response.json();
|
||||
const responseJson = await response?.json();
|
||||
if (response.status === HttpStatusCodes.Forbidden) {
|
||||
throw new Error(this.ConvertToForbiddenErrorString(responseJson));
|
||||
}
|
||||
return {
|
||||
status: response.status,
|
||||
data,
|
||||
data: responseJson,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (response.status === HttpStatusCodes.Forbidden) {
|
||||
error.status = HttpStatusCodes.Forbidden;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -107,6 +116,18 @@ export class PhoenixClient {
|
||||
});
|
||||
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkModalDialog(
|
||||
"Disconnected",
|
||||
"Disconnected from temporary workspace. Please click on connect button to connect to temporary workspace."
|
||||
);
|
||||
throw new AbortError(response.statusText);
|
||||
} else if (response?.status === HttpStatusCodes.Forbidden) {
|
||||
const validationMessage = this.ConvertToForbiddenErrorString(await response.json());
|
||||
if (validationMessage) {
|
||||
useDialog.getState().showOkModalDialog("Connection Failed", `${validationMessage}`);
|
||||
}
|
||||
throw new AbortError(response.statusText);
|
||||
}
|
||||
throw new Error(response.statusText);
|
||||
@ -139,13 +160,35 @@ export class PhoenixClient {
|
||||
}
|
||||
|
||||
public async isDbAcountWhitelisted(): Promise<boolean> {
|
||||
const startKey = TelemetryProcessor.traceStart(Action.PhoenixDBAccountAllowed, {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
});
|
||||
try {
|
||||
const response = await window.fetch(`${this.getPhoenixControlPlanePathPrefix()}`, {
|
||||
method: "GET",
|
||||
headers: PhoenixClient.getHeaders(),
|
||||
});
|
||||
if (response.status !== HttpStatusCodes.OK) {
|
||||
throw new Error(`Received status code: ${response?.status}`);
|
||||
}
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.PhoenixDBAccountAllowed,
|
||||
{
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
},
|
||||
startKey
|
||||
);
|
||||
return response.status === HttpStatusCodes.OK;
|
||||
} catch (error) {
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.PhoenixDBAccountAllowed,
|
||||
{
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
Logger.logError(getErrorMessage(error), "PhoenixClient/IsDbAcountWhitelisted");
|
||||
return false;
|
||||
}
|
||||
@ -154,7 +197,7 @@ export class PhoenixClient {
|
||||
public static getPhoenixEndpoint(): string {
|
||||
const phoenixEndpoint =
|
||||
userContext.features.phoenixEndpoint ?? userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
|
||||
if (configContext.allowedJunoOrigins.indexOf(new URL(phoenixEndpoint).origin) === -1) {
|
||||
if (!validateEndpoint(phoenixEndpoint, allowedJunoOrigins)) {
|
||||
const error = `${phoenixEndpoint} not allowed as juno endpoint`;
|
||||
console.error(error);
|
||||
throw new Error(error);
|
||||
@ -176,4 +219,48 @@ export class PhoenixClient {
|
||||
[HttpHeaders.contentType]: "application/json",
|
||||
};
|
||||
}
|
||||
|
||||
public ConvertToForbiddenErrorString(jsonData: IValidationError): string {
|
||||
const errInfo = jsonData;
|
||||
switch (errInfo?.type) {
|
||||
case PhoenixErrorType.MaxAllocationTimeExceeded: {
|
||||
const maxAllocationTimeExceeded = errInfo as IMaxAllocationTimeExceeded;
|
||||
const allocateAfterTimestamp = new Date(maxAllocationTimeExceeded?.earliestAllocationTimestamp);
|
||||
allocateAfterTimestamp.setDate(allocateAfterTimestamp.getDate() + 1);
|
||||
return (
|
||||
`${errInfo.message}` +
|
||||
" Max allocation time for a day to a user is " +
|
||||
`${maxAllocationTimeExceeded.maxAllocationTimePerDayPerUserInMinutes}` +
|
||||
". Please try again after " +
|
||||
`${allocateAfterTimestamp.toLocaleString()}`
|
||||
);
|
||||
}
|
||||
case PhoenixErrorType.MaxDbAccountsPerUserExceeded: {
|
||||
const maxDbAccountsPerUserExceeded = errInfo as IMaxDbAccountsPerUserExceeded;
|
||||
return (
|
||||
`${errInfo.message}` +
|
||||
" Max simultaneous connections allowed per user is " +
|
||||
`${maxDbAccountsPerUserExceeded.maxSimultaneousConnectionsPerUser}` +
|
||||
"."
|
||||
);
|
||||
}
|
||||
case PhoenixErrorType.MaxUsersPerDbAccountExceeded: {
|
||||
const maxUsersPerDbAccountExceeded = errInfo as IMaxUsersPerDbAccountExceeded;
|
||||
return (
|
||||
`${errInfo.message}` +
|
||||
" Max simultaneous users allowed per DbAccount is " +
|
||||
`${maxUsersPerDbAccountExceeded.maxSimultaneousUsersPerDbAccount}` +
|
||||
"."
|
||||
);
|
||||
}
|
||||
case PhoenixErrorType.AllocationValidationResult:
|
||||
case PhoenixErrorType.RegionNotServicable:
|
||||
case PhoenixErrorType.SubscriptionNotAllowed: {
|
||||
return `${errInfo.message}`;
|
||||
}
|
||||
default: {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
export type Features = {
|
||||
// set only via feature flags
|
||||
readonly canExceedMaximumValue: boolean;
|
||||
readonly cosmosdb: boolean;
|
||||
readonly enableChangeFeedPolicy: boolean;
|
||||
@ -8,11 +9,6 @@ export type Features = {
|
||||
readonly enableReactPane: boolean;
|
||||
readonly enableRightPanelV2: boolean;
|
||||
readonly enableSchema: boolean;
|
||||
autoscaleDefault: boolean;
|
||||
partitionKeyDefault: boolean;
|
||||
partitionKeyDefault2: boolean;
|
||||
phoenix: boolean;
|
||||
notebooksDownBanner: boolean;
|
||||
readonly enableSDKoperations: boolean;
|
||||
readonly enableSpark: boolean;
|
||||
readonly enableTtl: boolean;
|
||||
@ -22,7 +18,6 @@ export type Features = {
|
||||
readonly hostedDataExplorer: boolean;
|
||||
readonly junoEndpoint?: string;
|
||||
readonly phoenixEndpoint?: string;
|
||||
readonly livyEndpoint?: string;
|
||||
readonly notebookBasePath?: string;
|
||||
readonly notebookServerToken?: string;
|
||||
readonly notebookServerUrl?: string;
|
||||
@ -34,11 +29,23 @@ export type Features = {
|
||||
readonly mongoProxyEndpoint?: string;
|
||||
readonly mongoProxyAPIs?: string;
|
||||
readonly enableThroughputCap: boolean;
|
||||
readonly enableNewQuickstart: boolean;
|
||||
|
||||
// can be set via both flight and feature flag
|
||||
autoscaleDefault: boolean;
|
||||
partitionKeyDefault: boolean;
|
||||
partitionKeyDefault2: boolean;
|
||||
phoenixNotebooks?: boolean;
|
||||
phoenixFeatures?: boolean;
|
||||
notebooksDownBanner: boolean;
|
||||
publicGallery?: boolean;
|
||||
};
|
||||
|
||||
export function extractFeatures(given = new URLSearchParams(window.location.search)): Features {
|
||||
const downcased = new URLSearchParams();
|
||||
const set = (value: string, key: string) => downcased.set(key.toLowerCase(), value);
|
||||
const set = (value: string, key: string) => {
|
||||
downcased.set(key.toLowerCase(), value);
|
||||
};
|
||||
const get = (key: string, defaultValue?: string) =>
|
||||
downcased.get("feature." + key) ?? downcased.get(key) ?? defaultValue;
|
||||
|
||||
@ -71,7 +78,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
||||
mongoProxyAPIs: get("mongoproxyapis"),
|
||||
junoEndpoint: get("junoendpoint"),
|
||||
phoenixEndpoint: get("phoenixendpoint"),
|
||||
livyEndpoint: get("livyendpoint"),
|
||||
notebookBasePath: get("notebookbasepath"),
|
||||
notebookServerToken: get("notebookservertoken"),
|
||||
notebookServerUrl: get("notebookserverurl"),
|
||||
@ -83,9 +89,9 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
||||
autoscaleDefault: "true" === get("autoscaledefault"),
|
||||
partitionKeyDefault: "true" === get("partitionkeytest"),
|
||||
partitionKeyDefault2: "true" === get("pkpartitionkeytest"),
|
||||
phoenix: "true" === get("phoenix"),
|
||||
notebooksDownBanner: "true" === get("notebooksDownBanner"),
|
||||
enableThroughputCap: "true" === get("enablethroughputcap"),
|
||||
enableNewQuickstart: "true" === get("enablenewquickstart"),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -84,6 +84,7 @@ export enum Action {
|
||||
PhoenixConnection,
|
||||
PhoenixHeartBeat,
|
||||
PhoenixResetWorkspace,
|
||||
PhoenixDBAccountAllowed,
|
||||
DeleteCellFromMenu,
|
||||
OpenTerminal,
|
||||
CreateMongoCollectionWithWildcardIndex,
|
||||
|
@ -2,15 +2,61 @@
|
||||
* JupyterLab applications based on jupyterLab components
|
||||
*/
|
||||
import { ServerConnection, TerminalManager } from "@jupyterlab/services";
|
||||
import { IMessage } from "@jupyterlab/services/lib/terminal/terminal";
|
||||
import { Terminal } from "@jupyterlab/terminal";
|
||||
import { Panel, Widget } from "@phosphor/widgets";
|
||||
import { userContext } from "UserContext";
|
||||
|
||||
export class JupyterLabAppFactory {
|
||||
public static async createTerminalApp(serverSettings: ServerConnection.ISettings) {
|
||||
private isShellStarted: boolean | undefined;
|
||||
private checkShellStarted: ((content: string | undefined) => void) | undefined;
|
||||
private onShellExited: () => void;
|
||||
|
||||
private isShellExited(content: string | undefined) {
|
||||
return content?.includes("cosmosuser@");
|
||||
}
|
||||
|
||||
private isMongoShellStarted(content: string | undefined) {
|
||||
this.isShellStarted = content?.includes("MongoDB shell version");
|
||||
}
|
||||
|
||||
private isCassandraShellStarted(content: string | undefined) {
|
||||
this.isShellStarted = content?.includes("Connected to") && content?.includes("cqlsh");
|
||||
}
|
||||
|
||||
constructor(closeTab: () => void) {
|
||||
this.onShellExited = closeTab;
|
||||
this.isShellStarted = false;
|
||||
this.checkShellStarted = undefined;
|
||||
|
||||
switch (userContext.apiType) {
|
||||
case "Mongo":
|
||||
this.checkShellStarted = this.isMongoShellStarted;
|
||||
break;
|
||||
case "Cassandra":
|
||||
this.checkShellStarted = this.isCassandraShellStarted;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public async createTerminalApp(serverSettings: ServerConnection.ISettings) {
|
||||
const manager = new TerminalManager({
|
||||
serverSettings: serverSettings,
|
||||
});
|
||||
const session = await manager.startNew();
|
||||
session.messageReceived.connect(async (_, message: IMessage) => {
|
||||
const content = message.content && message.content[0]?.toString();
|
||||
|
||||
if (this.checkShellStarted && message.type == "stdout") {
|
||||
//Close the terminal tab once the shell closed messages are received
|
||||
if (!this.isShellStarted) {
|
||||
this.checkShellStarted(content);
|
||||
} else if (this.isShellExited(content)) {
|
||||
this.onShellExited();
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
|
||||
const term = new Terminal(session, { theme: "dark", shutdownOnClose: true });
|
||||
|
||||
if (!term) {
|
||||
|
@ -10,4 +10,5 @@ export interface TerminalProps {
|
||||
authType: AuthType;
|
||||
apiType: ApiType;
|
||||
subscriptionId: string;
|
||||
tabId: string;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ServerConnection } from "@jupyterlab/services";
|
||||
import "@jupyterlab/terminal/style/index.css";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import postRobot from "post-robot";
|
||||
import { HttpHeaders } from "../Common/Constants";
|
||||
import { Action } from "../Shared/Telemetry/TelemetryConstants";
|
||||
@ -54,13 +55,20 @@ const initTerminal = async (props: TerminalProps) => {
|
||||
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data);
|
||||
|
||||
try {
|
||||
await JupyterLabAppFactory.createTerminalApp(serverSettings);
|
||||
await new JupyterLabAppFactory(() => closeTab(props.tabId)).createTerminalApp(serverSettings);
|
||||
TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime);
|
||||
} catch (error) {
|
||||
TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime);
|
||||
}
|
||||
};
|
||||
|
||||
const closeTab = (tabId: string): void => {
|
||||
window.parent.postMessage(
|
||||
{ type: MessageTypes.CloseTab, data: { tabId: tabId }, signature: "pcIframe" },
|
||||
window.document.referrer
|
||||
);
|
||||
};
|
||||
|
||||
const main = async (): Promise<void> => {
|
||||
postRobot.on(
|
||||
"props",
|
||||
|
@ -1,12 +1,12 @@
|
||||
export const minAutoPilotThroughput = 4000;
|
||||
|
||||
export const autoPilotThroughput1K = 1000;
|
||||
export const autoPilotIncrementStep = 1000;
|
||||
export const autoPilotThroughput4K = 4000;
|
||||
|
||||
export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
|
||||
if (!maxThroughput) {
|
||||
return false;
|
||||
}
|
||||
if (maxThroughput < minAutoPilotThroughput) {
|
||||
if (maxThroughput < autoPilotThroughput1K) {
|
||||
return false;
|
||||
}
|
||||
if (maxThroughput % 1000) {
|
||||
|
87
src/Utils/EndpointValidation.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { JunoEndpoints } from "Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
|
||||
export function validateEndpoint(
|
||||
endpointToValidate: string | undefined,
|
||||
allowedEndpoints: ReadonlyArray<string>
|
||||
): boolean {
|
||||
try {
|
||||
return validateEndpointInternal(
|
||||
endpointToValidate,
|
||||
allowedEndpoints.map((e) => e)
|
||||
);
|
||||
} catch (reason) {
|
||||
Logger.logError(`${endpointToValidate} not allowed`, "validateEndpoint");
|
||||
Logger.logError(`${JSON.stringify(reason)}`, "validateEndpoint");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function validateEndpointInternal(
|
||||
endpointToValidate: string | undefined,
|
||||
allowedEndpoints: ReadonlyArray<string>
|
||||
): boolean {
|
||||
if (endpointToValidate === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const originToValidate: string = new URL(endpointToValidate).origin;
|
||||
const allowedOrigins: string[] = allowedEndpoints.map((allowedEndpoint) => new URL(allowedEndpoint).origin) || [];
|
||||
const valid = allowedOrigins.indexOf(originToValidate) >= 0;
|
||||
|
||||
if (!valid) {
|
||||
throw new Error(
|
||||
`${endpointToValidate} is not an allowed endpoint. Allowed endpoints are ${allowedEndpoints.toString()}`
|
||||
);
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
export const allowedArmEndpoints: ReadonlyArray<string> = [
|
||||
"https://management.azure.com",
|
||||
"https://management.usgovcloudapi.net",
|
||||
"https://management.chinacloudapi.cn",
|
||||
];
|
||||
|
||||
export const allowedAadEndpoints: ReadonlyArray<string> = ["https://login.microsoftonline.com/"];
|
||||
|
||||
export const allowedBackendEndpoints: ReadonlyArray<string> = [
|
||||
"https://main.documentdb.ext.azure.com",
|
||||
"https://main.documentdb.ext.azure.cn",
|
||||
"https://main.documentdb.ext.azure.us",
|
||||
"https://localhost:12901",
|
||||
"https://localhost:1234",
|
||||
];
|
||||
|
||||
export const allowedMongoProxyEndpoints: ReadonlyArray<string> = [
|
||||
"https://main.documentdb.ext.azure.com",
|
||||
"https://main.documentdb.ext.azure.cn",
|
||||
"https://main.documentdb.ext.azure.us",
|
||||
"https://localhost:12901",
|
||||
];
|
||||
|
||||
export const allowedEmulatorEndpoints: ReadonlyArray<string> = ["https://localhost:8081"];
|
||||
|
||||
export const allowedMongoBackendEndpoints: ReadonlyArray<string> = ["https://localhost:1234"];
|
||||
|
||||
export const allowedGraphEndpoints: ReadonlyArray<string> = ["https://graph.windows.net"];
|
||||
|
||||
export const allowedArcadiaEndpoints: ReadonlyArray<string> = ["https://workspaceartifacts.projectarcadia.net"];
|
||||
|
||||
export const allowedHostedExplorerEndpoints: ReadonlyArray<string> = ["https://cosmos.azure.com/"];
|
||||
|
||||
export const allowedMsalRedirectEndpoints: ReadonlyArray<string> = [
|
||||
"https://cosmos-explorer-preview.azurewebsites.net/",
|
||||
];
|
||||
|
||||
export const allowedJunoOrigins: ReadonlyArray<string> = [
|
||||
JunoEndpoints.Test,
|
||||
JunoEndpoints.Test2,
|
||||
JunoEndpoints.Test3,
|
||||
JunoEndpoints.Prod,
|
||||
JunoEndpoints.Stage,
|
||||
"https://localhost",
|
||||
];
|
||||
|
||||
export const allowedNotebookServerUrls: ReadonlyArray<string> = [];
|
@ -228,7 +228,7 @@ export function downloadItem(
|
||||
undefined,
|
||||
"Download",
|
||||
async () => {
|
||||
if (useNotebook.getState().isPhoenix) {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
await container.allocateContainer();
|
||||
}
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
|
@ -2,26 +2,28 @@ import { isInvalidParentFrameOrigin, isReadyMessage } from "./MessageValidation"
|
||||
|
||||
describe("isInvalidParentFrameOrigin", () => {
|
||||
test.each`
|
||||
domain | expected
|
||||
${"https://cosmos.azure.com"} | ${false}
|
||||
${"https://cosmos.azure.us"} | ${false}
|
||||
${"https://cosmos.azure.cn"} | ${false}
|
||||
${"https://portal.azure.com"} | ${false}
|
||||
${"https://portal.azure.us"} | ${false}
|
||||
${"https://portal.azure.cn"} | ${false}
|
||||
${"https://portal.microsoftazure.de"} | ${false}
|
||||
${"https://subdomain.portal.azure.com"} | ${false}
|
||||
${"https://subdomain.portal.azure.us"} | ${false}
|
||||
${"https://subdomain.portal.azure.cn"} | ${false}
|
||||
${"https://main.documentdb.ext.azure.com"} | ${false}
|
||||
${"https://main.documentdb.ext.azure.us"} | ${false}
|
||||
${"https://main.documentdb.ext.azure.cn"} | ${false}
|
||||
${"https://main.documentdb.ext.microsoftazure.de"} | ${false}
|
||||
${"https://random.domain"} | ${true}
|
||||
${"https://malicious.cloudapp.azure.com"} | ${true}
|
||||
${"https://malicious.germanycentral.cloudapp.microsoftazure.de"} | ${true}
|
||||
${"https://maliciousazure.com"} | ${true}
|
||||
${"https://maliciousportalsazure.com"} | ${true}
|
||||
domain | expected
|
||||
${"https://cosmos.azure.com"} | ${false}
|
||||
${"https://cosmos.azure.us"} | ${false}
|
||||
${"https://cosmos.azure.cn"} | ${false}
|
||||
${"https://portal.azure.com"} | ${false}
|
||||
${"https://portal.azure.us"} | ${false}
|
||||
${"https://portal.azure.cn"} | ${false}
|
||||
${"https://portal.microsoftazure.de"} | ${false}
|
||||
${"https://subdomain.portal.azure.com"} | ${false}
|
||||
${"https://subdomain.portal.azure.us"} | ${false}
|
||||
${"https://subdomain.portal.azure.cn"} | ${false}
|
||||
${"https://main.documentdb.ext.azure.com"} | ${false}
|
||||
${"https://main.documentdb.ext.azure.us"} | ${false}
|
||||
${"https://main.documentdb.ext.azure.cn"} | ${false}
|
||||
${"https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de"} | ${false}
|
||||
${"https://main.documentdb.ext.microsoftazure.de"} | ${false}
|
||||
${"https://random.domain"} | ${true}
|
||||
${"https://malicious.cloudapp.azure.com"} | ${true}
|
||||
${"https://malicious.germanycentral.cloudapp.microsoftazure.de"} | ${true}
|
||||
${"https://maliciousazure.com"} | ${true}
|
||||
${"https://maliciousportalsazure.com"} | ${true}
|
||||
${"https://cosmos-db-dataexplorer-germanycentralAazurewebsites.de"} | ${true}
|
||||
`("returns $expected when called with $domain", ({ domain, expected }) => {
|
||||
expect(isInvalidParentFrameOrigin({ origin: domain } as MessageEvent)).toBe(expected);
|
||||
});
|
||||
|
@ -4,7 +4,7 @@ export function isInvalidParentFrameOrigin(event: MessageEvent): boolean {
|
||||
return !isValidOrigin(configContext.allowedParentFrameOrigins, event);
|
||||
}
|
||||
|
||||
function isValidOrigin(allowedOrigins: string[], event: MessageEvent): boolean {
|
||||
function isValidOrigin(allowedOrigins: ReadonlyArray<string>, event: MessageEvent): boolean {
|
||||
const eventOrigin = (event && event.origin) || "";
|
||||
const windowOrigin = (window && window.origin) || "";
|
||||
if (eventOrigin === windowOrigin) {
|
||||
|
@ -3,15 +3,16 @@ import * as ViewModels from "../Contracts/ViewModels";
|
||||
|
||||
export function buildDocumentsQuery(
|
||||
filter: string,
|
||||
partitionKeyProperty: string,
|
||||
partitionKeyProperties: string[],
|
||||
partitionKey: DataModels.PartitionKey
|
||||
): string {
|
||||
let query = partitionKeyProperty
|
||||
? `select c.id, c._self, c._rid, c._ts, ${buildDocumentsQueryPartitionProjections(
|
||||
"c",
|
||||
partitionKey
|
||||
)} as _partitionKeyValue from c`
|
||||
: `select c.id, c._self, c._rid, c._ts from c`;
|
||||
let query =
|
||||
partitionKeyProperties && partitionKeyProperties.length > 0
|
||||
? `select c.id, c._self, c._rid, c._ts, [${buildDocumentsQueryPartitionProjections(
|
||||
"c",
|
||||
partitionKey
|
||||
)}] as _partitionKeyValue from c`
|
||||
: `select c.id, c._self, c._rid, c._ts from c`;
|
||||
|
||||
if (filter) {
|
||||
query += " " + filter;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { applyExplorerBindings } from "../applyExplorerBindings";
|
||||
import { AuthType } from "../AuthType";
|
||||
@ -43,6 +44,11 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
|
||||
useEffect(() => {
|
||||
const effect = async () => {
|
||||
if (platform) {
|
||||
//Updating phoenix feature flags for MPAC based of config context
|
||||
if (configContext.isPhoenixEnabled === true) {
|
||||
userContext.features.phoenixNotebooks = true;
|
||||
userContext.features.phoenixFeatures = true;
|
||||
}
|
||||
if (platform === Platform.Hosted) {
|
||||
const explorer = await configureHosted();
|
||||
setExplorer(explorer);
|
||||
@ -69,16 +75,38 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
|
||||
|
||||
async function configureHosted(): Promise<Explorer> {
|
||||
const win = (window as unknown) as HostedExplorerChildFrame;
|
||||
let explorer: Explorer;
|
||||
if (win.hostedConfig.authType === AuthType.EncryptedToken) {
|
||||
return configureHostedWithEncryptedToken(win.hostedConfig);
|
||||
explorer = configureHostedWithEncryptedToken(win.hostedConfig);
|
||||
} else if (win.hostedConfig.authType === AuthType.ResourceToken) {
|
||||
return configureHostedWithResourceToken(win.hostedConfig);
|
||||
explorer = configureHostedWithResourceToken(win.hostedConfig);
|
||||
} else if (win.hostedConfig.authType === AuthType.ConnectionString) {
|
||||
return configureHostedWithConnectionString(win.hostedConfig);
|
||||
explorer = configureHostedWithConnectionString(win.hostedConfig);
|
||||
} else if (win.hostedConfig.authType === AuthType.AAD) {
|
||||
return configureHostedWithAAD(win.hostedConfig);
|
||||
explorer = await configureHostedWithAAD(win.hostedConfig);
|
||||
} else {
|
||||
throw new Error(`Unknown hosted config: ${win.hostedConfig}`);
|
||||
}
|
||||
throw new Error(`Unknown hosted config: ${win.hostedConfig}`);
|
||||
|
||||
window.addEventListener(
|
||||
"message",
|
||||
(event) => {
|
||||
if (isInvalidParentFrameOrigin(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldProcessMessage(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data?.type === MessageTypes.CloseTab) {
|
||||
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
return explorer;
|
||||
}
|
||||
|
||||
async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
|
||||
@ -261,6 +289,8 @@ async function configurePortal(): Promise<Explorer> {
|
||||
}
|
||||
} else if (shouldForwardMessage(message, event.origin)) {
|
||||
sendMessage(message);
|
||||
} else if (event.data?.type === MessageTypes.CloseTab) {
|
||||
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
|
||||
}
|
||||
},
|
||||
false
|
||||
@ -339,12 +369,18 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
||||
if (inputs.flights.indexOf(Flights.PKPartitionKeyTest) !== -1) {
|
||||
userContext.features.partitionKeyDefault2 = true;
|
||||
}
|
||||
if (inputs.flights.indexOf(Flights.Phoenix) !== -1) {
|
||||
userContext.features.phoenix = true;
|
||||
if (inputs.flights.indexOf(Flights.PhoenixNotebooks) !== -1) {
|
||||
userContext.features.phoenixNotebooks = true;
|
||||
}
|
||||
if (inputs.flights.indexOf(Flights.PhoenixFeatures) !== -1) {
|
||||
userContext.features.phoenixFeatures = true;
|
||||
}
|
||||
if (inputs.flights.indexOf(Flights.NotebooksDownBanner) !== -1) {
|
||||
userContext.features.notebooksDownBanner = true;
|
||||
}
|
||||
if (inputs.flights.indexOf(Flights.PublicGallery) !== -1) {
|
||||
userContext.features.publicGallery = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|