Compare commits

..

2 Commits

Author SHA1 Message Date
vaidankarswapnil
052ce6c739 Fixed and updated 1 test snapshot 2022-02-01 15:31:17 +05:30
vaidankarswapnil
3021513d5c fixed a11y cassandra select column issue 2022-02-01 14:06:57 +05:30
121 changed files with 1395 additions and 35311 deletions

View File

@@ -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 --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 --subscription cosmosdb-portalteam-generaldemo --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 --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 --subscription cosmosdb-portalteam-generaldemo --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:

View File

@@ -1,5 +1,4 @@
{
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com",
"isTerminalEnabled" : true,
"isPhoenixEnabled" : true
"isTerminalEnabled" : true
}

View File

@@ -1,5 +1,4 @@
{
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
"isTerminalEnabled" : false,
"isPhoenixEnabled" : false
"isTerminalEnabled" : false
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,23 +0,0 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_480_185430)">
<path d="M35.4911 6.39638L33.4106 4.31591L37.559 0.167553C37.6611 0.0654497 37.7996 0.00808785 37.944 0.00808785C38.0884 0.00808801 38.2269 0.0654482 38.329 0.167551L39.641 1.47963C39.7431 1.58173 39.8005 1.72021 39.8005 1.86461C39.8005 2.009 39.7431 2.14749 39.641 2.24959L35.4927 6.39795L35.4911 6.39638Z" fill="#32BEDD"/>
<path d="M4.45313 33.6455L6.53988 35.7323L2.44494 39.8272C2.34367 39.9285 2.20632 39.9853 2.06311 39.9853C1.91989 39.9853 1.78254 39.9285 1.68127 39.8272L0.364478 38.5104C0.312974 38.4606 0.271905 38.401 0.243665 38.3351C0.215426 38.2692 0.20058 38.1984 0.199995 38.1267C0.19941 38.0551 0.213098 37.984 0.240258 37.9177C0.267419 37.8514 0.307509 37.7911 0.358193 37.7404L4.45313 33.6455Z" fill="#0078D4"/>
<path d="M5.09381 25.0287C3.78099 26.3415 3.04346 28.1221 3.04346 29.9787C3.04346 31.8353 3.78099 33.6159 5.09381 34.9287C6.40664 36.2415 8.1872 36.9791 10.0438 36.9791C11.9004 36.9791 13.681 36.2415 14.9938 34.9287L18.2027 31.7176L8.30492 21.8198L5.09381 25.0287Z" fill="url(#paint0_linear_480_185430)"/>
<path d="M17.4209 18.1581L17.6157 18.353C17.8133 18.5505 17.9242 18.8185 17.9242 19.0978C17.9242 19.3772 17.8133 19.6451 17.6157 19.8426L13.6009 23.8574L11.918 22.1745L15.9344 18.1581C16.1319 17.9606 16.3998 17.8496 16.6792 17.8496C16.9586 17.8496 17.2265 17.9606 17.424 18.1581L17.4209 18.1581Z" fill="#C3F1FF"/>
<path d="M21.5835 22.32L21.7783 22.5149C21.9759 22.7124 22.0868 22.9803 22.0868 23.2597C22.0868 23.539 21.9759 23.807 21.7783 24.0045L17.7588 28.024L16.0759 26.3411L20.097 22.32C20.2945 22.1225 20.5624 22.0115 20.8418 22.0115C21.1212 22.0115 21.3891 22.1225 21.5866 22.32L21.5835 22.32Z" fill="#C3F1FF"/>
<path d="M20.9363 30.0618L9.87241 18.9979C9.66673 18.7922 9.33327 18.7922 9.12759 18.9979L7.67566 20.4498C7.46999 20.6555 7.46999 20.989 7.67566 21.1946L18.7395 32.2585C18.9452 32.4642 19.2787 32.4642 19.4843 32.2585L20.9363 30.8066C21.1419 30.6009 21.1419 30.2674 20.9363 30.0618Z" fill="#5EA0EF"/>
<path d="M34.9067 14.9711C36.2196 13.6583 36.9571 11.8777 36.9571 10.0211C36.9571 8.1645 36.2196 6.38393 34.9067 5.07111C33.5939 3.75829 31.8134 3.02075 29.9567 3.02075C28.1001 3.02075 26.3196 3.75829 25.0067 5.07111L21.7979 8.28222L31.6956 18.18L34.9067 14.9711Z" fill="#ECF4FD"/>
<path d="M22.5828 21.8375L22.388 21.6426C22.1904 21.4451 22.0795 21.1772 22.0795 20.8978C22.0795 20.6184 22.1904 20.3505 22.388 20.153L26.4075 16.1335L28.092 17.8179L24.0677 21.8422C23.8698 22.0376 23.6025 22.1469 23.3243 22.146C23.0461 22.1451 22.7795 22.0342 22.5828 21.8375Z" fill="#ECF4FD"/>
<path d="M18.4178 17.6802L18.2229 17.4854C18.0254 17.2878 17.9144 17.0199 17.9144 16.7406C17.9144 16.4612 18.0254 16.1933 18.2229 15.9957L22.2409 11.9778L23.9254 13.6623L19.909 17.6787C19.7115 17.8762 19.4435 17.9872 19.1642 17.9872C18.8848 17.9872 18.6169 17.8762 18.4194 17.6787L18.4178 17.6802Z" fill="#ECF4FD"/>
<path d="M19.0642 9.93799L30.1281 21.0019C30.3338 21.2075 30.6672 21.2075 30.8729 21.0019L32.3248 19.5499C32.5305 19.3443 32.5305 19.0108 32.3248 18.8051L21.261 7.74125C21.0553 7.53557 20.7218 7.53557 20.5161 7.74125L19.0642 9.19317C18.8585 9.39885 18.8585 9.73232 19.0642 9.93799Z" fill="#ECF4FD"/>
</g>
<defs>
<linearGradient id="paint0_linear_480_185430" x1="10.6227" y1="21.8179" x2="10.6227" y2="36.9783" gradientUnits="userSpaceOnUse">
<stop stop-color="#5EA0EF"/>
<stop offset="0.997" stop-color="#0078D4"/>
</linearGradient>
<clipPath id="clip0_480_185430">
<rect width="40" height="40" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -1,8 +0,0 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M24 34.635C24 34.8143 23.9647 34.9918 23.8961 35.1574C23.8275 35.323 23.727 35.4734 23.6002 35.6002C23.4734 35.727 23.323 35.8275 23.1574 35.8961C22.9918 35.9647 22.8143 36 22.635 36H1.365C1.18575 36 1.00825 35.9647 0.842637 35.8961C0.677028 35.8275 0.526551 35.727 0.399799 35.6002C0.273047 35.4734 0.172502 35.323 0.103904 35.1574C0.0353068 34.9918 0 34.8143 0 34.635L0 13.365C0 13.003 0.143812 12.6558 0.399799 12.3998C0.655786 12.1438 1.00298 12 1.365 12H22.635C22.997 12 23.3442 12.1438 23.6002 12.3998C23.8562 12.6558 24 13.003 24 13.365V34.635Z" fill="#005BA1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M30 28.635C30 28.997 29.8562 29.3442 29.6002 29.6002C29.3442 29.8562 28.997 30 28.635 30H7.365C7.00298 30 6.65579 29.8562 6.3998 29.6002C6.14381 29.3442 6 28.997 6 28.635V7.365C6 7.00298 6.14381 6.65579 6.3998 6.3998C6.65579 6.14381 7.00298 6 7.365 6H28.635C28.997 6 29.3442 6.14381 29.6002 6.3998C29.8562 6.65579 30 7.00298 30 7.365V28.635Z" fill="#5EA0EF"/>
<path d="M22.635 12H6V28.635C6 28.997 6.14381 29.3442 6.3998 29.6002C6.65579 29.8562 7.00298 30 7.365 30H24V13.365C24 13.003 23.8562 12.6558 23.6002 12.3998C23.3442 12.1438 22.997 12 22.635 12Z" fill="#0078D4"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M36 22.635C36 22.8143 35.9647 22.9918 35.8961 23.1574C35.8275 23.323 35.727 23.4734 35.6002 23.6002C35.4734 23.727 35.323 23.8275 35.1574 23.8961C34.9918 23.9647 34.8143 24 34.635 24H13.365C13.003 24 12.6558 23.8562 12.3998 23.6002C12.1438 23.3442 12 22.997 12 22.635V1.365C12 1.00298 12.1438 0.655786 12.3998 0.399799C12.6558 0.143812 13.003 0 13.365 0L34.635 0C34.8143 0 34.9918 0.0353068 35.1574 0.103904C35.323 0.172502 35.4734 0.273047 35.6002 0.399799C35.727 0.526551 35.8275 0.677028 35.8961 0.842637C35.9647 1.00825 36 1.18575 36 1.365V22.635Z" fill="#E6E7E8"/>
<path d="M22.635 12H12V22.635C12 22.997 12.1438 23.3442 12.3998 23.6002C12.6558 23.8562 13.003 24 13.365 24H24V13.365C24 13.003 23.8562 12.6558 23.6002 12.3998C23.3442 12.1438 22.997 12 22.635 12Z" fill="#BCBEC0"/>
<path d="M28.635 6H12V12H22.635C22.997 12 23.3442 12.1438 23.6002 12.3998C23.8562 12.6558 24 13.003 24 13.365V24H30V7.365C30 7.00298 29.8562 6.65579 29.6002 6.3998C29.3442 6.14381 28.997 6 28.635 6Z" fill="#D1D3D4"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,3 +0,0 @@
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 6H11V11H0V0H5V1H1V10H10V6ZM11 0V5H10V1.71094L5.35156 6.35156L4.64844 5.64844L9.28906 1H6V0H11Z" fill="#0078D4"/>
</svg>

Before

Width:  |  Height:  |  Size: 229 B

View File

@@ -1,23 +0,0 @@
<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_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_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>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,11 +0,0 @@
<svg width="30" height="34" viewBox="0 0 30 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6457 18.9089H0.67782C0.520018 18.9215 0.361625 18.8893 0.235445 18.819C0.109264 18.7487 0.0249634 18.6456 0 18.5312C0.000766863 18.4748 0.0212497 18.4196 0.0595033 18.3706L14.4179 0.229509C14.4859 0.156313 14.5784 0.0970502 14.6866 0.0573734C14.7949 0.0176966 14.9152 -0.00107025 15.0362 0.00286318H29.1877C29.3456 -0.0102086 29.5043 0.0218054 29.6306 0.0922084C29.757 0.162611 29.8411 0.26595 29.8655 0.380607C29.8655 0.463721 29.823 0.54385 29.7465 0.605364L12.8941 14.7991H29.3222C29.48 14.7865 29.6384 14.8187 29.7646 14.889C29.8907 14.9593 29.975 15.0624 30 15.1768C29.998 15.2265 29.9814 15.2752 29.9516 15.3198C29.9217 15.3645 29.8791 15.4039 29.8267 15.4356L2.51725 33.5994C2.25854 33.6957 0.447568 34.6608 1.33236 33.1952L12.6457 18.9089Z" fill="url(#paint0_radial_480_182017)"/>
<defs>
<radialGradient id="paint0_radial_480_182017" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(15.0039 17.0718) scale(23.6442 13.4446)">
<stop offset="0.196" stop-color="#FFD70F"/>
<stop offset="0.438" stop-color="#FFCB12"/>
<stop offset="0.873" stop-color="#FEAC19"/>
<stop offset="1" stop-color="#FEA11B"/>
</radialGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

32195
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"main": "index.js",
"dependencies": {
"@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "3.16.1",
"@azure/cosmos": "3.10.5",
"@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "1.2.1",
"@azure/ms-rest-nodeauth": "3.0.7",
@@ -91,7 +91,6 @@
"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,7 +130,6 @@
"@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",

View File

@@ -4,7 +4,7 @@
"description": "",
"main": "index.js",
"scripts": {
"deploy": "az webapp up --name \"cosmos-explorer-preview\" --subscription \"cosmosdb-portalteam-generaltest-msft\" --resource-group \"stfaul\"",
"deploy": "az webapp up -n cosmos-explorer-preview --subscription cosmosdb-portalteam-generaldemo -g stfaul",
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
@@ -15,4 +15,4 @@
"http-proxy-middleware": "^1.1.0",
"node-fetch": "^2.6.1"
}
}
}

View File

@@ -1,25 +1,26 @@
{
"databaseId": "SampleDB",
"offerThroughput": 400,
"databaseLevelThroughput": false,
"collectionId": "Persons",
"createNewDatabase": true,
"partitionKey": { "kind": "Hash", "paths": ["/firstname"], "version": 1 },
"data": [
{ "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" }
{
"firstname": "Eva",
"age": 44
},
{
"firstname": "Véronique",
"age": 50
},
{
"firstname": "亜妃子",
"age": 5
},
{
"firstname": "John",
"age": 23
}
]
}

View File

