mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2024-11-28 16:36:46 +00:00
Merge branch 'master' of https://github.com/Azure/cosmos-explorer into user/balalakshmin/chatbot
This commit is contained in:
commit
79cc244351
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -92,11 +92,11 @@ jobs:
|
||||
name: dist
|
||||
path: dist/
|
||||
- name: Upload build to preview blob storage
|
||||
run: az storage blob upload-batch -d '$web' -s 'dist' --account-name cosmosexplorerpreview --subscription cosmosdb-portalteam-generaldemo --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --account-key="${PREVIEW_STORAGE_KEY}"
|
||||
run: az storage blob upload-batch -d '$web' -s 'dist' --account-name cosmosexplorerpreview --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --account-key="${PREVIEW_STORAGE_KEY}"
|
||||
env:
|
||||
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
|
||||
- name: Upload preview config to blob storage
|
||||
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name cosmosexplorerpreview --subscription cosmosdb-portalteam-generaldemo --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}"
|
||||
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name cosmosexplorerpreview --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}"
|
||||
env:
|
||||
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
|
||||
endtoendemulator:
|
||||
|
54
images/CarouselImage1.svg
Normal file
54
images/CarouselImage1.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 20 KiB |
66
images/CarouselImage2.svg
Normal file
66
images/CarouselImage2.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 18 KiB |
@ -1,16 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 3.0 KiB |
@ -1,4 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 767 B |
@ -1,28 +1,23 @@
|
||||
<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>
|
||||
<svg width="32" height="36" viewBox="0 0 32 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M30.1809 0H5.6045C5.45725 -2.06304e-07 5.31144 0.0290347 5.17541 0.0854435C5.03939 0.141852 4.91583 0.224528 4.81179 0.32874C4.70775 0.432952 4.62528 0.556655 4.5691 0.692771C4.51292 0.828887 4.48413 0.974745 4.48438 1.122V31.7149C4.48438 32.0121 4.60234 32.2972 4.81236 32.5075C5.02238 32.7178 5.30729 32.8361 5.6045 32.8365H30.1809C30.4781 32.8362 30.7631 32.7179 30.9731 32.5076C31.1832 32.2972 31.3011 32.0121 31.301 31.7149V1.122C31.301 0.824752 31.1831 0.53965 30.973 0.329288C30.763 0.118926 30.4781 0.000496739 30.1809 0V0Z" fill="url(#paint0_linear_1311_8669)"/>
|
||||
<path d="M16.1495 3.22485H2.17401C1.8837 3.22485 1.60528 3.34018 1.39999 3.54546C1.19471 3.75074 1.07939 4.02917 1.07939 4.31948V34.465C1.07815 34.6087 1.10524 34.7513 1.15912 34.8845C1.21299 35.0178 1.2926 35.1391 1.39338 35.2416C1.49416 35.3441 1.61414 35.4257 1.74648 35.4818C1.87881 35.5379 2.0209 35.5674 2.16464 35.5686H26.3926C26.6829 35.5686 26.9614 35.4533 27.1667 35.248C27.3719 35.0427 27.4873 34.7643 27.4873 34.474V14.5202C27.4874 14.3764 27.4591 14.234 27.4042 14.1011C27.3492 13.9682 27.2686 13.8475 27.1669 13.7457C27.0653 13.644 26.9446 13.5633 26.8117 13.5082C26.6788 13.4532 26.5364 13.4249 26.3926 13.4249H18.3549C18.0642 13.422 17.7865 13.3045 17.5819 13.098C17.3774 12.8915 17.2626 12.6126 17.2625 12.322V4.33185C17.2628 4.03998 17.1477 3.75982 16.9423 3.55245C16.7369 3.34508 16.4579 3.22733 16.166 3.22485H16.1495Z" fill="white"/>
|
||||
<path d="M16.175 3.16357H1.99513C1.69791 3.16397 1.41301 3.28232 1.20299 3.49262C0.992965 3.70292 0.875 3.98799 0.875 4.2852V34.8781C0.875 35.1753 0.992953 35.4604 1.20296 35.6708C1.41297 35.8812 1.69788 35.9996 1.99513 36.0001H26.5715C26.8687 35.9996 27.1537 35.8812 27.3637 35.6708C27.5737 35.4604 27.6916 35.1753 27.6916 34.8781V14.6307C27.6916 14.3336 27.5736 14.0487 27.3635 13.8387C27.1535 13.6286 26.8686 13.5106 26.5715 13.5106H18.4134C18.1163 13.5106 17.8314 13.3926 17.6213 13.1825C17.4113 12.9724 17.2933 12.6875 17.2933 12.3905V4.2852C17.2924 3.98858 17.1744 3.70432 16.9649 3.49426C16.7555 3.2842 16.4716 3.16535 16.175 3.16357Z" fill="url(#paint1_linear_1311_8669)"/>
|
||||
<path d="M27.2629 13.7335L16.9065 3.4082V11.8213C16.9035 12.3253 17.1007 12.8097 17.4548 13.1684C17.8088 13.527 18.2907 13.7304 18.7947 13.7338L27.2629 13.7335Z" fill="#83B9F9"/>
|
||||
<path d="M17.744 16.4092H4.76108C4.47196 16.4092 4.23608 16.5543 4.23608 16.7332V17.5323C4.23608 17.7112 4.47046 17.8559 4.76108 17.8559H17.744C18.0331 17.8559 18.269 17.7112 18.269 17.5323V16.7332C18.2675 16.5543 18.0331 16.4092 17.744 16.4092Z" fill="#83B9F9"/>
|
||||
<path d="M17.744 20.7498H4.76108C4.47196 20.7498 4.23608 20.8945 4.23608 21.0734V21.8725C4.23608 22.0514 4.47046 22.1965 4.76108 22.1965H17.744C18.0331 22.1965 18.269 22.0514 18.269 21.8725V21.0749C18.2675 20.8945 18.0331 20.7498 17.744 20.7498Z" fill="#83B9F9"/>
|
||||
<path d="M17.744 25.0906H4.76108C4.47196 25.0906 4.23608 25.2353 4.23608 25.4142V26.2126C4.23608 26.3915 4.47046 26.5366 4.76108 26.5366H17.744C18.0331 26.5366 18.269 26.3915 18.269 26.2126V25.4142C18.2675 25.2353 18.0331 25.0906 17.744 25.0906Z" fill="#83B9F9"/>
|
||||
<path d="M12.4729 29.4312H4.90167C4.53492 29.4312 4.23755 29.5759 4.23755 29.7548V30.5539C4.23755 30.7328 4.53492 30.8779 4.90167 30.8779H12.4729C12.8397 30.8779 13.137 30.7328 13.137 30.5539V29.7548C13.1374 29.5759 12.8397 29.4312 12.4729 29.4312Z" fill="#83B9F9"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_669_8707" x1="12.595" y1="1.4615" x2="12.595" y2="23.415" gradientUnits="userSpaceOnUse">
|
||||
<linearGradient id="paint0_linear_1311_8669" x1="17.8925" y1="2.19225" x2="17.8925" y2="35.1225" 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">
|
||||
<linearGradient id="paint1_linear_1311_8669" x1="14.2831" y1="5.35582" x2="14.2831" y2="38.2857" 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>
|
||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.9 KiB |
5264
package-lock.json
generated
5264
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,7 @@
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/cosmos": "3.10.5",
|
||||
"@azure/cosmos": "3.16.1",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@azure/identity": "1.2.1",
|
||||
"@azure/ms-rest-nodeauth": "3.0.7",
|
||||
@ -92,6 +92,7 @@
|
||||
"react-notification-system": "0.2.17",
|
||||
"react-redux": "7.1.3",
|
||||
"react-splitter-layout": "4.0.0",
|
||||
"react-youtube": "9.0.1",
|
||||
"redux": "4.0.4",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rx-jupyter": "5.5.12",
|
||||
@ -131,6 +132,7 @@
|
||||
"@types/sinon": "2.3.3",
|
||||
"@types/styled-components": "5.1.1",
|
||||
"@types/underscore": "1.7.36",
|
||||
"@types/youtube-player": "5.5.6",
|
||||
"@typescript-eslint/eslint-plugin": "4.22.0",
|
||||
"@typescript-eslint/parser": "4.22.0",
|
||||
"@webpack-cli/serve": "1.5.2",
|
||||
|
@ -4,7 +4,7 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"deploy": "az webapp up -n cosmos-explorer-preview --subscription cosmosdb-portalteam-generaldemo -g stfaul",
|
||||
"deploy": "az webapp up --name \"cosmos-explorer-preview\" --subscription \"cosmosdb-portalteam-generaltest-msft\" --resource-group \"stfaul\"",
|
||||
"start": "node index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
|
@ -1,26 +1,25 @@
|
||||
{
|
||||
"databaseId": "SampleDB",
|
||||
"offerThroughput": 400,
|
||||
"databaseLevelThroughput": false,
|
||||
"collectionId": "Persons",
|
||||
"createNewDatabase": true,
|
||||
"partitionKey": { "kind": "Hash", "paths": ["/firstname"], "version": 1 },
|
||||
"data": [
|
||||
{
|
||||
"firstname": "Eva",
|
||||
"age": 44
|
||||
},
|
||||
{
|
||||
"firstname": "Véronique",
|
||||
"age": 50
|
||||
},
|
||||
{
|
||||
"firstname": "亜妃子",
|
||||
"age": 5
|
||||
},
|
||||
{
|
||||
"firstname": "John",
|
||||
"age": 23
|
||||
}
|
||||
{ "address": "2007, NE 37TH PL" },
|
||||
{ "address": "11635, SE MAY CREEK PARK DR" },
|
||||
{ "address": "8923, 133RD AVE SE" },
|
||||
{ "address": "1124, N 33RD ST" },
|
||||
{ "address": "4288, 131ST PL SE" },
|
||||
{ "address": "10900, SE 66TH ST" },
|
||||
{ "address": "6260, 139TH AVE NE" },
|
||||
{ "address": "13427, NE SPRING BLVD" },
|
||||
{ "address": "13812, NE SPRING BLVD" },
|
||||
{ "address": "5029, 159TH PL SE" },
|
||||
{ "address": "8604, 117TH AVE SE" },
|
||||
{ "address": "1561, 139TH LN NE" },
|
||||
{ "address": "1575, 139TH CT NE" },
|
||||
{ "address": "13901, NE 15TH CT" },
|
||||
{ "address": "16365, NE 12TH PL" },
|
||||
{ "address": "12226, NE 37TH ST" },
|
||||
{ "address": "4021, 129TH CT SE" },
|
||||
{ "address": "1455, 159TH PL NE" },
|
||||
{ "address": "15825, NE 14TH RD" },
|
||||
{ "address": "1418, 157TH CT NE" },
|
||||
{ "address": "889, 131ST PL NE" }
|
||||
]
|
||||
}
|
||||
|
@ -354,6 +354,10 @@ export enum ContainerStatusType {
|
||||
Disconnected = "Disconnected",
|
||||
}
|
||||
|
||||
export enum PoolIdType {
|
||||
DefaultPoolId = "default",
|
||||
}
|
||||
|
||||
export const EmulatorMasterKey =
|
||||
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
|
||||
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ResourceType } from "@azure/cosmos/dist-esm/common/constants";
|
||||
import { ResourceType } from "@azure/cosmos";
|
||||
import { Platform, resetConfigContext, updateConfigContext } from "../ConfigContext";
|
||||
import { updateUserContext } from "../UserContext";
|
||||
import { endpoint, getTokenFromAuthService, requestPlugin, tokenProvider } from "./CosmosClient";
|
||||
|
@ -1,6 +1,4 @@
|
||||
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";
|
||||
@ -9,7 +7,7 @@ import { getErrorMessage } from "./ErrorHandlingUtils";
|
||||
|
||||
const _global = typeof self === "undefined" ? window : self;
|
||||
|
||||
export const tokenProvider = async (requestInfo: RequestInfo) => {
|
||||
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
const { verb, resourceId, resourceType, headers } = requestInfo;
|
||||
|
||||
if (userContext.features.enableAadDataPlane && userContext.aadToken) {
|
||||
@ -20,13 +18,13 @@ export const tokenProvider = async (requestInfo: RequestInfo) => {
|
||||
|
||||
if (configContext.platform === Platform.Emulator) {
|
||||
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
||||
await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
||||
await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
||||
return decodeURIComponent(headers.authorization);
|
||||
}
|
||||
|
||||
if (userContext.masterKey) {
|
||||
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
||||
await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
||||
await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
||||
return decodeURIComponent(headers.authorization);
|
||||
}
|
||||
|
||||
@ -89,7 +87,7 @@ let _client: Cosmos.CosmosClient;
|
||||
export function client(): Cosmos.CosmosClient {
|
||||
if (_client) return _client;
|
||||
|
||||
let _defaultHeaders: CosmosHeaders = {};
|
||||
let _defaultHeaders: Cosmos.CosmosHeaders = {};
|
||||
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
|
||||
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
||||
|
||||
|
@ -1,7 +1,4 @@
|
||||
import { ContainerResponse, DatabaseResponse } from "@azure/cosmos";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/ContainerRequest";
|
||||
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
|
||||
import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { useDatabases } from "../../Explorer/useDatabases";
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { DatabaseResponse } from "@azure/cosmos";
|
||||
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
|
||||
import { DatabaseRequest, DatabaseResponse } from "@azure/cosmos";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { useDatabases } from "../../Explorer/useDatabases";
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
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 deleteDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<void> => {
|
||||
const entityName: string = getEntityName();
|
||||
@ -13,7 +13,7 @@ export const deleteDocument = async (collection: CollectionBase, documentId: Doc
|
||||
await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.item(documentId.id(), documentId.partitionKeyValue)
|
||||
.item(documentId.id(), documentId.partitionKeyValue?.length === 0 ? undefined : documentId.partitionKeyValue)
|
||||
.delete();
|
||||
logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`);
|
||||
} catch (error) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { RequestOptions } from "@azure/cosmos";
|
||||
import { Offer } from "../../Contracts/DataModels";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { client } from "../CosmosClient";
|
||||
import { parseSDKOfferResponse } from "../OfferUtility";
|
||||
import { readOffers } from "./readOffers";
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { ContainerDefinition } from "@azure/cosmos";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { ContainerDefinition, RequestOptions } from "@azure/cosmos";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Collection } from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
@ -25,7 +25,7 @@ export const updateDocument = async (
|
||||
const response = await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.item(documentId.id(), documentId.partitionKeyValue)
|
||||
.item(documentId.id(), documentId.partitionKeyValue?.length === 0 ? undefined : documentId.partitionKeyValue)
|
||||
.replace(newDocument, options);
|
||||
|
||||
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { OfferDefinition } from "@azure/cosmos";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { OfferDefinition, RequestOptions } from "@azure/cosmos";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
@ -7,6 +7,11 @@ export interface DatabaseAccount {
|
||||
type: string;
|
||||
kind: string;
|
||||
properties: DatabaseAccountExtendedProperties;
|
||||
systemData?: DatabaseAccountSystemData;
|
||||
}
|
||||
|
||||
export interface DatabaseAccountSystemData {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface DatabaseAccountExtendedProperties {
|
||||
@ -433,44 +438,51 @@ export interface NotebookWorkspaceConnectionInfo {
|
||||
|
||||
export interface ContainerInfo {
|
||||
durationLeftInMinutes: number;
|
||||
notebookServerInfo: NotebookWorkspaceConnectionInfo;
|
||||
phoenixServerInfo: NotebookWorkspaceConnectionInfo;
|
||||
status: ContainerStatusType;
|
||||
}
|
||||
|
||||
export interface IProvisionData {
|
||||
cosmosEndpoint: string;
|
||||
poolId: string;
|
||||
}
|
||||
|
||||
export interface IContainerData {
|
||||
forwardingId: string;
|
||||
}
|
||||
|
||||
export interface IDbAccountAllow {
|
||||
status: number;
|
||||
message?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface IResponse<T> {
|
||||
status: number;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface IValidationError {
|
||||
export interface IPhoenixError {
|
||||
message: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface IMaxAllocationTimeExceeded extends IValidationError {
|
||||
export interface IMaxAllocationTimeExceeded extends IPhoenixError {
|
||||
earliestAllocationTimestamp: string;
|
||||
maxAllocationTimePerDayPerUserInMinutes: string;
|
||||
}
|
||||
|
||||
export interface IMaxDbAccountsPerUserExceeded extends IValidationError {
|
||||
export interface IMaxDbAccountsPerUserExceeded extends IPhoenixError {
|
||||
maxSimultaneousConnectionsPerUser: string;
|
||||
}
|
||||
|
||||
export interface IMaxUsersPerDbAccountExceeded extends IValidationError {
|
||||
export interface IMaxUsersPerDbAccountExceeded extends IPhoenixError {
|
||||
maxSimultaneousUsersPerDbAccount: string;
|
||||
}
|
||||
|
||||
export interface IPhoenixConnectionInfoResult {
|
||||
readonly notebookAuthToken?: string;
|
||||
readonly notebookServerUrl?: string;
|
||||
readonly authToken?: string;
|
||||
readonly phoenixServiceUrl?: string;
|
||||
readonly forwardingId?: string;
|
||||
}
|
||||
|
||||
@ -557,4 +569,6 @@ export enum PhoenixErrorType {
|
||||
AllocationValidationResult = "AllocationValidationResult",
|
||||
RegionNotServicable = "RegionNotServicable",
|
||||
SubscriptionNotAllowed = "SubscriptionNotAllowed",
|
||||
UnknownError = "UnknownError",
|
||||
PhoenixFlightFallback = "PhoenixFlightFallback",
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ export enum MessageTypes {
|
||||
CreateSparkPool,
|
||||
RefreshDatabaseAccount,
|
||||
CloseTab,
|
||||
OpenQuickstartBlade,
|
||||
}
|
||||
|
||||
export { Versions, ActionContracts, Diagnostics };
|
||||
|
@ -86,6 +86,7 @@ export interface Database extends TreeNode {
|
||||
offer: ko.Observable<DataModels.Offer>;
|
||||
isDatabaseExpanded: ko.Observable<boolean>;
|
||||
isDatabaseShared: ko.Computed<boolean>;
|
||||
isSampleDB?: boolean;
|
||||
|
||||
selectedSubnodeKind: ko.Observable<CollectionTabKind>;
|
||||
|
||||
@ -112,6 +113,7 @@ export interface CollectionBase extends TreeNode {
|
||||
selectedSubnodeKind: ko.Observable<CollectionTabKind>;
|
||||
children: ko.ObservableArray<TreeNode>;
|
||||
isCollectionExpanded: ko.Observable<boolean>;
|
||||
isSampleCollection?: boolean;
|
||||
|
||||
onDocumentDBDocumentsClick(): void;
|
||||
onNewQueryClick(source: any, event?: MouseEvent, queryText?: string): void;
|
||||
|
@ -39,7 +39,7 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
|
||||
const items: TreeNodeMenuItem[] = [
|
||||
{
|
||||
iconSrc: AddCollectionIcon,
|
||||
onClick: () => container.onNewCollectionClicked(databaseId),
|
||||
onClick: () => container.onNewCollectionClicked({ databaseId }),
|
||||
label: `New ${getCollectionName()}`,
|
||||
},
|
||||
];
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -181,7 +181,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
const descriptionElement = (
|
||||
<Stack>
|
||||
{labelElement}
|
||||
<Text id={`${dataFieldName}-text-display`} aria-labelledby={labelId}>
|
||||
<Text id={`${dataFieldName}-text-display`} aria-labelledby={labelId} style={{ whiteSpace: "pre-line" }}>
|
||||
{this.props.getTranslation(description.textTKey)}{" "}
|
||||
{description.link && (
|
||||
<Link target="_blank" href={description.link.href}>
|
||||
|
@ -27,6 +27,11 @@ exports[`SmartUiComponent disable all inputs 1`] = `
|
||||
<Text
|
||||
aria-labelledby="description-label"
|
||||
id="description-text-display"
|
||||
style={
|
||||
Object {
|
||||
"whiteSpace": "pre-line",
|
||||
}
|
||||
}
|
||||
>
|
||||
this is an example description text.
|
||||
|
||||
@ -341,6 +346,11 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
|
||||
<Text
|
||||
aria-labelledby="description-label"
|
||||
id="description-text-display"
|
||||
style={
|
||||
Object {
|
||||
"whiteSpace": "pre-line",
|
||||
}
|
||||
}
|
||||
>
|
||||
this is an example description text.
|
||||
|
||||
|
@ -16,6 +16,7 @@ export interface ThroughputInputProps {
|
||||
isSharded: boolean;
|
||||
isFreeTier: boolean;
|
||||
showFreeTierExceedThroughputTooltip: boolean;
|
||||
isQuickstart?: boolean;
|
||||
setThroughputValue: (throughput: number) => void;
|
||||
setIsAutoscale: (isAutoscale: boolean) => void;
|
||||
setIsThroughputCapExceeded: (isThroughputCapExceeded: boolean) => void;
|
||||
@ -226,6 +227,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
</Stack>
|
||||
|
||||
<TextField
|
||||
id="autoscaleRUValueField"
|
||||
type="number"
|
||||
styles={{
|
||||
fieldGroup: { width: 300, height: 27 },
|
||||
|
@ -1637,6 +1637,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
<StyledTextFieldBase
|
||||
aria-label="Max request units per second"
|
||||
errorMessage=""
|
||||
id="autoscaleRUValueField"
|
||||
key=".0:$.2"
|
||||
min={1000}
|
||||
onChange={[Function]}
|
||||
@ -1660,6 +1661,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
aria-label="Max request units per second"
|
||||
deferredValidationTime={200}
|
||||
errorMessage=""
|
||||
id="autoscaleRUValueField"
|
||||
min={1000}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
@ -1955,7 +1957,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
<input
|
||||
aria-invalid={false}
|
||||
className="ms-TextField-field field-64"
|
||||
id="TextField2"
|
||||
id="autoscaleRUValueField"
|
||||
min={1000}
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
|
@ -173,6 +173,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
onClick={(event: React.MouseEvent<HTMLDivElement>) => this.onNodeClick(event, node)}
|
||||
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) => this.onNodeKeyPress(event, node)}
|
||||
role="treeitem"
|
||||
id={node.id}
|
||||
>
|
||||
<div
|
||||
className={`treeNodeHeader ${this.state.isMenuShowing ? "showingMenu" : ""}`}
|
||||
|
@ -137,6 +137,7 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
|
||||
exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`] = `
|
||||
<div
|
||||
className="nodeClassname main12 nodeItem "
|
||||
id="id"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="treeitem"
|
||||
@ -359,6 +360,7 @@ exports[`TreeNodeComponent renders loading icon 1`] = `
|
||||
exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents separated 1`] = `
|
||||
<div
|
||||
className="nodeClassname main12 nodeItem "
|
||||
id="id"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="treeitem"
|
||||
|
@ -68,11 +68,10 @@ export class ContainerSampleGenerator {
|
||||
return database.findCollectionWithId(this.sampleDataFile.collectionId);
|
||||
}
|
||||
|
||||
private async populateContainerAsync(collection: ViewModels.Collection): Promise<void> {
|
||||
public async populateContainerAsync(collection: ViewModels.Collection): Promise<void> {
|
||||
if (!collection) {
|
||||
throw new Error("No container to populate");
|
||||
}
|
||||
const promises: Q.Promise<any>[] = [];
|
||||
|
||||
if (userContext.apiType === "Gremlin") {
|
||||
// For Gremlin, all queries are executed sequentially, because some queries might be dependent on other queries
|
||||
|
@ -10,7 +10,7 @@ import shallow from "zustand/shallow";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
||||
import * as Constants from "../Common/Constants";
|
||||
import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
|
||||
import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook, PoolIdType } from "../Common/Constants";
|
||||
import { readCollection } from "../Common/dataAccess/readCollection";
|
||||
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
||||
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
|
||||
@ -310,7 +310,7 @@ export default class Explorer {
|
||||
db1.id().localeCompare(db2.id())
|
||||
);
|
||||
useDatabases.setState({ databases: updatedDatabases });
|
||||
await this.refreshAndExpandNewDatabases(deltaDatabases.toAdd, currentDatabases);
|
||||
await this.refreshAndExpandNewDatabases(deltaDatabases.toAdd, updatedDatabases);
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
TelemetryProcessor.traceFailure(
|
||||
@ -369,6 +369,7 @@ export default class Explorer {
|
||||
) {
|
||||
const provisionData: IProvisionData = {
|
||||
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
|
||||
poolId: PoolIdType.DefaultPoolId,
|
||||
};
|
||||
const connectionStatus: ContainerConnectionInfo = {
|
||||
status: ConnectionStatusType.Connecting,
|
||||
@ -381,11 +382,8 @@ export default class Explorer {
|
||||
});
|
||||
useNotebook.getState().setIsAllocating(true);
|
||||
connectionInfo = await this.phoenixClient.allocateContainer(provisionData);
|
||||
if (connectionInfo.status !== HttpStatusCodes.OK) {
|
||||
throw new Error(`Received status code: ${connectionInfo?.status}`);
|
||||
}
|
||||
if (!connectionInfo?.data?.notebookServerUrl) {
|
||||
throw new Error(`NotebookServerUrl is invalid!`);
|
||||
if (!connectionInfo?.data?.phoenixServiceUrl) {
|
||||
throw new Error(`PhoenixServiceUrl is invalid!`);
|
||||
}
|
||||
await this.setNotebookInfo(connectionInfo, connectionStatus);
|
||||
TelemetryProcessor.traceSuccess(Action.PhoenixConnection, {
|
||||
@ -394,6 +392,7 @@ export default class Explorer {
|
||||
} catch (error) {
|
||||
TelemetryProcessor.traceFailure(Action.PhoenixConnection, {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
status: error.status,
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error),
|
||||
});
|
||||
@ -435,8 +434,8 @@ export default class Explorer {
|
||||
notebookServerEndpoint:
|
||||
(validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
|
||||
userContext.features.notebookServerUrl) ||
|
||||
connectionInfo.data.notebookServerUrl,
|
||||
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
|
||||
connectionInfo.data.phoenixServiceUrl,
|
||||
authToken: userContext.features.notebookServerToken || connectionInfo.data.authToken,
|
||||
forwardingId: connectionInfo.data.forwardingId,
|
||||
});
|
||||
this.notebookManager?.notebookClient
|
||||
@ -535,8 +534,8 @@ export default class Explorer {
|
||||
if (connectionInfo?.status !== HttpStatusCodes.OK) {
|
||||
throw new Error(`Reset Workspace: Received status code- ${connectionInfo?.status}`);
|
||||
}
|
||||
if (!connectionInfo?.data?.notebookServerUrl) {
|
||||
throw new Error(`Reset Workspace: NotebookServerUrl is invalid!`);
|
||||
if (!connectionInfo?.data?.phoenixServiceUrl) {
|
||||
throw new Error(`Reset Workspace: PhoenixServiceUrl is invalid!`);
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
await this.setNotebookInfo(connectionInfo, connectionStatus);
|
||||
@ -1167,7 +1166,12 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
public async onNewCollectionClicked(databaseId?: string): Promise<void> {
|
||||
public async onNewCollectionClicked(
|
||||
options: {
|
||||
databaseId?: string;
|
||||
isQuickstart?: boolean;
|
||||
} = {}
|
||||
): Promise<void> {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
useSidePanel
|
||||
.getState()
|
||||
@ -1182,7 +1186,7 @@ export default class Explorer {
|
||||
: await useDatabases.getState().loadDatabaseOffers();
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel("New " + getCollectionName(), <AddCollectionPanel explorer={this} databaseId={databaseId} />);
|
||||
.openSidePanel("New " + getCollectionName(), <AddCollectionPanel explorer={this} {...options} />);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -339,6 +339,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
|
||||
if (userContext.apiType === "SQL" || userContext.apiType === "Gremlin") {
|
||||
const label = "New SQL Query";
|
||||
return {
|
||||
id: "newQueryBtn",
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
@ -353,6 +354,7 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
|
||||
} else if (userContext.apiType === "Mongo") {
|
||||
const label = "New Query";
|
||||
return {
|
||||
id: "newQueryBtn",
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
@ -439,6 +441,7 @@ function applyNotebooksTemporarilyDownStyle(buttonProps: CommandButtonComponentP
|
||||
function createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "New Notebook";
|
||||
return {
|
||||
id: "newNotebookBtn",
|
||||
iconSrc: NewNotebookIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.onNewNotebookClicked(),
|
||||
|
@ -5,7 +5,7 @@ import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook } from "../../Common/Constants";
|
||||
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook, PoolIdType } from "../../Common/Constants";
|
||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
@ -154,6 +154,7 @@ export class NotebookContainerClient {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
const provisionData: IProvisionData = {
|
||||
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
|
||||
poolId: PoolIdType.DefaultPoolId,
|
||||
};
|
||||
return await this.phoenixClient.resetContainer(provisionData);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import Immutable from "immutable";
|
||||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
import * as Logger from "../../../Common/Logger";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import loadTransform from "../NotebookComponent/loadTransform";
|
||||
@ -100,6 +101,7 @@ export class SchemaAnalyzer extends React.Component<SchemaAnalyzerProps, SchemaA
|
||||
// Only in cases where CosmosMongoKernel runs into an error we get a single output
|
||||
if (outputs.size === 1) {
|
||||
traceFailure(Action.SchemaAnalyzerClickAnalyze, data, this.clickAnalyzeTelemetryStartKey);
|
||||
Logger.logError(`Failed to analyze schema: ${JSON.stringify(data)}`, "SchemaAnalyzer/traceClickAnalyzeComplete");
|
||||
} else {
|
||||
traceSuccess(Action.SchemaAnalyzerClickAnalyze, data, this.clickAnalyzeTelemetryStartKey);
|
||||
}
|
||||
|
@ -4,12 +4,12 @@ import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { ConnectionStatusType } from "../../Common/Constants";
|
||||
import { ConnectionStatusType, HttpStatusCodes } from "../../Common/Constants";
|
||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { ContainerConnectionInfo, ContainerInfo } from "../../Contracts/DataModels";
|
||||
import { ContainerConnectionInfo, ContainerInfo, PhoenixErrorType } from "../../Contracts/DataModels";
|
||||
import { useTabs } from "../../hooks/useTabs";
|
||||
import { IPinnedRepo } from "../../Juno/JunoClient";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
@ -96,7 +96,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
containerStatus: {
|
||||
status: undefined,
|
||||
durationLeftInMinutes: undefined,
|
||||
notebookServerInfo: undefined,
|
||||
phoenixServerInfo: undefined,
|
||||
},
|
||||
isPhoenixNotebooks: undefined,
|
||||
isPhoenixFeatures: undefined,
|
||||
@ -296,22 +296,30 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
useNotebook.getState().setContainerStatus({
|
||||
status: undefined,
|
||||
durationLeftInMinutes: undefined,
|
||||
notebookServerInfo: undefined,
|
||||
phoenixServerInfo: undefined,
|
||||
});
|
||||
},
|
||||
setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }),
|
||||
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
|
||||
getPhoenixStatus: async () => {
|
||||
if (get().isPhoenixNotebooks === undefined || get().isPhoenixFeatures === undefined) {
|
||||
let isPhoenix = false;
|
||||
if (userContext.features.phoenixNotebooks || userContext.features.phoenixFeatures) {
|
||||
const phoenixClient = new PhoenixClient();
|
||||
isPhoenix = isPublicInternetAccessAllowed() && (await phoenixClient.isDbAcountWhitelisted());
|
||||
let isPhoenixNotebooks = false;
|
||||
let isPhoenixFeatures = false;
|
||||
|
||||
const isPublicInternetAllowed = isPublicInternetAccessAllowed();
|
||||
const phoenixClient = new PhoenixClient();
|
||||
const dbAccountAllowedInfo = await phoenixClient.getDbAccountAllowedStatus();
|
||||
|
||||
if (dbAccountAllowedInfo.status === HttpStatusCodes.OK) {
|
||||
if (dbAccountAllowedInfo?.type === PhoenixErrorType.PhoenixFlightFallback) {
|
||||
isPhoenixNotebooks = isPublicInternetAllowed && userContext.features.phoenixNotebooks;
|
||||
isPhoenixFeatures = isPublicInternetAllowed && userContext.features.phoenixFeatures;
|
||||
} else {
|
||||
isPhoenixNotebooks = isPhoenixFeatures = isPublicInternetAllowed;
|
||||
}
|
||||
} else {
|
||||
isPhoenixNotebooks = isPhoenixFeatures = false;
|
||||
}
|
||||
|
||||
const isPhoenixNotebooks = userContext.features.phoenixNotebooks && isPhoenix;
|
||||
const isPhoenixFeatures = userContext.features.phoenixFeatures && isPhoenix;
|
||||
|
||||
set({ isPhoenixNotebooks: isPhoenixNotebooks });
|
||||
set({ isPhoenixFeatures: isPhoenixFeatures });
|
||||
}
|
||||
|
15
src/Explorer/Panes/AddCollectionPanel.test.tsx
Normal file
15
src/Explorer/Panes/AddCollectionPanel.test.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import Explorer from "../Explorer";
|
||||
import { AddCollectionPanel } from "./AddCollectionPanel";
|
||||
|
||||
const props = {
|
||||
explorer: new Explorer(),
|
||||
};
|
||||
|
||||
describe("AddCollectionPanel", () => {
|
||||
it("should render Default properly", () => {
|
||||
const wrapper = shallow(<AddCollectionPanel {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
@ -8,8 +8,10 @@ import {
|
||||
IconButton,
|
||||
IDropdownOption,
|
||||
Link,
|
||||
ProgressIndicator,
|
||||
Separator,
|
||||
Stack,
|
||||
TeachingBubble,
|
||||
Text,
|
||||
TooltipHost,
|
||||
} from "@fluentui/react";
|
||||
@ -20,6 +22,7 @@ import { configContext, Platform } from "ConfigContext";
|
||||
import * as DataModels from "Contracts/DataModels";
|
||||
import { SubscriptionType } from "Contracts/SubscriptionType";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||
import React from "react";
|
||||
import { CollectionCreation } from "Shared/Constants";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
@ -30,6 +33,7 @@ import { isCapabilityEnabled, isServerlessAccount } from "Utils/CapabilityUtils"
|
||||
import { getUpsellMessage } from "Utils/PricingUtils";
|
||||
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
|
||||
import { ContainerSampleGenerator } from "../DataSamples/ContainerSampleGenerator";
|
||||
import Explorer from "../Explorer";
|
||||
import { useDatabases } from "../useDatabases";
|
||||
import { PanelFooterComponent } from "./PanelFooterComponent";
|
||||
@ -39,6 +43,7 @@ import { PanelLoadingScreen } from "./PanelLoadingScreen";
|
||||
export interface AddCollectionPanelProps {
|
||||
explorer: Explorer;
|
||||
databaseId?: string;
|
||||
isQuickstart?: boolean;
|
||||
}
|
||||
|
||||
const SharedDatabaseDefault: DataModels.IndexingPolicy = {
|
||||
@ -93,6 +98,7 @@ export interface AddCollectionPanelState {
|
||||
showErrorDetails: boolean;
|
||||
isExecuting: boolean;
|
||||
isThroughputCapExceeded: boolean;
|
||||
teachingBubbleStep: number;
|
||||
}
|
||||
|
||||
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
|
||||
@ -107,11 +113,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
this.state = {
|
||||
createNewDatabase: userContext.apiType !== "Tables" && !this.props.databaseId,
|
||||
newDatabaseId: "",
|
||||
newDatabaseId: props.isQuickstart ? this.getSampleDBName() : "",
|
||||
isSharedThroughputChecked: this.getSharedThroughputDefault(),
|
||||
selectedDatabaseId:
|
||||
userContext.apiType === "Tables" ? CollectionCreation.TablesAPIDefaultDatabase : this.props.databaseId,
|
||||
collectionId: "",
|
||||
collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "",
|
||||
enableIndexing: true,
|
||||
isSharded: userContext.apiType !== "Tables",
|
||||
partitionKey: this.getPartitionKey(),
|
||||
@ -124,9 +130,16 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
showErrorDetails: false,
|
||||
isExecuting: false,
|
||||
isThroughputCapExceeded: false,
|
||||
teachingBubbleStep: 0,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
if (this.state.teachingBubbleStep === 0 && this.props.isQuickstart) {
|
||||
this.setState({ teachingBubbleStep: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const isFirstResourceCreated = useDatabases.getState().isFirstResourceCreated();
|
||||
|
||||
@ -150,6 +163,89 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
/>
|
||||
)}
|
||||
|
||||
{this.state.teachingBubbleStep === 1 && (
|
||||
<TeachingBubble
|
||||
headline="Create sample database"
|
||||
target={"#newDatabaseId"}
|
||||
calloutProps={{ gapSpace: 16 }}
|
||||
primaryButtonProps={{ text: "Next", onClick: () => this.setState({ teachingBubbleStep: 2 }) }}
|
||||
secondaryButtonProps={{ text: "Cancel", onClick: () => this.setState({ teachingBubbleStep: 0 }) }}
|
||||
onDismiss={() => this.setState({ teachingBubbleStep: 0 })}
|
||||
footerContent="Step 1 of 4"
|
||||
>
|
||||
<Stack>
|
||||
<Text style={{ color: "white" }}>
|
||||
Database is the parent of a container. You can create a new database or use an existing one. In this
|
||||
tutorial we are creating a new database named SampleDB.
|
||||
</Text>
|
||||
<Link
|
||||
style={{ color: "white", fontWeight: 600 }}
|
||||
target="_blank"
|
||||
href="https://aka.ms/TeachingbubbleResources"
|
||||
>
|
||||
Learn more about resources.
|
||||
</Link>
|
||||
</Stack>
|
||||
</TeachingBubble>
|
||||
)}
|
||||
|
||||
{this.state.teachingBubbleStep === 2 && (
|
||||
<TeachingBubble
|
||||
headline="Setting throughput"
|
||||
target={"#autoscaleRUValueField"}
|
||||
calloutProps={{ gapSpace: 16 }}
|
||||
primaryButtonProps={{ text: "Next", onClick: () => this.setState({ teachingBubbleStep: 3 }) }}
|
||||
secondaryButtonProps={{ text: "Previous", onClick: () => this.setState({ teachingBubbleStep: 1 }) }}
|
||||
onDismiss={() => this.setState({ teachingBubbleStep: 0 })}
|
||||
footerContent="Step 2 of 4"
|
||||
>
|
||||
<Stack>
|
||||
<Text style={{ color: "white" }}>
|
||||
Cosmos DB recommends sharing throughput across database. Autoscale will give you a flexible amount of
|
||||
throughput based on the max RU/s set (Request Units).
|
||||
</Text>
|
||||
<Link style={{ color: "white", fontWeight: 600 }} target="_blank" href="https://aka.ms/teachingbubbleRU">
|
||||
Learn more about RU/s.
|
||||
</Link>
|
||||
</Stack>
|
||||
</TeachingBubble>
|
||||
)}
|
||||
|
||||
{this.state.teachingBubbleStep === 3 && (
|
||||
<TeachingBubble
|
||||
headline="Naming container"
|
||||
target={"#collectionId"}
|
||||
calloutProps={{ gapSpace: 16 }}
|
||||
primaryButtonProps={{ text: "Next", onClick: () => this.setState({ teachingBubbleStep: 4 }) }}
|
||||
secondaryButtonProps={{ text: "Previous", onClick: () => this.setState({ teachingBubbleStep: 2 }) }}
|
||||
onDismiss={() => this.setState({ teachingBubbleStep: 0 })}
|
||||
footerContent="Step 3 of 4"
|
||||
>
|
||||
Name your container
|
||||
</TeachingBubble>
|
||||
)}
|
||||
|
||||
{this.state.teachingBubbleStep === 4 && (
|
||||
<TeachingBubble
|
||||
headline="Setting partition key"
|
||||
target={"#addCollection-partitionKeyValue"}
|
||||
calloutProps={{ gapSpace: 16 }}
|
||||
primaryButtonProps={{
|
||||
text: "Create container",
|
||||
onClick: () => {
|
||||
this.setState({ teachingBubbleStep: 5 });
|
||||
this.submit();
|
||||
},
|
||||
}}
|
||||
secondaryButtonProps={{ text: "Previous", onClick: () => this.setState({ teachingBubbleStep: 2 }) }}
|
||||
onDismiss={() => this.setState({ teachingBubbleStep: 0 })}
|
||||
footerContent="Step 4 of 4"
|
||||
>
|
||||
Last step - you will need to define a partition key for your collection. /address was chosen for this
|
||||
particular example. A good partition key should have a wide range of possible value
|
||||
</TeachingBubble>
|
||||
)}
|
||||
|
||||
<div className="panelMainContent">
|
||||
<Stack hidden={userContext.apiType === "Tables"}>
|
||||
<Stack horizontal>
|
||||
@ -688,7 +784,35 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={this.state.isThroughputCapExceeded} />
|
||||
|
||||
{this.state.isExecuting && <PanelLoadingScreen />}
|
||||
{this.state.isExecuting && (
|
||||
<div>
|
||||
<PanelLoadingScreen />
|
||||
{this.state.teachingBubbleStep === 5 && (
|
||||
<TeachingBubble
|
||||
headline="Creating sample container"
|
||||
target={"#loadingScreen"}
|
||||
onDismiss={() => this.setState({ teachingBubbleStep: 0 })}
|
||||
styles={{ footer: { width: "100%" } }}
|
||||
>
|
||||
A sample container is now being created and we are adding sample data for you. It should take about 1
|
||||
minute.
|
||||
<br />
|
||||
<br />
|
||||
Once the sample container is created, review your sample dataset and follow next steps
|
||||
<br />
|
||||
<br />
|
||||
<ProgressIndicator
|
||||
styles={{
|
||||
itemName: { color: "white" },
|
||||
progressTrack: { backgroundColor: "#A6A6A6" },
|
||||
progressBar: { background: "white" },
|
||||
}}
|
||||
label="Adding sample data set"
|
||||
/>
|
||||
</TeachingBubble>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@ -832,6 +956,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
if (userContext.features.partitionKeyDefault2) {
|
||||
return userContext.apiType === "SQL" ? "/pk" : "pk";
|
||||
}
|
||||
if (this.props.isQuickstart) {
|
||||
return userContext.apiType === "SQL" ? "/address" : "address";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@ -899,8 +1026,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}
|
||||
|
||||
private isSynapseLinkEnabled(): boolean {
|
||||
const { properties } = userContext.databaseAccount;
|
||||
if (!userContext.databaseAccount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { properties } = userContext.databaseAccount;
|
||||
if (!properties) {
|
||||
return false;
|
||||
}
|
||||
@ -996,8 +1126,25 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
document.getElementById("collapsibleSectionContent")?.scrollIntoView();
|
||||
}
|
||||
|
||||
private async submit(event: React.FormEvent<HTMLFormElement>): Promise<void> {
|
||||
event.preventDefault();
|
||||
private getSampleDBName(): string {
|
||||
const existingSampleDBs = useDatabases
|
||||
.getState()
|
||||
.databases?.filter((database) => database.id().startsWith("SampleDB"));
|
||||
const existingSampleDBNames = existingSampleDBs?.map((database) => database.id());
|
||||
if (!existingSampleDBNames || existingSampleDBNames.length === 0) {
|
||||
return "SampleDB";
|
||||
}
|
||||
|
||||
let i = 1;
|
||||
while (existingSampleDBNames.indexOf(`SampleDB${i}`) !== -1) {
|
||||
i++;
|
||||
}
|
||||
|
||||
return `SampleDB${i}`;
|
||||
}
|
||||
|
||||
private async submit(event?: React.FormEvent<HTMLFormElement>): Promise<void> {
|
||||
event?.preventDefault();
|
||||
|
||||
if (!this.validateInputs()) {
|
||||
return;
|
||||
@ -1046,6 +1193,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
useIndexingForSharedThroughput: this.state.enableIndexing,
|
||||
isQuickstart: !!this.props.isQuickstart,
|
||||
};
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, telemetryData);
|
||||
|
||||
@ -1090,8 +1238,27 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
try {
|
||||
await createCollection(createCollectionParams);
|
||||
await this.props.explorer.refreshAllDatabases();
|
||||
if (this.props.isQuickstart) {
|
||||
const database = useDatabases.getState().findDatabaseWithId(databaseId);
|
||||
if (database) {
|
||||
database.isSampleDB = true;
|
||||
// populate sample container with sample data
|
||||
await database.loadCollections();
|
||||
const collection = database.findCollectionWithId(collectionId);
|
||||
collection.isSampleCollection = true;
|
||||
useTeachingBubble.getState().setSampleCollection(collection);
|
||||
const sampleGenerator = await ContainerSampleGenerator.createSampleGeneratorAsync(this.props.explorer);
|
||||
await sampleGenerator.populateContainerAsync(collection);
|
||||
// auto-expand sample database + container and show teaching bubble
|
||||
await database.expandDatabase();
|
||||
collection.expandCollection();
|
||||
useDatabases.getState().updateDatabase(database);
|
||||
useTeachingBubble.getState().setIsSampleDBExpanded(true);
|
||||
TelemetryProcessor.traceOpen(Action.LaunchUITour);
|
||||
}
|
||||
}
|
||||
this.setState({ isExecuting: false });
|
||||
this.props.explorer.refreshAllDatabases();
|
||||
TelemetryProcessor.traceSuccess(Action.CreateCollection, telemetryData, startKey);
|
||||
useSidePanel.getState().closeSidePanel();
|
||||
} catch (error) {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,7 @@ import React from "react";
|
||||
import LoadingIndicator_3Squares from "../../../images/LoadingIndicator_3Squares.gif";
|
||||
|
||||
export const PanelLoadingScreen: React.FunctionComponent = () => (
|
||||
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer">
|
||||
<div id="loadingScreen" className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer">
|
||||
<img className="dataExplorerLoader" src={LoadingIndicator_3Squares} />
|
||||
</div>
|
||||
);
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,427 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
<form
|
||||
className="panelFormWrapper"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="panelMainContent"
|
||||
>
|
||||
<Stack
|
||||
hidden={false}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<span
|
||||
className="mandatoryStar"
|
||||
>
|
||||
*
|
||||
</span>
|
||||
<Text
|
||||
className="panelTextBold"
|
||||
variant="small"
|
||||
>
|
||||
Database
|
||||
id
|
||||
</Text>
|
||||
<StyledTooltipHostBase
|
||||
content="A database is analogous to a namespace. It is the unit of management for a set of containers."
|
||||
directionalHint={4}
|
||||
>
|
||||
<Icon
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<input
|
||||
aria-checked={true}
|
||||
aria-label="Create new database"
|
||||
checked={true}
|
||||
className="panelRadioBtn"
|
||||
id="databaseCreateNew"
|
||||
name="databaseType"
|
||||
onChange={[Function]}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
type="radio"
|
||||
/>
|
||||
<span
|
||||
className="panelRadioBtnLabel"
|
||||
>
|
||||
Create new
|
||||
</span>
|
||||
<input
|
||||
aria-checked={false}
|
||||
aria-label="Use existing database"
|
||||
checked={false}
|
||||
className="panelRadioBtn"
|
||||
name="databaseType"
|
||||
onChange={[Function]}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
type="radio"
|
||||
/>
|
||||
<span
|
||||
className="panelRadioBtnLabel"
|
||||
>
|
||||
Use existing
|
||||
</span>
|
||||
</Stack>
|
||||
<Stack
|
||||
className="panelGroupSpacing"
|
||||
>
|
||||
<input
|
||||
aria-label="New database id"
|
||||
aria-required={true}
|
||||
autoComplete="off"
|
||||
autoFocus={true}
|
||||
className="panelTextField"
|
||||
id="newDatabaseId"
|
||||
name="newDatabaseId"
|
||||
onChange={[Function]}
|
||||
pattern="[^/?#\\\\\\\\]*[^/?# \\\\\\\\]"
|
||||
placeholder="Type a new database id"
|
||||
required={true}
|
||||
size={40}
|
||||
tabIndex={0}
|
||||
title="May not end with space nor contain characters '\\\\' '/' '#' '?'"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<StyledCheckboxBase
|
||||
checked={true}
|
||||
label="Share throughput across containers"
|
||||
onChange={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"checkbox": Object {
|
||||
"height": 12,
|
||||
"width": 12,
|
||||
},
|
||||
"label": Object {
|
||||
"alignItems": "center",
|
||||
"padding": 0,
|
||||
},
|
||||
"text": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<StyledTooltipHostBase
|
||||
content="Throughput configured at the database level will be shared across all containers within the database."
|
||||
directionalHint={4}
|
||||
>
|
||||
<Icon
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
</Stack>
|
||||
<ThroughputInput
|
||||
isDatabase={true}
|
||||
isSharded={true}
|
||||
onCostAcknowledgeChange={[Function]}
|
||||
setIsAutoscale={[Function]}
|
||||
setIsThroughputCapExceeded={[Function]}
|
||||
setThroughputValue={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
<Separator
|
||||
className="panelSeparator"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<span
|
||||
className="mandatoryStar"
|
||||
>
|
||||
*
|
||||
</span>
|
||||
<Text
|
||||
className="panelTextBold"
|
||||
variant="small"
|
||||
>
|
||||
Container id
|
||||
</Text>
|
||||
<StyledTooltipHostBase
|
||||
content="Unique identifier for the container and used for id-based routing through REST and all SDKs."
|
||||
directionalHint={4}
|
||||
>
|
||||
<Icon
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
</Stack>
|
||||
<input
|
||||
aria-label="Container id"
|
||||
aria-required={true}
|
||||
autoComplete="off"
|
||||
className="panelTextField"
|
||||
id="collectionId"
|
||||
name="collectionId"
|
||||
onChange={[Function]}
|
||||
pattern="[^/?#\\\\\\\\]*[^/?# \\\\\\\\]"
|
||||
placeholder="e.g., Container1"
|
||||
required={true}
|
||||
size={40}
|
||||
title="May not end with space nor contain characters '\\\\' '/' '#' '?'"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<span
|
||||
className="mandatoryStar"
|
||||
>
|
||||
*
|
||||
</span>
|
||||
<Text
|
||||
className="panelTextBold"
|
||||
variant="small"
|
||||
>
|
||||
Partition key
|
||||
</Text>
|
||||
<StyledTooltipHostBase
|
||||
content="The partition key is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume. For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice."
|
||||
directionalHint={4}
|
||||
>
|
||||
<Icon
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
</Stack>
|
||||
<Text
|
||||
aria-label="pkDescription"
|
||||
variant="small"
|
||||
/>
|
||||
<input
|
||||
aria-label="Partition key"
|
||||
aria-required={true}
|
||||
className="panelTextField"
|
||||
id="addCollection-partitionKeyValue"
|
||||
onChange={[Function]}
|
||||
pattern=".*"
|
||||
placeholder="e.g., /address/zipCode"
|
||||
required={true}
|
||||
size={40}
|
||||
title=""
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<Text
|
||||
className="panelTextBold"
|
||||
variant="small"
|
||||
>
|
||||
Unique keys
|
||||
</Text>
|
||||
<StyledTooltipHostBase
|
||||
content="Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key."
|
||||
directionalHint={4}
|
||||
>
|
||||
<Icon
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
</Stack>
|
||||
<CustomizedActionButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Add",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"label": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
"root": Object {
|
||||
"padding": 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
Add unique key
|
||||
</CustomizedActionButton>
|
||||
</Stack>
|
||||
<Stack
|
||||
className="panelGroupSpacing"
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<Text
|
||||
className="panelTextBold"
|
||||
variant="small"
|
||||
>
|
||||
Analytical store
|
||||
</Text>
|
||||
<StyledTooltipHostBase
|
||||
content={
|
||||
<Text
|
||||
variant="small"
|
||||
>
|
||||
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.
|
||||
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/analytical-store-overview"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
}
|
||||
directionalHint={4}
|
||||
>
|
||||
<Icon
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<input
|
||||
aria-checked={false}
|
||||
aria-label="Enable analytical store"
|
||||
checked={false}
|
||||
className="panelRadioBtn"
|
||||
disabled={true}
|
||||
id="enableAnalyticalStoreBtn"
|
||||
name="analyticalStore"
|
||||
onChange={[Function]}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
type="radio"
|
||||
/>
|
||||
<span
|
||||
className="panelRadioBtnLabel"
|
||||
>
|
||||
On
|
||||
</span>
|
||||
<input
|
||||
aria-checked={true}
|
||||
aria-label="Disable analytical store"
|
||||
checked={true}
|
||||
className="panelRadioBtn"
|
||||
disabled={true}
|
||||
id="disableAnalyticalStoreBtn"
|
||||
name="analyticalStore"
|
||||
onChange={[Function]}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
type="radio"
|
||||
/>
|
||||
<span
|
||||
className="panelRadioBtnLabel"
|
||||
>
|
||||
Off
|
||||
</span>
|
||||
</Stack>
|
||||
<Stack
|
||||
className="panelGroupSpacing"
|
||||
>
|
||||
<Text
|
||||
variant="small"
|
||||
>
|
||||
Azure Synapse Link is required for creating an analytical store
|
||||
|
||||
container
|
||||
. Enable Synapse Link for this Cosmos DB account.
|
||||
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/cosmosdb-synapselink"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
<CustomizedDefaultButton
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": 27,
|
||||
"width": 80,
|
||||
}
|
||||
}
|
||||
styles={
|
||||
Object {
|
||||
"label": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
text="Enable"
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<CollapsibleSectionComponent
|
||||
isExpandedByDefault={false}
|
||||
onExpand={[Function]}
|
||||
title="Advanced"
|
||||
>
|
||||
<Stack
|
||||
className="panelGroupSpacing"
|
||||
id="collapsibleSectionContent"
|
||||
>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
label="My partition key is larger than 101 bytes"
|
||||
onChange={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"checkbox": Object {
|
||||
"height": 12,
|
||||
"width": 12,
|
||||
},
|
||||
"label": Object {
|
||||
"alignItems": "center",
|
||||
"padding": 0,
|
||||
},
|
||||
"text": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</CollapsibleSectionComponent>
|
||||
</div>
|
||||
<PanelFooterComponent
|
||||
buttonLabel="OK"
|
||||
isButtonDisabled={false}
|
||||
/>
|
||||
</form>
|
||||
`;
|
@ -57,7 +57,6 @@
|
||||
|
||||
.legend {
|
||||
font-family: @SemiboldFont;
|
||||
margin-bottom: @DefaultSpace;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
@ -114,14 +113,6 @@
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.twoLineContent {
|
||||
margin-top: -5px;
|
||||
|
||||
:nth-child(2) {
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 10px;
|
||||
color: @BaseMediumHigh;
|
||||
|
@ -9,31 +9,6 @@ const createExplorer = () => {
|
||||
};
|
||||
|
||||
describe("SplashScreen", () => {
|
||||
it("allows sample collection creation for supported api's", () => {
|
||||
const explorer = createExplorer();
|
||||
const dataSampleUtil = new DataSamplesUtil(explorer);
|
||||
const createStub = jest
|
||||
.spyOn(dataSampleUtil, "createGeneratorAsync")
|
||||
.mockImplementation(() => Promise.reject(undefined));
|
||||
|
||||
// Sample is supported
|
||||
jest.spyOn(dataSampleUtil, "isSampleContainerCreationSupported").mockImplementation(() => true);
|
||||
|
||||
const splashScreen = new SplashScreen({ explorer });
|
||||
jest.spyOn(splashScreen, "createDataSampleUtil").mockImplementation(() => dataSampleUtil);
|
||||
const mainButtons = splashScreen.createMainItems();
|
||||
|
||||
// Press all buttons and make sure create gets called
|
||||
mainButtons.forEach((button) => {
|
||||
try {
|
||||
button.onClick();
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
});
|
||||
expect(createStub).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not allow sample collection creation for non-supported api's", () => {
|
||||
const explorerStub = createExplorer();
|
||||
const dataSampleUtil = new DataSamplesUtil(explorerStub);
|
||||
|
@ -1,22 +1,21 @@
|
||||
/**
|
||||
* Accordion top class
|
||||
*/
|
||||
import { Image, Link, Stack, Text } from "@fluentui/react";
|
||||
import { Coachmark, DirectionalHint, Image, Link, Stack, TeachingBubbleContent, Text } from "@fluentui/react";
|
||||
import { useCarousel } from "hooks/useCarousel";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
import * as React from "react";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||
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 NotebookColorIcon 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";
|
||||
@ -39,8 +38,10 @@ import { useSelectedNode } from "../useSelectedNode";
|
||||
export interface SplashScreenItem {
|
||||
iconSrc: string;
|
||||
title: string;
|
||||
id?: string;
|
||||
info?: string;
|
||||
description: string;
|
||||
showLinkIcon?: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
@ -49,8 +50,6 @@ export interface SplashScreenProps {
|
||||
}
|
||||
|
||||
export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
private static readonly seeMoreItemTitle: string = "See more Cosmos DB documentation";
|
||||
private static readonly seeMoreItemUrl: string = "https://aka.ms/cosmosdbdocument";
|
||||
private static readonly dataModelingUrl = "https://docs.microsoft.com/azure/cosmos-db/modeling-data";
|
||||
private static readonly throughputEstimatorUrl = "https://cosmos.azure.com/capacitycalculator";
|
||||
private static readonly failoverUrl = "https://docs.microsoft.com/azure/cosmos-db/high-availability";
|
||||
@ -64,13 +63,13 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
this.subscriptions = [];
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
public componentWillUnmount(): void {
|
||||
while (this.subscriptions.length) {
|
||||
this.subscriptions.pop().dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
public componentDidMount(): void {
|
||||
this.subscriptions.push(
|
||||
{
|
||||
dispose: useNotebook.subscribe(
|
||||
@ -78,7 +77,13 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
(state) => state.isNotebookEnabled
|
||||
),
|
||||
},
|
||||
{ dispose: useSelectedNode.subscribe(() => this.setState({})) }
|
||||
{ dispose: useSelectedNode.subscribe(() => this.setState({})) },
|
||||
{
|
||||
dispose: useCarousel.subscribe(
|
||||
() => this.setState({}),
|
||||
(state) => state.showCoachMark
|
||||
),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -115,32 +120,58 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
<img src={item.iconSrc} alt="" />
|
||||
</div>
|
||||
<div className="legendContainer">
|
||||
<div className="legend">{item.title}</div>
|
||||
<div className={userContext.features.enableNewQuickstart ? "newDescription" : "description"}>
|
||||
<Stack horizontal verticalAlign="center" style={{ marginBottom: 8 }}>
|
||||
<div className="legend">{item.title}</div>
|
||||
{item.showLinkIcon && <Image style={{ marginLeft: 8, width: 16 }} src={LinkIcon} />}
|
||||
</Stack>
|
||||
|
||||
<div id={item.id} className="newDescription">
|
||||
{item.description}
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
))}
|
||||
</div>
|
||||
{useCarousel.getState().showCoachMark && (
|
||||
<Coachmark
|
||||
target="#quickstartDescription"
|
||||
positioningContainerProps={{ directionalHint: DirectionalHint.rightTopEdge }}
|
||||
persistentBeak
|
||||
>
|
||||
<TeachingBubbleContent
|
||||
headline={`Start with sample ${getCollectionName().toLocaleLowerCase()}`}
|
||||
hasCloseButton
|
||||
closeButtonAriaLabel="Close"
|
||||
primaryButtonProps={{
|
||||
text: "Get started",
|
||||
onClick: () => {
|
||||
useCarousel.getState().setShowCoachMark(false);
|
||||
this.container.onNewCollectionClicked({ isQuickstart: true });
|
||||
},
|
||||
}}
|
||||
secondaryButtonProps={{
|
||||
text: "Cancel",
|
||||
onClick: () => useCarousel.getState().setShowCoachMark(false),
|
||||
}}
|
||||
onDismiss={() => useCarousel.getState().setShowCoachMark(false)}
|
||||
>
|
||||
You will be guided to create a sample container with sample data, then we will give you a tour of
|
||||
data explorer. You can also cancel launching this tour and explore yourself
|
||||
</TeachingBubbleContent>
|
||||
</Coachmark>
|
||||
)}
|
||||
<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 className="title">Recents</div>
|
||||
{this.getRecentItems()}
|
||||
</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 className="title">Top 3 things you need to know</div>
|
||||
{this.top3Items()}
|
||||
</div>
|
||||
<div className="moreStuffColumn tipsContainer">
|
||||
<div className="title">
|
||||
{userContext.features.enableNewQuickstart ? "Learning Resources" : "Tips"}
|
||||
</div>
|
||||
{userContext.features.enableNewQuickstart ? this.getLearningResourceItems() : this.getTipItems()}
|
||||
<div className="title">Learning Resources</div>
|
||||
{this.getLearningResourceItems()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -161,63 +192,53 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
* public for testing purposes
|
||||
*/
|
||||
public createMainItems(): SplashScreenItem[] {
|
||||
const dataSampleUtil = this.createDataSampleUtil();
|
||||
const heroes: SplashScreenItem[] = [];
|
||||
|
||||
if (userContext.features.enableNewQuickstart) {
|
||||
if (userContext.apiType === "SQL" || userContext.apiType === "Mongo") {
|
||||
const launchQuickstartBtn = {
|
||||
id: "quickstartDescription",
|
||||
iconSrc: QuickStartIcon,
|
||||
title: "Launch quick start",
|
||||
description: "Launch a quick start tutorial to get started with sample data",
|
||||
// TODO: replace onClick function
|
||||
onClick: () => 1,
|
||||
showLinkIcon: userContext.apiType === "Mongo",
|
||||
onClick: () => {
|
||||
userContext.apiType === "Mongo"
|
||||
? window.open("http://aka.ms/mongodbquickstart", "_blank")
|
||||
: this.container.onNewCollectionClicked({ isQuickstart: true });
|
||||
traceOpen(Action.LaunchQuickstart, { apiType: userContext.apiType });
|
||||
},
|
||||
};
|
||||
|
||||
const newContainerBtn = {
|
||||
iconSrc: ContainersIcon,
|
||||
title: `New ${getCollectionName()}`,
|
||||
description: "Create a new container for storage and throughput",
|
||||
onClick: () => this.container.onNewCollectionClicked(),
|
||||
heroes.push(launchQuickstartBtn);
|
||||
} else if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
const newNotebookBtn = {
|
||||
iconSrc: NotebookColorIcon,
|
||||
title: "New notebook",
|
||||
description: "Visualize your data stored in Azure Cosmos DB",
|
||||
onClick: () => this.container.onNewNotebookClicked(),
|
||||
};
|
||||
|
||||
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 (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;
|
||||
heroes.push(newNotebookBtn);
|
||||
}
|
||||
|
||||
const newContainerBtn = {
|
||||
iconSrc: ContainersIcon,
|
||||
title: `New ${getCollectionName()}`,
|
||||
description: "Create a new container for storage and throughput",
|
||||
onClick: () => {
|
||||
this.container.onNewCollectionClicked();
|
||||
traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType });
|
||||
},
|
||||
};
|
||||
heroes.push(newContainerBtn);
|
||||
|
||||
const connectBtn = {
|
||||
iconSrc: ConnectIcon,
|
||||
title: "Connect",
|
||||
description: "Prefer using your own choice of tooling? Find the connection string you need to connect",
|
||||
onClick: () => useTabs.getState().openAndActivateConnectTab(),
|
||||
};
|
||||
heroes.push(connectBtn);
|
||||
|
||||
return heroes;
|
||||
}
|
||||
|
||||
private createCommonTaskItems(): SplashScreenItem[] {
|
||||
@ -312,9 +333,9 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
|
||||
private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
|
||||
return {
|
||||
iconSrc: NotebookIcon,
|
||||
iconSrc: CollectionIcon,
|
||||
title: collectionId,
|
||||
description: "Data",
|
||||
description: getCollectionName(),
|
||||
onClick: () => {
|
||||
const collection = useDatabases.getState().findCollection(databaseId, collectionId);
|
||||
collection?.openTab();
|
||||
@ -325,7 +346,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
private decorateOpenNotebookActivity({ name, path }: MostRecentActivity.OpenNotebookItem) {
|
||||
return {
|
||||
info: path,
|
||||
iconSrc: CollectionIcon,
|
||||
iconSrc: NotebookIcon,
|
||||
title: name,
|
||||
description: "Notebook",
|
||||
onClick: () => {
|
||||
@ -381,84 +402,123 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
let items: { link: string; title: string; description: string }[];
|
||||
switch (userContext.apiType) {
|
||||
case "SQL":
|
||||
items = [
|
||||
{
|
||||
link: "https://aka.ms/msl-modeling-partitioning-2",
|
||||
title: "Advanced Modeling Patterns",
|
||||
description: "Learn advanced strategies to optimize your database.",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/msl-modeling-partitioning-1",
|
||||
title: "Partitioning Best Practices",
|
||||
description: "Learn to apply data model and partitioning strategies.",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/msl-resource-planning",
|
||||
title: "Plan Your Resource Requirements",
|
||||
description: "Get to know the different configuration choices.",
|
||||
},
|
||||
];
|
||||
break;
|
||||
case "Mongo":
|
||||
items = [
|
||||
{
|
||||
link: "https://aka.ms/mongodbintro",
|
||||
title: "What is the MongoDB API?",
|
||||
description: "Understand the Cosmos DB API for MongoDB and its features.",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/mongodbfeaturesupport",
|
||||
title: "Features and Syntax",
|
||||
description: "Discover the advantages and features",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/mongodbpremigration",
|
||||
title: "Migrate Your Data",
|
||||
description: "Pre-migration steps for moving data",
|
||||
},
|
||||
];
|
||||
break;
|
||||
case "Cassandra":
|
||||
items = [
|
||||
{
|
||||
link: "https://aka.ms/cassandrajava",
|
||||
title: "Build a Java App",
|
||||
description: "Create a Java app using an SDK.",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/cassandrapartitioning",
|
||||
title: "Partitioning Best Practices",
|
||||
description: "Learn how partitioning works.",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/cassandraRu",
|
||||
title: "Request Units (RUs)",
|
||||
description: "Understand RU charges.",
|
||||
},
|
||||
];
|
||||
break;
|
||||
case "Gremlin":
|
||||
items = [
|
||||
{
|
||||
link: "https://aka.ms/Graphdatamodeling",
|
||||
title: "Data Modeling",
|
||||
description: "Graph data modeling recommendations",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/graphpartitioning",
|
||||
title: "Partitioning Best Practices",
|
||||
description: "Learn how partitioning works",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/graphapiquery",
|
||||
title: "Query Data",
|
||||
description: "Querying data with Gremlin",
|
||||
},
|
||||
];
|
||||
break;
|
||||
case "Tables":
|
||||
items = [
|
||||
{
|
||||
link: "https://aka.ms/tableintro",
|
||||
title: "What is the Table API?",
|
||||
description: "Understand the Table API in Cosmos DB and its features",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/tableimport",
|
||||
title: "Migrate your data",
|
||||
description: "Learn how to migrate your data",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/tablefaq",
|
||||
title: "Table API FAQs",
|
||||
description: "Common questions about the Table API",
|
||||
},
|
||||
];
|
||||
break;
|
||||
}
|
||||
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} />
|
||||
{items.map((item, i) => (
|
||||
<Stack key={`top${i}`} style={{ marginBottom: 26 }}>
|
||||
<Stack horizontal verticalAlign="center" style={{ fontSize: 14 }}>
|
||||
<Link
|
||||
onClick={() => traceOpen(Action.Top3ItemsClicked, { item: i + 1, apiType: userContext.apiType })}
|
||||
href={item.link}
|
||||
target="_blank"
|
||||
style={{ marginRight: 5 }}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
<Image src={LinkIcon} />
|
||||
</Stack>
|
||||
<Text>{item.description}</Text>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@ -471,13 +531,15 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
<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>
|
||||
<Stack style={{ marginBottom: 26 }}>
|
||||
<Stack horizontal>
|
||||
<Image style={{ marginRight: 8 }} src={item.iconSrc} />
|
||||
<Link style={{ fontSize: 14 }} onClick={item.onClick} title={item.info}>
|
||||
{item.title}
|
||||
</Link>
|
||||
</Stack>
|
||||
<Text style={{ color: "#605E5C" }}>{item.description}</Text>
|
||||
</Stack>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@ -487,67 +549,125 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
}
|
||||
|
||||
private getLearningResourceItems(): JSX.Element {
|
||||
let items: { link: string; title: string; description: string }[];
|
||||
switch (userContext.apiType) {
|
||||
case "SQL":
|
||||
items = [
|
||||
{
|
||||
link: "https://aka.ms/msl-sdk-connect",
|
||||
title: "Get Started using an SDK",
|
||||
description: "Learn about the Azure Cosmos DB SDK.",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/msl-complex-queries",
|
||||
title: "Master Complex Queries",
|
||||
description: "Learn how to author complex queries.",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/msl-move-data",
|
||||
title: "Migrate Your Data",
|
||||
description: "Migrate data using Azure services and open-source solutions.",
|
||||
},
|
||||
];
|
||||
break;
|
||||
case "Mongo":
|
||||
items = [
|
||||
{
|
||||
link: "https://aka.ms/mongonodejs",
|
||||
title: "Build an app with Node.js",
|
||||
description: "Create a Node.js app.",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/mongopython",
|
||||
title: "Getting Started Guide",
|
||||
description: "Learn the basics to get started.",
|
||||
},
|
||||
{
|
||||
link: "http://aka.ms/mongodotnet",
|
||||
title: "Build a web API",
|
||||
description: "Create a web API with the.NET SDK.",
|
||||
},
|
||||
];
|
||||
break;
|
||||
case "Cassandra":
|
||||
items = [
|
||||
{
|
||||
link: "https://aka.ms/cassandracontainer",
|
||||
title: "Create a Container",
|
||||
description: "Get to know the create a container options.",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/cassandraserverdiagnostics",
|
||||
title: "Run Server Diagnostics",
|
||||
description: "Learn how to run server diagnostics.",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/Cassandrathroughput",
|
||||
title: "Provision Throughput",
|
||||
description: "Learn how to configure throughput.",
|
||||
},
|
||||
];
|
||||
break;
|
||||
case "Gremlin":
|
||||
items = [
|
||||
{
|
||||
link: "https://aka.ms/graphquickstart",
|
||||
title: "Get Started ",
|
||||
description: "Create, query, and traverse using the Gremlin console",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/graphimport",
|
||||
title: "Import Graph Data",
|
||||
description: "Learn Bulk ingestion data using BulkExecutor",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/graphoptimize",
|
||||
title: "Optimize your Queries",
|
||||
description: "Learn how to evaluate your Gremlin queries",
|
||||
},
|
||||
];
|
||||
break;
|
||||
case "Tables":
|
||||
items = [
|
||||
{
|
||||
link: "https://aka.ms/tabledotnet",
|
||||
title: "Build a .NET App",
|
||||
description: "How to access Table API from a .NET app.",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/Tablejava",
|
||||
title: "Build a Java App",
|
||||
description: "Create a Table API app with Java SDK ",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/tablenodejs",
|
||||
title: "Build a Node.js App",
|
||||
description: "Create a Table API app with Node.js SDK",
|
||||
},
|
||||
];
|
||||
break;
|
||||
}
|
||||
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} />
|
||||
{items.map((item, i) => (
|
||||
<Stack key={`learningResource${i}`} style={{ marginBottom: 26 }}>
|
||||
<Stack horizontal verticalAlign="center" style={{ fontSize: 14 }}>
|
||||
<Link
|
||||
onClick={() =>
|
||||
traceOpen(Action.LearningResourcesClicked, { item: i + 1, apiType: userContext.apiType })
|
||||
}
|
||||
href={item.link}
|
||||
target="_blank"
|
||||
style={{ marginRight: 5 }}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
<Image src={LinkIcon} />
|
||||
</Stack>
|
||||
<Text>{item.description}</Text>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
212
src/Explorer/Tabs/ConnectTab.tsx
Normal file
212
src/Explorer/Tabs/ConnectTab.tsx
Normal file
@ -0,0 +1,212 @@
|
||||
import { IconButton, ITextFieldStyles, Pivot, PivotItem, PrimaryButton, Stack, Text, TextField } from "@fluentui/react";
|
||||
import { handleError } from "Common/ErrorHandlingUtils";
|
||||
import { sendMessage } from "Common/MessageHandler";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { userContext } from "UserContext";
|
||||
import { listKeys, listReadOnlyKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import {
|
||||
DatabaseAccountListKeysResult,
|
||||
DatabaseAccountListReadOnlyKeysResult,
|
||||
} from "Utils/arm/generatedClients/cosmos/types";
|
||||
|
||||
export const ConnectTab: React.FC = (): JSX.Element => {
|
||||
const [primaryMasterKey, setPrimaryMasterKey] = useState<string>("");
|
||||
const [secondaryMasterKey, setSecondaryMasterKey] = useState<string>("");
|
||||
const [primaryReadonlyMasterKey, setPrimaryReadonlyMasterKey] = useState<string>("");
|
||||
const [secondaryReadonlyMasterKey, setSecondaryReadonlyMasterKey] = useState<string>("");
|
||||
const uri: string = userContext.databaseAccount.properties?.documentEndpoint;
|
||||
const primaryConnectionStr = `AccountEndpoint=${uri};AccountKey=${primaryMasterKey}`;
|
||||
const secondaryConnectionStr = `AccountEndpoint=${uri};AccountKey=${secondaryMasterKey}`;
|
||||
const primaryReadonlyConnectionStr = `AccountEndpoint=${uri};AccountKey=${primaryReadonlyMasterKey}`;
|
||||
const secondaryReadonlyConnectionStr = `AccountEndpoint=${uri};AccountKey=${secondaryReadonlyMasterKey}`;
|
||||
|
||||
useEffect(() => {
|
||||
fetchKeys();
|
||||
}, []);
|
||||
|
||||
const fetchKeys = async (): Promise<void> => {
|
||||
try {
|
||||
if (userContext.hasWriteAccess) {
|
||||
const listKeysResult: DatabaseAccountListKeysResult = await listKeys(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name
|
||||
);
|
||||
setPrimaryMasterKey(listKeysResult.primaryMasterKey);
|
||||
setSecondaryMasterKey(listKeysResult.secondaryMasterKey);
|
||||
setPrimaryReadonlyMasterKey(listKeysResult.primaryReadonlyMasterKey);
|
||||
setSecondaryReadonlyMasterKey(listKeysResult.secondaryReadonlyMasterKey);
|
||||
} else {
|
||||
const listReadonlyKeysResult: DatabaseAccountListReadOnlyKeysResult = await listReadOnlyKeys(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name
|
||||
);
|
||||
setPrimaryReadonlyMasterKey(listReadonlyKeysResult.primaryReadonlyMasterKey);
|
||||
setSecondaryReadonlyMasterKey(listReadonlyKeysResult.secondaryReadonlyMasterKey);
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, "listKeys", "listKeys request has failed: ");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const onCopyBtnClicked = (selector: string): void => {
|
||||
const textfield: HTMLInputElement = document.querySelector(selector);
|
||||
textfield.select();
|
||||
document.execCommand("copy");
|
||||
};
|
||||
|
||||
const textfieldStyles: Partial<ITextFieldStyles> = {
|
||||
root: { width: "100%" },
|
||||
field: { backgroundColor: "rgb(230, 230, 230)" },
|
||||
fieldGroup: { borderColor: "rgb(138, 136, 134)" },
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", padding: 16 }}>
|
||||
<Pivot>
|
||||
{userContext.hasWriteAccess && (
|
||||
<PivotItem headerText="Read-write Keys">
|
||||
<Stack style={{ margin: 10 }}>
|
||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||
<TextField label="URI" id="uriTextfield" readOnly value={uri} styles={textfieldStyles} />
|
||||
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#uriTextfield")} />
|
||||
</Stack>
|
||||
|
||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||
<TextField
|
||||
label="PRIMARY KEY"
|
||||
id="primaryKeyTextfield"
|
||||
readOnly
|
||||
value={primaryMasterKey}
|
||||
styles={textfieldStyles}
|
||||
/>
|
||||
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#primaryKeyTextfield")} />
|
||||
</Stack>
|
||||
|
||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||
<TextField
|
||||
label="SECONDARY KEY"
|
||||
id="secondaryKeyTextfield"
|
||||
readOnly
|
||||
value={secondaryMasterKey}
|
||||
styles={textfieldStyles}
|
||||
/>
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Copy" }}
|
||||
onClick={() => onCopyBtnClicked("#secondaryKeyTextfield")}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||
<TextField
|
||||
label="PRIMARY CONNECTION STRING"
|
||||
id="primaryConStrTextfield"
|
||||
readOnly
|
||||
value={primaryConnectionStr}
|
||||
styles={textfieldStyles}
|
||||
/>
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Copy" }}
|
||||
onClick={() => onCopyBtnClicked("#primaryConStrTextfield")}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||
<TextField
|
||||
label="SECONDARY CONNECTION STRING"
|
||||
id="secondaryConStrTextfield"
|
||||
readOnly
|
||||
value={secondaryConnectionStr}
|
||||
styles={textfieldStyles}
|
||||
/>
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Copy" }}
|
||||
onClick={() => onCopyBtnClicked("#secondaryConStrTextfield")}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
)}
|
||||
<PivotItem headerText="Read-only Keys">
|
||||
<Stack style={{ margin: 10 }}>
|
||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||
<TextField label="URI" id="uriReadOnlyTextfield" readOnly value={uri} styles={textfieldStyles} />
|
||||
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#uriReadOnlyTextfield")} />
|
||||
</Stack>
|
||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||
<TextField
|
||||
label="PRIMARY READ-ONLY KEY"
|
||||
id="primaryReadonlyKeyTextfield"
|
||||
readOnly
|
||||
value={primaryReadonlyMasterKey}
|
||||
styles={textfieldStyles}
|
||||
/>
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Copy" }}
|
||||
onClick={() => onCopyBtnClicked("#primaryReadonlyKeyTextfield")}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||
<TextField
|
||||
label="SECONDARY READ-ONLY KEY"
|
||||
id="secondaryReadonlyKeyTextfield"
|
||||
readOnly
|
||||
value={secondaryReadonlyMasterKey}
|
||||
styles={textfieldStyles}
|
||||
/>
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Copy" }}
|
||||
onClick={() => onCopyBtnClicked("#secondaryReadonlyKeyTextfield")}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||
<TextField
|
||||
label="PRIMARY READ-ONLY CONNECTION STRING"
|
||||
id="primaryReadonlyConStrTextfield"
|
||||
readOnly
|
||||
value={primaryReadonlyConnectionStr}
|
||||
styles={textfieldStyles}
|
||||
/>
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Copy" }}
|
||||
onClick={() => onCopyBtnClicked("#primaryReadonlyConStrTextfield")}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||
<TextField
|
||||
label="SECONDARY READ-ONLY CONNECTION STRING"
|
||||
id="secondaryReadonlyConStrTextfield"
|
||||
value={secondaryReadonlyConnectionStr}
|
||||
readOnly
|
||||
styles={textfieldStyles}
|
||||
/>
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Copy" }}
|
||||
onClick={() => onCopyBtnClicked("#secondaryReadonlyConStrTextfield")}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
|
||||
<Stack style={{ margin: 10 }}>
|
||||
<Text style={{ fontWeight: 600, marginBottom: 8 }}>Download sample app</Text>
|
||||
<Text style={{ marginBottom: 8 }}>
|
||||
Don’t have an app ready? No worries, download one of our sample app with a platform of your choice. Connection
|
||||
string is already included in the app.
|
||||
</Text>
|
||||
<PrimaryButton
|
||||
style={{ width: 185 }}
|
||||
onClick={() =>
|
||||
sendMessage({
|
||||
type: MessageTypes.OpenQuickstartBlade,
|
||||
})
|
||||
}
|
||||
text="Download sample app"
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -909,6 +909,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
public static _createUploadButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Upload Item";
|
||||
return {
|
||||
id: "uploadItemBtn",
|
||||
iconSrc: UploadIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
|
@ -1,3 +1,6 @@
|
||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||
import { ConnectTab } from "Explorer/Tabs/ConnectTab";
|
||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||
import ko from "knockout";
|
||||
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||
import loadingIcon from "../../../images/circular_loader_black_16x16.gif";
|
||||
@ -10,17 +13,21 @@ type Tab = TabsBase | (TabsBase & { render: () => JSX.Element });
|
||||
|
||||
export const Tabs = (): JSX.Element => {
|
||||
const { openedTabs, activeTab } = useTabs();
|
||||
const isConnectTabOpen = useTabs((state) => state.isConnectTabOpen);
|
||||
const isConnectTabActive = useTabs((state) => state.isConnectTabActive);
|
||||
return (
|
||||
<div className="tabsManagerContainer">
|
||||
<div id="content" className="flexContainer hideOverflows">
|
||||
<div className="nav-tabs-margin">
|
||||
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
|
||||
{isConnectTabOpen && <TabNav key="connect" tab={undefined} active={isConnectTabActive} />}
|
||||
{openedTabs.map((tab) => (
|
||||
<TabNav key={tab.tabId} tab={tab} active={activeTab === tab} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="tabPanesContainer">
|
||||
{isConnectTabActive && <ConnectTab />}
|
||||
{openedTabs.map((tab) => (
|
||||
<TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />
|
||||
))}
|
||||
@ -33,6 +40,7 @@ export const Tabs = (): JSX.Element => {
|
||||
function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
|
||||
const [hovering, setHovering] = useState(false);
|
||||
const focusTab = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
|
||||
const tabId = tab ? tab.tabId : "connect";
|
||||
|
||||
useEffect(() => {
|
||||
if (active && focusTab.current) {
|
||||
@ -43,27 +51,27 @@ function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
|
||||
<li
|
||||
onMouseOver={() => setHovering(true)}
|
||||
onMouseLeave={() => setHovering(false)}
|
||||
onClick={() => tab.onTabClick()}
|
||||
onKeyPress={({ nativeEvent: e }) => tab.onKeyPressActivate(undefined, e)}
|
||||
onClick={() => (tab ? tab.onTabClick() : useTabs.getState().activateConnectTab())}
|
||||
onKeyPress={({ nativeEvent: e }) => (tab ? tab.onKeyPressActivate(undefined, e) : onKeyPressConnectTab(e))}
|
||||
className={active ? "active tabList" : "tabList"}
|
||||
title={useObservable(tab.tabPath)}
|
||||
title={useObservable(tab?.tabPath || ko.observable(""))}
|
||||
aria-selected={active}
|
||||
aria-expanded={active}
|
||||
aria-controls={tab.tabId}
|
||||
aria-controls={tabId}
|
||||
tabIndex={0}
|
||||
role="tab"
|
||||
ref={focusTab}
|
||||
>
|
||||
<span className="tabNavContentContainer">
|
||||
<a data-toggle="tab" href={"#" + tab.tabId} tabIndex={-1}>
|
||||
<a data-toggle="tab" href={"#" + tabId} tabIndex={-1}>
|
||||
<div className="tab_Content">
|
||||
<span className="statusIconContainer">
|
||||
{useObservable(tab.isExecutionError) && <ErrorIcon tab={tab} active={active} />}
|
||||
{useObservable(tab.isExecuting) && (
|
||||
{useObservable(tab?.isExecutionError || ko.observable(false)) && <ErrorIcon tab={tab} active={active} />}
|
||||
{useObservable(tab?.isExecuting || ko.observable(false)) && (
|
||||
<img className="loadingIcon" title="Loading" src={loadingIcon} alt="Loading" />
|
||||
)}
|
||||
</span>
|
||||
<span className="tabNavText">{useObservable(tab.tabTitle)}</span>
|
||||
<span className="tabNavText">{useObservable(tab?.tabTitle || ko.observable("Connect"))}</span>
|
||||
<span className="tabIconSection">
|
||||
<CloseButton tab={tab} active={active} hovering={hovering} />
|
||||
</span>
|
||||
@ -81,7 +89,7 @@ const CloseButton = ({ tab, active, hovering }: { tab: Tab; active: boolean; hov
|
||||
role="button"
|
||||
aria-label="Close Tab"
|
||||
className="cancelButton"
|
||||
onClick={() => tab.onCloseTabButtonClick()}
|
||||
onClick={() => (tab ? tab.onCloseTabButtonClick() : useTabs.getState().closeConnectTab())}
|
||||
tabIndex={active ? 0 : undefined}
|
||||
onKeyPress={({ nativeEvent: e }) => tab.onKeyPressClose(undefined, e)}
|
||||
>
|
||||
@ -113,6 +121,10 @@ function TabPane({ tab, active }: { tab: Tab; active: boolean }) {
|
||||
};
|
||||
|
||||
useEffect((): (() => void) | void => {
|
||||
if (tab.tabKind === CollectionTabKind.Documents && tab.collection?.isSampleCollection) {
|
||||
useTeachingBubble.getState().setIsDocumentsTabOpened(true);
|
||||
}
|
||||
|
||||
const { current: element } = ref;
|
||||
if (element) {
|
||||
ko.applyBindings(tab, element);
|
||||
@ -123,9 +135,18 @@ function TabPane({ tab, active }: { tab: Tab; active: boolean }) {
|
||||
}
|
||||
}, [ref, tab]);
|
||||
|
||||
if ("render" in tab) {
|
||||
return <div {...attrs}>{tab.render()}</div>;
|
||||
if (tab) {
|
||||
if ("render" in tab) {
|
||||
return <div {...attrs}>{tab.render()}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
return <div {...attrs} ref={ref} data-bind="html:html" />;
|
||||
}
|
||||
|
||||
const onKeyPressConnectTab = (e: KeyboardEvent): void => {
|
||||
if (e.key === "Enter" || e.key === "Space") {
|
||||
useTabs.getState().activateConnectTab();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
@ -97,6 +97,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
public storedProceduresFocused: ko.Observable<boolean>;
|
||||
public userDefinedFunctionsFocused: ko.Observable<boolean>;
|
||||
public triggersFocused: ko.Observable<boolean>;
|
||||
public isSampleCollection: boolean;
|
||||
private isOfferRead: boolean;
|
||||
|
||||
constructor(container: Explorer, databaseId: string, data: DataModels.Collection) {
|
||||
@ -133,7 +134,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
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;
|
||||
this.partitionKeyPropertyHeaders[i] = "/" + partitionKeyProperty;
|
||||
}
|
||||
}
|
||||
|
||||
@ -216,6 +217,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
this.isStoredProceduresExpanded = ko.observable<boolean>(false);
|
||||
this.isUserDefinedFunctionsExpanded = ko.observable<boolean>(false);
|
||||
this.isTriggersExpanded = ko.observable<boolean>(false);
|
||||
this.isSampleCollection = false;
|
||||
this.isOfferRead = false;
|
||||
}
|
||||
|
||||
|
@ -37,6 +37,7 @@ export default class Database implements ViewModels.Database {
|
||||
public isDatabaseShared: ko.Computed<boolean>;
|
||||
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
|
||||
public junoClient: JunoClient;
|
||||
public isSampleDB: boolean;
|
||||
private isOfferRead: boolean;
|
||||
|
||||
constructor(container: Explorer, data: DataModels.Database) {
|
||||
@ -54,6 +55,7 @@ export default class Database implements ViewModels.Database {
|
||||
return this.offer && !!this.offer();
|
||||
});
|
||||
this.junoClient = new JunoClient();
|
||||
this.isSampleDB = false;
|
||||
this.isOfferRead = false;
|
||||
}
|
||||
|
||||
|
@ -436,7 +436,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
const databaseNode: TreeNode = {
|
||||
label: database.id(),
|
||||
iconSrc: CosmosDBIcon,
|
||||
isExpanded: false,
|
||||
isExpanded: database.isDatabaseExpanded(),
|
||||
className: "databaseHeader",
|
||||
children: [],
|
||||
isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()),
|
||||
@ -461,6 +461,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
|
||||
if (database.isDatabaseShared()) {
|
||||
databaseNode.children.push({
|
||||
id: database.isSampleDB ? "sampleScaleSettings" : "",
|
||||
label: "Scale",
|
||||
isSelected: () =>
|
||||
useSelectedNode
|
||||
@ -497,6 +498,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
const children: TreeNode[] = [];
|
||||
children.push({
|
||||
label: collection.getLabel(),
|
||||
id: collection.isSampleCollection ? "sampleItems" : "",
|
||||
onClick: () => {
|
||||
collection.openTab();
|
||||
// push to most recent
|
||||
@ -530,6 +532,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
|
||||
if (userContext.apiType !== "Cassandra" || !isServerlessAccount()) {
|
||||
children.push({
|
||||
id: collection.isSampleCollection && !database.isDatabaseShared() ? "sampleScaleSettings" : "",
|
||||
label: database.isDatabaseShared() || isServerlessAccount() ? "Settings" : "Scale & Settings",
|
||||
onClick: collection.onSettingsClick.bind(collection),
|
||||
isSelected: () =>
|
||||
@ -572,7 +575,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
return {
|
||||
label: collection.id(),
|
||||
iconSrc: CollectionIcon,
|
||||
isExpanded: false,
|
||||
isExpanded: collection.isCollectionExpanded(),
|
||||
children: children,
|
||||
className: "collectionHeader",
|
||||
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
|
||||
@ -611,7 +614,8 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
]),
|
||||
contextMenu: ResourceTreeContextMenuButtonFactory.createStoreProcedureContextMenuItems(container, sp),
|
||||
})),
|
||||
onClick: () => {
|
||||
onClick: async () => {
|
||||
await collection.loadStoredProcedures();
|
||||
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.StoredProcedures);
|
||||
refreshActiveTab(
|
||||
(tab: TabsBase) =>
|
||||
@ -635,7 +639,8 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
]),
|
||||
contextMenu: ResourceTreeContextMenuButtonFactory.createUserDefinedFunctionContextMenuItems(container, udf),
|
||||
})),
|
||||
onClick: () => {
|
||||
onClick: async () => {
|
||||
await collection.loadUserDefinedFunctions();
|
||||
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.UserDefinedFunctions);
|
||||
refreshActiveTab(
|
||||
(tab: TabsBase) =>
|
||||
@ -657,7 +662,8 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Triggers]),
|
||||
contextMenu: ResourceTreeContextMenuButtonFactory.createTriggerContextMenuItems(container, trigger),
|
||||
})),
|
||||
onClick: () => {
|
||||
onClick: async () => {
|
||||
await collection.loadTriggers();
|
||||
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Triggers);
|
||||
refreshActiveTab(
|
||||
(tab: TabsBase) =>
|
||||
|
103
src/Explorer/Tutorials/QuickstartCarousel.tsx
Normal file
103
src/Explorer/Tutorials/QuickstartCarousel.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import { DefaultButton, IconButton, Image, Modal, PrimaryButton, Stack, Text } from "@fluentui/react";
|
||||
import { useCarousel } from "hooks/useCarousel";
|
||||
import React, { useState } from "react";
|
||||
import Youtube from "react-youtube";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "UserContext";
|
||||
import Image1 from "../../../images/CarouselImage1.svg";
|
||||
import Image2 from "../../../images/CarouselImage2.svg";
|
||||
|
||||
interface QuickstartCarouselProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export const QuickstartCarousel: React.FC<QuickstartCarouselProps> = ({
|
||||
isOpen,
|
||||
}: QuickstartCarouselProps): JSX.Element => {
|
||||
const [page, setPage] = useState<number>(1);
|
||||
return (
|
||||
<Modal
|
||||
styles={{ main: { width: 640 } }}
|
||||
isOpen={isOpen && page < 4}
|
||||
onDismissed={() => userContext.apiType === "SQL" && useCarousel.getState().setShowCoachMark(true)}
|
||||
>
|
||||
<Stack>
|
||||
<Stack horizontal horizontalAlign="space-between" style={{ padding: 16 }}>
|
||||
<Text variant="xLarge">{getHeaderText(page)}</Text>
|
||||
<IconButton iconProps={{ iconName: "Cancel" }} onClick={() => setPage(4)} />
|
||||
</Stack>
|
||||
{getContent(page)}
|
||||
<Text variant="medium" style={{ padding: "0 16px" }}>
|
||||
{getDescriptionText(page)}
|
||||
</Text>
|
||||
<Stack horizontal horizontalAlign="end">
|
||||
{page !== 1 && (
|
||||
<DefaultButton text="Previous" style={{ margin: "16px 8px 16px 0" }} onClick={() => setPage(page - 1)} />
|
||||
)}
|
||||
<PrimaryButton
|
||||
style={{ margin: "16px 16px 16px 0" }}
|
||||
text={page === 3 ? "Finish" : "Next"}
|
||||
onClick={() => {
|
||||
if (
|
||||
userContext.apiType === "Cassandra" ||
|
||||
userContext.apiType === "Tables" ||
|
||||
userContext.apiType === "Gremlin"
|
||||
) {
|
||||
setPage(page + 2);
|
||||
} else {
|
||||
if (page === 3 && userContext.apiType === "SQL") {
|
||||
useCarousel.getState().setShowCoachMark(true);
|
||||
}
|
||||
setPage(page + 1);
|
||||
}
|
||||
|
||||
if (page === 3) {
|
||||
traceSuccess(Action.CompleteCarousel);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const getHeaderText = (page: number): string => {
|
||||
switch (page) {
|
||||
case 1:
|
||||
return "Welcome! What is Cosmos DB?";
|
||||
case 2:
|
||||
return "Get Started with Sample Data";
|
||||
case 3:
|
||||
return "Connect to your database";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const getContent = (page: number): JSX.Element => {
|
||||
switch (page) {
|
||||
case 1:
|
||||
return <Youtube videoId="Jvgh64rvdXU" onPlay={() => traceSuccess(Action.PlayCarouselVideo)} />;
|
||||
case 2:
|
||||
return <Image style={{ width: 640 }} src={Image1} />;
|
||||
case 3:
|
||||
return <Image style={{ width: 640 }} src={Image2} />;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
|
||||
const getDescriptionText = (page: number): string => {
|
||||
switch (page) {
|
||||
case 1:
|
||||
return "Azure Cosmos DB is a fully managed NoSQL database service for modern app development. ";
|
||||
case 2:
|
||||
return "Launch the quickstart for a tutotrial to learn how to create a database, add sample data, connect to a sample app and more.";
|
||||
case 3:
|
||||
return "Already have an existing app? Connect your database to an app, or tooling of your choice from Data Explorer.";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
172
src/Explorer/Tutorials/QuickstartTutorial.tsx
Normal file
172
src/Explorer/Tutorials/QuickstartTutorial.tsx
Normal file
@ -0,0 +1,172 @@
|
||||
import { Link, Stack, TeachingBubble, Text } from "@fluentui/react";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||
import React from "react";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceCancel } from "Shared/Telemetry/TelemetryProcessor";
|
||||
|
||||
export const QuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
const { step, isSampleDBExpanded, isDocumentsTabOpened, sampleCollection, setStep } = useTeachingBubble();
|
||||
|
||||
const onDimissTeachingBubble = (): void => {
|
||||
setStep(0);
|
||||
traceCancel(Action.CancelUITour, { step });
|
||||
};
|
||||
|
||||
switch (step) {
|
||||
case 1:
|
||||
return isSampleDBExpanded ? (
|
||||
<TeachingBubble
|
||||
headline="View sample data"
|
||||
target={"#sampleItems"}
|
||||
hasCloseButton
|
||||
primaryButtonProps={{
|
||||
text: "Open Items",
|
||||
onClick: () => {
|
||||
sampleCollection.openTab();
|
||||
setStep(2);
|
||||
},
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 1 of 7"
|
||||
>
|
||||
Start viewing and working with your data by opening Items under Data
|
||||
</TeachingBubble>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
case 2:
|
||||
return isDocumentsTabOpened ? (
|
||||
<TeachingBubble
|
||||
headline="View item"
|
||||
target={".queryButton"}
|
||||
hasCloseButton
|
||||
primaryButtonProps={{
|
||||
text: "Next",
|
||||
onClick: () => setStep(3),
|
||||
}}
|
||||
secondaryButtonProps={{
|
||||
text: "Previous",
|
||||
onClick: () => setStep(1),
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 2 of 7"
|
||||
>
|
||||
View item here using the items window. Additionally you can also filter items to be reviewed with the filter
|
||||
function
|
||||
</TeachingBubble>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<TeachingBubble
|
||||
headline="Add new item"
|
||||
target={"#uploadItemBtn"}
|
||||
hasCloseButton
|
||||
primaryButtonProps={{
|
||||
text: "Next",
|
||||
onClick: () => setStep(4),
|
||||
}}
|
||||
secondaryButtonProps={{
|
||||
text: "Previous",
|
||||
onClick: () => setStep(2),
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 3 of 7"
|
||||
>
|
||||
Add new item by copy / pasting JSON; or uploading a JSON
|
||||
</TeachingBubble>
|
||||
);
|
||||
case 4:
|
||||
return (
|
||||
<TeachingBubble
|
||||
headline="Run a query"
|
||||
target={"#newQueryBtn"}
|
||||
hasCloseButton
|
||||
primaryButtonProps={{
|
||||
text: "Next",
|
||||
onClick: () => setStep(5),
|
||||
}}
|
||||
secondaryButtonProps={{
|
||||
text: "Previous",
|
||||
onClick: () => setStep(3),
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 4 of 7"
|
||||
>
|
||||
Query your data using either the filter function or new query.
|
||||
</TeachingBubble>
|
||||
);
|
||||
case 5:
|
||||
return (
|
||||
<TeachingBubble
|
||||
headline="Scale throughput"
|
||||
target={"#sampleScaleSettings"}
|
||||
hasCloseButton
|
||||
primaryButtonProps={{
|
||||
text: "Next",
|
||||
onClick: () => setStep(6),
|
||||
}}
|
||||
secondaryButtonProps={{
|
||||
text: "Previous",
|
||||
onClick: () => setStep(4),
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 5 of 7"
|
||||
>
|
||||
Change throughput provisioned to your container according to your needs
|
||||
</TeachingBubble>
|
||||
);
|
||||
case 6:
|
||||
return (
|
||||
<TeachingBubble
|
||||
headline="Create notebook"
|
||||
target={"#newNotebookBtn"}
|
||||
hasCloseButton
|
||||
primaryButtonProps={{
|
||||
text: "Next",
|
||||
onClick: () => setStep(7),
|
||||
}}
|
||||
secondaryButtonProps={{
|
||||
text: "Previous",
|
||||
onClick: () => setStep(5),
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 6 of 7"
|
||||
>
|
||||
Visualize your data, store queries in an interactive document
|
||||
</TeachingBubble>
|
||||
);
|
||||
case 7:
|
||||
return (
|
||||
<TeachingBubble
|
||||
headline="Congratulations!"
|
||||
target={"#newNotebookBtn"}
|
||||
hasCloseButton
|
||||
primaryButtonProps={{
|
||||
text: "Launch connect",
|
||||
onClick: () => useTabs.getState().openAndActivateConnectTab(),
|
||||
}}
|
||||
secondaryButtonProps={{
|
||||
text: "Previous",
|
||||
onClick: () => setStep(6),
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 7 of 7"
|
||||
>
|
||||
<Stack>
|
||||
<Text style={{ color: "white" }}>
|
||||
You have finished the tour in data explorer. For next steps, you may want to launch connect and start
|
||||
connecting with your current app.
|
||||
</Text>
|
||||
<Link style={{ color: "white", fontWeight: 600 }} target="_blank" href="https://aka.ms/cosmosdbsurvey">
|
||||
Share your feedback
|
||||
</Link>
|
||||
</Stack>
|
||||
</TeachingBubble>
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
};
|
@ -2,6 +2,7 @@ import { initializeIcons, Link, Text } from "@fluentui/react";
|
||||
import "bootstrap/dist/css/bootstrap.css";
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import { userContext } from "UserContext";
|
||||
import { initializeConfiguration } from "../ConfigContext";
|
||||
import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent";
|
||||
import {
|
||||
@ -25,7 +26,9 @@ const onInit = async () => {
|
||||
|
||||
const props: GalleryAndNotebookViewerComponentProps = {
|
||||
junoClient: new JunoClient(),
|
||||
selectedTab: galleryViewerProps.selectedTab || GalleryTab.PublicGallery,
|
||||
selectedTab:
|
||||
galleryViewerProps.selectedTab ||
|
||||
(userContext.features.publicGallery ? GalleryTab.PublicGallery : GalleryTab.OfficialSamples),
|
||||
sortBy: galleryViewerProps.sortBy || SortBy.MostRecent,
|
||||
searchText: galleryViewerProps.searchText,
|
||||
};
|
||||
|
48
src/Localization/en/MaterializedViewsBuilder.json
Normal file
48
src/Localization/en/MaterializedViewsBuilder.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"MaterializedViewsBuilderDescription": "Provision a Materializedviews builder cluster for your Azure Cosmos DB account. Materializedviews builder is compute in your account that performs read operations on source collection for any updates and applies them on materialized views as per the materializedview definition.",
|
||||
"MaterializedViewsBuilder": "Materializedviews Builder",
|
||||
"Provisioned": "Provisioned",
|
||||
"Deprovisioned": "Deprovisioned",
|
||||
"LearnAboutMaterializedViews": "Learn more about materializedviews.",
|
||||
"DeprovisioningDetailsText": "Learn more about materializedviews.",
|
||||
"MaterializedviewsBuilderPricing": "Learn more about materializedviews pricing.",
|
||||
"SKUs": "SKUs",
|
||||
"SKUsPlaceHolder": "Select SKUs",
|
||||
"NumberOfInstances": "Number of instances",
|
||||
"CosmosD2s": "Cosmos.D2s (General Purpose Cosmos Compute with 2 vCPUs, 8 GB Memory)",
|
||||
"CosmosD4s": "Cosmos.D4s (General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory)",
|
||||
"CosmosD8s": "Cosmos.D8s (General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory)",
|
||||
"CosmosD16s": "Cosmos.D16s (General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory)",
|
||||
"CosmosD32s": "Cosmos.D32s (General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory)",
|
||||
"CreateMessage": "MaterializedViewsBuilder resource is being created.",
|
||||
"CreateInitializeTitle": "Provisioning resource",
|
||||
"CreateInitializeMessage": "Materializedviews Builder resource will be provisioned.",
|
||||
"CreateSuccessTitle": "Resource provisioned",
|
||||
"CreateSuccesseMessage": "Materializedviews Builder resource provisioned.",
|
||||
"CreateFailureTitle": "Failed to provision resource",
|
||||
"CreateFailureMessage": "Materializedviews Builder resource provisioning failed.",
|
||||
"UpdateMessage": "MaterializedViewsBuilder resource is being updated.",
|
||||
"UpdateInitializeTitle": "Updating resource",
|
||||
"UpdateInitializeMessage": "Materializedviews Builder resource will be updated.",
|
||||
"UpdateSuccessTitle": "Resource updated",
|
||||
"UpdateSuccesseMessage": "Materializedviews Builder resource updated.",
|
||||
"UpdateFailureTitle": "Failed to update resource",
|
||||
"UpdateFailureMessage": "Materializedviews Builder resource updation failed.",
|
||||
"DeleteMessage": "MaterializedViewsBuilder resource is being deleted.",
|
||||
"DeleteInitializeTitle": "Deleting resource",
|
||||
"DeleteInitializeMessage": "Materializedviews Builder resource will be deleted.",
|
||||
"DeleteSuccessTitle": "Resource deleted",
|
||||
"DeleteSuccesseMessage": "Materializedviews Builder resource deleted.",
|
||||
"DeleteFailureTitle": "Failed to delete resource",
|
||||
"DeleteFailureMessage": "Materializedviews Builder resource deletion failed.",
|
||||
"ApproximateCost": "Approximate Cost Per Hour",
|
||||
"CostText": "Hourly cost of the Materializedviews Builder resource depends on the SKU selection, number of instances per region, and number of regions.",
|
||||
"MetricsString": "Metrics",
|
||||
"MetricsText": "Monitor the CPU and memory usage for the Materializedviews Builder instances in ",
|
||||
"MetricsBlade": "the metrics blade.",
|
||||
"MonitorUsage": "Monitor Usage",
|
||||
"ResizingDecisionText": "To understand if the Materializedviews Builder is the right size, ",
|
||||
"ResizingDecisionLink": "learn more about Materializedviews Builder sizing.",
|
||||
"WarningBannerOnUpdate": "Adding or modifying Materializedviews Builder instances may affect your bill.",
|
||||
"WarningBannerOnDelete": "After deprovisioning the Materializedviews Builder, your materializedviews will not be updated with new source changes anymore. Materializedviews builder is compute in your account that performs read operations on source collection for any updates and applies them on materialized views as per the materializedview definition."
|
||||
}
|
@ -7,7 +7,7 @@
|
||||
"RegionsAndAccountNameValidationError": "Regions and account name should not be empty.",
|
||||
"DbThroughputValidationError": "Please update throughput for database.",
|
||||
"DescriptionLabel": "Description",
|
||||
"DescriptionText": "This class sets collection and database throughput.",
|
||||
"DescriptionText": "This class sets collection and database throughput.\nTo know more -",
|
||||
"DecriptionLinkText": "Click here for more information",
|
||||
"Regions": "Regions",
|
||||
"RegionsPlaceholder": "Select a region",
|
||||
|
@ -1,6 +1,9 @@
|
||||
// CSS Dependencies
|
||||
import { initializeIcons } from "@fluentui/react";
|
||||
import "bootstrap/dist/css/bootstrap.css";
|
||||
import { QuickstartCarousel } from "Explorer/Tutorials/QuickstartCarousel";
|
||||
import { QuickstartTutorial } from "Explorer/Tutorials/QuickstartTutorial";
|
||||
import { useCarousel } from "hooks/useCarousel";
|
||||
import React, { useState } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import "../externals/jquery-ui.min.css";
|
||||
@ -57,6 +60,8 @@ initializeIcons();
|
||||
const App: React.FunctionComponent = () => {
|
||||
const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState<boolean>(true);
|
||||
const openedTabs = useTabs((state) => state.openedTabs);
|
||||
const isConnectTabOpen = useTabs((state) => state.isConnectTabOpen);
|
||||
const isCarouselOpen = useCarousel((state) => state.shouldOpen);
|
||||
|
||||
const config = useConfig();
|
||||
const explorer = useKnockoutExplorer(config?.platform);
|
||||
@ -100,7 +105,7 @@ const App: React.FunctionComponent = () => {
|
||||
</div>
|
||||
</div>
|
||||
{/* Collections Tree - End */}
|
||||
{openedTabs.length === 0 && <SplashScreen explorer={explorer} />}
|
||||
{openedTabs.length === 0 && !isConnectTabOpen && <SplashScreen explorer={explorer} />}
|
||||
<Tabs />
|
||||
</div>
|
||||
{/* Collections Tree and Tabs - End */}
|
||||
@ -115,6 +120,8 @@ const App: React.FunctionComponent = () => {
|
||||
</div>
|
||||
<SidePanel />
|
||||
<Dialog />
|
||||
{<QuickstartCarousel isOpen={isCarouselOpen} />}
|
||||
{<QuickstartTutorial />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -17,13 +17,14 @@ import {
|
||||
ContainerConnectionInfo,
|
||||
ContainerInfo,
|
||||
IContainerData,
|
||||
IDbAccountAllow,
|
||||
IMaxAllocationTimeExceeded,
|
||||
IMaxDbAccountsPerUserExceeded,
|
||||
IMaxUsersPerDbAccountExceeded,
|
||||
IPhoenixConnectionInfoResult,
|
||||
IPhoenixError,
|
||||
IProvisionData,
|
||||
IResponse,
|
||||
IValidationError,
|
||||
PhoenixErrorType,
|
||||
} from "../Contracts/DataModels";
|
||||
import { useNotebook } from "../Explorer/Notebook/useNotebook";
|
||||
@ -59,17 +60,19 @@ export class PhoenixClient {
|
||||
body: JSON.stringify(provisionData),
|
||||
});
|
||||
const responseJson = await response?.json();
|
||||
if (response.status === HttpStatusCodes.Forbidden) {
|
||||
throw new Error(this.ConvertToForbiddenErrorString(responseJson));
|
||||
if (response.ok) {
|
||||
return {
|
||||
status: response.status,
|
||||
data: responseJson,
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: response.status,
|
||||
data: responseJson,
|
||||
};
|
||||
const phoenixError = responseJson as IPhoenixError;
|
||||
if (response.status === HttpStatusCodes.Forbidden) {
|
||||
throw new Error(this.ConvertToForbiddenErrorString(phoenixError));
|
||||
}
|
||||
throw new Error(phoenixError.message);
|
||||
} catch (error) {
|
||||
if (response.status === HttpStatusCodes.Forbidden) {
|
||||
error.status = HttpStatusCodes.Forbidden;
|
||||
}
|
||||
error.status = response?.status;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -101,7 +104,7 @@ export class PhoenixClient {
|
||||
const containerStatus = await response.json();
|
||||
return {
|
||||
durationLeftInMinutes: containerStatus?.durationLeftInMinutes,
|
||||
notebookServerInfo: containerStatus?.notebookServerInfo,
|
||||
phoenixServerInfo: containerStatus?.phoenixServerInfo,
|
||||
status: ContainerStatusType.Active,
|
||||
};
|
||||
} else if (response.status === HttpStatusCodes.NotFound) {
|
||||
@ -145,7 +148,7 @@ export class PhoenixClient {
|
||||
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||
return {
|
||||
durationLeftInMinutes: undefined,
|
||||
notebookServerInfo: undefined,
|
||||
phoenixServerInfo: undefined,
|
||||
status: ContainerStatusType.Disconnected,
|
||||
};
|
||||
}
|
||||
@ -159,15 +162,17 @@ export class PhoenixClient {
|
||||
}
|
||||
}
|
||||
|
||||
public async isDbAcountWhitelisted(): Promise<boolean> {
|
||||
public async getDbAccountAllowedStatus(): Promise<IDbAccountAllow> {
|
||||
const startKey = TelemetryProcessor.traceStart(Action.PhoenixDBAccountAllowed, {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
});
|
||||
let responseJson;
|
||||
try {
|
||||
const response = await window.fetch(`${this.getPhoenixControlPlanePathPrefix()}`, {
|
||||
method: "GET",
|
||||
headers: PhoenixClient.getHeaders(),
|
||||
});
|
||||
responseJson = await response?.json();
|
||||
if (response.status !== HttpStatusCodes.OK) {
|
||||
throw new Error(`Received status code: ${response?.status}`);
|
||||
}
|
||||
@ -178,7 +183,11 @@ export class PhoenixClient {
|
||||
},
|
||||
startKey
|
||||
);
|
||||
return response.status === HttpStatusCodes.OK;
|
||||
return {
|
||||
status: response.status,
|
||||
message: responseJson?.message,
|
||||
type: responseJson?.type,
|
||||
};
|
||||
} catch (error) {
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.PhoenixDBAccountAllowed,
|
||||
@ -190,7 +199,11 @@ export class PhoenixClient {
|
||||
startKey
|
||||
);
|
||||
Logger.logError(getErrorMessage(error), "PhoenixClient/IsDbAcountWhitelisted");
|
||||
return false;
|
||||
return {
|
||||
status: HttpStatusCodes.Forbidden,
|
||||
message: responseJson?.message,
|
||||
type: responseJson?.type,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,7 +233,7 @@ export class PhoenixClient {
|
||||
};
|
||||
}
|
||||
|
||||
public ConvertToForbiddenErrorString(jsonData: IValidationError): string {
|
||||
public ConvertToForbiddenErrorString(jsonData: IPhoenixError): string {
|
||||
const errInfo = jsonData;
|
||||
switch (errInfo?.type) {
|
||||
case PhoenixErrorType.MaxAllocationTimeExceeded: {
|
||||
|
@ -29,7 +29,6 @@ export type Features = {
|
||||
readonly mongoProxyEndpoint?: string;
|
||||
readonly mongoProxyAPIs?: string;
|
||||
readonly enableThroughputCap: boolean;
|
||||
readonly enableNewQuickstart: boolean;
|
||||
readonly enableChatbot?: boolean;
|
||||
|
||||
// can be set via both flight and feature flag
|
||||
@ -92,7 +91,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
||||
partitionKeyDefault2: "true" === get("pkpartitionkeytest"),
|
||||
notebooksDownBanner: "true" === get("notebooksDownBanner"),
|
||||
enableThroughputCap: "true" === get("enablethroughputcap"),
|
||||
enableNewQuickstart: "true" === get("enablenewquickstart"),
|
||||
enableChatbot: "true" === get("enablechatbot"),
|
||||
};
|
||||
}
|
||||
|
@ -179,6 +179,15 @@ export default class SelfServeExample extends SelfServeBaseClass {
|
||||
})
|
||||
description: string;
|
||||
|
||||
@Values({
|
||||
description: {
|
||||
textTKey: `This UI can be used to dynamically change the throughput.
|
||||
This is an alternative to updating the throughput from the 'scale & settings' tab.`,
|
||||
type: DescriptionType.Text,
|
||||
},
|
||||
})
|
||||
multiLineDescription: string;
|
||||
|
||||
@Values({
|
||||
labelTKey: "Current Region",
|
||||
isDynamicDescription: true,
|
||||
|
@ -0,0 +1,228 @@
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { armRequestWithoutPolling } from "../../Utils/arm/request";
|
||||
import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor";
|
||||
import { RefreshResult } from "../SelfServeTypes";
|
||||
import MaterializedViewsBuilder from "./MaterializedViewsBuilder";
|
||||
import {
|
||||
FetchPricesResponse,
|
||||
PriceMapAndCurrencyCode,
|
||||
RegionsResponse,
|
||||
MaterializedViewsBuilderServiceResource,
|
||||
UpdateMaterializedViewsBuilderRequestParameters,
|
||||
} from "./MaterializedViewsBuilderTypes";
|
||||
|
||||
const apiVersion = "2021-07-01-preview";
|
||||
|
||||
export enum ResourceStatus {
|
||||
Running = "Running",
|
||||
Creating = "Creating",
|
||||
Updating = "Updating",
|
||||
Deleting = "Deleting",
|
||||
}
|
||||
|
||||
export interface MaterializedViewsBuilderResponse {
|
||||
sku: string;
|
||||
instances: number;
|
||||
status: string;
|
||||
endpoint: string;
|
||||
}
|
||||
|
||||
export const getPath = (subscriptionId: string, resourceGroup: string, name: string): string => {
|
||||
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}/services/materializedviewsBuilder`;
|
||||
};
|
||||
|
||||
export const updateMaterializedViewsBuilderResource = async (sku: string, instances: number): Promise<string> => {
|
||||
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
|
||||
const body: UpdateMaterializedViewsBuilderRequestParameters = {
|
||||
properties: {
|
||||
instanceSize: sku,
|
||||
instanceCount: instances,
|
||||
serviceType: "materializedviewsBuilder",
|
||||
},
|
||||
};
|
||||
const telemetryData = { ...body, httpMethod: "PUT", selfServeClassName: MaterializedViewsBuilder.name };
|
||||
const updateTimeStamp = selfServeTraceStart(telemetryData);
|
||||
let armRequestResult;
|
||||
try {
|
||||
armRequestResult = await armRequestWithoutPolling({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path,
|
||||
method: "PUT",
|
||||
apiVersion,
|
||||
body,
|
||||
});
|
||||
selfServeTraceSuccess(telemetryData, updateTimeStamp);
|
||||
} catch (e) {
|
||||
const failureTelemetry = { ...body, e, selfServeClassName: MaterializedViewsBuilder.name };
|
||||
selfServeTraceFailure(failureTelemetry, updateTimeStamp);
|
||||
throw e;
|
||||
}
|
||||
return armRequestResult?.operationStatusUrl;
|
||||
};
|
||||
|
||||
export const deleteMaterializedViewsBuilderResource = async (): Promise<string> => {
|
||||
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
|
||||
const telemetryData = { httpMethod: "DELETE", selfServeClassName: MaterializedViewsBuilder.name };
|
||||
const deleteTimeStamp = selfServeTraceStart(telemetryData);
|
||||
let armRequestResult;
|
||||
try {
|
||||
armRequestResult = await armRequestWithoutPolling({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path,
|
||||
method: "DELETE",
|
||||
apiVersion,
|
||||
});
|
||||
selfServeTraceSuccess(telemetryData, deleteTimeStamp);
|
||||
} catch (e) {
|
||||
const failureTelemetry = { e, selfServeClassName: MaterializedViewsBuilder.name };
|
||||
selfServeTraceFailure(failureTelemetry, deleteTimeStamp);
|
||||
throw e;
|
||||
}
|
||||
return armRequestResult?.operationStatusUrl;
|
||||
};
|
||||
|
||||
export const getMaterializedViewsBuilderResource = async (): Promise<MaterializedViewsBuilderServiceResource> => {
|
||||
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
|
||||
const telemetryData = { httpMethod: "GET", selfServeClassName: MaterializedViewsBuilder.name };
|
||||
const getResourceTimeStamp = selfServeTraceStart(telemetryData);
|
||||
let armRequestResult;
|
||||
try {
|
||||
armRequestResult = await armRequestWithoutPolling<MaterializedViewsBuilderServiceResource>({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path,
|
||||
method: "GET",
|
||||
apiVersion,
|
||||
});
|
||||
selfServeTraceSuccess(telemetryData, getResourceTimeStamp);
|
||||
} catch (e) {
|
||||
const failureTelemetry = { e, selfServeClassName: MaterializedViewsBuilder.name };
|
||||
selfServeTraceFailure(failureTelemetry, getResourceTimeStamp);
|
||||
throw e;
|
||||
}
|
||||
return armRequestResult?.result;
|
||||
};
|
||||
|
||||
export const getCurrentProvisioningState = async (): Promise<MaterializedViewsBuilderResponse> => {
|
||||
try {
|
||||
const response = await getMaterializedViewsBuilderResource();
|
||||
return {
|
||||
sku: response.properties.instanceSize,
|
||||
instances: response.properties.instanceCount,
|
||||
status: response.properties.status,
|
||||
endpoint: response.properties.MaterializedViewsBuilderEndPoint,
|
||||
};
|
||||
} catch (e) {
|
||||
return { sku: undefined, instances: undefined, status: undefined, endpoint: undefined };
|
||||
}
|
||||
};
|
||||
|
||||
export const refreshMaterializedViewsBuilderProvisioning = async (): Promise<RefreshResult> => {
|
||||
try {
|
||||
const response = await getMaterializedViewsBuilderResource();
|
||||
if (response.properties.status === ResourceStatus.Running.toString()) {
|
||||
return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined };
|
||||
} else if (response.properties.status === ResourceStatus.Creating.toString()) {
|
||||
return { isUpdateInProgress: true, updateInProgressMessageTKey: "CreateMessage" };
|
||||
} else if (response.properties.status === ResourceStatus.Deleting.toString()) {
|
||||
return { isUpdateInProgress: true, updateInProgressMessageTKey: "DeleteMessage" };
|
||||
} else {
|
||||
return { isUpdateInProgress: true, updateInProgressMessageTKey: "UpdateMessage" };
|
||||
}
|
||||
} catch {
|
||||
//TODO differentiate between different failures
|
||||
return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined };
|
||||
}
|
||||
};
|
||||
|
||||
const getGeneralPath = (subscriptionId: string, resourceGroup: string, name: string): string => {
|
||||
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}`;
|
||||
};
|
||||
|
||||
export const getRegions = async (): Promise<Array<string>> => {
|
||||
const telemetryData = {
|
||||
feature: "Calculate approximate cost",
|
||||
function: "getRegions",
|
||||
description: "",
|
||||
selfServeClassName: MaterializedViewsBuilder.name,
|
||||
};
|
||||
const getRegionsTimestamp = selfServeTraceStart(telemetryData);
|
||||
|
||||
try {
|
||||
const regions = new Array<string>();
|
||||
|
||||
const response = await armRequestWithoutPolling<RegionsResponse>({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path: getGeneralPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name),
|
||||
method: "GET",
|
||||
apiVersion: "2021-07-01-preview",
|
||||
});
|
||||
|
||||
if (response.result.location !== undefined) {
|
||||
regions.push(response.result.location.split(" ").join("").toLowerCase());
|
||||
} else {
|
||||
for (const location of response.result.locations) {
|
||||
regions.push(location.locationName.split(" ").join("").toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
selfServeTraceSuccess(telemetryData, getRegionsTimestamp);
|
||||
return regions;
|
||||
} catch (err) {
|
||||
const failureTelemetry = { err, selfServeClassName: MaterializedViewsBuilder.name };
|
||||
selfServeTraceFailure(failureTelemetry, getRegionsTimestamp);
|
||||
return new Array<string>();
|
||||
}
|
||||
};
|
||||
|
||||
const getFetchPricesPathForRegion = (subscriptionId: string): string => {
|
||||
return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`;
|
||||
};
|
||||
|
||||
export const getPriceMapAndCurrencyCode = async (regions: Array<string>): Promise<PriceMapAndCurrencyCode> => {
|
||||
const telemetryData = {
|
||||
feature: "Calculate approximate cost",
|
||||
function: "getPriceMapAndCurrencyCode",
|
||||
description: "fetch prices API call",
|
||||
selfServeClassName: MaterializedViewsBuilder.name,
|
||||
};
|
||||
const getPriceMapAndCurrencyCodeTimestamp = selfServeTraceStart(telemetryData);
|
||||
|
||||
try {
|
||||
const priceMap = new Map<string, Map<string, number>>();
|
||||
let currencyCode;
|
||||
for (const region of regions) {
|
||||
const regionPriceMap = new Map<string, number>();
|
||||
|
||||
const response = await armRequestWithoutPolling<FetchPricesResponse>({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path: getFetchPricesPathForRegion(userContext.subscriptionId),
|
||||
method: "POST",
|
||||
apiVersion: "2020-01-01-preview",
|
||||
queryParams: {
|
||||
filter:
|
||||
"armRegionName eq '" +
|
||||
region +
|
||||
"' and serviceFamily eq 'Databases' and productName eq 'Azure Cosmos DB MaterializedViews Builder - General Purpose'",
|
||||
},
|
||||
});
|
||||
|
||||
for (const item of response.result.Items) {
|
||||
if (currencyCode === undefined) {
|
||||
currencyCode = item.currencyCode;
|
||||
} else if (item.currencyCode !== currencyCode) {
|
||||
throw Error("Currency Code Mismatch: Currency code not same for all regions / skus.");
|
||||
}
|
||||
regionPriceMap.set(item.skuName, item.retailPrice);
|
||||
}
|
||||
priceMap.set(region, regionPriceMap);
|
||||
}
|
||||
|
||||
selfServeTraceSuccess(telemetryData, getPriceMapAndCurrencyCodeTimestamp);
|
||||
return { priceMap: priceMap, currencyCode: currencyCode };
|
||||
} catch (err) {
|
||||
const failureTelemetry = { err, selfServeClassName: MaterializedViewsBuilder.name };
|
||||
selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp);
|
||||
return { priceMap: undefined, currencyCode: undefined };
|
||||
}
|
||||
};
|
@ -0,0 +1,416 @@
|
||||
import { IsDisplayable, OnChange, PropertyInfo, RefreshOptions, Values } from "../Decorators";
|
||||
import {
|
||||
selfServeTrace,
|
||||
selfServeTraceFailure,
|
||||
selfServeTraceStart,
|
||||
selfServeTraceSuccess,
|
||||
} from "../SelfServeTelemetryProcessor";
|
||||
import {
|
||||
ChoiceItem,
|
||||
Description,
|
||||
DescriptionType,
|
||||
Info,
|
||||
InputType,
|
||||
NumberUiType,
|
||||
OnSaveResult,
|
||||
RefreshResult,
|
||||
SelfServeBaseClass,
|
||||
SmartUiInput,
|
||||
} from "../SelfServeTypes";
|
||||
import { BladeType, generateBladeLink } from "../SelfServeUtils";
|
||||
import {
|
||||
deleteMaterializedViewsBuilderResource,
|
||||
getCurrentProvisioningState,
|
||||
getPriceMapAndCurrencyCode,
|
||||
getRegions,
|
||||
refreshMaterializedViewsBuilderProvisioning,
|
||||
updateMaterializedViewsBuilderResource,
|
||||
} from "./MaterializedViewsBuilder.rp";
|
||||
|
||||
const costPerHourDefaultValue: Description = {
|
||||
textTKey: "CostText",
|
||||
type: DescriptionType.Text,
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
|
||||
textTKey: "MaterializedviewsBuilderPricing",
|
||||
},
|
||||
};
|
||||
|
||||
const metricsStringValue: Description = {
|
||||
textTKey: "MetricsText",
|
||||
type: DescriptionType.Text,
|
||||
link: {
|
||||
href: generateBladeLink(BladeType.Metrics),
|
||||
textTKey: "MetricsBlade",
|
||||
},
|
||||
};
|
||||
|
||||
const CosmosD2s = "Cosmos.D2s";
|
||||
const CosmosD4s = "Cosmos.D4s";
|
||||
const CosmosD8s = "Cosmos.D8s";
|
||||
const CosmosD16s = "Cosmos.D16s";
|
||||
|
||||
const onSKUChange = (newValue: InputType, currentValues: Map<string, SmartUiInput>): Map<string, SmartUiInput> => {
|
||||
currentValues.set("sku", { value: newValue });
|
||||
currentValues.set("costPerHour", {
|
||||
value: calculateCost(newValue as string, currentValues.get("instances").value as number),
|
||||
});
|
||||
|
||||
return currentValues;
|
||||
};
|
||||
|
||||
const onNumberOfInstancesChange = (
|
||||
newValue: InputType,
|
||||
currentValues: Map<string, SmartUiInput>,
|
||||
baselineValues: Map<string, SmartUiInput>
|
||||
): Map<string, SmartUiInput> => {
|
||||
currentValues.set("instances", { value: newValue });
|
||||
const MaterializedViewsBuilderOriginallyEnabled = baselineValues.get("enableMaterializedViewsBuilder")
|
||||
?.value as boolean;
|
||||
const baselineInstances = baselineValues.get("instances")?.value as number;
|
||||
if (!MaterializedViewsBuilderOriginallyEnabled || baselineInstances !== newValue) {
|
||||
currentValues.set("warningBanner", {
|
||||
value: {
|
||||
textTKey: "WarningBannerOnUpdate",
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
|
||||
textTKey: "MaterializedviewsBuilderPricing",
|
||||
},
|
||||
} as Description,
|
||||
hidden: false,
|
||||
});
|
||||
} else {
|
||||
currentValues.set("warningBanner", undefined);
|
||||
}
|
||||
|
||||
currentValues.set("costPerHour", {
|
||||
value: calculateCost(currentValues.get("sku").value as string, newValue as number),
|
||||
});
|
||||
|
||||
return currentValues;
|
||||
};
|
||||
|
||||
const onEnableMaterializedViewsBuilderChange = (
|
||||
newValue: InputType,
|
||||
currentValues: Map<string, SmartUiInput>,
|
||||
baselineValues: ReadonlyMap<string, SmartUiInput>
|
||||
): Map<string, SmartUiInput> => {
|
||||
currentValues.set("enableMaterializedViewsBuilder", { value: newValue });
|
||||
const MaterializedViewsBuilderOriginallyEnabled = baselineValues.get("enableMaterializedViewsBuilder")
|
||||
?.value as boolean;
|
||||
if (MaterializedViewsBuilderOriginallyEnabled === newValue) {
|
||||
currentValues.set("sku", baselineValues.get("sku"));
|
||||
currentValues.set("instances", baselineValues.get("instances"));
|
||||
currentValues.set("costPerHour", baselineValues.get("costPerHour"));
|
||||
currentValues.set("warningBanner", baselineValues.get("warningBanner"));
|
||||
currentValues.set("metricsString", baselineValues.get("metricsString"));
|
||||
return currentValues;
|
||||
}
|
||||
|
||||
currentValues.set("warningBanner", undefined);
|
||||
if (newValue === true) {
|
||||
currentValues.set("warningBanner", {
|
||||
value: {
|
||||
textTKey: "WarningBannerOnUpdate",
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
|
||||
textTKey: "MaterializedviewsBuilderPricing",
|
||||
},
|
||||
} as Description,
|
||||
hidden: false,
|
||||
});
|
||||
|
||||
currentValues.set("costPerHour", {
|
||||
value: calculateCost(baselineValues.get("sku").value as string, baselineValues.get("instances").value as number),
|
||||
hidden: false,
|
||||
});
|
||||
} else {
|
||||
currentValues.set("warningBanner", {
|
||||
value: {
|
||||
textTKey: "WarningBannerOnDelete",
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviews",
|
||||
textTKey: "DeprovisioningDetailsText",
|
||||
},
|
||||
} as Description,
|
||||
hidden: false,
|
||||
});
|
||||
|
||||
currentValues.set("costPerHour", { value: costPerHourDefaultValue, hidden: true });
|
||||
}
|
||||
const sku = currentValues.get("sku");
|
||||
const instances = currentValues.get("instances");
|
||||
const hideAttributes = newValue === undefined || !(newValue as boolean);
|
||||
currentValues.set("sku", {
|
||||
value: sku.value,
|
||||
hidden: hideAttributes,
|
||||
disabled: MaterializedViewsBuilderOriginallyEnabled,
|
||||
});
|
||||
currentValues.set("instances", {
|
||||
value: instances.value,
|
||||
hidden: hideAttributes,
|
||||
disabled: MaterializedViewsBuilderOriginallyEnabled,
|
||||
});
|
||||
|
||||
currentValues.set("metricsString", {
|
||||
value: metricsStringValue,
|
||||
hidden: !newValue || !MaterializedViewsBuilderOriginallyEnabled,
|
||||
});
|
||||
|
||||
return currentValues;
|
||||
};
|
||||
|
||||
const skuDropDownItems: ChoiceItem[] = [
|
||||
{ labelTKey: "CosmosD2s", key: CosmosD2s },
|
||||
{ labelTKey: "CosmosD4s", key: CosmosD4s },
|
||||
{ labelTKey: "CosmosD8s", key: CosmosD8s },
|
||||
{ labelTKey: "CosmosD16s", key: CosmosD16s },
|
||||
];
|
||||
|
||||
const getSkus = async (): Promise<ChoiceItem[]> => {
|
||||
return skuDropDownItems;
|
||||
};
|
||||
|
||||
const getInstancesMin = async (): Promise<number> => {
|
||||
return 1;
|
||||
};
|
||||
|
||||
const getInstancesMax = async (): Promise<number> => {
|
||||
return 5;
|
||||
};
|
||||
|
||||
const NumberOfInstancesDropdownInfo: Info = {
|
||||
messageTKey: "ResizingDecisionText",
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-size",
|
||||
textTKey: "ResizingDecisionLink",
|
||||
},
|
||||
};
|
||||
|
||||
const ApproximateCostDropDownInfo: Info = {
|
||||
messageTKey: "CostText",
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
|
||||
textTKey: "MaterializedviewsBuilderPricing",
|
||||
},
|
||||
};
|
||||
|
||||
let priceMap: Map<string, Map<string, number>>;
|
||||
let currencyCode: string;
|
||||
let regions: Array<string>;
|
||||
|
||||
const calculateCost = (skuName: string, instanceCount: number): Description => {
|
||||
const telemetryData = {
|
||||
feature: "Calculate approximate cost",
|
||||
function: "calculateCost",
|
||||
description: "performs final calculation",
|
||||
selfServeClassName: MaterializedViewsBuilder.name,
|
||||
};
|
||||
const calculateCostTimestamp = selfServeTraceStart(telemetryData);
|
||||
|
||||
try {
|
||||
let costPerHour = 0;
|
||||
for (const region of regions) {
|
||||
const incrementalCost = priceMap.get(region).get(skuName.replace("Cosmos.", ""));
|
||||
if (incrementalCost === undefined) {
|
||||
throw new Error("Value not found in map.");
|
||||
}
|
||||
costPerHour += incrementalCost;
|
||||
}
|
||||
|
||||
if (costPerHour === 0) {
|
||||
throw new Error("Cost per hour = 0");
|
||||
}
|
||||
|
||||
costPerHour *= instanceCount;
|
||||
costPerHour = Math.round(costPerHour * 100) / 100;
|
||||
|
||||
selfServeTraceSuccess(telemetryData, calculateCostTimestamp);
|
||||
return {
|
||||
textTKey: `${costPerHour} ${currencyCode}`,
|
||||
type: DescriptionType.Text,
|
||||
};
|
||||
} catch (err) {
|
||||
const failureTelemetry = { err, regions, priceMap, selfServeClassName: MaterializedViewsBuilder.name };
|
||||
selfServeTraceFailure(failureTelemetry, calculateCostTimestamp);
|
||||
|
||||
return costPerHourDefaultValue;
|
||||
}
|
||||
};
|
||||
|
||||
@IsDisplayable()
|
||||
@RefreshOptions({ retryIntervalInMs: 20000 })
|
||||
export default class MaterializedViewsBuilder extends SelfServeBaseClass {
|
||||
public onRefresh = async (): Promise<RefreshResult> => {
|
||||
return await refreshMaterializedViewsBuilderProvisioning();
|
||||
};
|
||||
|
||||
public onSave = async (
|
||||
currentValues: Map<string, SmartUiInput>,
|
||||
baselineValues: Map<string, SmartUiInput>
|
||||
): Promise<OnSaveResult> => {
|
||||
selfServeTrace({ selfServeClassName: MaterializedViewsBuilder.name });
|
||||
|
||||
const MaterializedViewsBuilderCurrentlyEnabled = currentValues.get("enableMaterializedViewsBuilder")
|
||||
?.value as boolean;
|
||||
const MaterializedViewsBuilderOriginallyEnabled = baselineValues.get("enableMaterializedViewsBuilder")
|
||||
?.value as boolean;
|
||||
|
||||
currentValues.set("warningBanner", undefined);
|
||||
|
||||
if (MaterializedViewsBuilderOriginallyEnabled) {
|
||||
if (!MaterializedViewsBuilderCurrentlyEnabled) {
|
||||
const operationStatusUrl = await deleteMaterializedViewsBuilderResource();
|
||||
return {
|
||||
operationStatusUrl: operationStatusUrl,
|
||||
portalNotification: {
|
||||
initialize: {
|
||||
titleTKey: "DeleteInitializeTitle",
|
||||
messageTKey: "DeleteInitializeMessage",
|
||||
},
|
||||
success: {
|
||||
titleTKey: "DeleteSuccessTitle",
|
||||
messageTKey: "DeleteSuccesseMessage",
|
||||
},
|
||||
failure: {
|
||||
titleTKey: "DeleteFailureTitle",
|
||||
messageTKey: "DeleteFailureMessage",
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
const sku = currentValues.get("sku")?.value as string;
|
||||
const instances = currentValues.get("instances").value as number;
|
||||
const operationStatusUrl = await updateMaterializedViewsBuilderResource(sku, instances);
|
||||
return {
|
||||
operationStatusUrl: operationStatusUrl,
|
||||
portalNotification: {
|
||||
initialize: {
|
||||
titleTKey: "UpdateInitializeTitle",
|
||||
messageTKey: "UpdateInitializeMessage",
|
||||
},
|
||||
success: {
|
||||
titleTKey: "UpdateSuccessTitle",
|
||||
messageTKey: "UpdateSuccesseMessage",
|
||||
},
|
||||
failure: {
|
||||
titleTKey: "UpdateFailureTitle",
|
||||
messageTKey: "UpdateFailureMessage",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
} else {
|
||||
const sku = currentValues.get("sku")?.value as string;
|
||||
const instances = currentValues.get("instances").value as number;
|
||||
const operationStatusUrl = await updateMaterializedViewsBuilderResource(sku, instances);
|
||||
return {
|
||||
operationStatusUrl: operationStatusUrl,
|
||||
portalNotification: {
|
||||
initialize: {
|
||||
titleTKey: "CreateInitializeTitle",
|
||||
messageTKey: "CreateInitializeMessage",
|
||||
},
|
||||
success: {
|
||||
titleTKey: "CreateSuccessTitle",
|
||||
messageTKey: "CreateSuccesseMessage",
|
||||
},
|
||||
failure: {
|
||||
titleTKey: "CreateFailureTitle",
|
||||
messageTKey: "CreateFailureMessage",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
public initialize = async (): Promise<Map<string, SmartUiInput>> => {
|
||||
// Based on the RP call enableMaterializedViewsBuilder will be true if it has not yet been enabled and false if it has.
|
||||
const defaults = new Map<string, SmartUiInput>();
|
||||
defaults.set("enableMaterializedViewsBuilder", { value: false });
|
||||
defaults.set("sku", { value: CosmosD2s, hidden: true });
|
||||
defaults.set("instances", { value: await getInstancesMin(), hidden: true });
|
||||
defaults.set("costPerHour", undefined);
|
||||
defaults.set("metricsString", {
|
||||
value: undefined,
|
||||
hidden: true,
|
||||
});
|
||||
|
||||
regions = await getRegions();
|
||||
const priceMapAndCurrencyCode = await getPriceMapAndCurrencyCode(regions);
|
||||
priceMap = priceMapAndCurrencyCode.priceMap;
|
||||
currencyCode = priceMapAndCurrencyCode.currencyCode;
|
||||
|
||||
const response = await getCurrentProvisioningState();
|
||||
if (response.status && response.status !== "Deleting") {
|
||||
defaults.set("enableMaterializedViewsBuilder", { value: true });
|
||||
defaults.set("sku", { value: response.sku, disabled: true });
|
||||
defaults.set("instances", { value: response.instances, disabled: false });
|
||||
defaults.set("costPerHour", { value: calculateCost(response.sku, response.instances) });
|
||||
|
||||
defaults.set("metricsString", {
|
||||
value: metricsStringValue,
|
||||
hidden: false,
|
||||
});
|
||||
}
|
||||
defaults.set("warningBanner", undefined);
|
||||
return defaults;
|
||||
};
|
||||
|
||||
@Values({
|
||||
isDynamicDescription: true,
|
||||
})
|
||||
warningBanner: string;
|
||||
|
||||
@Values({
|
||||
description: {
|
||||
textTKey: "MaterializedViewsBuilderDescription",
|
||||
type: DescriptionType.Text,
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviews",
|
||||
textTKey: "LearnAboutMaterializedViews",
|
||||
},
|
||||
},
|
||||
})
|
||||
description: string;
|
||||
|
||||
@OnChange(onEnableMaterializedViewsBuilderChange)
|
||||
@Values({
|
||||
labelTKey: "MaterializedViewsBuilder",
|
||||
trueLabelTKey: "Provisioned",
|
||||
falseLabelTKey: "Deprovisioned",
|
||||
})
|
||||
enableMaterializedViewsBuilder: boolean;
|
||||
|
||||
@OnChange(onSKUChange)
|
||||
@Values({
|
||||
labelTKey: "SKUs",
|
||||
choices: getSkus,
|
||||
placeholderTKey: "SKUsPlaceHolder",
|
||||
})
|
||||
sku: ChoiceItem;
|
||||
|
||||
@OnChange(onNumberOfInstancesChange)
|
||||
@PropertyInfo(NumberOfInstancesDropdownInfo)
|
||||
@Values({
|
||||
labelTKey: "NumberOfInstances",
|
||||
min: getInstancesMin,
|
||||
max: getInstancesMax,
|
||||
step: 1,
|
||||
uiType: NumberUiType.Spinner,
|
||||
})
|
||||
instances: number;
|
||||
|
||||
@PropertyInfo(ApproximateCostDropDownInfo)
|
||||
@Values({
|
||||
labelTKey: "ApproximateCost",
|
||||
isDynamicDescription: true,
|
||||
})
|
||||
costPerHour: string;
|
||||
|
||||
@Values({
|
||||
labelTKey: "MonitorUsage",
|
||||
description: metricsStringValue,
|
||||
})
|
||||
metricsString: string;
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
export type MaterializedViewsBuilderServiceResource = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
properties: MaterializedViewsBuilderServiceProps;
|
||||
locations: MaterializedViewsBuilderServiceLocations;
|
||||
};
|
||||
export type MaterializedViewsBuilderServiceProps = {
|
||||
serviceType: string;
|
||||
creationTime: string;
|
||||
status: string;
|
||||
instanceSize: string;
|
||||
instanceCount: number;
|
||||
MaterializedViewsBuilderEndPoint: string;
|
||||
};
|
||||
|
||||
export type MaterializedViewsBuilderServiceLocations = {
|
||||
location: string;
|
||||
status: string;
|
||||
MaterializedViewsBuilderEndpoint: string;
|
||||
};
|
||||
|
||||
export type UpdateMaterializedViewsBuilderRequestParameters = {
|
||||
properties: UpdateMaterializedViewsBuilderRequestProperties;
|
||||
};
|
||||
|
||||
export type UpdateMaterializedViewsBuilderRequestProperties = {
|
||||
instanceSize: string;
|
||||
instanceCount: number;
|
||||
serviceType: string;
|
||||
};
|
||||
|
||||
export type FetchPricesResponse = {
|
||||
Items: Array<PriceItem>;
|
||||
NextPageLink: string | undefined;
|
||||
Count: number;
|
||||
};
|
||||
|
||||
export type PriceMapAndCurrencyCode = {
|
||||
priceMap: Map<string, Map<string, number>>;
|
||||
currencyCode: string;
|
||||
};
|
||||
|
||||
export type PriceItem = {
|
||||
retailPrice: number;
|
||||
skuName: string;
|
||||
currencyCode: string;
|
||||
};
|
||||
|
||||
export type RegionsResponse = {
|
||||
locations: Array<RegionItem>;
|
||||
location: string;
|
||||
};
|
||||
|
||||
export type RegionItem = {
|
||||
locationName: string;
|
||||
};
|
@ -58,6 +58,14 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
|
||||
await loadTranslations(graphAPICompute.constructor.name);
|
||||
return graphAPICompute.toSelfServeDescriptor();
|
||||
}
|
||||
case SelfServeType.materializedviewsbuilder: {
|
||||
const MaterializedViewsBuilder = await import(
|
||||
/* webpackChunkName: "MaterializedViewsBuilder" */ "./MaterializedViewsBuilder/MaterializedViewsBuilder"
|
||||
);
|
||||
const materializedViewsBuilder = new MaterializedViewsBuilder.default();
|
||||
await loadTranslations(materializedViewsBuilder.constructor.name);
|
||||
return materializedViewsBuilder.toSelfServeDescriptor();
|
||||
}
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ export enum SelfServeType {
|
||||
example = "example",
|
||||
sqlx = "sqlx",
|
||||
graphapicompute = "graphapicompute",
|
||||
materializedviewsbuilder = "materializedviewsbuilder",
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -7,6 +7,7 @@ import SqlX from "./SqlX";
|
||||
import {
|
||||
FetchPricesResponse,
|
||||
PriceMapAndCurrencyCode,
|
||||
RegionItem,
|
||||
RegionsResponse,
|
||||
SqlxServiceResource,
|
||||
UpdateDedicatedGatewayRequestParameters,
|
||||
@ -139,7 +140,7 @@ const getGeneralPath = (subscriptionId: string, resourceGroup: string, name: str
|
||||
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}`;
|
||||
};
|
||||
|
||||
export const getRegions = async (): Promise<Array<string>> => {
|
||||
export const getRegions = async (): Promise<Array<RegionItem>> => {
|
||||
const telemetryData = {
|
||||
feature: "Calculate approximate cost",
|
||||
function: "getRegions",
|
||||
@ -149,8 +150,6 @@ export const getRegions = async (): Promise<Array<string>> => {
|
||||
const getRegionsTimestamp = selfServeTraceStart(telemetryData);
|
||||
|
||||
try {
|
||||
const regions = new Array<string>();
|
||||
|
||||
const response = await armRequestWithoutPolling<RegionsResponse>({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path: getGeneralPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name),
|
||||
@ -158,20 +157,12 @@ export const getRegions = async (): Promise<Array<string>> => {
|
||||
apiVersion: "2021-04-01-preview",
|
||||
});
|
||||
|
||||
if (response.result.location !== undefined) {
|
||||
regions.push(response.result.location.split(" ").join("").toLowerCase());
|
||||
} else {
|
||||
for (const location of response.result.locations) {
|
||||
regions.push(location.locationName.split(" ").join("").toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
selfServeTraceSuccess(telemetryData, getRegionsTimestamp);
|
||||
return regions;
|
||||
return response.result.properties.locations;
|
||||
} catch (err) {
|
||||
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
||||
selfServeTraceFailure(failureTelemetry, getRegionsTimestamp);
|
||||
return new Array<string>();
|
||||
return new Array<RegionItem>();
|
||||
}
|
||||
};
|
||||
|
||||
@ -179,7 +170,7 @@ const getFetchPricesPathForRegion = (subscriptionId: string): string => {
|
||||
return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`;
|
||||
};
|
||||
|
||||
export const getPriceMapAndCurrencyCode = async (regions: Array<string>): Promise<PriceMapAndCurrencyCode> => {
|
||||
export const getPriceMapAndCurrencyCode = async (regions: Array<RegionItem>): Promise<PriceMapAndCurrencyCode> => {
|
||||
const telemetryData = {
|
||||
feature: "Calculate approximate cost",
|
||||
function: "getPriceMapAndCurrencyCode",
|
||||
@ -191,7 +182,7 @@ export const getPriceMapAndCurrencyCode = async (regions: Array<string>): Promis
|
||||
try {
|
||||
const priceMap = new Map<string, Map<string, number>>();
|
||||
let currencyCode;
|
||||
for (const region of regions) {
|
||||
for (const regionItem of regions) {
|
||||
const regionPriceMap = new Map<string, number>();
|
||||
|
||||
const response = await armRequestWithoutPolling<FetchPricesResponse>({
|
||||
@ -202,7 +193,7 @@ export const getPriceMapAndCurrencyCode = async (regions: Array<string>): Promis
|
||||
queryParams: {
|
||||
filter:
|
||||
"armRegionName eq '" +
|
||||
region +
|
||||
regionItem.locationName.split(" ").join("").toLowerCase() +
|
||||
"' and serviceFamily eq 'Databases' and productName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'",
|
||||
},
|
||||
});
|
||||
@ -215,7 +206,7 @@ export const getPriceMapAndCurrencyCode = async (regions: Array<string>): Promis
|
||||
}
|
||||
regionPriceMap.set(item.skuName, item.retailPrice);
|
||||
}
|
||||
priceMap.set(region, regionPriceMap);
|
||||
priceMap.set(regionItem.locationName, regionPriceMap);
|
||||
}
|
||||
|
||||
selfServeTraceSuccess(telemetryData, getPriceMapAndCurrencyCodeTimestamp);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { RegionItem } from "SelfServe/SqlX/SqlxTypes";
|
||||
import { IsDisplayable, OnChange, PropertyInfo, RefreshOptions, Values } from "../Decorators";
|
||||
import {
|
||||
selfServeTrace,
|
||||
@ -208,7 +209,7 @@ const ApproximateCostDropDownInfo: Info = {
|
||||
|
||||
let priceMap: Map<string, Map<string, number>>;
|
||||
let currencyCode: string;
|
||||
let regions: Array<string>;
|
||||
let regions: Array<RegionItem>;
|
||||
|
||||
const calculateCost = (skuName: string, instanceCount: number): Description => {
|
||||
const telemetryData = {
|
||||
@ -221,27 +222,47 @@ const calculateCost = (skuName: string, instanceCount: number): Description => {
|
||||
|
||||
try {
|
||||
let costPerHour = 0;
|
||||
for (const region of regions) {
|
||||
const incrementalCost = priceMap.get(region).get(skuName.replace("Cosmos.", ""));
|
||||
let costBreakdown = "";
|
||||
for (const regionItem of regions) {
|
||||
const incrementalCost = priceMap.get(regionItem.locationName).get(skuName.replace("Cosmos.", ""));
|
||||
if (incrementalCost === undefined) {
|
||||
throw new Error("Value not found in map.");
|
||||
throw new Error(`${regionItem.locationName} not found in price map.`);
|
||||
} else if (incrementalCost === 0) {
|
||||
throw new Error(`${regionItem.locationName} cost per hour = 0`);
|
||||
}
|
||||
costPerHour += incrementalCost;
|
||||
|
||||
let regionalInstanceCount = instanceCount;
|
||||
if (regionItem.isZoneRedundant) {
|
||||
regionalInstanceCount = Math.ceil(instanceCount * 1.5);
|
||||
}
|
||||
|
||||
const regionalCostPerHour = incrementalCost * regionalInstanceCount;
|
||||
costBreakdown += `
|
||||
${regionItem.locationName} ${regionItem.isZoneRedundant ? "(AZ)" : ""}
|
||||
${regionalCostPerHour} ${currencyCode} (${regionalInstanceCount} instances * ${incrementalCost} ${currencyCode})\
|
||||
`;
|
||||
|
||||
if (regionalCostPerHour === 0) {
|
||||
throw new Error(`${regionItem.locationName} Cost per hour = 0`);
|
||||
}
|
||||
|
||||
costPerHour += regionalCostPerHour;
|
||||
}
|
||||
|
||||
if (costPerHour === 0) {
|
||||
throw new Error("Cost per hour = 0");
|
||||
}
|
||||
|
||||
costPerHour *= instanceCount;
|
||||
costPerHour = Math.round(costPerHour * 100) / 100;
|
||||
|
||||
selfServeTraceSuccess(telemetryData, calculateCostTimestamp);
|
||||
return {
|
||||
textTKey: `${costPerHour} ${currencyCode}`,
|
||||
textTKey: `${costPerHour} ${currencyCode}
|
||||
${costBreakdown}`,
|
||||
type: DescriptionType.Text,
|
||||
};
|
||||
} catch (err) {
|
||||
alert(err);
|
||||
const failureTelemetry = { err, regions, priceMap, selfServeClassName: SqlX.name };
|
||||
selfServeTraceFailure(failureTelemetry, calculateCostTimestamp);
|
||||
|
||||
|
@ -48,10 +48,14 @@ export type PriceItem = {
|
||||
};
|
||||
|
||||
export type RegionsResponse = {
|
||||
properties: RegionsProperties;
|
||||
};
|
||||
|
||||
export type RegionsProperties = {
|
||||
locations: Array<RegionItem>;
|
||||
location: string;
|
||||
};
|
||||
|
||||
export type RegionItem = {
|
||||
locationName: string;
|
||||
isZoneRedundant: boolean;
|
||||
};
|
||||
|
@ -14,4 +14,5 @@ export enum StorageKey {
|
||||
MostRecentActivity,
|
||||
SetPartitionKeyUndefined,
|
||||
GalleryCalloutDismissed,
|
||||
VisitedAccounts,
|
||||
}
|
||||
|
@ -121,6 +121,15 @@ export enum Action {
|
||||
ExpandAddCollectionPaneAdvancedSection,
|
||||
SchemaAnalyzerClickAnalyze,
|
||||
SelfServeComponent,
|
||||
LaunchQuickstart,
|
||||
NewContainerHomepage,
|
||||
Top3ItemsClicked,
|
||||
LearningResourcesClicked,
|
||||
PlayCarouselVideo,
|
||||
OpenCarousel,
|
||||
CompleteCarousel,
|
||||
LaunchUITour,
|
||||
CancelUITour,
|
||||
}
|
||||
|
||||
export const ActionModifiers = {
|
||||
|
@ -1,3 +1,6 @@
|
||||
import { useCarousel } from "hooks/useCarousel";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { AuthType } from "./AuthType";
|
||||
import { DatabaseAccount } from "./Contracts/DataModels";
|
||||
import { SubscriptionType } from "./Contracts/SubscriptionType";
|
||||
@ -55,6 +58,8 @@ interface UserContext {
|
||||
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra";
|
||||
export type PortalEnv = "localhost" | "blackforest" | "fairfax" | "mooncake" | "prod" | "dev";
|
||||
|
||||
const ONE_WEEK_IN_MS = 604800000;
|
||||
|
||||
const features = extractFeatures();
|
||||
const { enableSDKoperations: useSDKOperations } = features;
|
||||
|
||||
@ -70,9 +75,34 @@ const userContext: UserContext = {
|
||||
collectionCreationDefaults: CollectionCreationDefaults,
|
||||
};
|
||||
|
||||
function isAccountNewerThanThresholdInMs(createdAt: string, threshold: number) {
|
||||
let createdAtMs: number = Date.parse(createdAt);
|
||||
if (isNaN(createdAtMs)) {
|
||||
createdAtMs = 0;
|
||||
}
|
||||
|
||||
const nowMs: number = Date.now();
|
||||
const millisecsSinceAccountCreation = nowMs - createdAtMs;
|
||||
return threshold > millisecsSinceAccountCreation;
|
||||
}
|
||||
|
||||
function updateUserContext(newContext: Partial<UserContext>): void {
|
||||
if (newContext.databaseAccount) {
|
||||
newContext.apiType = apiType(newContext.databaseAccount);
|
||||
|
||||
const isNewAccount = isAccountNewerThanThresholdInMs(
|
||||
newContext.databaseAccount?.systemData?.createdAt || "",
|
||||
ONE_WEEK_IN_MS
|
||||
);
|
||||
|
||||
if (
|
||||
!localStorage.getItem(newContext.databaseAccount.id) &&
|
||||
(userContext.isTryCosmosDBSubscription || isNewAccount)
|
||||
) {
|
||||
useCarousel.getState().setShouldOpen(true);
|
||||
localStorage.setItem(newContext.databaseAccount.id, "true");
|
||||
traceOpen(Action.OpenCarousel);
|
||||
}
|
||||
}
|
||||
Object.assign(userContext, newContext);
|
||||
}
|
||||
|
15
src/hooks/useCarousel.ts
Normal file
15
src/hooks/useCarousel.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import create, { UseStore } from "zustand";
|
||||
|
||||
interface CarouselState {
|
||||
shouldOpen: boolean;
|
||||
showCoachMark: boolean;
|
||||
setShouldOpen: (shouldOpen: boolean) => void;
|
||||
setShowCoachMark: (showCoachMark: boolean) => void;
|
||||
}
|
||||
|
||||
export const useCarousel: UseStore<CarouselState> = create((set) => ({
|
||||
shouldOpen: false,
|
||||
showCoachMark: false,
|
||||
setShouldOpen: (shouldOpen: boolean) => set({ shouldOpen }),
|
||||
setShowCoachMark: (showCoachMark: boolean) => set({ showCoachMark }),
|
||||
}));
|
@ -352,6 +352,7 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
||||
hasWriteAccess: inputs.hasWriteAccess ?? true,
|
||||
addCollectionFlight: inputs.addCollectionDefaultFlight || CollectionCreation.DefaultAddCollectionDefaultFlight,
|
||||
collectionCreationDefaults: inputs.defaultCollectionThroughput,
|
||||
isTryCosmosDBSubscription: inputs.isTryCosmosDBSubscription,
|
||||
});
|
||||
if (inputs.features) {
|
||||
Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features)));
|
||||
|
@ -7,6 +7,8 @@ import TabsBase from "../Explorer/Tabs/TabsBase";
|
||||
interface TabsState {
|
||||
openedTabs: TabsBase[];
|
||||
activeTab: TabsBase;
|
||||
isConnectTabOpen: boolean;
|
||||
isConnectTabActive: boolean;
|
||||
activateTab: (tab: TabsBase) => void;
|
||||
activateNewTab: (tab: TabsBase) => void;
|
||||
updateTab: (tab: TabsBase) => void;
|
||||
@ -15,19 +17,24 @@ interface TabsState {
|
||||
closeTabsByComparator: (comparator: (tab: TabsBase) => boolean) => void;
|
||||
closeTab: (tab: TabsBase) => void;
|
||||
closeAllNotebookTabs: (hardClose: boolean) => void;
|
||||
activateConnectTab: () => void;
|
||||
openAndActivateConnectTab: () => void;
|
||||
closeConnectTab: () => void;
|
||||
}
|
||||
|
||||
export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||
openedTabs: [],
|
||||
activeTab: undefined,
|
||||
isConnectTabOpen: false,
|
||||
isConnectTabActive: false,
|
||||
activateTab: (tab: TabsBase): void => {
|
||||
if (get().openedTabs.some((openedTab) => openedTab.tabId === tab.tabId)) {
|
||||
set({ activeTab: tab });
|
||||
set({ activeTab: tab, isConnectTabActive: false });
|
||||
tab.onActivate();
|
||||
}
|
||||
},
|
||||
activateNewTab: (tab: TabsBase): void => {
|
||||
set((state) => ({ openedTabs: [...state.openedTabs, tab], activeTab: tab }));
|
||||
set((state) => ({ openedTabs: [...state.openedTabs, tab], activeTab: tab, isConnectTabActive: false }));
|
||||
tab.onActivate();
|
||||
},
|
||||
updateTab: (tab: TabsBase) => {
|
||||
@ -66,7 +73,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||
return true;
|
||||
});
|
||||
if (updatedTabs.length === 0) {
|
||||
set({ activeTab: undefined });
|
||||
set({ activeTab: undefined, isConnectTabActive: get().isConnectTabOpen });
|
||||
}
|
||||
|
||||
if (tab.tabId === activeTab.tabId && tabIndex !== -1) {
|
||||
@ -104,8 +111,21 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||
});
|
||||
|
||||
if (get().openedTabs.length === 0) {
|
||||
set({ activeTab: undefined });
|
||||
set({ activeTab: undefined, isConnectTabActive: get().isConnectTabOpen });
|
||||
}
|
||||
}
|
||||
},
|
||||
activateConnectTab: () => {
|
||||
if (get().isConnectTabOpen) {
|
||||
set({ isConnectTabActive: true, activeTab: undefined });
|
||||
}
|
||||
},
|
||||
openAndActivateConnectTab: () => set({ isConnectTabActive: true, isConnectTabOpen: true, activeTab: undefined }),
|
||||
closeConnectTab: () => {
|
||||
const { isConnectTabActive, openedTabs } = get();
|
||||
if (isConnectTabActive && openedTabs?.length > 0) {
|
||||
set({ activeTab: openedTabs[0] });
|
||||
}
|
||||
set({ isConnectTabActive: false, isConnectTabOpen: false });
|
||||
},
|
||||
}));
|
||||
|
24
src/hooks/useTeachingBubble.ts
Normal file
24
src/hooks/useTeachingBubble.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Collection } from "Contracts/ViewModels";
|
||||
import create, { UseStore } from "zustand";
|
||||
|
||||
interface TeachingBubbleState {
|
||||
step: number;
|
||||
isSampleDBExpanded: boolean;
|
||||
isDocumentsTabOpened: boolean;
|
||||
sampleCollection: Collection;
|
||||
setStep: (step: number) => void;
|
||||
setIsSampleDBExpanded: (isReady: boolean) => void;
|
||||
setIsDocumentsTabOpened: (isOpened: boolean) => void;
|
||||
setSampleCollection: (sampleCollection: Collection) => void;
|
||||
}
|
||||
|
||||
export const useTeachingBubble: UseStore<TeachingBubbleState> = create((set) => ({
|
||||
step: 1,
|
||||
isSampleDBExpanded: false,
|
||||
isDocumentsTabOpened: false,
|
||||
sampleCollection: undefined,
|
||||
setStep: (step: number) => set({ step }),
|
||||
setIsSampleDBExpanded: (isSampleDBExpanded: boolean) => set({ isSampleDBExpanded }),
|
||||
setIsDocumentsTabOpened: (isDocumentsTabOpened: boolean) => set({ isDocumentsTabOpened }),
|
||||
setSampleCollection: (sampleCollection: Collection) => set({ sampleCollection }),
|
||||
}));
|
@ -85,35 +85,23 @@
|
||||
Learn more about Azure Cosmos DB
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
class="atags"
|
||||
href="https://docs.microsoft.com/azure/documentdb/documentdb-dotnet-samples"
|
||||
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/samples/dotnet"
|
||||
>Code Samples</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
class="atags"
|
||||
href="https://docs.microsoft.com/azure/documentdb/documentdb-nosql-local-emulator"
|
||||
>Documentation</a
|
||||
>
|
||||
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/docs">Documentation</a>
|
||||
</li>
|
||||
<li>
|
||||
<a target="_blank" class="atags" href="https://azure.microsoft.com/pricing/details/documentdb/"
|
||||
>Pricing</a
|
||||
>
|
||||
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/pricing">Pricing</a>
|
||||
</li>
|
||||
<li>
|
||||
<a target="_blank" class="atags" href="https://www.documentdb.com/capacityplanner"
|
||||
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/capacity-planner"
|
||||
>Capacity Planner</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a target="_blank" class="atags" href="http://stackoverflow.com/questions/tagged/azure-documentdb"
|
||||
>Forum</a
|
||||
>
|
||||
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/stackoverflow">Forum</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -140,35 +128,23 @@
|
||||
Learn more about Azure Cosmos DB.
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
class="atags"
|
||||
href="https://docs.microsoft.com/azure/documentdb/documentdb-dotnet-samples"
|
||||
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/samples/dotnet"
|
||||
>Code Samples</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
class="atags"
|
||||
href="https://docs.microsoft.com/azure/documentdb/documentdb-nosql-local-emulator"
|
||||
>Documentation</a
|
||||
>
|
||||
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/docs">Documentation</a>
|
||||
</li>
|
||||
<li>
|
||||
<a target="_blank" class="atags" href="https://azure.microsoft.com/pricing/details/documentdb/"
|
||||
>Pricing</a
|
||||
>
|
||||
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/pricing">Pricing</a>
|
||||
</li>
|
||||
<li>
|
||||
<a target="_blank" class="atags" href="https://www.documentdb.com/capacityplanner"
|
||||
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/capacity-planner"
|
||||
>Capacity Planner</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a target="_blank" class="atags" href="http://stackoverflow.com/questions/tagged/azure-documentdb"
|
||||
>Forum</a
|
||||
>
|
||||
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/stackoverflow">Forum</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -197,29 +173,20 @@
|
||||
<div class="numberheading">
|
||||
Learn more about Azure Cosmos DB.
|
||||
<ul>
|
||||
<!--<li><a target="_blank" class="atags" href="https://docs.microsoft.com/azure/documentdb/documentdb-java-samples">Code Samples</a></li>-->
|
||||
<!--<li><a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/samples/java">Code Samples</a></li>-->
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
class="atags"
|
||||
href="https://docs.microsoft.com/azure/documentdb/documentdb-nosql-local-emulator"
|
||||
>Documentation</a
|
||||
>
|
||||
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/docs">Documentation</a>
|
||||
</li>
|
||||
<li>
|
||||
<a target="_blank" class="atags" href="https://azure.microsoft.com/pricing/details/documentdb/"
|
||||
>Pricing</a
|
||||
>
|
||||
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/pricing">Pricing</a>
|
||||
</li>
|
||||
<li>
|
||||
<a target="_blank" class="atags" href="https://www.documentdb.com/capacityplanner"
|
||||
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/capacity-planner"
|
||||
>Capacity Planner</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a target="_blank" class="atags" href="http://stackoverflow.com/questions/tagged/azure-documentdb"
|
||||
>Forum</a
|
||||
>
|
||||
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/stackoverflow">Forum</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -251,35 +218,23 @@
|
||||
Learn more about Azure Cosmos DB.
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
class="atags"
|
||||
href="https://docs.microsoft.com/azure/documentdb/documentdb-nodejs-samples"
|
||||
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/samples/nodejs"
|
||||
>Code Samples</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
class="atags"
|
||||
href="https://docs.microsoft.com/azure/documentdb/documentdb-nosql-local-emulator"
|
||||
>Documentation</a
|
||||
>
|
||||
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/docs">Documentation</a>
|
||||
</li>
|
||||
<li>
|
||||
<a target="_blank" class="atags" href="https://azure.microsoft.com/pricing/details/documentdb/"
|
||||
>Pricing</a
|
||||
>
|
||||
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/pricing">Pricing</a>
|
||||
</li>
|
||||
<li>
|
||||
<a target="_blank" class="atags" href="https://www.documentdb.com/capacityplanner"
|
||||
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/capacity-planner"
|
||||
>Capacity Planner</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a target="_blank" class="atags" href="http://stackoverflow.com/questions/tagged/azure-documentdb"
|
||||
>Forum</a
|
||||
>
|
||||
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/stackoverflow">Forum</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -293,11 +248,7 @@
|
||||
Create a new Python app.
|
||||
<p>
|
||||
Follow this
|
||||
<a
|
||||
href="https://azure.microsoft.com/documentation/articles/documentdb-python-application/"
|
||||
target="_blank"
|
||||
>tutorial</a
|
||||
>
|
||||
<a href="https://aka.ms/cosmos-db-emulator/tutorial/python" target="_blank">tutorial</a>
|
||||
to create a new Python app connected to Azure Cosmos DB.
|
||||
</p>
|
||||
</div>
|
||||
@ -309,28 +260,18 @@
|
||||
Learn more about Azure Cosmos DB.
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
class="atags"
|
||||
href="https://docs.microsoft.com/azure/documentdb/documentdb-python-samples"
|
||||
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/samples/python"
|
||||
>Code Samples</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
class="atags"
|
||||
href="https://docs.microsoft.com/azure/documentdb/documentdb-nosql-local-emulator"
|
||||
>Documentation</a
|
||||
>
|
||||
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/docs">Documentation</a>
|
||||
</li>
|
||||
<li>
|
||||
<a target="_blank" class="atags" href="https://azure.microsoft.com/pricing/details/documentdb/"
|
||||
>Pricing</a
|
||||
>
|
||||
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/pricing">Pricing</a>
|
||||
</li>
|
||||
<li>
|
||||
<a target="_blank" class="atags" href="https://www.documentdb.com/capacityplanner"
|
||||
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/capacity-planner"
|
||||
>Capacity planner</a
|
||||
>
|
||||
</li>
|
||||
|
@ -11,7 +11,7 @@ const fileToUpload = `GettingStarted-ignore${Math.floor(Math.random() * 100000)}
|
||||
fs.copyFileSync(path.join(__dirname, filename), path.join(__dirname, fileToUpload));
|
||||
|
||||
test("Notebooks", async () => {
|
||||
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner");
|
||||
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us");
|
||||
const explorer = await waitForExplorer();
|
||||
// Upload and Delete Notebook
|
||||
await explorer.click('[data-test="My Notebooks"] [aria-label="More"]');
|
||||
|
@ -9,8 +9,9 @@ test("SQL CRUD", async () => {
|
||||
const containerId = generateUniqueName("container");
|
||||
page.setDefaultTimeout(50000);
|
||||
|
||||
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner");
|
||||
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us");
|
||||
const explorer = await waitForExplorer();
|
||||
|
||||
await explorer.click('[data-test="New Container"]');
|
||||
await explorer.fill('[aria-label="New database id"]', databaseId);
|
||||
await explorer.fill('[aria-label="Container id"]', containerId);
|
||||
|
@ -15,8 +15,8 @@ const resourceGroupName = "runners";
|
||||
test("Resource token", async () => {
|
||||
const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId);
|
||||
const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
|
||||
const account = await armClient.databaseAccounts.get(resourceGroupName, "portal-sql-runner");
|
||||
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, "portal-sql-runner");
|
||||
const account = await armClient.databaseAccounts.get(resourceGroupName, "portal-sql-runner-west-us");
|
||||
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, "portal-sql-runner-west-us");
|
||||
const dbId = generateUniqueName("db");
|
||||
const collectionId = generateUniqueName("col");
|
||||
const client = new CosmosClient({
|
||||
|
@ -8,7 +8,7 @@ import { get, listKeys } from "../../src/Utils/arm/generatedClients/cosmos/datab
|
||||
const resourceGroup = process.env.RESOURCE_GROUP || "";
|
||||
const subscriptionId = process.env.SUBSCRIPTION_ID || "";
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
const accountName = urlSearchParams.get("accountName") || "portal-sql-runner";
|
||||
const accountName = urlSearchParams.get("accountName") || "portal-sql-runner-west-us";
|
||||
const selfServeType = urlSearchParams.get("selfServeType") || "example";
|
||||
const iframeSrc = urlSearchParams.get("iframeSrc") || "explorer.html?platform=Portal&disablePortalInitCache";
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user