@@ -99,7 +99,6 @@ export class Flights {
public static readonly PhoenixNotebooks = "phoenixnotebooks";
public static readonly PhoenixFeatures = "phoenixfeatures";
public static readonly NotebooksDownBanner = "notebooksdownbanner";
public static readonly PublicGallery = "publicgallery";
}
export class AfecFeatures {
@@ -354,10 +353,6 @@ 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==";
@@ -398,10 +393,6 @@ export class Notebook {
public static cosmosNotebookHomePageUrl = "https://aka.ms/cosmos-notebooks-limits";
public static cosmosNotebookGitDocumentationUrl = "https://aka.ms/cosmos-notebooks-github";
public static learnMore = "Learn more.";
public static notebookDisabledText =
"This feature is disabled for this user, this can happen because of region restriction, key permissions etc..";
public static newShellDisabledText =
"This feature is disabled for this user, this is for users with region restriction, key permissions etc..";
}
export class SparkLibrary {

View File

@@ -1,4 +1,4 @@
import { ResourceType } from "@azure/cosmos";
import { ResourceType } from "@azure/cosmos/dist-esm/common/constants";
import { Platform, resetConfigContext, updateConfigContext } from "../ConfigContext";
import { updateUserContext } from "../UserContext";
import { endpoint, getTokenFromAuthService, requestPlugin, tokenProvider } from "./CosmosClient";

View File

@@ -1,4 +1,6 @@
import * as Cosmos from "@azure/cosmos";
import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos";
import { CosmosHeaders } from "@azure/cosmos/dist-esm";
import { configContext, Platform } from "../ConfigContext";
import { userContext } from "../UserContext";
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
@@ -7,7 +9,7 @@ import { getErrorMessage } from "./ErrorHandlingUtils";
const _global = typeof self === "undefined" ? window : self;
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
export const tokenProvider = async (requestInfo: RequestInfo) => {
const { verb, resourceId, resourceType, headers } = requestInfo;
if (userContext.features.enableAadDataPlane && userContext.aadToken) {
@@ -18,13 +20,13 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
if (configContext.platform === Platform.Emulator) {
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
await 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 Cosmos.setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
return decodeURIComponent(headers.authorization);
}
@@ -87,8 +89,8 @@ let _client: Cosmos.CosmosClient;
export function client(): Cosmos.CosmosClient {
if (_client) return _client;
let _defaultHeaders: Cosmos.CosmosHeaders = {};
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
let _defaultHeaders: CosmosHeaders = {};
_defaultHeaders["x-ms-cosmos-sdk-supported-capabilities"] =
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
const options: Cosmos.CosmosClientOptions = {

View File

@@ -25,12 +25,12 @@ const fetchMock = () => {
});
};
const partitionKeyProperties = ["pk"];
const partitionKeyProperty = "pk";
const collection = {
id: () => "testCollection",
rid: "testCollectionrid",
partitionKeyProperties,
partitionKeyProperty,
partitionKey: {
paths: ["/pk"],
kind: "Hash",
@@ -41,7 +41,7 @@ const collection = {
const documentId = ({
partitionKeyHeader: () => "[]",
self: "db/testDB/db/testCollection/docs/testId",
partitionKeyProperties,
partitionKeyProperty,
partitionKey: {
paths: ["/pk"],
kind: "Hash",

View File

@@ -76,7 +76,7 @@ export function queryDocuments(
dba: databaseAccount.name,
pk:
collection && collection.partitionKey && !collection.partitionKey.systemKey
? collection.partitionKeyProperties?.[0]
? collection.partitionKeyProperty
: "",
};
@@ -139,7 +139,7 @@ export function readDocument(
dba: databaseAccount.name,
pk:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
? documentId.partitionKeyProperties?.[0]
? documentId.partitionKeyProperty
: "",
};
@@ -225,7 +225,7 @@ export function updateDocument(
dba: databaseAccount.name,
pk:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
? documentId.partitionKeyProperties?.[0]
? documentId.partitionKeyProperty
: "",
};
const endpoint = getFeatureEndpointOrDefault("updateDocument");
@@ -266,7 +266,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
dba: databaseAccount.name,
pk:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
? documentId.partitionKeyProperties?.[0]
? documentId.partitionKeyProperty
: "",
};
const endpoint = getFeatureEndpointOrDefault("deleteDocument");

View File

@@ -149,10 +149,10 @@ export class QueriesClient {
const documentId = new DocumentId(
{
partitionKey: QueriesClient.PartitionKey,
partitionKeyProperties: ["id"],
partitionKeyProperty: "id",
} as DocumentsTab,
query,
[query.queryName]
query.queryName
); // TODO: Remove DocumentId's dependency on DocumentsTab
const options: any = { partitionKey: query.resourceId };
return deleteDocument(queriesCollection, documentId)

View File

@@ -1,4 +1,7 @@
import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos";
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 { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels";
import { useDatabases } from "../../Explorer/useDatabases";

View File

@@ -1,4 +1,5 @@
import { DatabaseRequest, DatabaseResponse } from "@azure/cosmos";
import { DatabaseResponse } from "@azure/cosmos";
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels";
import { useDatabases } from "../../Explorer/useDatabases";

View File

@@ -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?.length === 0 ? undefined : documentId.partitionKeyValue)
.item(documentId.id(), documentId.partitionKeyValue)
.delete();
logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`);
} catch (error) {

View File

@@ -1,29 +1,21 @@
import { Item, RequestOptions } from "@azure/cosmos";
import { Item } from "@azure/cosmos";
import { CollectionBase } from "../../Contracts/ViewModels";
import DocumentId from "../../Explorer/Tree/DocumentId";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { HttpHeaders } from "../Constants";
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import DocumentId from "../../Explorer/Tree/DocumentId";
export const readDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<Item> => {
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Reading ${entityName} ${documentId.id()}`);
try {
const options: RequestOptions =
documentId.partitionKey.kind === "MultiHash"
? {
[HttpHeaders.partitionKey]: documentId.partitionKeyValue,
}
: {};
const response = await client()
.database(collection.databaseId)
.container(collection.id())
// use undefined if the partitionKeyValue is empty
.item(documentId.id(), documentId.partitionKeyValue?.length === 0 ? undefined : documentId.partitionKeyValue)
.read(options);
.item(documentId.id(), documentId.partitionKeyValue)
.read();
return response?.resource;
} catch (error) {

View File

@@ -1,6 +1,6 @@
import { RequestOptions } from "@azure/cosmos";
import { Offer } from "../../Contracts/DataModels";
import { HttpHeaders } from "../Constants";
import { Offer } from "../../Contracts/DataModels";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { parseSDKOfferResponse } from "../OfferUtility";
import { readOffers } from "./readOffers";

View File

@@ -1,4 +1,5 @@
import { ContainerDefinition, RequestOptions } from "@azure/cosmos";
import { ContainerDefinition } from "@azure/cosmos";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { AuthType } from "../../AuthType";
import { Collection } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext";

View File

@@ -1,11 +1,10 @@
import { Item, RequestOptions } from "@azure/cosmos";
import { HttpHeaders } from "Common/Constants";
import { CollectionBase } from "../../Contracts/ViewModels";
import DocumentId from "../../Explorer/Tree/DocumentId";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { Item } from "@azure/cosmos";
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import DocumentId from "../../Explorer/Tree/DocumentId";
export const updateDocument = async (
collection: CollectionBase,
@@ -16,17 +15,11 @@ export const updateDocument = async (
const clearMessage = logConsoleProgress(`Updating ${entityName} ${documentId.id()}`);
try {
const options: RequestOptions =
documentId.partitionKey.kind === "MultiHash"
? {
[HttpHeaders.partitionKey]: documentId.partitionKeyValue,
}
: {};
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), documentId.partitionKeyValue?.length === 0 ? undefined : documentId.partitionKeyValue)
.replace(newDocument, options);
.item(documentId.id(), documentId.partitionKeyValue)
.replace(newDocument);
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);
return response?.resource;

View File

@@ -1,4 +1,5 @@
import { OfferDefinition, RequestOptions } from "@azure/cosmos";
import { OfferDefinition } from "@azure/cosmos";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { AuthType } from "../../AuthType";
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext";

View File

@@ -40,7 +40,6 @@ export interface ConfigContext {
GITHUB_TEST_ENV_CLIENT_ID: string;
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
isTerminalEnabled: boolean;
isPhoenixEnabled: boolean;
hostedExplorerURL: string;
armAPIVersion?: string;
msalRedirectURI?: string;
@@ -52,10 +51,10 @@ let configContext: Readonly<ConfigContext> = {
allowedParentFrameOrigins: [
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]*portal\\.microsoftazure\\.de$`,
`^https:\\/\\/[\\.\\w]*portal\\.microsoftazure.de$`,
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
`^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`,
`^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$`,
], // Webpack injects this at build time
gitSha: process.env.GIT_SHA,
hostedExplorerURL: "https://cosmos.azure.com/",
@@ -72,7 +71,6 @@ let configContext: Readonly<ConfigContext> = {
JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
isTerminalEnabled: false,
isPhoenixEnabled: false,
};
export function resetConfigContext(): void {

View File

@@ -7,11 +7,6 @@ export interface DatabaseAccount {
type: string;
kind: string;
properties: DatabaseAccountExtendedProperties;
systemData?: DatabaseAccountSystemData;
}
export interface DatabaseAccountSystemData {
createdAt: string;
}
export interface DatabaseAccountExtendedProperties {
@@ -438,51 +433,26 @@ export interface NotebookWorkspaceConnectionInfo {
export interface ContainerInfo {
durationLeftInMinutes: number;
phoenixServerInfo: NotebookWorkspaceConnectionInfo;
notebookServerInfo: 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 IPhoenixError {
message: string;
type: string;
}
export interface IMaxAllocationTimeExceeded extends IPhoenixError {
earliestAllocationTimestamp: string;
maxAllocationTimePerDayPerUserInMinutes: string;
}
export interface IMaxDbAccountsPerUserExceeded extends IPhoenixError {
maxSimultaneousConnectionsPerUser: string;
}
export interface IMaxUsersPerDbAccountExceeded extends IPhoenixError {
maxSimultaneousUsersPerDbAccount: string;
}
export interface IPhoenixConnectionInfoResult {
readonly authToken?: string;
readonly phoenixServiceUrl?: string;
readonly notebookAuthToken?: string;
readonly notebookServerUrl?: string;
readonly forwardingId?: string;
}
@@ -561,14 +531,3 @@ export interface ContainerConnectionInfo {
status: ConnectionStatusType;
//need to add ram and rom info
}
export enum PhoenixErrorType {
MaxAllocationTimeExceeded = "MaxAllocationTimeExceeded",
MaxDbAccountsPerUserExceeded = "MaxDbAccountsPerUserExceeded",
MaxUsersPerDbAccountExceeded = "MaxUsersPerDbAccountExceeded",
AllocationValidationResult = "AllocationValidationResult",
RegionNotServicable = "RegionNotServicable",
SubscriptionNotAllowed = "SubscriptionNotAllowed",
UnknownError = "UnknownError",
PhoenixFlightFallback = "PhoenixFlightFallback",
}

View File

@@ -34,7 +34,6 @@ export enum MessageTypes {
CreateSparkPool,
RefreshDatabaseAccount,
CloseTab,
OpenQuickstartBlade,
}
export { Versions, ActionContracts, Diagnostics };

View File

@@ -86,7 +86,6 @@ export interface Database extends TreeNode {
offer: ko.Observable<DataModels.Offer>;
isDatabaseExpanded: ko.Observable<boolean>;
isDatabaseShared: ko.Computed<boolean>;
isSampleDB?: boolean;
selectedSubnodeKind: ko.Observable<CollectionTabKind>;
@@ -107,13 +106,12 @@ export interface CollectionBase extends TreeNode {
self: string;
rawDataModel: DataModels.Collection;
partitionKey: DataModels.PartitionKey;
partitionKeyProperties: string[];
partitionKeyPropertyHeaders: string[];
partitionKeyProperty: string;
partitionKeyPropertyHeader: string;
id: ko.Observable<string>;
selectedSubnodeKind: ko.Observable<CollectionTabKind>;
children: ko.ObservableArray<TreeNode>;
isCollectionExpanded: ko.Observable<boolean>;
isSampleCollection?: boolean;
onDocumentDBDocumentsClick(): void;
onNewQueryClick(source: any, event?: MouseEvent, queryText?: string): void;

View File

@@ -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()}`,
},
];

View File

@@ -21,7 +21,6 @@ import {
Text,
} from "@fluentui/react";
import * as React from "react";
import { userContext } from "UserContext";
import { HttpStatusCodes } from "../../../Common/Constants";
import { handleError } from "../../../Common/ErrorHandlingUtils";
import { IGalleryItem, IJunoResponse, IPublicGalleryData, JunoClient } from "../../../Juno/JunoClient";
@@ -149,23 +148,18 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
public render(): JSX.Element {
this.traceViewGallery();
const tabs: GalleryTabInfo[] = [];
if (userContext.features.publicGallery) {
tabs.push(
this.createPublicGalleryTab(
GalleryTab.PublicGallery,
this.state.publicNotebooks,
this.state.isCodeOfConductAccepted
)
);
}
tabs.push(this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks));
const tabs: GalleryTabInfo[] = [
this.createPublicGalleryTab(
GalleryTab.PublicGallery,
this.state.publicNotebooks,
this.state.isCodeOfConductAccepted
),
this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks),
];
if (this.props.container) {
tabs.push(this.createFavoritesTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
if (userContext.features.publicGallery) {
tabs.push(this.createPublishedNotebooksTab(GalleryTab.Published, this.state.publishedNotebooks));
}
tabs.push(this.createPublishedNotebooksTab(GalleryTab.Published, this.state.publishedNotebooks));
}
const pivotProps: IPivotProps = {

View File

@@ -8,6 +8,95 @@ exports[`GalleryViewerComponent renders 1`] = `
onLinkClick={[Function]}
selectedKey="OfficialSamples"
>
<PivotItem
headerText="Public gallery"
itemKey="PublicGallery"
key="PublicGallery"
style={
Object {
"marginTop": 20,
}
}
>
<div
className="publicGalleryTabContainer"
>
<Stack
tokens={
Object {
"childrenGap": 10,
}
}
>
<Stack
horizontal={true}
tokens={
Object {
"childrenGap": 20,
"padding": 10,
}
}
wrap={true}
>
<StackItem
grow={true}
>
<StyledSearchBox
onChange={[Function]}
placeholder="Search"
/>
</StackItem>
<StackItem>
<StyledLabelBase>
Sort by
</StyledLabelBase>
</StackItem>
<StackItem
styles={
Object {
"root": Object {
"minWidth": 200,
},
}
}
>
<Dropdown
onChange={[Function]}
options={
Array [
Object {
"key": 0,
"text": "Most viewed",
},
Object {
"key": 1,
"text": "Most downloaded",
},
Object {
"key": 3,
"text": "Most recent",
},
Object {
"key": 2,
"text": "Most favorited",
},
]
}
selectedKey={0}
/>
</StackItem>
<StackItem>
<InfoComponent />
</StackItem>
</Stack>
<StackItem>
<StyledSpinnerBase
size={3}
/>
</StackItem>
</Stack>
</div>
</PivotItem>
<PivotItem
headerText="Official samples"
itemKey="OfficialSamples"

View File

@@ -149,7 +149,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.offer = this.database?.offer();
}
const initialState: SettingsComponentState = {
this.state = {
throughput: undefined,
throughputBaseline: undefined,
autoPilotThroughput: undefined,
@@ -199,12 +199,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
selectedTab: SettingsV2TabTypes.ScaleTab,
};
this.state = {
...initialState,
...this.getBaselineValues(),
...this.getAutoscaleBaselineValues(),
};
this.saveSettingsButton = {
isEnabled: this.isSaveSettingsButtonEnabled,
isVisible: () => {
@@ -231,6 +225,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.loadMongoIndexes();
}
this.setAutoPilotStates();
this.setBaseline();
if (this.props.settingsTab.isActive()) {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
@@ -291,24 +286,17 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
);
};
private getAutoscaleBaselineValues = (): Partial<SettingsComponentState> => {
private setAutoPilotStates = (): void => {
const autoscaleMaxThroughput = this.offer?.autoscaleMaxThroughput;
if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
return {
this.setState({
isAutoPilotSelected: true,
wasAutopilotOriginallySet: true,
autoPilotThroughput: autoscaleMaxThroughput,
autoPilotThroughputBaseline: autoscaleMaxThroughput,
};
});
}
return {
isAutoPilotSelected: false,
wasAutopilotOriginallySet: false,
autoPilotThroughput: undefined,
autoPilotThroughputBaseline: undefined,
};
};
public hasProvisioningTypeChanged = (): boolean =>
@@ -573,25 +561,21 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
};
public setBaseline = (): void => {
const baselineValues = this.getBaselineValues();
const autoscaleBaselineValues = this.getAutoscaleBaselineValues();
this.setState({ ...baselineValues, ...autoscaleBaselineValues } as SettingsComponentState);
};
private getBaselineValues = (): Partial<SettingsComponentState> => {
const offerThroughput = this.offer?.manualThroughput;
if (!this.isCollectionSettingsTab) {
return {
this.setState({
throughput: offerThroughput,
throughputBaseline: offerThroughput,
};
});
return;
}
const defaultTtl = this.collection.defaultTtl();
let timeToLive: TtlType;
let timeToLiveSeconds: number;
let timeToLive: TtlType = this.state.timeToLive;
let timeToLiveSeconds = this.state.timeToLiveSeconds;
switch (defaultTtl) {
case undefined:
case 0:
@@ -636,7 +620,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
(this.collection.geospatialConfig && this.collection.geospatialConfig()?.type) || GeospatialConfigType.Geometry;
const geoSpatialConfigType = GeospatialConfigType[geospatialConfigTypeString as keyof typeof GeospatialConfigType];
return {
this.setState({
throughput: offerThroughput,
throughputBaseline: offerThroughput,
changeFeedPolicy: changeFeedPolicy,
@@ -659,7 +643,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
conflictResolutionPolicyProcedureBaseline: conflictResolutionPolicyProcedure,
geospatialConfigType: geoSpatialConfigType,
geospatialConfigTypeBaseline: geoSpatialConfigType,
};
});
};
private getTabsButtons = (): CommandButtonComponentProps[] => {

View File

@@ -65,8 +65,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
constructor(props: SubSettingsComponentProps) {
super(props);
this.geospatialVisible = userContext.apiType === "SQL";
this.partitionKeyValue = "/" + this.props.collection.partitionKeyProperty;
this.partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
this.partitionKeyValue = this.getPartitionKeyValue();
}
componentDidMount(): void {
@@ -291,14 +291,6 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
);
};
private getPartitionKeyValue = (): string => {
if (userContext.apiType === "Mongo") {
return this.props.collection.partitionKeyProperties?.[0] || "";
}
return (this.props.collection.partitionKeyProperties || []).map((property) => "/" + property).join(", ");
};
private getPartitionKeyComponent = (): JSX.Element => (
<Stack {...titleAndInputStackProps}>
{this.getPartitionKeyVisible() && (
@@ -318,8 +310,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
if (
userContext.apiType === "Cassandra" ||
userContext.apiType === "Tables" ||
!this.props.collection.partitionKeyProperties ||
this.props.collection.partitionKeyProperties.length === 0 ||
!this.props.collection.partitionKeyProperty ||
(userContext.apiType === "Mongo" && this.props.collection.partitionKey.systemKey)
) {
return false;

View File

@@ -19,7 +19,7 @@ import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/Telemet
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../../../UserContext";
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
import { autoPilotThroughput1K } from "../../../../../Utils/AutoPilotUtils";
import { minAutoPilotThroughput } from "../../../../../Utils/AutoPilotUtils";
import { calculateEstimateNumber, usageInGB } from "../../../../../Utils/PricingUtils";
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import {
@@ -540,7 +540,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
step={AutoPilotUtils.autoPilotIncrementStep}
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
onChange={this.onAutoPilotThroughputChange}
min={autoPilotThroughput1K}
min={minAutoPilotThroughput}
errorMessage={this.props.throughputError}
/>
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}

View File

@@ -145,7 +145,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
id="autopilotInput"
key="auto pilot throughput input"
label="Max RU/s"
min={1000}
min={4000}
onChange={[Function]}
required={true}
step={1000}

View File

@@ -39,7 +39,7 @@ export const collection = ({
kind: "hash",
version: 2,
},
partitionKeyProperties: ["partitionKey"],
partitionKeyProperty: "partitionKey",
readSettings: () => {
return;
},

View File

@@ -64,9 +64,7 @@ exports[`SettingsComponent renders 1`] = `
"paths": Array [],
"version": 2,
},
"partitionKeyProperties": Array [
"partitionKey",
],
"partitionKeyProperty": "partitionKey",
"readSettings": [Function],
"uniqueKeyPolicy": Object {},
"usageSizeInKB": [Function],
@@ -140,9 +138,7 @@ exports[`SettingsComponent renders 1`] = `
"paths": Array [],
"version": 2,
},
"partitionKeyProperties": Array [
"partitionKey",
],
"partitionKeyProperty": "partitionKey",
"readSettings": [Function],
"uniqueKeyPolicy": Object {},
"usageSizeInKB": [Function],

View File

@@ -181,7 +181,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
const descriptionElement = (
<Stack>
{labelElement}
<Text id={`${dataFieldName}-text-display`} aria-labelledby={labelId} style={{ whiteSpace: "pre-line" }}>
<Text id={`${dataFieldName}-text-display`} aria-labelledby={labelId}>
{this.props.getTranslation(description.textTKey)}{" "}
{description.link && (
<Link target="_blank" href={description.link.href}>

View File

@@ -27,11 +27,6 @@ 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.
@@ -346,11 +341,6 @@ 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.

View File

@@ -5,7 +5,6 @@ const props = {
isDatabase: false,
showFreeTierExceedThroughputTooltip: true,
isSharded: true,
isFreeTier: false,
setThroughputValue: () => jest.fn(),
setIsAutoscale: () => jest.fn(),
setIsThroughputCapExceeded: () => jest.fn(),

View File

@@ -14,9 +14,7 @@ import "./ThroughputInput.less";
export interface ThroughputInputProps {
isDatabase: boolean;
isSharded: boolean;
isFreeTier: boolean;
showFreeTierExceedThroughputTooltip: boolean;
isQuickstart?: boolean;
setThroughputValue: (throughput: number) => void;
setIsAutoscale: (isAutoscale: boolean) => void;
setIsThroughputCapExceeded: (isThroughputCapExceeded: boolean) => void;
@@ -25,18 +23,15 @@ export interface ThroughputInputProps {
export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
isDatabase,
isSharded,
isFreeTier,
showFreeTierExceedThroughputTooltip,
setThroughputValue,
setIsAutoscale,
setIsThroughputCapExceeded,
isSharded,
onCostAcknowledgeChange,
}: ThroughputInputProps) => {
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
const [throughput, setThroughput] = useState<number>(
isFreeTier ? AutoPilotUtils.autoPilotThroughput1K : AutoPilotUtils.autoPilotThroughput4K
);
const [throughput, setThroughput] = useState<number>(AutoPilotUtils.minAutoPilotThroughput);
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
const [throughputError, setThroughputError] = useState<string>("");
const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0);
@@ -156,14 +151,11 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
if (mode === "Autoscale") {
const defaultThroughput = isFreeTier
? AutoPilotUtils.autoPilotThroughput1K
: AutoPilotUtils.autoPilotThroughput4K;
setThroughput(defaultThroughput);
setThroughput(AutoPilotUtils.minAutoPilotThroughput);
setIsAutoScaleSelected(true);
setThroughputValue(defaultThroughput);
setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
setIsAutoscale(true);
checkThroughputCap(defaultThroughput);
checkThroughputCap(AutoPilotUtils.minAutoPilotThroughput);
} else {
setThroughput(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
setIsAutoScaleSelected(false);
@@ -227,7 +219,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
</Stack>
<TextField
id="autoscaleRUValueField"
type="number"
styles={{
fieldGroup: { width: 300, height: 27 },
@@ -235,7 +226,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
}}
onChange={(event, newInput?: string) => onThroughputValueChange(newInput)}
step={AutoPilotUtils.autoPilotIncrementStep}
min={AutoPilotUtils.autoPilotThroughput1K}
min={AutoPilotUtils.minAutoPilotThroughput}
value={throughput.toString()}
aria-label="Max request units per second"
required={true}

View File

@@ -3,7 +3,6 @@
exports[`ThroughputInput Pane should render Default properly 1`] = `
<ThroughputInput
isDatabase={false}
isFreeTier={false}
isSharded={true}
onCostAcknowledgeChange={[Function]}
setIsAutoscale={[Function]}
@@ -1637,9 +1636,8 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
<StyledTextFieldBase
aria-label="Max request units per second"
errorMessage=""
id="autoscaleRUValueField"
key=".0:$.2"
min={1000}
min={4000}
onChange={[Function]}
required={true}
step={1000}
@@ -1661,8 +1659,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
aria-label="Max request units per second"
deferredValidationTime={200}
errorMessage=""
id="autoscaleRUValueField"
min={1000}
min={4000}
onChange={[Function]}
required={true}
resizable={true}
@@ -1957,8 +1954,8 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
<input
aria-invalid={false}
className="ms-TextField-field field-64"
id="autoscaleRUValueField"
min={1000}
id="TextField2"
min={4000}
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}

View File

@@ -42,8 +42,6 @@ export interface TreeNode {
timestamp?: number;
isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves
isLoading?: boolean;
isDisabled?: boolean;
toolTip?: string;
isSelected?: () => boolean;
onClick?: (isExpanded: boolean) => void; // Only if a leaf, other click will expand/collapse
onExpanded?: () => void;
@@ -171,17 +169,10 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
return (
<div
className={`${this.props.node.className || ""} main${generation} nodeItem ${showSelected ? "selected" : ""} ${
this.props.node.isDisabled ? "disable" : ""
}`}
onClick={(event: React.MouseEvent<HTMLDivElement>) =>
!this.props.node.isDisabled && this.onNodeClick(event, node)
}
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) =>
!this.props.node.isDisabled && this.onNodeKeyPress(event, node)
}
className={`${this.props.node.className || ""} main${generation} nodeItem ${showSelected ? "selected" : ""}`}
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" : ""}`}
@@ -192,7 +183,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
{this.renderCollapseExpandIcon(node)}
{node.iconSrc && <img className="nodeIcon" src={node.iconSrc} alt="" />}
{node.label && (
<span className="nodeLabel" title={`${node.toolTip ? node.toolTip : node.label}`}>
<span className="nodeLabel" title={node.label}>
{node.label}
</span>
)}
@@ -203,7 +194,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
</div>
{node.children && (
<AnimateHeight duration={TreeNodeComponent.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}>
<div className="nodeChildren" data-test={node.label} aria-disabled={node.isDisabled}>
<div className="nodeChildren" data-test={node.label}>
{TreeNodeComponent.getSortedChildren(node).map((childNode: TreeNode) => (
<TreeNodeComponent
key={`${childNode.label}-${generation + 1}-${childNode.timestamp}`}

View File

@@ -35,7 +35,7 @@ exports[`TreeComponent renders a simple tree 1`] = `
exports[`TreeNodeComponent does not render children by default 1`] = `
<div
className=" main2 nodeItem "
className=" main2 nodeItem "
onClick={[Function]}
onKeyPress={[Function]}
role="treeitem"
@@ -136,8 +136,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"
className="nodeClassname main12 nodeItem "
onClick={[Function]}
onKeyPress={[Function]}
role="treeitem"
@@ -287,7 +286,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
exports[`TreeNodeComponent renders loading icon 1`] = `
<div
className=" main2 nodeItem "
className=" main2 nodeItem "
onClick={[Function]}
onKeyPress={[Function]}
role="treeitem"
@@ -359,8 +358,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"
className="nodeClassname main12 nodeItem "
onClick={[Function]}
onKeyPress={[Function]}
role="treeitem"
@@ -529,7 +527,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
exports[`TreeNodeComponent renders unsorted children by default 1`] = `
<div
className=" main2 nodeItem "
className=" main2 nodeItem "
onClick={[Function]}
onKeyPress={[Function]}
role="treeitem"

View File

@@ -66,11 +66,7 @@
}
}
}
.disable{
background-color: rgb(255, 255, 255);
filter: grayscale();
color: rgb(161, 159, 157);
}
.treeComponentMenuItemContainer {
font-size: @mediumFontSize;

View File

@@ -68,10 +68,11 @@ export class ContainerSampleGenerator {
return database.findCollectionWithId(this.sampleDataFile.collectionId);
}
public async populateContainerAsync(collection: ViewModels.Collection): Promise<void> {
private 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

View File

@@ -9,7 +9,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, PoolIdType } from "../Common/Constants";
import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases";
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
@@ -298,7 +298,7 @@ export default class Explorer {
db1.id().localeCompare(db2.id())
);
useDatabases.setState({ databases: updatedDatabases });
await this.refreshAndExpandNewDatabases(deltaDatabases.toAdd, updatedDatabases);
await this.refreshAndExpandNewDatabases(deltaDatabases.toAdd, currentDatabases);
} catch (error) {
const errorMessage = getErrorMessage(error);
TelemetryProcessor.traceFailure(
@@ -357,21 +357,22 @@ export default class Explorer {
) {
const provisionData: IProvisionData = {
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
poolId: PoolIdType.DefaultPoolId,
};
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Connecting,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
let connectionInfo;
try {
TelemetryProcessor.traceStart(Action.PhoenixConnection, {
dataExplorerArea: Areas.Notebook,
});
useNotebook.getState().setIsAllocating(true);
connectionInfo = await this.phoenixClient.allocateContainer(provisionData);
if (!connectionInfo?.data?.phoenixServiceUrl) {
throw new Error(`PhoenixServiceUrl is invalid!`);
const 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!`);
}
await this.setNotebookInfo(connectionInfo, connectionStatus);
TelemetryProcessor.traceSuccess(Action.PhoenixConnection, {
@@ -380,22 +381,11 @@ export default class Explorer {
} catch (error) {
TelemetryProcessor.traceFailure(Action.PhoenixConnection, {
dataExplorerArea: Areas.Notebook,
status: error.status,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
connectionStatus.status = ConnectionStatusType.Failed;
useNotebook.getState().resetContainerConnection(connectionStatus);
if (error?.status === HttpStatusCodes.Forbidden && error.message) {
useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`);
} else {
useDialog
.getState()
.showOkModalDialog(
"Connection Failed",
"We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket."
);
}
throw error;
} finally {
useNotebook.getState().setIsAllocating(false);
@@ -422,8 +412,8 @@ export default class Explorer {
notebookServerEndpoint:
(validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
userContext.features.notebookServerUrl) ||
connectionInfo.data.phoenixServiceUrl,
authToken: userContext.features.notebookServerToken || connectionInfo.data.authToken,
connectionInfo.data.notebookServerUrl,
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
forwardingId: connectionInfo.data.forwardingId,
});
this.notebookManager?.notebookClient
@@ -498,8 +488,8 @@ export default class Explorer {
if (connectionInfo?.status !== HttpStatusCodes.OK) {
throw new Error(`Reset Workspace: Received status code- ${connectionInfo?.status}`);
}
if (!connectionInfo?.data?.phoenixServiceUrl) {
throw new Error(`Reset Workspace: PhoenixServiceUrl is invalid!`);
if (!connectionInfo?.data?.notebookServerUrl) {
throw new Error(`Reset Workspace: NotebookServerUrl is invalid!`);
}
if (useNotebook.getState().isPhoenixNotebooks) {
await this.setNotebookInfo(connectionInfo, connectionStatus);
@@ -1027,7 +1017,7 @@ export default class Explorer {
}
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
if (useNotebook.getState().isPhoenixFeatures && !useNotebook.getState().isPhoenixDisabled) {
if (useNotebook.getState().isPhoenixFeatures) {
await this.allocateContainer();
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
@@ -1130,12 +1120,7 @@ export default class Explorer {
}
}
public async onNewCollectionClicked(
options: {
databaseId?: string;
isQuickstart?: boolean;
} = {}
): Promise<void> {
public async onNewCollectionClicked(databaseId?: string): Promise<void> {
if (userContext.apiType === "Cassandra") {
useSidePanel
.getState()
@@ -1150,7 +1135,7 @@ export default class Explorer {
: await useDatabases.getState().loadDatabaseOffers();
useSidePanel
.getState()
.openSidePanel("New " + getCollectionName(), <AddCollectionPanel explorer={this} {...options} />);
.openSidePanel("New " + getCollectionName(), <AddCollectionPanel explorer={this} databaseId={databaseId} />);
}
}
@@ -1253,10 +1238,7 @@ export default class Explorer {
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
const isNotebookEnabled =
userContext.features.notebooksDownBanner ||
useNotebook.getState().isPhoenixNotebooks ||
useNotebook.getState().isPhoenixFeatures;
const isNotebookEnabled = userContext.features.notebooksDownBanner || useNotebook.getState().isPhoenixNotebooks;
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
useNotebook
.getState()

View File

@@ -75,11 +75,7 @@ export function createStaticCommandBarButtons(
if (container.notebookManager?.gitHubOAuthService) {
notebookButtons.push(createManageGitHubAccountButton(container));
}
if (
useNotebook.getState().isPhoenixFeatures &&
!useNotebook.getState().isPhoenixDisabled &&
configContext.isTerminalEnabled
) {
if (useNotebook.getState().isPhoenixFeatures && configContext.isTerminalEnabled) {
notebookButtons.push(createOpenTerminalButton(container));
}
if (useNotebook.getState().isPhoenixNotebooks && selectedNodeState.isConnectedToContainer()) {
@@ -87,8 +83,8 @@ export function createStaticCommandBarButtons(
}
if (
(userContext.apiType === "Mongo" &&
selectedNodeState.isDatabaseNodeOrNoneSelected() &&
useNotebook.getState().isShellEnabled) ||
useNotebook.getState().isShellEnabled &&
selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
userContext.apiType === "Cassandra"
) {
notebookButtons.push(createDivider());
@@ -100,26 +96,16 @@ export function createStaticCommandBarButtons(
}
notebookButtons.forEach((btn) => {
const isPhoenixFeaturesDownMsg =
(!useNotebook.getState().isPhoenixFeatures && !useNotebook.getState().isPhoenixDisabled) ||
(!useNotebook.getState().isPhoenixFeatures && useNotebook.getState().isPhoenixDisabled);
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
if (isPhoenixFeaturesDownMsg) {
applyNotebooksStyleProps(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
} else if (useNotebook.getState().isPhoenixDisabled) {
applyNotebooksStyleProps(btn, Constants.Notebook.notebookDisabledText);
if (!useNotebook.getState().isPhoenixFeatures) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
}
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
if (isPhoenixFeaturesDownMsg) {
applyNotebooksStyleProps(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
} else if (useNotebook.getState().isPhoenixDisabled) {
applyNotebooksStyleProps(btn, Constants.Notebook.notebookDisabledText);
if (!useNotebook.getState().isPhoenixFeatures) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
}
} else if (isPhoenixFeaturesDownMsg) {
applyNotebooksStyleProps(btn, Constants.Notebook.temporarilyDownMsg);
} else if (useNotebook.getState().isPhoenixDisabled) {
applyNotebooksStyleProps(btn, Constants.Notebook.notebookDisabledText);
} else if (!useNotebook.getState().isPhoenixNotebooks) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
}
buttons.push(btn);
});
@@ -168,38 +154,25 @@ export function createContextCommandBarButtons(
selectedNodeState: SelectedNodeState
): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
const isPhoenixShellDisabled = !useNotebook.getState().isShellEnabled || useNotebook.getState().isPhoenixDisabled;
const phoenixMongoShellBtn: CommandButtonComponentProps = {
const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell";
const newMongoShellBtn: CommandButtonComponentProps = {
iconSrc: HostedTerminalIcon,
iconAlt: "Open Mongo Shell",
iconAlt: label,
onCommandClick: () => {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
},
commandButtonLabel: "Open Mongo Shell",
ariaLabel: "Open Mongo Shell",
hasPopup: true,
disabled: isPhoenixShellDisabled,
tooltipText: isPhoenixShellDisabled ? Constants.Notebook.notebookDisabledText : undefined,
};
buttons.push(phoenixMongoShellBtn);
if (!useNotebook.getState().isShellEnabled) {
const label = "New Shell";
const newMongoShellBtn: CommandButtonComponentProps = {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
if (useNotebook.getState().isShellEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: false,
tooltipText: label,
};
buttons.push(newMongoShellBtn);
}
}
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
};
buttons.push(newMongoShellBtn);
}
return buttons;
@@ -336,7 +309,6 @@ 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: () => {
@@ -351,7 +323,6 @@ function createNewSQLQueryButton(selectedNodeState: SelectedNodeState): CommandB
} else if (userContext.apiType === "Mongo") {
const label = "New Query";
return {
id: "newQueryBtn",
iconSrc: AddSqlQueryIcon,
iconAlt: label,
onCommandClick: () => {
@@ -428,7 +399,7 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState)
return buttons;
}
function applyNotebooksStyleProps(buttonProps: CommandButtonComponentProps, tooltip: string): void {
function applyNotebooksTemporarilyDownStyle(buttonProps: CommandButtonComponentProps, tooltip: string): void {
if (!buttonProps.isDivider) {
buttonProps.disabled = true;
buttonProps.tooltipText = tooltip;
@@ -438,7 +409,6 @@ function applyNotebooksStyleProps(buttonProps: CommandButtonComponentProps, tool
function createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
const label = "New Notebook";
return {
id: "newNotebookBtn",
iconSrc: NewNotebookIcon,
iconAlt: label,
onCommandClick: () => container.onNewNotebookClicked(),
@@ -504,11 +474,10 @@ function createOpenTerminalButton(container: Explorer): CommandButtonComponentPr
function createOpenMongoTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Mongo Shell";
const tooltip =
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
const disableButton =
!useNotebook.getState().isNotebooksEnabledForAccount &&
(!useNotebook.getState().isNotebookEnabled ||
!useNotebook.getState().isShellEnabled ||
useNotebook.getState().isPhoenixDisabled);
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
@@ -521,7 +490,7 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
hasPopup: false,
disabled: disableButton,
ariaLabel: label,
tooltipText: !disableButton ? undefined : Constants.Notebook.notebookDisabledText,
tooltipText: !disableButton ? "" : tooltip,
};
}

View File

@@ -36,11 +36,6 @@
outline: 0px;
}
}
.disableText{
background-color: rgb(255, 255, 255)!important;
filter: grayscale();
color: rgb(161, 159, 157)!important;
}
.connectIcon{
margin: 0px 4px;
height: 18px;

View File

@@ -23,8 +23,6 @@ interface Props {
}
export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Element => {
const connectionInfo = useNotebook((state) => state.connectionInfo);
const isPhoenixDisabled = useNotebook((state) => state.isPhoenixDisabled);
const [second, setSecond] = React.useState("00");
const [minute, setMinute] = React.useState("00");
const [isActive, setIsActive] = React.useState(false);
@@ -79,12 +77,6 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
}
}, [connectionInfo.status]);
React.useEffect(() => {
if (isPhoenixDisabled) {
setToolTipContent(Notebook.notebookDisabledText);
}
}, [isPhoenixDisabled]);
const stopTimer = () => {
setIsActive(false);
setCounter(0);
@@ -101,18 +93,11 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
(connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.Reconnect)
) {
return (
<ActionButton
className={isPhoenixDisabled ? "disableText commandReactBtn" : "commandReactBtn"}
disabled={isPhoenixDisabled}
onClick={() => !isPhoenixDisabled && container.allocateContainer()}
>
<ActionButton className="commandReactBtn" onClick={() => container.allocateContainer()}>
<TooltipHost content={toolTipContent}>
<Stack className="connectionStatusContainer" horizontal>
<Icon
iconName="ConnectVirtualMachine"
className={isPhoenixDisabled ? "connectIcon disableText" : "connectIcon"}
/>
<span className={isPhoenixDisabled ? "disableText" : ""}>{connectionInfo.status}</span>
<Icon iconName="ConnectVirtualMachine" className="connectIcon" />
<span>{connectionInfo.status}</span>
</Stack>
</TooltipHost>
</ActionButton>

View File

@@ -1,11 +1,10 @@
/**
* Notebook container related stuff
*/
import { useDialog } from "Explorer/Controls/Dialog";
import promiseRetry, { AbortError } from "p-retry";
import { PhoenixClient } from "Phoenix/PhoenixClient";
import * as Constants from "../../Common/Constants";
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook, PoolIdType } from "../../Common/Constants";
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook } from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import * as DataModels from "../../Contracts/DataModels";
@@ -20,7 +19,6 @@ export class NotebookContainerClient {
private isResettingWorkspace: boolean;
private phoenixClient: PhoenixClient;
private retryOptions: promiseRetry.Options;
private scheduleTimerId: NodeJS.Timeout;
constructor(private onConnectionLost: () => void) {
this.phoenixClient = new PhoenixClient();
@@ -29,36 +27,36 @@ export class NotebookContainerClient {
maxTimeout: Notebook.retryAttemptDelayMs,
minTimeout: Notebook.retryAttemptDelayMs,
};
this.initHeartbeat(Constants.Notebook.heartbeatDelayMs);
}
private initHeartbeat(delayMs: number): void {
this.scheduleHeartbeat(delayMs);
useNotebook.subscribe(
() => this.scheduleHeartbeat(delayMs),
(state) => state.notebookServerInfo
);
}
private scheduleHeartbeat(delayMs: number) {
if (this.scheduleTimerId) {
clearInterval(this.scheduleTimerId);
}
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo?.notebookServerEndpoint) {
this.scheduleTimerId = setInterval(async () => {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo?.notebookServerEndpoint) {
const memoryUsageInfo = await this.getMemoryUsage();
useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo);
}
}, delayMs);
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
} else {
const unsub = useNotebook.subscribe(
(newServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => {
if (newServerInfo?.notebookServerEndpoint) {
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
}
unsub();
},
(state) => state.notebookServerInfo
);
}
}
/**
* Heartbeat: each ping schedules another ping
*/
private scheduleHeartbeat(delayMs: number): void {
setTimeout(async () => {
const memoryUsageInfo = await this.getMemoryUsage();
useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo);
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo?.notebookServerEndpoint) {
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
}
}, delayMs);
}
public async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
@@ -154,23 +152,12 @@ export class NotebookContainerClient {
if (useNotebook.getState().isPhoenixNotebooks) {
const provisionData: IProvisionData = {
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
poolId: PoolIdType.DefaultPoolId,
};
return await this.phoenixClient.resetContainer(provisionData);
}
return null;
} catch (error) {
Logger.logError(getErrorMessage(error), "NotebookContainerClient/resetWorkspace");
if (error?.status === HttpStatusCodes.Forbidden && error.message) {
useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`);
} else {
useDialog
.getState()
.showOkModalDialog(
"Connection Failed",
"We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket."
);
}
throw error;
}
}

View File

@@ -303,8 +303,8 @@ export class NotebookContentClient {
private getServerConfig(): ServerConfig {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
return {
endpoint: notebookServerInfo?.notebookServerEndpoint,
token: notebookServerInfo?.authToken,
endpoint: notebookServerInfo.notebookServerEndpoint,
token: notebookServerInfo.authToken,
crossDomain: true,
};
}

View File

@@ -5,7 +5,6 @@ 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";
@@ -101,7 +100,6 @@ 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);
}

View File

@@ -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, HttpStatusCodes } from "../../Common/Constants";
import { ConnectionStatusType } 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, PhoenixErrorType } from "../../Contracts/DataModels";
import { ContainerConnectionInfo, ContainerInfo } from "../../Contracts/DataModels";
import { useTabs } from "../../hooks/useTabs";
import { IPinnedRepo } from "../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
@@ -40,7 +40,6 @@ interface NotebookState {
containerStatus: ContainerInfo;
isPhoenixNotebooks: boolean;
isPhoenixFeatures: boolean;
isPhoenixDisabled: boolean;
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
@@ -65,7 +64,6 @@ interface NotebookState {
getPhoenixStatus: () => Promise<void>;
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => void;
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => void;
setIsPhoenixDisabled: (isPhoenixDisabled: boolean) => void;
}
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
@@ -98,11 +96,10 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
containerStatus: {
status: undefined,
durationLeftInMinutes: undefined,
phoenixServerInfo: undefined,
notebookServerInfo: undefined,
},
isPhoenixNotebooks: undefined,
isPhoenixFeatures: undefined,
isPhoenixDisabled: undefined,
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
@@ -299,47 +296,26 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
useNotebook.getState().setContainerStatus({
status: undefined,
durationLeftInMinutes: undefined,
phoenixServerInfo: undefined,
notebookServerInfo: undefined,
});
},
setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }),
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
getPhoenixStatus: async () => {
if (get().isPhoenixNotebooks === undefined || get().isPhoenixFeatures === undefined) {
let isPhoenixNotebooks = false;
let isPhoenixFeatures = false;
let isPhoenixDisabled = false;
const isPublicInternetAllowed = isPublicInternetAccessAllowed();
const phoenixClient = new PhoenixClient();
const dbAccountAllowedInfo = await phoenixClient.getDbAccountAllowedStatus();
if (dbAccountAllowedInfo.status === HttpStatusCodes.OK) {
if (dbAccountAllowedInfo?.type === PhoenixErrorType.PhoenixFlightFallback) {
isPhoenixNotebooks = userContext.features.phoenixNotebooks;
isPhoenixFeatures = userContext.features.phoenixFeatures;
isPhoenixDisabled = !isPublicInternetAllowed && (isPhoenixNotebooks || isPhoenixFeatures);
} else {
isPhoenixNotebooks = isPhoenixFeatures = true;
isPhoenixDisabled = !isPublicInternetAllowed;
}
} else if (
dbAccountAllowedInfo.status === HttpStatusCodes.Forbidden &&
(userContext.features.phoenixNotebooks || userContext.features.phoenixFeatures)
) {
isPhoenixNotebooks = userContext.features.phoenixNotebooks;
isPhoenixFeatures = userContext.features.phoenixFeatures;
isPhoenixDisabled = true;
} else {
isPhoenixNotebooks = isPhoenixFeatures = false;
let isPhoenix = false;
if (userContext.features.phoenixNotebooks || userContext.features.phoenixFeatures) {
const phoenixClient = new PhoenixClient();
isPhoenix = isPublicInternetAccessAllowed() && (await phoenixClient.isDbAcountWhitelisted());
}
const isPhoenixNotebooks = userContext.features.phoenixNotebooks && isPhoenix;
const isPhoenixFeatures = userContext.features.phoenixFeatures && isPhoenix;
set({ isPhoenixNotebooks: isPhoenixNotebooks });
set({ isPhoenixFeatures: isPhoenixFeatures });
set({ isPhoenixDisabled: isPhoenixDisabled });
}
},
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }),
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => set({ isPhoenixFeatures: isPhoenixFeatures }),
setIsPhoenixDisabled: (isPhoenixDisabled: boolean) => set({ isPhoenixDisabled: isPhoenixDisabled }),
}));

View File

@@ -8,23 +8,23 @@ import { CassandraAddCollectionPane } from "../Panes/CassandraAddCollectionPane/
import { SettingsPane } from "../Panes/SettingsPane/SettingsPane";
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
function generateQueryText(action: ActionContracts.OpenQueryTab, partitionKeyProperties: string[]): string {
function generateQueryText(action: ActionContracts.OpenQueryTab, partitionKeyProperty: string): string {
if (!action.query) {
return "SELECT * FROM c";
} else if (action.query.text) {
return action.query.text;
} else if (action.query.partitionKeys?.length > 0 && partitionKeyProperties?.length > 0) {
} else if (!!action.query.partitionKeys && action.query.partitionKeys.length > 0) {
let query = "SELECT * FROM c WHERE";
for (let i = 0; i < action.query.partitionKeys.length; i++) {
const partitionKey = action.query.partitionKeys[i];
if (!partitionKey) {
// null partition key case
query = query.concat(` c.${partitionKeyProperties[i]} = ${action.query.partitionKeys[i]}`);
query = query.concat(` c.${partitionKeyProperty} = ${action.query.partitionKeys[i]}`);
} else if (typeof partitionKey !== "string") {
// Undefined partition key case
query = query.concat(` NOT IS_DEFINED(c.${partitionKeyProperties[i]})`);
query = query.concat(` NOT IS_DEFINED(c.${partitionKeyProperty})`);
} else {
query = query.concat(` c.${partitionKeyProperties[i]} = "${action.query.partitionKeys[i]}"`);
query = query.concat(` c.${partitionKeyProperty} = "${action.query.partitionKeys[i]}"`);
}
if (i !== action.query.partitionKeys.length - 1) {
query = query.concat(" OR");
@@ -109,7 +109,7 @@ function openCollectionTab(
collection.onNewQueryClick(
collection,
undefined,
generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperties)
generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperty)
);
break;
}

View File

@@ -1,15 +0,0 @@
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();
});
});

View File

@@ -8,10 +8,8 @@ import {
IconButton,
IDropdownOption,
Link,
ProgressIndicator,
Separator,
Stack,
TeachingBubble,
Text,
TooltipHost,
} from "@fluentui/react";
@@ -22,7 +20,6 @@ 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";
@@ -33,7 +30,6 @@ 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";
@@ -43,7 +39,6 @@ import { PanelLoadingScreen } from "./PanelLoadingScreen";
export interface AddCollectionPanelProps {
explorer: Explorer;
databaseId?: string;
isQuickstart?: boolean;
}
const SharedDatabaseDefault: DataModels.IndexingPolicy = {
@@ -98,7 +93,6 @@ export interface AddCollectionPanelState {
showErrorDetails: boolean;
isExecuting: boolean;
isThroughputCapExceeded: boolean;
teachingBubbleStep: number;
}
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
@@ -113,11 +107,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
this.state = {
createNewDatabase: userContext.apiType !== "Tables" && !this.props.databaseId,
newDatabaseId: props.isQuickstart ? this.getSampleDBName() : "",
newDatabaseId: "",
isSharedThroughputChecked: this.getSharedThroughputDefault(),
selectedDatabaseId:
userContext.apiType === "Tables" ? CollectionCreation.TablesAPIDefaultDatabase : this.props.databaseId,
collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "",
collectionId: "",
enableIndexing: true,
isSharded: userContext.apiType !== "Tables",
partitionKey: this.getPartitionKey(),
@@ -130,16 +124,9 @@ 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();
@@ -163,89 +150,6 @@ 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>
@@ -345,7 +249,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={true}
isSharded={this.state.isSharded}
isFreeTier={this.isFreeTierAccount()}
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
@@ -580,7 +483,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={false}
isSharded={this.state.isSharded}
isFreeTier={this.isFreeTierAccount()}
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
@@ -784,35 +686,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={this.state.isThroughputCapExceeded} />
{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>
)}
{this.state.isExecuting && <PanelLoadingScreen />}
</form>
);
}
@@ -956,9 +830,6 @@ 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 "";
}
@@ -1026,11 +897,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
private isSynapseLinkEnabled(): boolean {
if (!userContext.databaseAccount) {
return false;
}
const { properties } = userContext.databaseAccount;
if (!properties) {
return false;
}
@@ -1126,25 +994,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
document.getElementById("collapsibleSectionContent")?.scrollIntoView();
}
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();
private async submit(event: React.FormEvent<HTMLFormElement>): Promise<void> {
event.preventDefault();
if (!this.validateInputs()) {
return;
@@ -1193,7 +1044,6 @@ 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);
@@ -1238,27 +1088,8 @@ 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) {

View File

@@ -147,7 +147,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
if (isAutoscaleSelected) {
if (!AutoPilotUtils.isValidAutoPilotThroughput(throughput)) {
setFormErrors(
`Please enter a value greater than ${AutoPilotUtils.autoPilotThroughput1K} for autopilot throughput`
`Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
);
return false;
}
@@ -241,7 +241,6 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
isDatabase={true}
isSharded={databaseCreateNewShared}
isFreeTier={isFreeTierAccount}
setThroughputValue={(newThroughput: number) => (throughput = newThroughput)}
setIsAutoscale={(isAutoscale: boolean) => (isAutoscaleSelected = isAutoscale)}
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}

View File

@@ -262,7 +262,6 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
}
isDatabase
isSharded
isFreeTier={isFreeTierAccount}
setThroughputValue={(throughput: number) => (newKeySpaceThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (isNewKeySpaceAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
@@ -336,7 +335,6 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
isDatabase={false}
isSharded
isFreeTier={isFreeTierAccount}
setThroughputValue={(throughput: number) => (tableThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (isTableAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}

View File

@@ -10,6 +10,7 @@ import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUti
import Explorer from "../../Explorer";
import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem";
import { useNotebook } from "../../Notebook/useNotebook";
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
@@ -103,14 +104,11 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
switch (location.type) {
case "MyNotebooks":
parent = {
name: useNotebook.getState().notebookFolderName,
name: ResourceTreeAdapter.MyNotebooksTitle,
path: useNotebook.getState().notebookBasePath,
type: NotebookContentItemType.Directory,
};
isGithubTree = false;
if (useNotebook.getState().isPhoenixNotebooks) {
await container.allocateContainer();
}
break;
case "GitHub":

View File

@@ -2,7 +2,7 @@ import React from "react";
import LoadingIndicator_3Squares from "../../../images/LoadingIndicator_3Squares.gif";
export const PanelLoadingScreen: React.FunctionComponent = () => (
<div id="loadingScreen" className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer">
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer">
<img className="dataExplorerLoader" src={LoadingIndicator_3Squares} />
</div>
);

View File

@@ -29,7 +29,6 @@ describe("Table query select Panel", () => {
it("Should checked availableCheckbox by default", () => {
const wrapper = mount(<TableQuerySelectPanel {...props} />);
expect(wrapper.find("#availableCheckbox").first().props()).toEqual({
ariaPositionInSet: 0,
id: "availableCheckbox",
label: "Available Columns",
checked: true,

View File

@@ -128,9 +128,8 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
label="Available Columns"
checked={isAvailableColumnChecked}
onChange={availableColumnsCheckboxClick}
ariaPositionInSet={0}
/>
{columnOptions.map((column, index) => {
{columnOptions.map((column) => {
return (
<Checkbox
label={column.columnName}
@@ -138,7 +137,6 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
key={column.columnName}
checked={column.selected}
disabled={!column.editable}
ariaPositionInSet={index + 1}
/>
);
})}

View File

@@ -37,14 +37,12 @@ exports[`Table query select Panel should render Default properly 1`] = `
className="column-select-view"
>
<StyledCheckboxBase
ariaPositionInSet={0}
checked={true}
id="availableCheckbox"
label="Available Columns"
onChange={[Function]}
>
<CheckboxBase
ariaPositionInSet={0}
checked={true}
id="availableCheckbox"
label="Available Columns"
@@ -330,7 +328,6 @@ exports[`Table query select Panel should render Default properly 1`] = `
<input
aria-checked="true"
aria-label="Available Columns"
aria-posinset={0}
checked={true}
className="input-55"
data-ktp-execute-target={true}
@@ -649,7 +646,6 @@ exports[`Table query select Panel should render Default properly 1`] = `
</CheckboxBase>
</StyledCheckboxBase>
<StyledCheckboxBase
ariaPositionInSet={1}
checked={true}
disabled={false}
key=""
@@ -657,7 +653,6 @@ exports[`Table query select Panel should render Default properly 1`] = `
onChange={[Function]}
>
<CheckboxBase
ariaPositionInSet={1}
checked={true}
disabled={false}
label=""
@@ -944,7 +939,6 @@ exports[`Table query select Panel should render Default properly 1`] = `
aria-checked="true"
aria-disabled={false}
aria-label=""
aria-posinset={1}
checked={true}
className="input-55"
data-ktp-execute-target={true}

View File

@@ -35,60 +35,6 @@ const {
Smallint,
Tinyint,
Timestamp,
// List
List_Ascii,
List_Bigint,
List_Blob,
List_Boolean,
List_Date,
List_Decimal,
List_Double,
List_Float,
List_Int,
List_Text,
List_Timestamp,
List_Uuid,
List_Varchar,
List_Varint,
List_Inet,
List_Smallint,
List_Tinyint,
// Map
Map_Ascii,
Map_Bigint,
Map_Blob,
Map_Boolean,
Map_Date,
Map_Decimal,
Map_Double,
Map_Float,
Map_Int,
Map_Text,
Map_Timestamp,
Map_Uuid,
Map_Varchar,
Map_Varint,
Map_Inet,
Map_Smallint,
Map_Tinyint,
// Set
Set_Ascii,
Set_Bigint,
Set_Blob,
Set_Boolean,
Set_Date,
Set_Decimal,
Set_Double,
Set_Float,
Set_Int,
Set_Text,
Set_Timestamp,
Set_Uuid,
Set_Varchar,
Set_Varint,
Set_Inet,
Set_Smallint,
Set_Tinyint,
} = TableConstants.CassandraType;
export const cassandraOptions = [
{ key: Text, text: Text },
@@ -108,60 +54,6 @@ export const cassandraOptions = [
{ key: Smallint, text: Smallint },
{ key: Tinyint, text: Tinyint },
{ key: Timestamp, text: Timestamp },
// List
{ key: List_Ascii, text: List_Ascii },
{ key: List_Bigint, text: List_Bigint },
{ key: List_Blob, text: List_Blob },
{ key: List_Boolean, text: List_Boolean },
{ key: List_Date, text: List_Date },
{ key: List_Decimal, text: List_Decimal },
{ key: List_Double, text: List_Double },
{ key: List_Float, text: List_Float },
{ key: List_Int, text: List_Int },
{ key: List_Text, text: List_Text },
{ key: List_Timestamp, text: List_Timestamp },
{ key: List_Uuid, text: List_Uuid },
{ key: List_Varchar, text: List_Varchar },
{ key: List_Varint, text: List_Varint },
{ key: List_Inet, text: List_Inet },
{ key: List_Smallint, text: List_Smallint },
{ key: List_Tinyint, text: List_Tinyint },
// Map
{ key: Map_Ascii, text: Map_Ascii },
{ key: Map_Bigint, text: Map_Bigint },
{ key: Map_Blob, text: Map_Blob },
{ key: Map_Boolean, text: Map_Boolean },
{ key: Map_Date, text: Map_Date },
{ key: Map_Decimal, text: Map_Decimal },
{ key: Map_Double, text: Map_Double },
{ key: Map_Float, text: Map_Float },
{ key: Map_Int, text: Map_Int },
{ key: Map_Text, text: Map_Text },
{ key: Map_Timestamp, text: Map_Timestamp },
{ key: Map_Uuid, text: Map_Uuid },
{ key: Map_Varchar, text: Map_Varchar },
{ key: Map_Varint, text: Map_Varint },
{ key: Map_Inet, text: Map_Inet },
{ key: Map_Smallint, text: Map_Smallint },
{ key: Map_Tinyint, text: Map_Tinyint },
// Set
{ key: Set_Ascii, text: Set_Ascii },
{ key: Set_Bigint, text: Set_Bigint },
{ key: Set_Blob, text: Set_Blob },
{ key: Set_Boolean, text: Set_Boolean },
{ key: Set_Date, text: Set_Date },
{ key: Set_Decimal, text: Set_Decimal },
{ key: Set_Double, text: Set_Double },
{ key: Set_Float, text: Set_Float },
{ key: Set_Int, text: Set_Int },
{ key: Set_Text, text: Set_Text },
{ key: Set_Timestamp, text: Set_Timestamp },
{ key: Set_Uuid, text: Set_Uuid },
{ key: Set_Varchar, text: Set_Varchar },
{ key: Set_Varint, text: Set_Varint },
{ key: Set_Inet, text: Set_Inet },
{ key: Set_Smallint, text: Set_Smallint },
{ key: Set_Tinyint, text: Set_Tinyint },
];
export const imageProps: IImageProps = {

View File

@@ -1,427 +0,0 @@
// 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>
`;

View File

@@ -12,13 +12,13 @@
margin: auto;
padding-left: 21px;
padding-right: 16px;
max-width: 1168px;
max-width: 1168px;;
> * {
>* {
justify-content: space-between;
}
> .title {
>.title {
position: relative; // To attach FeaturePanelLauncher as absolute
color: @BaseHigh;
font-size: 48px;
@@ -27,7 +27,7 @@
text-align: center;
}
> .subtitle {
>.subtitle {
color: @BaseHigh;
font-size: 18px;
padding-left: 0px;
@@ -41,36 +41,34 @@
cursor: pointer;
margin: 40px auto;
> .mainButton {
>.mainButton {
min-width: 124px;
max-width: 296px;
padding: 32px 16px;
display: flex;
background-color: @BaseLight;
border: 1px solid #949494;
box-sizing: border-box;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
border-radius: 4px;
> .legendContainer {
>.legendContainer {
margin-left: 16px;
text-align: left;
.legend {
font-family: @SemiboldFont;
margin-bottom: @DefaultSpace;
font-size: 18px;
}
.description {
font-size: 10px;
}
.newDescription {
font-size: 13px;
}
}
}
> :nth-child(n + 2) {
>:nth-child(n+2) {
margin-left: 32px;
}
}
@@ -85,7 +83,7 @@
min-width: 124px;
max-width: 296px;
> .title {
>.title {
font-size: 18px;
font-family: @SemiboldFont;
color: @BaseDark;
@@ -93,7 +91,7 @@
margin-bottom: 16px;
}
> ul {
>ul {
list-style: none;
padding-left: 0px;
margin-bottom: 0px;
@@ -103,7 +101,7 @@
.flex-display();
align-items: flex-start;
> img {
>img {
margin-right: @DefaultSpace;
width: 24px;
height: 24px;
@@ -113,6 +111,14 @@
margin-top: 4px;
}
.twoLineContent {
margin-top: -5px;
:nth-child(2) {
font-size: 9px;
}
}
.description {
font-size: 10px;
color: @BaseMediumHigh;
@@ -127,12 +133,12 @@
.flex-display();
.flex-direction();
> .title {
>.title {
color: @BaseDark;
padding: 0px;
font-size: 12px;
}
> .description {
>.description {
color: @BaseDark;
}
@@ -161,21 +167,12 @@
}
&:focus {
.focus();
.focus();
}
&:active {
.active();
}
}
.notebookSplashScreenItem {
padding: 12px 0 12px 12px;
.itemText {
margin-left: 12px;
font-family: @SemiboldFont;
.active();
}
}
}
}
}

View File

@@ -9,6 +9,31 @@ 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);

View File

@@ -1,22 +1,16 @@
/**
* Accordion top class
*/
import { Coachmark, DirectionalHint, Image, Link, Stack, TeachingBubbleContent, Text } from "@fluentui/react";
import { useCarousel } from "hooks/useCarousel";
import { useTabs } from "hooks/useTabs";
import { Link } from "@fluentui/react";
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 LinkIcon from "../../../images/Link_blue.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 NotebookIcon from "../../../images/notebook/Notebook-resource.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";
import { AuthType } from "../../AuthType";
@@ -38,10 +32,8 @@ import { useSelectedNode } from "../useSelectedNode";
export interface SplashScreenItem {
iconSrc: string;
title: string;
id?: string;
info?: string;
description: string;
showLinkIcon?: boolean;
onClick: () => void;
}
@@ -50,6 +42,8 @@ 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";
@@ -63,13 +57,13 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
this.subscriptions = [];
}
public componentWillUnmount(): void {
public componentWillUnmount() {
while (this.subscriptions.length) {
this.subscriptions.pop().dispose();
}
}
public componentDidMount(): void {
public componentDidMount() {
this.subscriptions.push(
{
dispose: useNotebook.subscribe(
@@ -77,13 +71,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
(state) => state.isNotebookEnabled
),
},
{ dispose: useSelectedNode.subscribe(() => this.setState({})) },
{
dispose: useCarousel.subscribe(
() => this.setState({}),
(state) => state.showCoachMark
),
}
{ dispose: useSelectedNode.subscribe(() => this.setState({})) }
);
}
@@ -94,89 +82,110 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
public render(): JSX.Element {
const mainItems = this.createMainItems();
const commonTaskItems = this.createCommonTaskItems();
let recentItems = this.createRecentItems();
recentItems = recentItems.filter((item) => item.description !== "Notebook");
return (
const tipsItems = this.createTipsItems();
const onClearRecent = this.clearMostRecent;
const formContainer = (jsx: JSX.Element) => (
<div className="connectExplorerContainer">
<form className="connectExplorerFormContainer">
<div className="splashScreenContainer">
<div className="splashScreen">
<div className="title">
Welcome to Cosmos DB
<FeaturePanelLauncher />
<form className="connectExplorerFormContainer">{jsx}</form>
</div>
);
return formContainer(
<div className="splashScreenContainer">
<div className="splashScreen">
<div className="title">
Welcome to Cosmos DB
<FeaturePanelLauncher />
</div>
<div className="subtitle">Globally distributed, multi-model database service for any scale</div>
<div className="mainButtonsContainer">
{mainItems.map((item) => (
<div
className="mainButton focusable"
key={`${item.title}`}
onClick={item.onClick}
onKeyPress={(event: React.KeyboardEvent) => this.onSplashScreenItemKeyPress(event, item.onClick)}
tabIndex={0}
role="button"
>
<img src={item.iconSrc} alt="" />
<div className="legendContainer">
<div className="legend">{item.title}</div>
<div className="description">{item.description}</div>
</div>
</div>
<div className="subtitle">Globally distributed, multi-model database service for any scale</div>
<div className="mainButtonsContainer">
{mainItems.map((item) => (
<Stack
horizontal
className="mainButton focusable"
key={`${item.title}`}
))}
</div>
<div className="moreStuffContainer">
<div className="moreStuffColumn commonTasks">
<div className="title">Common Tasks</div>
<ul>
{commonTaskItems.map((item) => (
<li
className="focusable"
key={`${item.title}${item.description}`}
onClick={item.onClick}
onKeyPress={(event: React.KeyboardEvent) => this.onSplashScreenItemKeyPress(event, item.onClick)}
tabIndex={0}
role="button"
>
<div>
<img src={item.iconSrc} alt="" />
</div>
<div className="legendContainer">
<Stack horizontal verticalAlign="center" style={{ marginBottom: 8 }}>
<div className="legend">{item.title}</div>
{item.showLinkIcon && <Image style={{ marginLeft: 8, width: 16 }} src={LinkIcon} />}
</Stack>
<div id={item.id} className="newDescription">
{item.description}
</div>
</div>
</Stack>
<img src={item.iconSrc} alt="" />
<span className="oneLineContent" title={item.info}>
{item.title}
</span>
</li>
))}
</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)}
</ul>
</div>
<div className="moreStuffColumn">
<div className="title">Recents</div>
<ul>
{recentItems.map((item, index) => (
<li key={`${item.title}${item.description}${index}`}>
<img src={item.iconSrc} alt="" />
<span className="twoLineContent">
<Link onClick={item.onClick} title={item.info}>
{item.title}
</Link>
<div className="description">{item.description}</div>
</span>
</li>
))}
</ul>
{recentItems.length > 0 && <Link onClick={() => onClearRecent()}>Clear Recents</Link>}
</div>
<div className="moreStuffColumn tipsContainer">
<div className="title">Tips</div>
<ul>
{tipsItems.map((item) => (
<li
className="tipContainer focusable"
key={`${item.title}${item.description}`}
onClick={item.onClick}
onKeyPress={(event: React.KeyboardEvent) => this.onSplashScreenItemKeyPress(event, item.onClick)}
tabIndex={0}
role="link"
>
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">Recents</div>
{this.getRecentItems()}
</div>
<div className="moreStuffColumn">
<div className="title">Top 3 things you need to know</div>
{this.top3Items()}
</div>
<div className="moreStuffColumn tipsContainer">
<div className="title">Learning Resources</div>
{this.getLearningResourceItems()}
</div>
</div>
<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>
</div>
</div>
</form>
</div>
</div>
);
}
@@ -192,51 +201,34 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
* public for testing purposes
*/
public createMainItems(): SplashScreenItem[] {
const heroes: SplashScreenItem[] = [];
const dataSampleUtil = this.createDataSampleUtil();
const heroes: SplashScreenItem[] = [
{
iconSrc: NewContainerIcon,
title: `New ${getCollectionName()}`,
description: "Create a new container for storage and throughput",
onClick: () => this.container.onNewCollectionClicked(),
},
];
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",
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 });
},
};
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(),
};
heroes.push(newNotebookBtn);
if (dataSampleUtil.isSampleContainerCreationSupported()) {
// Insert at the front
heroes.unshift({
iconSrc: SampleIcon,
title: "Start with Sample",
description: "Get started with a sample provided by Cosmos DB",
onClick: () => dataSampleUtil.createSampleContainerAsync(),
});
}
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);
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;
}
@@ -333,9 +325,9 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
return {
iconSrc: CollectionIcon,
iconSrc: NotebookIcon,
title: collectionId,
description: getCollectionName(),
description: "Data",
onClick: () => {
const collection = useDatabases.getState().findCollection(databaseId, collectionId);
collection?.openTab();
@@ -346,7 +338,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
private decorateOpenNotebookActivity({ name, path }: MostRecentActivity.OpenNotebookItem) {
return {
info: path,
iconSrc: NotebookIcon,
iconSrc: CollectionIcon,
title: name,
description: "Notebook",
onClick: () => {
@@ -401,273 +393,4 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
event.stopPropagation();
}
}
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>
{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>
))}
</Stack>
);
}
private getRecentItems(): JSX.Element {
const recentItems = this.createRecentItems()?.filter((item) => item.description !== "Notebook");
return (
<Stack>
<ul>
{recentItems.map((item, index) => (
<li key={`${item.title}${item.description}${index}`}>
<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>
{recentItems.length > 0 && <Link onClick={() => this.clearMostRecent()}>Clear Recents</Link>}
</Stack>
);
}
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>
{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>
))}
</Stack>
);
}
}

View File

@@ -27,60 +27,6 @@ export const CassandraType = {
Inet: "Inet",
Smallint: "Smallint",
Tinyint: "Tinyint",
List_Ascii: "List<Ascii>",
List_Bigint: "List<Bigint>",
List_Blob: "List<Blob>",
List_Boolean: "List<Boolean>",
List_Date: "List<Date>",
List_Decimal: "List<Decimal>",
List_Double: "List<Double>",
List_Float: "List<Float>",
List_Int: "List<Int>",
List_Text: "List<Text>",
List_Timestamp: "List<Timestamp>",
List_Uuid: "List<Uuid>",
List_Varchar: "List<Varchar>",
List_Varint: "List<Varint>",
List_Inet: "List<Inet>",
List_Smallint: "List<Smallint>",
List_Tinyint: "List<Tinyint>",
Map_Ascii: "Map<Ascii>",
Map_Bigint: "Map<Bigint>",
Map_Blob: "Map<Blob>",
Map_Boolean: "Map<Boolean>",
Map_Date: "Map<Date>",
Map_Decimal: "Map<Decimal>",
Map_Double: "Map<Double>",
Map_Float: "Map<Float>",
Map_Int: "Map<Int>",
Map_Text: "Map<Text>",
Map_Timestamp: "Map<Timestamp>",
Map_Uuid: "Map<Uuid>",
Map_Varchar: "Map<Varchar>",
Map_Varint: "Map<Varint>",
Map_Inet: "Map<Inet>",
Map_Smallint: "Map<Smallint>",
Map_Tinyint: "Map<Tinyint>",
Set_Ascii: "Set<Ascii>",
Set_Bigint: "Set<Bigint>",
Set_Blob: "Set<Blob>",
Set_Boolean: "Set<Boolean>",
Set_Date: "Set<Date>",
Set_Decimal: "Set<Decimal>",
Set_Double: "Set<Double>",
Set_Float: "Set<Float>",
Set_Int: "Set<Int>",
Set_Text: "Set<Text>",
Set_Timestamp: "Set<Timestamp>",
Set_Uuid: "Set<Uuid>",
Set_Varchar: "Set<Varchar>",
Set_Varint: "Set<Varint>",
Set_Inet: "Set<Inet>",
Set_Smallint: "Set<Smallint>",
Set_Tinyint: "Set<Tinyint>",
};
export const ClauseRule = {

View File

@@ -126,7 +126,7 @@ export function convertEntitiesToDocuments(
};
if (collection.partitionKey) {
document["partitionKey"] = collection.partitionKey;
document[collection.partitionKeyProperties[0]] = entity.PartitionKey._;
document[collection.partitionKeyProperty] = entity.PartitionKey._;
document["partitionKeyValue"] = entity.PartitionKey._;
}
for (var property in entity) {

View File

@@ -74,7 +74,7 @@ export default class ConflictsTab extends TabsBase {
this.partitionKey = options.partitionKey || (this.collection && this.collection.partitionKey);
this.conflictIds = options.conflictIds;
this.partitionKeyPropertyHeader =
this.collection?.partitionKeyPropertyHeaders?.[0] || this._getPartitionKeyPropertyHeader();
(this.collection && this.collection.partitionKeyPropertyHeader) || this._getPartitionKeyPropertyHeader();
this.partitionKeyProperty = !!this.partitionKeyPropertyHeader
? this.partitionKeyPropertyHeader.replace(/[/]+/g, ".").substr(1).replace(/[']+/g, "")
: null;

View File

@@ -1,212 +0,0 @@
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 }}>
Dont 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>
);
};

View File

@@ -143,18 +143,16 @@
<tr>
<th class="documentsGridHeader" data-bind="text: idHeader" tabindex="0"></th>
<!-- ko if: showPartitionKey -->
<!-- ko foreach: partitionKeyPropertyHeaders -->
<th
class="documentsGridHeader documentsGridPartition evenlySpacedHeader"
data-bind="
attr: {
title: $data
title: partitionKeyPropertyHeader
},
text: $data"
text: partitionKeyPropertyHeader"
tabindex="0"
></th>
<!-- /ko -->
<!-- /ko -->
<th
class="refreshColHeader"
role="button"
@@ -184,13 +182,13 @@
tabindex="0"
>
<td class="tabdocumentsGridElement"><a data-bind="text: $data.id, attr: { title: $data.id }"></a></td>
<!-- ko if: $data.partitionKeyProperties -->
<!-- ko foreach: $data.stringPartitionKeyValues -->
<td class="tabdocumentsGridElement" data-bind="colspan: $parent.stringPartitionKeyValues.length + 1">
<a data-bind="text: $data, attr: { title: $data }"></a>
<!-- ko if: $data.partitionKeyProperty -->
<td class="tabdocumentsGridElement" colspan="2">
<a
data-bind="text: $data.stringPartitionKeyValue, attr: { title: $data.stringPartitionKeyValue }"
></a>
</td>
<!-- /ko -->
<!-- /ko -->
</tr>
<!-- /ko -->
</tbody>

View File

@@ -50,7 +50,7 @@ export default class DocumentsTab extends TabsBase {
public editorState: ko.Observable<ViewModels.DocumentExplorerState>;
public newDocumentButton: ViewModels.Button;
public saveNewDocumentButton: ViewModels.Button;
public saveExistingDocumentButton: ViewModels.Button;
public saveExisitingDocumentButton: ViewModels.Button;
public discardNewDocumentChangesButton: ViewModels.Button;
public discardExisitingDocumentChangesButton: ViewModels.Button;
public deleteExisitingDocumentButton: ViewModels.Button;
@@ -65,8 +65,8 @@ export default class DocumentsTab extends TabsBase {
// TODO need to refactor
public partitionKey: DataModels.PartitionKey;
public partitionKeyPropertyHeaders: string[];
public partitionKeyProperties: string[];
public partitionKeyPropertyHeader: string;
public partitionKeyProperty: string;
public documentIds: ko.ObservableArray<DocumentId>;
private _documentsIterator: QueryIterator<ItemDefinition & Resource>;
@@ -90,10 +90,11 @@ export default class DocumentsTab extends TabsBase {
this._resourceTokenPartitionKey = options.resourceTokenPartitionKey;
this.documentIds = options.documentIds;
this.partitionKeyPropertyHeaders = this.collection?.partitionKeyPropertyHeaders || this.partitionKey?.paths;
this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) =>
partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, "")
);
this.partitionKeyPropertyHeader =
(this.collection && this.collection.partitionKeyPropertyHeader) || this._getPartitionKeyPropertyHeader();
this.partitionKeyProperty = !!this.partitionKeyPropertyHeader
? this.partitionKeyPropertyHeader.replace(/[/]+/g, ".").substr(1).replace(/[']+/g, "")
: null;
this.isFilterExpanded = ko.observable<boolean>(false);
this.isFilterCreated = ko.observable<boolean>(true);
@@ -226,7 +227,7 @@ export default class DocumentsTab extends TabsBase {
}),
};
this.saveExistingDocumentButton = {
this.saveExisitingDocumentButton = {
enabled: ko.computed<boolean>(() => {
switch (this.editorState()) {
case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid:
@@ -444,7 +445,8 @@ export default class DocumentsTab extends TabsBase {
savedDocument,
this.partitionKey as PartitionKeyDefinition
);
let id = new DocumentId(this, savedDocument, partitionKeyValueArray);
const partitionKeyValue = partitionKeyValueArray && partitionKeyValueArray[0];
let id = new DocumentId(this, savedDocument, partitionKeyValue);
let ids = this.documentIds();
ids.push(id);
@@ -487,12 +489,14 @@ export default class DocumentsTab extends TabsBase {
return Q();
};
public onSaveExistingDocumentClick = (): Promise<any> => {
public onSaveExisitingDocumentClick = (): Promise<any> => {
const selectedDocumentId = this.selectedDocumentId();
const documentContent = JSON.parse(this.selectedDocumentContent());
const partitionKeyValueArray = extractPartitionKey(documentContent, this.partitionKey as PartitionKeyDefinition);
selectedDocumentId.partitionKeyValue = partitionKeyValueArray;
const partitionKeyValue = partitionKeyValueArray && partitionKeyValueArray[0];
selectedDocumentId.partitionKeyValue = partitionKeyValue;
this.isExecutionError(false);
const startKey: number = TelemetryProcessor.traceStart(Action.UpdateDocument, {
@@ -796,7 +800,7 @@ export default class DocumentsTab extends TabsBase {
}
public buildQuery(filter: string): string {
return QueryUtils.buildDocumentsQuery(filter, this.partitionKeyProperties, this.partitionKey);
return QueryUtils.buildDocumentsQuery(filter, this.partitionKeyProperty, this.partitionKey);
}
protected getTabsButtons(): CommandButtonComponentProps[] {
@@ -840,16 +844,16 @@ export default class DocumentsTab extends TabsBase {
});
}
if (this.saveExistingDocumentButton.visible()) {
if (this.saveExisitingDocumentButton.visible()) {
const label = "Update";
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
onCommandClick: this.onSaveExistingDocumentClick,
onCommandClick: this.onSaveExisitingDocumentClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.saveExistingDocumentButton.enabled(),
disabled: !this.saveExisitingDocumentButton.enabled(),
});
}
@@ -895,8 +899,8 @@ export default class DocumentsTab extends TabsBase {
this.saveNewDocumentButton.enabled,
this.discardNewDocumentChangesButton.visible,
this.discardNewDocumentChangesButton.enabled,
this.saveExistingDocumentButton.visible,
this.saveExistingDocumentButton.enabled,
this.saveExisitingDocumentButton.visible,
this.saveExisitingDocumentButton.enabled,
this.discardExisitingDocumentChangesButton.visible,
this.discardExisitingDocumentChangesButton.enabled,
this.deleteExisitingDocumentButton.visible,
@@ -906,10 +910,19 @@ export default class DocumentsTab extends TabsBase {
this.updateNavbarWithTabsButtons();
}
private _getPartitionKeyPropertyHeader(): string {
return (
(this.partitionKey &&
this.partitionKey.paths &&
this.partitionKey.paths.length > 0 &&
this.partitionKey.paths[0]) ||
null
);
}
public static _createUploadButton(container: Explorer): CommandButtonComponentProps {
const label = "Upload Item";
return {
id: "uploadItemBtn",
iconSrc: UploadIcon,
iconAlt: label,
onCommandClick: () => {

View File

@@ -29,19 +29,15 @@ export default class MongoDocumentsTab extends DocumentsTab {
super(options);
this.lastFilterContents = ko.observableArray<string>(['{"id":"foo"}', "{ qty: { $gte: 20 } }"]);
this.partitionKeyProperties = this.partitionKeyProperties?.map((partitionKeyProperty, i) => {
if (partitionKeyProperty && ~partitionKeyProperty.indexOf(`"`)) {
partitionKeyProperty = partitionKeyProperty.replace(/["]+/g, "");
}
if (this.partitionKeyProperty && ~this.partitionKeyProperty.indexOf(`"`)) {
this.partitionKeyProperty = this.partitionKeyProperty.replace(/["]+/g, "");
}
if (partitionKeyProperty && partitionKeyProperty.indexOf("$v") > -1) {
// From $v.shard.$v.key.$v > shard.key
partitionKeyProperty = partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, "");
this.partitionKeyPropertyHeaders[i] = "/" + partitionKeyProperty;
}
return partitionKeyProperty;
});
if (this.partitionKeyProperty && this.partitionKeyProperty.indexOf("$v") > -1) {
// From $v.shard.$v.key.$v > shard.key
this.partitionKeyProperty = this.partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, "");
this.partitionKeyPropertyHeader = "/" + this.partitionKeyProperty;
}
this.isFilterExpanded = ko.observable<boolean>(true);
super.buildCommandBarOptions.bind(this);
@@ -56,9 +52,12 @@ export default class MongoDocumentsTab extends DocumentsTab {
tabTitle: this.tabTitle(),
});
const partitionKeyProperty = this.partitionKeyProperties?.[0];
if (partitionKeyProperty !== "_id" && !this._hasShardKeySpecified(documentContent)) {
const message = `The document is lacking the shard property: ${partitionKeyProperty}`;
if (
this.partitionKeyProperty &&
this.partitionKeyProperty !== "_id" &&
!this._hasShardKeySpecified(documentContent)
) {
const message = `The document is lacking the shard property: ${this.partitionKeyProperty}`;
this.displayedError(message);
let that = this;
setTimeout(() => {
@@ -80,12 +79,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
this.isExecutionError(false);
this.isExecuting(true);
return createDocument(
this.collection.databaseId,
this.collection,
this.partitionKeyProperties?.[0],
documentContent
)
return createDocument(this.collection.databaseId, this.collection, this.partitionKeyProperty, documentContent)
.then(
(savedDocument: any) => {
let partitionKeyArray = extractPartitionKey(
@@ -93,7 +87,9 @@ export default class MongoDocumentsTab extends DocumentsTab {
this._getPartitionKeyDefinition() as PartitionKeyDefinition
);
let id = new ObjectId(this, savedDocument, partitionKeyArray);
let partitionKeyValue = partitionKeyArray && partitionKeyArray[0];
let id = new ObjectId(this, savedDocument, partitionKeyValue);
let ids = this.documentIds();
ids.push(id);
delete savedDocument._self;
@@ -132,7 +128,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
.finally(() => this.isExecuting(false));
};
public onSaveExistingDocumentClick = (): Promise<any> => {
public onSaveExisitingDocumentClick = (): Promise<any> => {
const selectedDocumentId = this.selectedDocumentId();
const documentContent = this.selectedDocumentContent();
this.isExecutionError(false);
@@ -155,7 +151,9 @@ export default class MongoDocumentsTab extends DocumentsTab {
this._getPartitionKeyDefinition() as PartitionKeyDefinition
);
const id = new ObjectId(this, updatedDocument, partitionKeyArray);
let partitionKeyValue = partitionKeyArray && partitionKeyArray[0];
const id = new ObjectId(this, updatedDocument, partitionKeyValue);
documentId.id(id.id());
}
});
@@ -216,7 +214,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
})
.map((rawDocument: any) => {
const partitionKeyValue = rawDocument._partitionKeyValue;
return new DocumentId(this, rawDocument, [partitionKeyValue]);
return new DocumentId(this, rawDocument, partitionKeyValue);
});
const merged = currentDocuments.concat(nextDocumentIds);
@@ -305,7 +303,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
// Convert BsonSchema2 to /path format
partitionKey = {
kind: partitionKey.kind,
paths: ["/" + this.partitionKeyProperties?.[0].replace(/\./g, "/")],
paths: ["/" + this.partitionKeyProperty.replace(/\./g, "/")],
version: partitionKey.version,
};
}

View File

@@ -1,7 +1,6 @@
import { stringifyNotebook, toJS } from "@nteract/commutable";
import * as ko from "knockout";
import * as Q from "q";
import { userContext } from "UserContext";
import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg";
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
import CutIcon from "../../../images/notebook/Notebook-cut.svg";
@@ -130,16 +129,14 @@ export default class NotebookTabV2 extends NotebookTabBase {
});
}
if (userContext.features.publicGallery) {
saveButtonChildren.push({
iconName: "PublishContent",
onCommandClick: async () => await this.publishToGallery(),
commandButtonLabel: publishLabel,
hasPopup: false,
disabled: false,
ariaLabel: publishLabel,
});
}
saveButtonChildren.push({
iconName: "PublishContent",
onCommandClick: async () => await this.publishToGallery(),
commandButtonLabel: publishLabel,
hasPopup: false,
disabled: false,
ariaLabel: publishLabel,
});
let buttons: CommandButtonComponentProps[] = [
{

View File

@@ -1,6 +1,3 @@
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";
@@ -13,21 +10,17 @@ 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} />
))}
@@ -40,7 +33,6 @@ 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) {
@@ -51,27 +43,27 @@ function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
<li
onMouseOver={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
onClick={() => (tab ? tab.onTabClick() : useTabs.getState().activateConnectTab())}
onKeyPress={({ nativeEvent: e }) => (tab ? tab.onKeyPressActivate(undefined, e) : onKeyPressConnectTab(e))}
onClick={() => tab.onTabClick()}
onKeyPress={({ nativeEvent: e }) => tab.onKeyPressActivate(undefined, e)}
className={active ? "active tabList" : "tabList"}
title={useObservable(tab?.tabPath || ko.observable(""))}
title={useObservable(tab.tabPath)}
aria-selected={active}
aria-expanded={active}
aria-controls={tabId}
aria-controls={tab.tabId}
tabIndex={0}
role="tab"
ref={focusTab}
>
<span className="tabNavContentContainer">
<a data-toggle="tab" href={"#" + tabId} tabIndex={-1}>
<a data-toggle="tab" href={"#" + tab.tabId} tabIndex={-1}>
<div className="tab_Content">
<span className="statusIconContainer">
{useObservable(tab?.isExecutionError || ko.observable(false)) && <ErrorIcon tab={tab} active={active} />}
{useObservable(tab?.isExecuting || ko.observable(false)) && (
{useObservable(tab.isExecutionError) && <ErrorIcon tab={tab} active={active} />}
{useObservable(tab.isExecuting) && (
<img className="loadingIcon" title="Loading" src={loadingIcon} alt="Loading" />
)}
</span>
<span className="tabNavText">{useObservable(tab?.tabTitle || ko.observable("Connect"))}</span>
<span className="tabNavText">{useObservable(tab.tabTitle)}</span>
<span className="tabIconSection">
<CloseButton tab={tab} active={active} hovering={hovering} />
</span>
@@ -89,7 +81,7 @@ const CloseButton = ({ tab, active, hovering }: { tab: Tab; active: boolean; hov
role="button"
aria-label="Close Tab"
className="cancelButton"
onClick={() => (tab ? tab.onCloseTabButtonClick() : useTabs.getState().closeConnectTab())}
onClick={() => tab.onCloseTabButtonClick()}
tabIndex={active ? 0 : undefined}
onKeyPress={({ nativeEvent: e }) => tab.onKeyPressClose(undefined, e)}
>
@@ -121,10 +113,6 @@ 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);
@@ -135,18 +123,9 @@ function TabPane({ tab, active }: { tab: Tab; active: boolean }) {
}
}, [ref, tab]);
if (tab) {
if ("render" in tab) {
return <div {...attrs}>{tab.render()}</div>;
}
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();
}
};

View File

@@ -37,8 +37,7 @@ describe("Collection", () => {
version: 2,
});
collection = generateMockCollectionWithDataModel(collectionsDataModel);
expect(collection.partitionKeyProperties.length).toBe(1);
expect(collection.partitionKeyProperties[0]).toBe("somePartitionKey.anotherPartitionKey");
expect(collection.partitionKeyProperty).toBe("somePartitionKey.anotherPartitionKey");
});
it("should strip out forward slashes from single partition key paths", () => {
@@ -48,8 +47,7 @@ describe("Collection", () => {
version: 2,
});
collection = generateMockCollectionWithDataModel(collectionsDataModel);
expect(collection.partitionKeyProperties.length).toBe(1);
expect(collection.partitionKeyProperties[0]).toBe("somePartitionKey");
expect(collection.partitionKeyProperty).toBe("somePartitionKey");
});
});
@@ -63,8 +61,7 @@ describe("Collection", () => {
version: 2,
});
collection = generateMockCollectionWithDataModel(collectionsDataModel);
expect(collection.partitionKeyPropertyHeaders.length).toBe(1);
expect(collection.partitionKeyPropertyHeaders[0]).toBe("/somePartitionKey/anotherPartitionKey");
expect(collection.partitionKeyPropertyHeader).toBe("/somePartitionKey/anotherPartitionKey");
});
it("should preserve forward slash on a single partition key", () => {
@@ -74,8 +71,7 @@ describe("Collection", () => {
version: 2,
});
collection = generateMockCollectionWithDataModel(collectionsDataModel);
expect(collection.partitionKeyPropertyHeaders.length).toBe(1);
expect(collection.partitionKeyPropertyHeaders[0]).toBe("/somePartitionKey");
expect(collection.partitionKeyPropertyHeader).toBe("/somePartitionKey");
});
it("should be null if there is no partition key", () => {
@@ -85,7 +81,7 @@ describe("Collection", () => {
kind: "Hash",
});
collection = generateMockCollectionWithDataModel(collectionsDataModel);
expect(collection.partitionKeyPropertyHeaders.length).toBe(0);
expect(collection.partitionKeyPropertyHeader).toBeNull();
});
});
});

View File

@@ -50,8 +50,8 @@ export default class Collection implements ViewModels.Collection {
public rid: string;
public databaseId: string;
public partitionKey: DataModels.PartitionKey;
public partitionKeyPropertyHeaders: string[];
public partitionKeyProperties: string[];
public partitionKeyPropertyHeader: string;
public partitionKeyProperty: string;
public id: ko.Observable<string>;
public defaultTtl: ko.Observable<number>;
public indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
@@ -97,7 +97,6 @@ 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) {
@@ -121,25 +120,31 @@ export default class Collection implements ViewModels.Collection {
this.requestSchema = data.requestSchema;
this.geospatialConfig = ko.observable(data.geospatialConfig);
this.partitionKeyPropertyHeaders = this.partitionKey?.paths;
this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader, i) => {
// TODO fix this to only replace non-excaped single quotes
let partitionKeyProperty = partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, "");
// TODO fix this to only replace non-excaped single quotes
this.partitionKeyProperty =
(this.partitionKey &&
this.partitionKey.paths &&
this.partitionKey.paths.length &&
this.partitionKey.paths.length > 0 &&
this.partitionKey.paths[0].replace(/[/]+/g, ".").substr(1).replace(/[']+/g, "")) ||
null;
this.partitionKeyPropertyHeader =
(this.partitionKey &&
this.partitionKey.paths &&
this.partitionKey.paths.length > 0 &&
this.partitionKey.paths[0]) ||
null;
if (userContext.apiType === "Mongo" && partitionKeyProperty) {
if (~partitionKeyProperty.indexOf(`"`)) {
partitionKeyProperty = partitionKeyProperty.replace(/["]+/g, "");
}
// TODO #10738269 : Add this logic in a derived class for Mongo
if (partitionKeyProperty.indexOf("$v") > -1) {
// From $v.shard.$v.key.$v > shard.key
partitionKeyProperty = partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, "");
this.partitionKeyPropertyHeaders[i] = "/" + partitionKeyProperty;
}
}
if (userContext.apiType === "Mongo" && this.partitionKeyProperty && ~this.partitionKeyProperty.indexOf(`"`)) {
this.partitionKeyProperty = this.partitionKeyProperty.replace(/["]+/g, "");
}
return partitionKeyProperty;
});
// TODO #10738269 : Add this logic in a derived class for Mongo
if (userContext.apiType === "Mongo" && this.partitionKeyProperty && this.partitionKeyProperty.indexOf("$v") > -1) {
// From $v.shard.$v.key.$v > shard.key
this.partitionKeyProperty = this.partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, "");
this.partitionKeyPropertyHeader = "/" + this.partitionKeyProperty;
}
this.documentIds = ko.observableArray<DocumentId>([]);
this.isCollectionExpanded = ko.observable<boolean>(false);
@@ -217,7 +222,6 @@ 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;
}
@@ -304,7 +308,7 @@ export default class Collection implements ViewModels.Collection {
collectionName: this.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.rawDataModel.id + " - Items",
tabTitle: "Items",
});
this.documentIds([]);
@@ -312,7 +316,7 @@ export default class Collection implements ViewModels.Collection {
partitionKey: this.partitionKey,
documentIds: ko.observableArray<DocumentId>([]),
tabKind: ViewModels.CollectionTabKind.Documents,
title: this.rawDataModel.id + " - Items",
title: "Items",
collection: this,
node: this,
tabPath: `${this.databaseId}>${this.id()}>Documents`,
@@ -467,7 +471,7 @@ export default class Collection implements ViewModels.Collection {
collection: this,
masterKey: userContext.masterKey || "",
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
collectionPartitionKeyProperty: this.partitionKeyProperty,
collectionId: this.id(),
databaseId: this.databaseId,
isTabsContentExpanded: this.container.isTabsContentExpanded,
@@ -706,7 +710,7 @@ export default class Collection implements ViewModels.Collection {
tabPath: "",
collection: this,
masterKey: userContext.masterKey || "",
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
collectionPartitionKeyProperty: this.partitionKeyProperty,
collectionId: this.id(),
databaseId: this.databaseId,
isTabsContentExpanded: this.container.isTabsContentExpanded,

View File

@@ -150,7 +150,7 @@ export default class ConflictId {
partitionKeyValueResolved
);
documentId.partitionKeyProperties = [this.partitionKeyProperty];
documentId.partitionKeyProperty = this.partitionKeyProperty;
documentId.partitionKey = this.partitionKey;
return documentId;

View File

@@ -37,7 +37,6 @@ 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) {
@@ -55,7 +54,6 @@ export default class Database implements ViewModels.Database {
return this.offer && !!this.offer();
});
this.junoClient = new JunoClient();
this.isSampleDB = false;
this.isOfferRead = false;
}

View File

@@ -9,21 +9,21 @@ export default class DocumentId {
public self: string;
public ts: string;
public id: ko.Observable<string>;
public partitionKeyProperties: string[];
public partitionKeyProperty: string;
public partitionKey: DataModels.PartitionKey;
public partitionKeyValue: any[];
public stringPartitionKeyValues: string[];
public partitionKeyValue: any;
public stringPartitionKeyValue: string;
public isDirty: ko.Observable<boolean>;
constructor(container: DocumentsTab, data: any, partitionKeyValue: any[]) {
constructor(container: DocumentsTab, data: any, partitionKeyValue: any) {
this.container = container;
this.self = data._self;
this.rid = data._rid;
this.ts = data._ts;
this.partitionKeyValue = partitionKeyValue;
this.partitionKeyProperties = container?.partitionKeyProperties;
this.partitionKeyProperty = container && container.partitionKeyProperty;
this.partitionKey = container && container.partitionKey;
this.stringPartitionKeyValues = this.getPartitionKeyValueAsString();
this.stringPartitionKeyValue = this.getPartitionKeyValueAsString();
this.id = ko.observable(data.id);
this.isDirty = ko.observable(false);
}
@@ -46,35 +46,34 @@ export default class DocumentId {
}
public partitionKeyHeader(): Object {
if (!this.partitionKeyProperties || this.partitionKeyProperties.length === 0) {
if (!this.partitionKeyProperty) {
return undefined;
}
if (!this.partitionKeyValue || this.partitionKeyValue.length === 0) {
if (this.partitionKeyValue === undefined) {
return [{}];
}
return [this.partitionKeyValue];
}
public getPartitionKeyValueAsString(): string[] {
return this.partitionKeyValue?.map((partitionKeyValue) => {
const typeOfPartitionKeyValue: string = typeof partitionKeyValue;
public getPartitionKeyValueAsString(): string {
const partitionKeyValue: any = this.partitionKeyValue;
const typeOfPartitionKeyValue: string = typeof partitionKeyValue;
if (
typeOfPartitionKeyValue === "undefined" ||
typeOfPartitionKeyValue === "null" ||
typeOfPartitionKeyValue === "object"
) {
return "";
}
if (
typeOfPartitionKeyValue === "undefined" ||
typeOfPartitionKeyValue === "null" ||
typeOfPartitionKeyValue === "object"
) {
return "";
}
if (typeOfPartitionKeyValue === "string") {
return partitionKeyValue;
}
if (typeOfPartitionKeyValue === "string") {
return partitionKeyValue;
}
return JSON.stringify(partitionKeyValue);
});
return JSON.stringify(partitionKeyValue);
}
public async loadDocument(): Promise<void> {

View File

@@ -22,8 +22,8 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
public rid: string;
public rawDataModel: DataModels.Collection;
public partitionKey: DataModels.PartitionKey;
public partitionKeyProperties: string[];
public partitionKeyPropertyHeaders: string[];
public partitionKeyProperty: string;
public partitionKeyPropertyHeader: string;
public id: ko.Observable<string>;
public children: ko.ObservableArray<ViewModels.TreeNode>;
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;

View File

@@ -156,10 +156,8 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
label: "Gallery",
iconSrc: GalleryIcon,
className: "notebookHeader galleryHeader",
toolTip: useNotebook.getState().isPhoenixDisabled ? Notebook.notebookDisabledText : undefined,
onClick: () => container.openGallery(),
isSelected: () => activeTab?.tabKind === ViewModels.CollectionTabKind.Gallery,
isDisabled: useNotebook.getState().isPhoenixDisabled,
};
};
@@ -301,7 +299,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
},
];
if (item.type === NotebookContentItemType.Notebook && userContext.features.publicGallery) {
if (item.type === NotebookContentItemType.Notebook) {
items.push({
label: "Publish to gallery",
iconSrc: PublishIcon,
@@ -438,7 +436,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
const databaseNode: TreeNode = {
label: database.id(),
iconSrc: CosmosDBIcon,
isExpanded: database.isDatabaseExpanded(),
isExpanded: false,
className: "databaseHeader",
children: [],
isSelected: () => useSelectedNode.getState().isDataNodeSelected(database.id()),
@@ -463,7 +461,6 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
if (database.isDatabaseShared()) {
databaseNode.children.push({
id: database.isSampleDB ? "sampleScaleSettings" : "",
label: "Scale",
isSelected: () =>
useSelectedNode
@@ -500,7 +497,6 @@ 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
@@ -525,8 +521,6 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
children.push({
label: "Schema (Preview)",
onClick: collection.onSchemaAnalyzerClick.bind(collection),
toolTip: useNotebook.getState().isPhoenixDisabled ? Notebook.notebookDisabledText : undefined,
isDisabled: useNotebook.getState().isPhoenixDisabled,
isSelected: () =>
useSelectedNode
.getState()
@@ -536,7 +530,6 @@ 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: () =>
@@ -579,7 +572,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
return {
label: collection.id(),
iconSrc: CollectionIcon,
isExpanded: collection.isCollectionExpanded(),
isExpanded: false,
children: children,
className: "collectionHeader",
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
@@ -618,8 +611,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
]),
contextMenu: ResourceTreeContextMenuButtonFactory.createStoreProcedureContextMenuItems(container, sp),
})),
onClick: async () => {
await collection.loadStoredProcedures();
onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.StoredProcedures);
refreshActiveTab(
(tab: TabsBase) =>
@@ -643,8 +635,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
]),
contextMenu: ResourceTreeContextMenuButtonFactory.createUserDefinedFunctionContextMenuItems(container, udf),
})),
onClick: async () => {
await collection.loadUserDefinedFunctions();
onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.UserDefinedFunctions);
refreshActiveTab(
(tab: TabsBase) =>
@@ -666,8 +657,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Triggers]),
contextMenu: ResourceTreeContextMenuButtonFactory.createTriggerContextMenuItems(container, trigger),
})),
onClick: async () => {
await collection.loadTriggers();
onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Triggers);
refreshActiveTab(
(tab: TabsBase) =>

View File

@@ -1,103 +0,0 @@
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 "";
}
};

View File

@@ -1,172 +0,0 @@
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 <></>;
}
};

View File

@@ -2,7 +2,6 @@ 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 {
@@ -26,9 +25,7 @@ const onInit = async () => {
const props: GalleryAndNotebookViewerComponentProps = {
junoClient: new JunoClient(),
selectedTab:
galleryViewerProps.selectedTab ||
(userContext.features.publicGallery ? GalleryTab.PublicGallery : GalleryTab.OfficialSamples),
selectedTab: galleryViewerProps.selectedTab || GalleryTab.PublicGallery,
sortBy: galleryViewerProps.sortBy || SortBy.MostRecent,
searchText: galleryViewerProps.searchText,
};

View File

@@ -1,48 +0,0 @@
{
"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."
}

View File

@@ -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.\nTo know more -",
"DescriptionText": "This class sets collection and database throughput.",
"DecriptionLinkText": "Click here for more information",
"Regions": "Regions",
"RegionsPlaceholder": "Select a region",

View File

@@ -1,9 +1,6 @@
// 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";
@@ -60,8 +57,6 @@ 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);
@@ -105,7 +100,7 @@ const App: React.FunctionComponent = () => {
</div>
</div>
{/* Collections Tree - End */}
{openedTabs.length === 0 && !isConnectTabOpen && <SplashScreen explorer={explorer} />}
{openedTabs.length === 0 && <SplashScreen explorer={explorer} />}
<Tabs />
</div>
{/* Collections Tree and Tabs - End */}
@@ -120,8 +115,6 @@ const App: React.FunctionComponent = () => {
</div>
<SidePanel />
<Dialog />
{<QuickstartCarousel isOpen={isCarouselOpen} />}
{<QuickstartTutorial />}
</div>
);
};

View File

@@ -1,4 +1,3 @@
import { useDialog } from "Explorer/Controls/Dialog";
import promiseRetry, { AbortError } from "p-retry";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation";
@@ -10,22 +9,16 @@ import {
HttpStatusCodes,
Notebook,
} from "../Common/Constants";
import { getErrorMessage, getErrorStack } from "../Common/ErrorHandlingUtils";
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
import * as Logger from "../Common/Logger";
import { configContext } from "../ConfigContext";
import {
ContainerConnectionInfo,
ContainerInfo,
IContainerData,
IDbAccountAllow,
IMaxAllocationTimeExceeded,
IMaxDbAccountsPerUserExceeded,
IMaxUsersPerDbAccountExceeded,
IPhoenixConnectionInfoResult,
IPhoenixError,
IProvisionData,
IResponse,
PhoenixErrorType,
} from "../Contracts/DataModels";
import { useNotebook } from "../Explorer/Notebook/useNotebook";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
@@ -52,27 +45,23 @@ export class PhoenixClient {
provisionData: IProvisionData,
operation: string
): Promise<IResponse<IPhoenixConnectionInfoResult>> {
let response;
try {
response = await fetch(`${this.getPhoenixControlPlanePathPrefix()}/containerconnections`, {
const response = await fetch(`${this.getPhoenixControlPlanePathPrefix()}/containerconnections`, {
method: operation === "allocate" ? "POST" : "PATCH",
headers: PhoenixClient.getHeaders(),
body: JSON.stringify(provisionData),
});
const responseJson = await response?.json();
if (response.ok) {
return {
status: response.status,
data: responseJson,
};
let data: IPhoenixConnectionInfoResult;
if (response.status === HttpStatusCodes.OK) {
data = await response.json();
}
const phoenixError = responseJson as IPhoenixError;
if (response.status === HttpStatusCodes.Forbidden) {
throw new Error(this.ConvertToForbiddenErrorString(phoenixError));
}
throw new Error(phoenixError.message);
return {
status: response.status,
data,
};
} catch (error) {
error.status = response?.status;
console.error(error);
throw error;
}
}
@@ -104,7 +93,7 @@ export class PhoenixClient {
const containerStatus = await response.json();
return {
durationLeftInMinutes: containerStatus?.durationLeftInMinutes,
phoenixServerInfo: containerStatus?.phoenixServerInfo,
notebookServerInfo: containerStatus?.notebookServerInfo,
status: ContainerStatusType.Active,
};
} else if (response.status === HttpStatusCodes.NotFound) {
@@ -119,18 +108,6 @@ export class PhoenixClient {
});
useNotebook.getState().resetContainerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
useDialog
.getState()
.showOkModalDialog(
"Disconnected",
"Disconnected from temporary workspace. Please click on connect button to connect to temporary workspace."
);
throw new AbortError(response.statusText);
} else if (response?.status === HttpStatusCodes.Forbidden) {
const validationMessage = this.ConvertToForbiddenErrorString(await response.json());
if (validationMessage) {
useDialog.getState().showOkModalDialog("Connection Failed", `${validationMessage}`);
}
throw new AbortError(response.statusText);
}
throw new Error(response.statusText);
@@ -148,7 +125,7 @@ export class PhoenixClient {
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
return {
durationLeftInMinutes: undefined,
phoenixServerInfo: undefined,
notebookServerInfo: undefined,
status: ContainerStatusType.Disconnected,
};
}
@@ -162,48 +139,16 @@ export class PhoenixClient {
}
}
public async getDbAccountAllowedStatus(): Promise<IDbAccountAllow> {
const startKey = TelemetryProcessor.traceStart(Action.PhoenixDBAccountAllowed, {
dataExplorerArea: Areas.Notebook,
});
let responseJson;
public async isDbAcountWhitelisted(): Promise<boolean> {
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}`);
}
TelemetryProcessor.traceSuccess(
Action.PhoenixDBAccountAllowed,
{
dataExplorerArea: Areas.Notebook,
},
startKey
);
return {
status: response.status,
message: responseJson?.message,
type: responseJson?.type,
};
return response.status === HttpStatusCodes.OK;
} catch (error) {
TelemetryProcessor.traceFailure(
Action.PhoenixDBAccountAllowed,
{
dataExplorerArea: Areas.Notebook,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
Logger.logError(getErrorMessage(error), "PhoenixClient/IsDbAcountWhitelisted");
return {
status: HttpStatusCodes.Forbidden,
message: responseJson?.message,
type: responseJson?.type,
};
return false;
}
}
@@ -232,48 +177,4 @@ export class PhoenixClient {
[HttpHeaders.contentType]: "application/json",
};
}
public ConvertToForbiddenErrorString(jsonData: IPhoenixError): string {
const errInfo = jsonData;
switch (errInfo?.type) {
case PhoenixErrorType.MaxAllocationTimeExceeded: {
const maxAllocationTimeExceeded = errInfo as IMaxAllocationTimeExceeded;
const allocateAfterTimestamp = new Date(maxAllocationTimeExceeded?.earliestAllocationTimestamp);
allocateAfterTimestamp.setDate(allocateAfterTimestamp.getDate() + 1);
return (
`${errInfo.message}` +
" Max allocation time for a day to a user is " +
`${maxAllocationTimeExceeded.maxAllocationTimePerDayPerUserInMinutes}` +
". Please try again after " +
`${allocateAfterTimestamp.toLocaleString()}`
);
}
case PhoenixErrorType.MaxDbAccountsPerUserExceeded: {
const maxDbAccountsPerUserExceeded = errInfo as IMaxDbAccountsPerUserExceeded;
return (
`${errInfo.message}` +
" Max simultaneous connections allowed per user is " +
`${maxDbAccountsPerUserExceeded.maxSimultaneousConnectionsPerUser}` +
"."
);
}
case PhoenixErrorType.MaxUsersPerDbAccountExceeded: {
const maxUsersPerDbAccountExceeded = errInfo as IMaxUsersPerDbAccountExceeded;
return (
`${errInfo.message}` +
" Max simultaneous users allowed per DbAccount is " +
`${maxUsersPerDbAccountExceeded.maxSimultaneousUsersPerDbAccount}` +
"."
);
}
case PhoenixErrorType.AllocationValidationResult:
case PhoenixErrorType.RegionNotServicable:
case PhoenixErrorType.SubscriptionNotAllowed: {
return `${errInfo.message}`;
}
default: {
return undefined;
}
}
}
}

View File

@@ -1,5 +1,4 @@
export type Features = {
// set only via feature flags
readonly canExceedMaximumValue: boolean;
readonly cosmosdb: boolean;
readonly enableChangeFeedPolicy: boolean;
@@ -9,6 +8,12 @@ export type Features = {
readonly enableReactPane: boolean;
readonly enableRightPanelV2: boolean;
readonly enableSchema: boolean;
autoscaleDefault: boolean;
partitionKeyDefault: boolean;
partitionKeyDefault2: boolean;
phoenixNotebooks: boolean;
phoenixFeatures: boolean;
notebooksDownBanner: boolean;
readonly enableSDKoperations: boolean;
readonly enableSpark: boolean;
readonly enableTtl: boolean;
@@ -29,22 +34,11 @@ export type Features = {
readonly mongoProxyEndpoint?: string;
readonly mongoProxyAPIs?: string;
readonly enableThroughputCap: boolean;
// can be set via both flight and feature flag
autoscaleDefault: boolean;
partitionKeyDefault: boolean;
partitionKeyDefault2: boolean;
phoenixNotebooks?: boolean;
phoenixFeatures?: boolean;
notebooksDownBanner: boolean;
publicGallery?: boolean;
};
export function extractFeatures(given = new URLSearchParams(window.location.search)): Features {
const downcased = new URLSearchParams();
const set = (value: string, key: string) => {
downcased.set(key.toLowerCase(), value);
};
const set = (value: string, key: string) => downcased.set(key.toLowerCase(), value);
const get = (key: string, defaultValue?: string) =>
downcased.get("feature." + key) ?? downcased.get(key) ?? defaultValue;
@@ -88,6 +82,8 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
autoscaleDefault: "true" === get("autoscaledefault"),
partitionKeyDefault: "true" === get("partitionkeytest"),
partitionKeyDefault2: "true" === get("pkpartitionkeytest"),
phoenixNotebooks: "true" === get("phoenixnotebooks"),
phoenixFeatures: "true" === get("phoenixfeatures"),
notebooksDownBanner: "true" === get("notebooksDownBanner"),
enableThroughputCap: "true" === get("enablethroughputcap"),
};

View File

@@ -179,15 +179,6 @@ 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,

View File

@@ -1,228 +0,0 @@
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:
"armRegionNameeq '" +
region +
"'andserviceFamilyeq '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 };
}
};

View File

@@ -1,416 +0,0 @@
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;
}

Some files were not shown because too many files have changed in this diff Show More