diff --git a/.eslintignore b/.eslintignore index 2eeecaed5..f97291787 100644 --- a/.eslintignore +++ b/.eslintignore @@ -23,8 +23,6 @@ src/Common/MongoUtility.ts src/Common/NotificationsClientBase.ts src/Common/QueriesClient.ts src/Common/Splitter.ts -src/Controls/Heatmap/Heatmap.test.ts -src/Controls/Heatmap/Heatmap.ts src/Definitions/datatables.d.ts src/Definitions/gif.d.ts src/Definitions/globals.d.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56ed46edc..80640f594 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -164,24 +164,42 @@ jobs: strategy: fail-fast: false matrix: - shardIndex: [1, 2, 3, 4, 5, 6, 7, 8] - shardTotal: [8] + shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + shardTotal: [16] steps: - uses: actions/checkout@v4 - - name: "Az CLI login" - uses: azure/login@v1 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Use Node.js 18.x uses: actions/setup-node@v4 with: node-version: 18.x - run: npm ci - run: npx playwright install --with-deps + - name: "Az CLI login" + uses: Azure/login@v2 + with: + client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + # We can't use MSAL within playwright so we acquire tokens prior to running the tests + - name: "Acquire RBAC tokens for test accounts" + uses: azure/cli@v2 + with: + azcliversion: latest + inlineScript: | + NOSQL_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql.documents.azure.com/.default" -o tsv --query accessToken) + echo "::add-mask::$NOSQL_TESTACCOUNT_TOKEN" + echo NOSQL_TESTACCOUNT_TOKEN=$NOSQL_TESTACCOUNT_TOKEN >> $GITHUB_ENV + NOSQL_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-readonly.documents.azure.com/.default" -o tsv --query accessToken) + echo "::add-mask::$NOSQL_READONLY_TESTACCOUNT_TOKEN" + echo NOSQL_READONLY_TESTACCOUNT_TOKEN=$NOSQL_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV + TABLE_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-tables.documents.azure.com/.default" -o tsv --query accessToken) + echo "::add-mask::$TABLE_TESTACCOUNT_TOKEN" + echo TABLE_TESTACCOUNT_TOKEN=$TABLE_TESTACCOUNT_TOKEN >> $GITHUB_ENV + GREMLIN_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-gremlin.documents.azure.com/.default" -o tsv --query accessToken) + echo "::add-mask::$GREMLIN_TESTACCOUNT_TOKEN" + echo GREMLIN_TESTACCOUNT_TOKEN=$GREMLIN_TESTACCOUNT_TOKEN >> $GITHUB_ENV - name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}} - run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3 - name: Upload blob report to GitHub Actions Artifacts if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml index 6698951ae..6eed6ca0b 100644 --- a/.github/workflows/cleanup.yml +++ b/.github/workflows/cleanup.yml @@ -27,7 +27,7 @@ jobs: - name: "Az CLI login" uses: azure/login@v1 with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} + client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} diff --git a/.npmrc b/.npmrc index bcfa17a91..0a81fc707 100644 --- a/.npmrc +++ b/.npmrc @@ -1,4 +1,4 @@ save-exact=true # Ignore peer dependency conflicts -force=true # TODO: Remove this when we update to React 17 or higher! \ No newline at end of file +force=true # TODO: Remove this when we update to React 17 or higher! diff --git a/configs/mpac.json b/configs/mpac.json index 0a8e7eaba..ea8f902d8 100644 --- a/configs/mpac.json +++ b/configs/mpac.json @@ -1,5 +1,4 @@ { "JUNO_ENDPOINT": "https://tools.cosmos.azure.com", - "isTerminalEnabled": true, "isPhoenixEnabled": true -} +} \ No newline at end of file diff --git a/configs/prod.json b/configs/prod.json index 656d09561..12d7e289b 100644 --- a/configs/prod.json +++ b/configs/prod.json @@ -1,5 +1,4 @@ { - "JUNO_ENDPOINT": "https://tools.cosmos.azure.com", - "isTerminalEnabled" : false, - "isPhoenixEnabled" : false -} + "JUNO_ENDPOINT": "https://tools.cosmos.azure.com", + "isPhoenixEnabled": false +} \ No newline at end of file diff --git a/images/VisualStudio.svg b/images/VisualStudio.svg new file mode 100644 index 000000000..27b43feca --- /dev/null +++ b/images/VisualStudio.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/images/dotnet.png b/images/dotnet.png index c8972efdc..fb00ecf91 100644 Binary files a/images/dotnet.png and b/images/dotnet.png differ diff --git a/images/golang.svg b/images/golang.svg new file mode 100644 index 000000000..d706fb571 --- /dev/null +++ b/images/golang.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/images/springboot.svg b/images/springboot.svg new file mode 100644 index 000000000..ee451deaa --- /dev/null +++ b/images/springboot.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/images/vscode.svg b/images/vscode.svg new file mode 100644 index 000000000..137be57f0 --- /dev/null +++ b/images/vscode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/less/documentDB.less b/less/documentDB.less index b7d0982a9..9dbbdeb4b 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -2862,6 +2862,7 @@ a:link { z-index: 1000; overflow-y: auto; overflow-x: clip; + min-height: fit-content; } .uniqueIndexesContainer { diff --git a/less/documentDBFabric.less b/less/documentDBFabric.less index 198d3ec6f..a82f2c7bc 100644 --- a/less/documentDBFabric.less +++ b/less/documentDBFabric.less @@ -229,3 +229,12 @@ a:focus { .fileImportImg img { filter: brightness(0) saturate(100%); } + +.tabPanesContainer { + overflow: auto !important; +} + +.tabs-container { + min-height: 500px; + min-width: 500px; +} diff --git a/less/quickstart.less b/less/quickstart.less index dd9b2e33a..539d63448 100644 --- a/less/quickstart.less +++ b/less/quickstart.less @@ -1,927 +1,923 @@ @import "./Common/Constants"; html { - font-family: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; - padding: 0px; - margin: 0px; - overflow: hidden; + font-family: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; + padding: 0px; + margin: 0px; + overflow: hidden; } body { - font-family: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; - font-size: 12px; + font-family: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; + font-size: 12px; } .fixedleftpane { - background: #2f2d2d; - height: 100vh; - width: 80px; - float: left; + background: #2f2d2d; + height: 100vh; + width: 80px; + float: left; } #divQuickStart, #divExplorer { - display: inline-block; - width: 100%; - white-space: nowrap; + display: inline-block; + width: 100%; + white-space: nowrap; } #imgiconwidth1 { - width: 72%; + width: 72%; } #Quickstart { - text-align: center; - width: 80px; - height: 60px; - margin: 0 auto; - padding-top: 5px; - position: relative; + text-align: center; + width: 80px; + height: 60px; + margin: 0 auto; + padding-top: 5px; + position: relative; } .collectionheading { - text-transform: uppercase; - font-size: 10px; + text-transform: uppercase; + font-size: 10px; } #Quickstart #imgiconwidth1 { - width: 24px; - height: 24px; - position: absolute; - right: 27px; + width: 24px; + height: 24px; + position: absolute; + right: 27px; } .topSelected { - border-left: 4px solid @AccentMediumHigh; - background: #666666; + border-left: 4px solid @AccentMediumHigh; + background: #666666; } .topSelected:hover { - border-left: 4px solid @AccentMediumHigh; - background: #666666!important; - cursor: default!important; + border-left: 4px solid @AccentMediumHigh; + background: #666666 !important; + cursor: default !important; } #Quickstart:hover span.activemenu, #Quickstart:active span.activemenu { - color: #fff; + color: #fff; } #Explorer:hover span.menuExplorer, #Explorer:active span.menuExplorer { - color: #fff; + color: #fff; } menuQuickStart { - margin-left: 0; - padding-left: 0; - display: block; - right: 12px; - top: 30px; - position: absolute; + margin-left: 0; + padding-left: 0; + display: block; + right: 12px; + top: 30px; + position: absolute; } #Explorer { - text-align: center; - display: inline-block; - width: 80px; - height: 60px; - margin: 0 auto; - padding-top: 9px; - position: relative; + text-align: center; + display: inline-block; + width: 80px; + height: 60px; + margin: 0 auto; + padding-top: 9px; + position: relative; } #Explorer #imgiconwidth1, .feedbackstyle #imgiconwidth1, .settingstyle #imgiconwidth1 { - width: 24px; - height: 24px; - position: absolute; - right: 30px; + width: 24px; + height: 24px; + position: absolute; + right: 30px; } #Explorer span.menuExplorer { - margin-left: 0; - padding-left: 0; - display: block; - right: 19px; - top: 33px; - position: absolute; + margin-left: 0; + padding-left: 0; + display: block; + right: 19px; + top: 33px; + position: absolute; } .feedbackstyle span.menuExplorer { - margin-left: 0; - padding-left: 0; - display: block; - right: 19px; - top: 33px; - position: absolute; + margin-left: 0; + padding-left: 0; + display: block; + right: 19px; + top: 33px; + position: absolute; } .settingstyle span.menuExplorer { - margin-left: 0; - padding-left: 0; - display: block; - right: 19px; - top: 33px; - position: absolute; + margin-left: 0; + padding-left: 0; + display: block; + right: 19px; + top: 33px; + position: absolute; } .content { - display: inline-block; - width: 100%; - transition: all .4s ease-in-out; - -ms-transition: all .4s ease-in-out; - -webkit-transition: all .4s ease-in-out; - -moz-transition: all .4s ease-in-out; - height: 100vh; + display: inline-block; + width: 100%; + transition: all 0.4s ease-in-out; + -ms-transition: all 0.4s ease-in-out; + -webkit-transition: all 0.4s ease-in-out; + -moz-transition: all 0.4s ease-in-out; + height: 100vh; } .mini { - width: 0%; - float: left; - transition: all .4s ease-in-out; - -webkit-transition: all .4s ease-in-out; - -moz-transition: all .4s ease-in-out; - height: 100vh; - background-color: white; + width: 0%; + float: left; + transition: all 0.4s ease-in-out; + -webkit-transition: all 0.4s ease-in-out; + -moz-transition: all 0.4s ease-in-out; + height: 100vh; + background-color: white; } #sidebar-wrapper { - z-index: 1000; - position: fixed; - left: 250px; - width: 0; - height: 100%; - margin-left: -250px; - overflow-y: auto; - background: white; - -webkit-transition: all 0.5s ease; - -moz-transition: all 0.5s ease; - -o-transition: all 0.5s ease; - transition: all 0.5s ease; + z-index: 1000; + position: fixed; + left: 250px; + width: 0; + height: 100%; + margin-left: -250px; + overflow-y: auto; + background: white; + -webkit-transition: all 0.5s ease; + -moz-transition: all 0.5s ease; + -o-transition: all 0.5s ease; + transition: all 0.5s ease; } .toggle-left { - width: 0%; - overflow: hidden; + width: 0%; + overflow: hidden; } .toggle-minicontent { - width: 100%; + width: 100%; } .toggle-maincontent { - width: 100%; + width: 100%; } .toggle-mini { - width: 50px; + width: 50px; } .toggle-main { - width: 100%; + width: 100%; } .activepartitionmode { - background-color: @AccentMediumHigh; + background-color: @AccentMediumHigh; } .paddingpartition { - color: white; - padding-left: 15px; - padding-top: 25px; + color: white; + padding-left: 15px; + padding-top: 25px; } .paddingspan2 { - padding-top: 20px; - color: #000; - padding-left: 15px; + padding-top: 20px; + color: #000; + padding-left: 15px; } .paddingspan4 { - padding-top: 20px; - padding-bottom: 20px; - color: white; - padding-left: 15px; + padding-top: 20px; + padding-bottom: 20px; + color: white; + padding-left: 15px; } .whitegroove { - width: 344px; - border: groove; + width: 344px; + border: groove; } .dropdownbtn { - color: white; - width: 340px; - background: #262626; + color: white; + width: 340px; + background: #262626; } .queryclr { - color: white; - background: #262626; + color: white; + background: #262626; } .pointer { - cursor: pointer; + cursor: pointer; } -#tbodycontent>tr>td { - border-bottom: 1px solid #cccccc; +#tbodycontent > tr > td { + border-bottom: 1px solid #cccccc; } -#tbodycontent>tr:last-child>td { - border-bottom: 1px solid #ddd; +#tbodycontent > tr:last-child > td { + border-bottom: 1px solid #ddd; } .gridRowSelected { - background-color: #DEF; + background-color: #def; } .gridRowSelected:hover { - background-color: #DEF!important; - cursor: initial; + background-color: #def !important; + cursor: initial; } .collectionNodeSelected { - background-color: #DEF; + background-color: #def; } .collectionNodeSelected:hover { - background-color: #DEF!important; - cursor: default!important; + background-color: #def !important; + cursor: default !important; } .databaseNodeSelected { - background-color: #DEF; + background-color: #def; } .databaseNodeSelected:hover { - background-color: #DEF!important; - cursor: default!important; + background-color: #def !important; + cursor: default !important; } .leftsidepanle-hr { - margin: 16px 0px; - border-top: 1px solid #eee; - margin-left: -17px; - width: 100%; - color: 1px solid #53575B; + margin: 16px 0px; + border-top: 1px solid #eee; + margin-left: -17px; + width: 100%; + color: 1px solid #53575b; } .partitioning-btn { - padding-bottom: 16px; + padding-bottom: 16px; } .btncreatecoll1 { - border: 1px solid @AccentMediumHigh; - background-color: @AccentMediumHigh; - color: #fff; - padding: 2px 30px; - cursor: pointer; - font-size: 12px; + border: 1px solid @AccentMediumHigh; + background-color: @AccentMediumHigh; + color: #fff; + padding: 2px 30px; + cursor: pointer; + font-size: 12px; } .btncreatecoll1:hover { - background: @AccentMediumHigh; - color: #fff; - border-color: @AccentMediumHigh; - cursor: pointer; - font-size: 12px; + background: @AccentMediumHigh; + color: #fff; + border-color: @AccentMediumHigh; + cursor: pointer; + font-size: 12px; } .btncreatecoll1:active { - border: 1px solid #0072c6; - background-color: #0072c6; - color: white; - padding: 2px 30px; - cursor: pointer; - font-size: 12px; + border: 1px solid #0072c6; + background-color: #0072c6; + color: white; + padding: 2px 30px; + cursor: pointer; + font-size: 12px; } .btncreatecoll1-off { - border: 1px solid #969696; - background-color: #000; - color: white; - padding: 2px 30px; - cursor: pointer; - font-size: 12px; - margin-left: -5px; + border: 1px solid #969696; + background-color: #000; + color: white; + padding: 2px 30px; + cursor: pointer; + font-size: 12px; + margin-left: -5px; } .leftpanel-okbut { - padding: 20px 0px 24px 30px; + padding: 20px 0px 24px 30px; } .btnpricepad { - margin-left: 24px; + margin-left: 24px; } .collid { - background: #fff; - width: calc(~"100% - 80px"); + background: #fff; + width: calc(~"100% - 80px"); } .textfontclr { - color: #000; + color: #000; } .collid-white { - width: 100%; - border: solid 1px #DDD; + width: 100%; + border: solid 1px #ddd; } .plusimg-but { - background-image: url(../images/plus_normal.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/plus_normal.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .plusimg-but:hover { - background-image: url(../images/plus_hover.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/plus_hover.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .plusimg-but:active { - background-image: url(../images/plus_pressed.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/plus_pressed.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .plusimg-but:disabled { - background-image: url(../images/plus_disabled.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/plus_disabled.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .minusimg-but { - background-image: url(../images/minus_normal.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/minus_normal.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .minusimg-but:hover { - background-image: url(../images/minus_hover.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/minus_hover.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .minusimg-but:active { - background-image: url(../images/minus_pressed.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/minus_pressed.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .minusimg-but:disabled { - background-image: url(../images/minus_disabled.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/minus_disabled.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .firstdivbg { - padding: @MediumSpace 0px @DefaultSpace (2 * @LargeSpace); - background-color: @BaseLight; + padding: @MediumSpace 0px @DefaultSpace (2 * @LargeSpace); + background-color: @BaseLight; } p { - margin: 0 0 4px; - color: #000; + margin: 0 0 4px; + color: #000; } .closeImg { - float: right; - margin: 0px 20px 0px 0px; - cursor: pointer; - padding: 6px 20px 20px 6px; - width: 20px; - height: 20px; + float: right; + margin: 0px 20px 0px 0px; + cursor: pointer; + padding: 6px 20px 20px 6px; + width: 20px; + height: 20px; } .seconddivpadding { - padding-top: 16px; + padding-top: 16px; } .seconddivbg { - height: 100vh; - padding-left: 32px; - padding-top: 16px; + height: 100vh; + padding-left: 32px; + padding-top: 16px; } .pkPadding { - padding-top: 12px; + padding-top: 12px; } .mandatoryStar { - color: #ff0707; - font-size: 14px; - font-weight: bold; + color: #ff0707; + font-size: 14px; + font-weight: bold; } .pricingtierimg { - padding-left: 20px; - padding-top: 10px; - padding-bottom: 20px; + padding-left: 20px; + padding-top: 10px; + padding-bottom: 20px; } .headerline { - color: @BaseDark; - font-size: 16px; - border-bottom: 1px solid @BaseMedium; + color: @BaseDark; + font-size: 16px; + border-bottom: 1px solid @BaseMedium; } .partitionkeystyle { - font-size: 10px; + font-size: 10px; } .arrowprice { - margin-left: 230px; + margin-left: 230px; } .paddingspan { - padding: 20px; - color: white; - font-size: 14px; + padding: 20px; + color: white; + font-size: 14px; } input::-webkit-calendar-picker-indicator { - opacity: 100; + opacity: 100; } .paddingspan3 { - color: white; - font-size: 14px; - position: absolute; - width: 100%; - height: 100px; - bottom: 150px; + color: white; + font-size: 14px; + position: absolute; + width: 100%; + height: 100px; + bottom: 150px; } .paddingspan4 { - padding-top: 20px; - padding-left: 20px; - color: white; - font-size: 14px; + padding-top: 20px; + padding-left: 20px; + color: white; + font-size: 14px; } .closebtnn { - float: right; - padding: 0 10px; - cursor: pointer; + float: right; + padding: 0 10px; + cursor: pointer; } label { - white-space: nowrap; - font: 12px "Segoe UI"; + white-space: nowrap; + font: 12px "Segoe UI"; } .Introlines { - padding-top: 27px; - padding-left: 25px; + padding-top: 27px; + padding-left: 25px; } .Introline1 { - font-size: 16px; + font-size: 16px; } .Introline2 { - font-size: 14px; - padding-top: 10px; + font-size: 14px; + padding-top: 10px; } .datalist-arrow { - position: relative; + position: relative; } .datalist-arrow:hover:after { - background: #969696; + background: #969696; } .datalist-arrow:focus:after, .datalist-arrow:active:after { - background: #1EBBEE; + background: #1ebbee; } input::-webkit-calendar-picker-indicator::after { - content: '\276F'; - right: 0; - top: -8%; - display: block; - width: 27px; - height: 25px; - line-height: 25px; - color: #fff; - text-align: center; - pointer-events: none; - transform: rotate(90deg); + content: "\276F"; + right: 0; + top: -8%; + display: block; + width: 27px; + height: 25px; + line-height: 25px; + color: #fff; + text-align: center; + pointer-events: none; + transform: rotate(90deg); } .datalist-arrow:after:hover { - content: '\276F'; - position: absolute; - right: 1px; - top: 6%; - transform: rotate(90deg); - display: block; - width: 27px; - height: 25px; - line-height: 25px; - color: #fff; - text-align: center; - pointer-events: none; - background-color: #1EBBEE; + content: "\276F"; + position: absolute; + right: 1px; + top: 6%; + transform: rotate(90deg); + display: block; + width: 27px; + height: 25px; + line-height: 25px; + color: #fff; + text-align: center; + pointer-events: none; + background-color: #1ebbee; } .Introline3 { - padding-top: 10px; - font-size: 14px; - font-weight: 1000; + padding-top: 10px; + font-size: 14px; + font-weight: 1000; } .collectionCollapsed { - color: black; - font-weight: 400; - font-size: 14px; - position: relative; - display: block; - padding: 8px 15px; - cursor: pointer; - margin-right: 13px; - border: 1px solid #fff; + color: black; + font-weight: 400; + font-size: 14px; + position: relative; + display: block; + padding: 8px 15px; + cursor: pointer; + margin-right: 13px; + border: 1px solid #fff; } .collectionCollapsed:hover { - background: #EEEEEE; + background: #eeeeee; } .collectionCollapsed:active { - border: solid 1px @AccentMediumHigh; + border: solid 1px @AccentMediumHigh; } .collectionCollapsed:focus { - border: Solid 1px @AccentMediumHigh; + border: Solid 1px @AccentMediumHigh; } .arrowCollapsed { - cursor: pointer; - width: 16px; - height: 16px; - transform: rotate(-90deg) translateX(-100%); - -webkit-transform: rotate(-90deg) translateX(-100%); - -ms-transform: rotate(-90deg) translateX(-100%); - margin: -30px 3px 0px 2px; + cursor: pointer; + width: 16px; + height: 16px; + transform: rotate(-90deg) translateX(-100%); + -webkit-transform: rotate(-90deg) translateX(-100%); + -ms-transform: rotate(-90deg) translateX(-100%); + margin: -30px 3px 0px 2px; } .leftarrowCollapsed { - padding: 2px 4px 4px 5px; - border: solid 1px #FFF; - margin: 6px 4px 0px -11px; + padding: 2px 4px 4px 5px; + border: solid 1px #fff; + margin: 6px 4px 0px -11px; } .leftarrowCollapsed:hover { - background-color: #EEEEEE; + background-color: #eeeeee; } .leftarrowCollapsed:active { - border: solid 1px @AccentMediumHigh; + border: solid 1px @AccentMediumHigh; } .leftarrowCollapsed:focus { - border: Solid 1px @AccentMediumHigh; + border: Solid 1px @AccentMediumHigh; } .qslevel { - padding-top: 10px; - padding-left: 10px; - width: 60%; - min-width: 960px; + padding-top: 10px; + padding-left: 10px; + width: 60%; + min-width: 960px; } .nav-tabs-margin { - margin-top: 20px; + margin-top: 20px; } .numbersize { - font-size: 30px; - display: inline; - font-weight: 600; + font-size: 30px; + display: inline; + font-weight: 600; } .numbersizePadding { - padding-right: 5px; + padding-right: 5px; } .numberheading { - display: inline; - position: absolute; - padding-top: 10px; - font-size: 16px; - padding-left: 15px; + display: inline; + position: absolute; + padding-top: 10px; + font-size: 16px; + padding-left: 15px; } -.numberheading>p { - padding-top: 10px; - font-size: 12px; +.numberheading > p { + padding-top: 10px; + font-size: 12px; } -.numberheading>ul { - padding-top: 10px; - padding-left: 0px; - list-style-type: none; +.numberheading > ul { + padding-top: 10px; + padding-left: 0px; + list-style-type: none; } .numberheading ul li { - padding-bottom: 5px; + padding-bottom: 5px; } -.numberheading>ul>li>a { - font-size: 12px; - color: #0058ad; +.numberheading > ul > li > a { + font-size: 12px; + color: #0058ad; } -.netApp { - padding-bottom: 80px; -} - -.pythonApp { - padding-bottom: 45px; +.sampleApp { + padding-bottom: 45px; } .step1 { - padding-bottom: 110px; + padding-bottom: 110px; } -.step1>input { - font-size: 12px; +.step1 > input { + font-size: 12px; } .btncreatecoll { - background: #0058ad; - color: #fff; - padding: 5px 20px; - cursor: pointer; - font-size: 12px; - border: 1px solid #0058ad; + background: #0058ad; + color: #fff; + padding: 5px 20px; + cursor: pointer; + font-size: 12px; + border: 1px solid #0058ad; } .btncreatecoll:hover { - background-color: #0074e0; + background-color: #0074e0; } .atags:focus { - color: @AccentMediumHigh; + color: @AccentMediumHigh; } .atags { - color: @AccentMediumHigh; - font-weight: 400; - cursor: pointer + color: @AccentMediumHigh; + font-weight: 400; + cursor: pointer; } .qsmenuicons { - width: 25px; - height: 25px; - margin-right: 5px; + width: 25px; + height: 25px; + margin-right: 5px; } .HeaderBg { - background-color: #202428; - height: 60px; + background-color: #202428; + height: 60px; } .title { - color: @AccentMediumHigh; - font-size: 20px; - padding-left: 10px; + color: @AccentMediumHigh; + font-size: 20px; + padding-left: 10px; } .items { - padding-left: 24px; - padding-top: 15px; + padding-left: 24px; + padding-top: 15px; } .divmenuquickstartpadding { - padding-left: 24px; - padding-bottom: 8px; + padding-left: 24px; + padding-bottom: 8px; } .menuQuickStart { - font-size: 12px; - color: white; - padding-left: 10px; + font-size: 12px; + color: white; + padding-left: 10px; } .menuExplorer { - font-size: 12px; - color: white; - padding-left: 20px; + font-size: 12px; + color: white; + padding-left: 20px; } .activemenu { - color: #fff; + color: #fff; } .rightarrowimg { - padding-left: 5px; - padding-bottom: 2px; + padding-left: 5px; + padding-bottom: 2px; } a:hover, a:visited, a:active, a:link { - text-decoration: none; + text-decoration: none; } .command { - padding: 8px; + padding: 8px; } .command:hover { - background-color: #E6E6E6; - cursor: pointer; - padding-bottom: 12px; + background-color: #e6e6e6; + cursor: pointer; + padding-bottom: 12px; } .command:active { - background-color: #CCCCCC; - border: solid 1px @AccentMediumHigh; + background-color: #cccccc; + border: solid 1px @AccentMediumHigh; } .command:focus { - padding: 7px 8px 11px 8px; - border: solid 1px @AccentMediumHigh; - outline: none; + padding: 7px 8px 11px 8px; + border: solid 1px @AccentMediumHigh; + outline: none; } -.nav>li>a:focus { - background-color: white; +.nav > li > a:focus { + background-color: white; } .commandIcon { - margin: 0 5px 0 0; - vertical-align: text-top; - width: 18px; - height: 18px; + margin: 0 5px 0 0; + vertical-align: text-top; + width: 18px; + height: 18px; } .iconpadclick { - background-color: #e6e6e6; - cursor: pointer; - border: 1px solid #1ebbee; - padding: 5px; + background-color: #e6e6e6; + cursor: pointer; + border: 1px solid #1ebbee; + padding: 5px; } .divimgleftarrow { - display: inline-block; - margin-top: 16px; - margin-right: 10px; + display: inline-block; + margin-top: 16px; + margin-right: 10px; } .divimgleftarrow:hover { - background-color: #e6e6e6; - cursor: pointer; - border: 1px solid #1ebbee; + background-color: #e6e6e6; + cursor: pointer; + border: 1px solid #1ebbee; } .adddeliconspan { - display: none; - float: right; - padding: 5px; + display: none; + float: right; + padding: 5px; } .spanparent:hover .adddeliconspan { - display: inline; + display: inline; } .spanchild:hover .adddeliconspan { - display: inline; + display: inline; } .collectiontitle { - font-size: 14px; - text-transform: uppercase; + font-size: 14px; + text-transform: uppercase; } .titlepadcol { - padding-left: 20px; - font-weight: 500; - height: 28px; - display: inline-block; - padding-top: 5px; + padding-left: 20px; + font-weight: 500; + height: 28px; + display: inline-block; + padding-top: 5px; } .btnmainslide { - height: 14px; - margin-top: 14px; - cursor: pointer; + height: 14px; + margin-top: 14px; + cursor: pointer; } .well { - padding: 19px 0px; - padding-top: 0px; - margin-bottom: 20px; - border: 0px; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0); - background: white; + padding: 19px 0px; + padding-top: 0px; + margin-bottom: 20px; + border: 0px; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0); + background: white; } .splitter { - z-index: 1; - border-left: 5px solid white; - width: 8px; - border-right: 1px solid #cccccc; - float: left; - height: 100%; - position: absolute; - margin-left: 240px; - padding: 0px; - background-color: white; + z-index: 1; + border-left: 5px solid white; + width: 8px; + border-right: 1px solid #cccccc; + float: left; + height: 100%; + position: absolute; + margin-left: 240px; + padding: 0px; + background-color: white; } .testClass { - padding-left: 30px; + padding-left: 30px; } .level { - padding-left: 16px; - padding-top: 0px; + padding-left: 16px; + padding-top: 0px; } .imgiconwidth { - margin-right: 5px; + margin-right: 5px; } .id { - padding-left: 8px; - color: #000; - font-weight: bold; - margin-left: 6px; + padding-left: 8px; + color: #000; + font-weight: bold; + margin-left: 6px; } .documentsGridHeaderContainer { - padding-left: 5px; - padding-right: 15px; - width: 200px; + padding-left: 5px; + padding-right: 15px; + width: 200px; } .documentsGridHeader { - padding: 8px; - color: #000; - font-weight: bold; + padding: 8px; + color: #000; + font-weight: bold; } .fixedWidthHeader { - width: 82px; + width: 82px; } .tabdocuments { - padding: 4px 4px -1px 0px; + padding: 4px 4px -1px 0px; } -#divcontent>.mongoDocumentEditor .monaco-editor.vs .redsquiggly { - display: none !important; +#divcontent > .mongoDocumentEditor .monaco-editor.vs .redsquiggly { + display: none !important; } td a { - color: #393939; + color: #393939; } td a:hover { - color: #393939; + color: #393939; } .loadMore { - padding-left: 32%; - cursor: pointer; + padding-left: 32%; + cursor: pointer; } .table-fixed thead { - width: 97%; - padding-left: 18px; + width: 97%; + padding-left: 18px; } .table-fixed tbody { - height: 510px; - overflow-y: auto; - width: 100%; - overflow-x: hidden; + height: 510px; + overflow-y: auto; + width: 100%; + overflow-x: hidden; } .table-fixed thead, @@ -929,383 +925,383 @@ td a:hover { .table-fixed tr, .table-fixed td, .table-fixed th { - display: block; + display: block; } .table-fixed tbody td, -.table-fixed thead>tr>th { - float: left; - border-bottom-width: 0; +.table-fixed thead > tr > th { + float: left; + border-bottom-width: 0; } a:hover, a:visited, a:active, a:link { - text-decoration: none; + text-decoration: none; } .tabs { - position: relative; - clear: both; - margin: 15px 0 25px 0; - display: table; - width: 100%; + position: relative; + clear: both; + margin: 15px 0 25px 0; + display: table; + width: 100%; } .tab { - float: left; + float: left; } .tab label { - padding: 10px; - border: 1px solid #bbbbbb; - margin-left: -1px; - position: inherit; - left: 1px; - color: #393939; + padding: 10px; + border: 1px solid #bbbbbb; + margin-left: -1px; + position: inherit; + left: 1px; + color: #393939; } -.tab [type=radio] { - display: none; +.tab [type="radio"] { + display: none; } .tabcontent { - position: absolute; - top: 30px; - left: 0; - right: 0; - bottom: 0; - padding: 15px 0px 20px 0; + position: absolute; + top: 30px; + left: 0; + right: 0; + bottom: 0; + padding: 15px 0px 20px 0; } -.tab [type=radio]:checked~label { - border: 1px solid #0072c6; - background-color: @AccentMediumHigh; - color: white; - z-index: 2; +.tab [type="radio"]:checked ~ label { + border: 1px solid #0072c6; + background-color: @AccentMediumHigh; + color: white; + z-index: 2; } -.tab [type=radio]:checked~label:hover { - border: 1px solid @AccentMediumHigh; - background-color: @AccentMediumHigh; - color: white; - z-index: 2; +.tab [type="radio"]:checked ~ label:hover { + border: 1px solid @AccentMediumHigh; + background-color: @AccentMediumHigh; + color: white; + z-index: 2; } -.tab [type=radio]:checked~label:active { - border: 1px solid #0072c6; - background-color: #0072c6; - color: white; - z-index: 2; +.tab [type="radio"]:checked ~ label:active { + border: 1px solid #0072c6; + background-color: #0072c6; + color: white; + z-index: 2; } -.tab [type=radio]:checked~label~.tabcontent { - z-index: 1; - display: initial; +.tab [type="radio"]:checked ~ label ~ .tabcontent { + z-index: 1; + display: initial; } -.tab [type=radio]:not(:checked)~label:hover { - border: 1px solid #969696; - background-color: #969696; - color: white; - cursor: pointer; +.tab [type="radio"]:not(:checked) ~ label:hover { + border: 1px solid #969696; + background-color: #969696; + color: white; + cursor: pointer; } -.tab [type=radio]:not(:checked)~label~.tabcontent { - display: none; +.tab [type="radio"]:not(:checked) ~ label ~ .tabcontent { + display: none; } ::-webkit-input-placeholder { - color: #969696; + color: #969696; } ::-moz-placeholder { - color: #969696; + color: #969696; } :-ms-input-placeholder { - color: #969696; + color: #969696; } :-moz-placeholder { - color: #969696; + color: #969696; } ::-ms-expand { - color: #969696; + color: #969696; } .form-errors { - color: white; - padding-left: 12px; + color: white; + padding-left: 12px; } .atagdetails { - padding-left: 55px!important; + padding-left: 55px !important; } .path { - color: lightgray; - font-style: italic; - padding-top: 12px; - padding-left: 20px; + color: lightgray; + font-style: italic; + padding-top: 12px; + padding-left: 20px; } .queryPath { - line-height: 16px; - padding-left: 33px; - padding-bottom: 12px; + line-height: 16px; + padding-left: 33px; + padding-bottom: 12px; } .filterDocCollapsed { - padding-left: 20px; + padding-left: 20px; } .filterDocCollapsed.active { - display: block; + display: block; } .filterDocExpanded { - padding-left: 20px; + padding-left: 20px; } .filterDocExpanded.active { - display: block; + display: block; } .filterbuttonpad { - padding-top: 10px; + padding-top: 10px; } .filterbtnstyle { - background: @AccentMediumHigh; - width: 90px; - height: 25px; - color: white; - border: none; - margin-left: 16px !important; + background: @AccentMediumHigh; + width: 90px; + height: 25px; + color: white; + border: none; + margin-left: 16px !important; } .filterbtnstyle:hover { - background: @AccentMediumHigh; - width: 90px; - height: 25px; - color: white; - border: none; - margin-left: 16px; + background: @AccentMediumHigh; + width: 90px; + height: 25px; + color: white; + border: none; + margin-left: 16px; } .filterbtnstyle:active { - background: #0072c6; - width: 90px; - height: 25px; - color: white; - border: none; - margin-left: 16px; + background: #0072c6; + width: 90px; + height: 25px; + color: white; + border: none; + margin-left: 16px; } .filterbtnstyle:focus { - background: #0072c6; - width: 90px; - height: 25px; - color: white; - border: none; - margin-left: 16px; - border: 1px solid #0072c6; + background: #0072c6; + width: 90px; + height: 25px; + color: white; + border: none; + margin-left: 16px; + border: 1px solid #0072c6; } .filterbtnstyle:not(:enabled) { - background: lightgray; - width: 90px; - height: 25px; - color: white; - border: none; + background: lightgray; + width: 90px; + height: 25px; + color: white; + border: none; } .hrline1 { - color: #d6d7d8; - margin-left: -20px; + color: #d6d7d8; + margin-left: -20px; } .filtdocheader { - font-size: 18px; + font-size: 18px; } .editFilter { - margin-left: 20px; + margin-left: 20px; } .filterdivs { - padding-top: 24px; - height: 45px; - margin-bottom: 20px; + padding-top: 24px; + height: 45px; + margin-bottom: 20px; } .filterclose { - padding: 0 10px; - cursor: pointer; + padding: 0 10px; + cursor: pointer; } .queryResultpreviousImg { - height: 14px; - width: 14px; - margin-right: 2px; + height: 14px; + width: 14px; + margin-right: 2px; } .queryResultnextImg { - height: 14px; - width: 14px; - margin-left: 2px; + height: 14px; + width: 14px; + margin-left: 2px; } .rowoverride { - margin-left: 7px; - margin-top: 20px; + margin-left: 7px; + margin-top: 20px; } .tab-content-override { - padding-left: 5px; - padding-top: 20px; + padding-left: 5px; + padding-top: 20px; } .paddingspan4 { - padding-top: 20px; - color: white; - padding-left: 25px; - padding-right: 25px; + padding-top: 20px; + color: white; + padding-left: 25px; + padding-right: 25px; } .colResizePointer { - cursor: col-resize; + cursor: col-resize; } -.nav-tabs>li>a { - border-radius: 2px 2px 0 0; - padding: 8px 0px 4px 0px; - color: #393939; - width: 130px; - text-align: center; - margin-right: 0px; - position: relative; +.nav-tabs > li > a { + border-radius: 2px 2px 0 0; + padding: 8px 0px 4px 0px; + color: #393939; + width: 130px; + text-align: center; + margin-right: 0px; + position: relative; } -.nav-tabs>li.active>a, -.nav-tabs>li.active>a:focus, -.nav-tabs>li.active>a:hover { - border-bottom-color: #FFF; +.nav-tabs > li.active > a, +.nav-tabs > li.active > a:focus, +.nav-tabs > li.active > a:hover { + border-bottom-color: #fff; } .tabList { - float: left; - margin-bottom: -15px !important; + float: left; + margin-bottom: -15px !important; } .tab_Content { - width: 130px; - border-right: 1px solid #e0e0e0; - padding: 0px 22px 0px 17px; - margin-left: -1px; + width: 130px; + border-right: 1px solid #e0e0e0; + padding: 0px 22px 0px 17px; + margin-left: -1px; } .tab_Content:hover { - width: 130px; - border-right: 1px solid #e0e0e0; - padding: 0px 22px 0px 17px; - margin-left: -1px; + width: 130px; + border-right: 1px solid #e0e0e0; + padding: 0px 22px 0px 17px; + margin-left: -1px; } .tab_Content:active { - width: 130px; - border-right: 1px; - padding: 0px 22px 0px 17px; - margin-left: -1px; + width: 130px; + border-right: 1px; + padding: 0px 22px 0px 17px; + margin-left: -1px; } .tabtext-center { - max-width: 110px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - padding-left: 2px; + max-width: 110px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding-left: 2px; } .tabIconSection { - width: 30px; - float: right; - top: -16px; - position: relative; - padding: 2px 12px 0 13px; + width: 30px; + float: right; + top: -16px; + position: relative; + padding: 2px 12px 0 13px; } -.nav-tabs>li>a:active { - background-color: #e0e0e0; - border-color: @AccentMediumHigh; +.nav-tabs > li > a:active { + background-color: #e0e0e0; + border-color: @AccentMediumHigh; } -.nav-tabs>li>a:active .tab_Content { - border: transparent; - width: 130px; +.nav-tabs > li > a:active .tab_Content { + border: transparent; + width: 130px; } .close-Icon { - background-image: url(../images/close-black.svg); - background-repeat: no-repeat; - padding: 0px 0px 0px 11px; + background-image: url(../images/close-black.svg); + background-repeat: no-repeat; + padding: 0px 0px 0px 11px; } .close-Icon:hover { - background-image: url(../images/close-black-hover.svg); - background-repeat: no-repeat; - padding: 0px 0px 0px 11px; + background-image: url(../images/close-black-hover.svg); + background-repeat: no-repeat; + padding: 0px 0px 0px 11px; } .clickableLink { - color: @AccentMediumHigh; - font-family: 'Segoe UI'; - font-size: 12px; - cursor: pointer; + color: @AccentMediumHigh; + font-family: "Segoe UI"; + font-size: 12px; + cursor: pointer; } .clickableLink:hover { - background-color: #e7f6fc; + background-color: #e7f6fc; } .clickableLink:active { - background-color: #e6f8fe; + background-color: #e6f8fe; } .clickableLink:focus { - outline: 1px dashed #000000; - outline-offset: 0px; + outline: 1px dashed #000000; + outline-offset: 0px; } .paneselect { - height: 23px; + height: 23px; } .headerWithoutPartitionKey { - width: 172px; + width: 172px; } .headerWithPartitionKey { - width: 86px; + width: 86px; } -input.codeblock{ - background-color: @BaseMediumLow; - color: #252525; - border: 1px solid @BaseMediumHigh; - box-sizing: border-box; - font-size: @mediumFontSize; - height: 23px; - outline: 0; - padding: 2px 8px 4px; - width: 60%; - min-width: 960px; - cursor: text; +input.codeblock { + background-color: @BaseMediumLow; + color: #252525; + border: 1px solid @BaseMediumHigh; + box-sizing: border-box; + font-size: @mediumFontSize; + height: 23px; + outline: 0; + padding: 2px 8px 4px; + width: 60%; + min-width: 960px; + cursor: text; } -#divQuickStartConnections{ - padding-bottom: 10px; -} \ No newline at end of file +#divQuickStartConnections { + padding-bottom: 10px; +} diff --git a/package-lock.json b/package-lock.json index 2b1c80df0..80558f6d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "dependencies": { "@azure/arm-cosmosdb": "9.1.0", - "@azure/cosmos": "4.2.0-beta.1", + "@azure/cosmos": "4.5.0", "@azure/cosmos-language-service": "0.0.5", "@azure/identity": "4.5.0", "@azure/msal-browser": "2.14.2", @@ -51,6 +51,8 @@ "@types/mkdirp": "1.0.1", "@types/node-fetch": "2.5.7", "@xmldom/xmldom": "0.7.13", + "@xterm/addon-fit": "0.10.0", + "@xterm/xterm": "5.5.0", "allotment": "1.20.2", "applicationinsights": "1.8.0", "bootstrap": "3.4.1", @@ -288,57 +290,69 @@ "version": "2.6.2", "license": "0BSD" }, - "node_modules/@azure/core-rest-pipeline": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.18.0.tgz", - "integrity": "sha512-QSoGUp4Eq/gohEFNJaUOwTN7BCc2nHTjjbm75JT0aD7W65PWM1H/tItz0GsABn22uaKyGxiMhWQLt2r+FGU89Q==", + "node_modules/@azure/core-http-compat": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.0.tgz", + "integrity": "sha512-qLQujmUypBBG0gxHd0j6/Jdmul6ttl24c8WGiLXIk7IHXdBlfoBqW27hyz3Xn6xbfdyVSarl1Ttbk0AwnZBYCw==", "dependencies": { "@azure/abort-controller": "^2.0.0", - "@azure/core-auth": "^1.8.0", - "@azure/core-tracing": "^1.0.1", - "@azure/core-util": "^1.11.0", + "@azure/core-client": "^1.3.0", + "@azure/core-rest-pipeline": "^1.20.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", "@azure/logger": "^1.0.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@azure/core-rest-pipeline/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "node_modules/@azure/core-lro/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", "dependencies": { - "debug": "^4.3.4" + "tslib": "^2.6.2" }, "engines": { - "node": ">= 14" + "node": ">=18.0.0" } }, - "node_modules/@azure/core-rest-pipeline/node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } + "node_modules/@azure/core-paging/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, - "node_modules/@azure/core-rest-pipeline/node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "node_modules/@azure/core-rest-pipeline": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.20.0.tgz", + "integrity": "sha512-ASoP8uqZBS3H/8N8at/XwFr6vYrRP3syTK0EUjDXQy0Y1/AUS+QeIRThKmTNJO2RggvBBxaXDPM7YoIwDGeA0g==", "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.8.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@typespec/ts-http-runtime": "^0.2.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">= 14" + "node": ">=18.0.0" } }, "node_modules/@azure/core-rest-pipeline/node_modules/tslib": { @@ -377,23 +391,25 @@ "license": "0BSD" }, "node_modules/@azure/cosmos": { - "version": "4.2.0-beta.1", - "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.2.0-beta.1.tgz", - "integrity": "sha512-mREONehm1DxjEKXGaNU6Wmpf9Ckb9IrhKFXhDFVs45pxmoEb3y2s/Ub0owuFmqlphpcS1zgtYQn5exn+lwnJuQ==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.5.0.tgz", + "integrity": "sha512-JsTh4twb6FcwP7rJwxQiNZQ/LGtuF6gmciaxY9Rnp6/A325Lhsw/SH4R2ArpT0yCvozbZpweIwdPfUkXVBtp5w==", + "license": "MIT", "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-auth": "^1.7.1", - "@azure/core-rest-pipeline": "^1.15.1", - "@azure/core-tracing": "^1.1.1", - "@azure/core-util": "^1.8.1", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-rest-pipeline": "^1.19.1", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-keys": "^4.9.0", + "@azure/logger": "^1.1.4", "fast-json-stable-stringify": "^2.1.0", - "jsbi": "^4.3.0", "priorityqueuejs": "^2.0.0", "semaphore": "^1.1.0", - "tslib": "^2.6.2" + "tslib": "^2.8.1" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@azure/cosmos-language-service": { @@ -423,8 +439,9 @@ } }, "node_modules/@azure/cosmos/node_modules/tslib": { - "version": "2.6.2", - "license": "0BSD" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/@azure/identity": { "version": "4.5.0", @@ -490,14 +507,66 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, - "node_modules/@azure/logger": { - "version": "1.0.4", - "license": "MIT", + "node_modules/@azure/keyvault-common": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.0.0.tgz", + "integrity": "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==", "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.5.0", + "@azure/core-rest-pipeline": "^1.8.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.10.0", + "@azure/logger": "^1.1.4", "tslib": "^2.2.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@azure/keyvault-common/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@azure/keyvault-keys": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.9.0.tgz", + "integrity": "sha512-ZBP07+K4Pj3kS4TF4XdkqFcspWwBHry3vJSOFM5k5ZABvf7JfiMonvaFk2nBF6xjlEbMpz5PE1g45iTMme0raQ==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.5.0", + "@azure/core-http-compat": "^2.0.1", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.1.1", + "@azure/core-rest-pipeline": "^1.8.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.0.0", + "@azure/keyvault-common": "^2.0.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/keyvault-keys/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@azure/logger": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.2.0.tgz", + "integrity": "sha512-0hKEzLhpw+ZTAfNJyRrn6s+V0nDWzXk9OjBr2TiGIu0OfMr5s2V4FpKLTAK3Ca5r5OKLbf4hkOGDPyiRjie/jA==", + "dependencies": { + "@typespec/ts-http-runtime": "^0.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@azure/logger/node_modules/tslib": { @@ -13072,6 +13141,56 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.2.2.tgz", + "integrity": "sha512-Gz/Sm64+Sq/vklJu1tt9t+4R2lvnud8NbTD/ZfpZtMiUX7YeVpCA8j6NSW8ptwcoLL+NmYANwqP8DV0q/bwl2w==", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@typespec/ts-http-runtime/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@typespec/ts-http-runtime/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@typespec/ts-http-runtime/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@typespec/ts-http-runtime/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "node_modules/@ungap/url-search-params": { "version": "0.2.2", "license": "ISC" @@ -13240,6 +13359,19 @@ "node": ">=10.0.0" } }, + "node_modules/@xterm/addon-fit": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", + "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + } + }, + "node_modules/@xterm/xterm": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==" + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "license": "BSD-3-Clause" @@ -27048,11 +27180,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbi": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.0.tgz", - "integrity": "sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g==" - }, "node_modules/jsbn": { "version": "0.1.1", "license": "MIT" diff --git a/package.json b/package.json index f4b10a66d..570542692 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "dependencies": { "@azure/arm-cosmosdb": "9.1.0", - "@azure/cosmos": "4.2.0-beta.1", + "@azure/cosmos": "4.5.0", "@azure/cosmos-language-service": "0.0.5", "@azure/identity": "4.5.0", "@azure/msal-browser": "2.14.2", @@ -46,6 +46,8 @@ "@types/mkdirp": "1.0.1", "@types/node-fetch": "2.5.7", "@xmldom/xmldom": "0.7.13", + "@xterm/xterm": "5.5.0", + "@xterm/addon-fit": "0.10.0", "allotment": "1.20.2", "applicationinsights": "1.8.0", "bootstrap": "3.4.1", diff --git a/playwright.config.ts b/playwright.config.ts index 4c5ad3c14..b1f6a622d 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,5 +1,4 @@ import { defineConfig, devices } from "@playwright/test"; - /** * See https://playwright.dev/docs/test-configuration. */ @@ -29,24 +28,60 @@ export default defineConfig({ projects: [ { name: "chromium", - use: { ...devices["Desktop Chrome"] }, + use: { + ...devices["Desktop Chrome"], + launchOptions: { + args: ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"], + }, + }, }, { name: "firefox", - use: { ...devices["Desktop Firefox"] }, + use: { + ...devices["Desktop Firefox"], + launchOptions: { + firefoxUserPrefs: { + "security.fileuri.strict_origin_policy": false, + "network.http.referer.XOriginPolicy": 0, + "network.http.referer.trimmingPolicy": 0, + "privacy.file_unique_origin": false, + "security.csp.enable": false, + "network.cors_preflight.allow_client_cert": true, + "dom.security.https_first": false, + "network.http.cross-origin-embedder-policy": false, + "network.http.cross-origin-opener-policy": false, + "browser.tabs.remote.useCrossOriginPolicy": false, + "browser.tabs.remote.useCORP": false, + }, + args: ["--disable-web-security"], + }, + }, }, { name: "webkit", - use: { ...devices["Desktop Safari"] }, + use: { + ...devices["Desktop Safari"], + }, }, - /* Test against branded browsers. */ { name: "Google Chrome", - use: { ...devices["Desktop Chrome"], channel: "chrome" }, // or 'chrome-beta' + use: { + ...devices["Desktop Chrome"], + channel: "chrome", + launchOptions: { + args: ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"], + }, + }, }, { name: "Microsoft Edge", - use: { ...devices["Desktop Edge"], channel: "msedge" }, // or 'msedge-dev' + use: { + ...devices["Desktop Edge"], + channel: "msedge", + launchOptions: { + args: ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"], + }, + }, }, ], diff --git a/preview/package-lock.json b/preview/package-lock.json index daf213a84..ab6c6ecbb 100644 --- a/preview/package-lock.json +++ b/preview/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "dependencies": { "@azure/arm-cosmosdb": "9.1.0", - "@azure/cosmos": "4.2.0-beta.1", + "@azure/cosmos": "4.3.0", "@azure/cosmos-language-service": "0.0.5", "@azure/identity": "4.5.0", "@azure/msal-browser": "2.14.2", @@ -377,8 +377,8 @@ "license": "0BSD" }, "node_modules/@azure/cosmos": { - "version": "4.2.0-beta.1", - "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.2.0-beta.1.tgz", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.3.0.tgz", "integrity": "sha512-mREONehm1DxjEKXGaNU6Wmpf9Ckb9IrhKFXhDFVs45pxmoEb3y2s/Ub0owuFmqlphpcS1zgtYQn5exn+lwnJuQ==", "dependencies": { "@azure/abort-controller": "^2.0.0", diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 94ca16c27..44d2b8b90 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -138,15 +138,6 @@ export enum MongoBackendEndpointType { remote, } -export class BackendApi { - public static readonly GenerateToken: string = "GenerateToken"; - public static readonly PortalSettings: string = "PortalSettings"; - public static readonly AccountRestrictions: string = "AccountRestrictions"; - public static readonly RuntimeProxy: string = "RuntimeProxy"; - public static readonly DisallowedLocations: string = "DisallowedLocations"; - public static readonly SampleData: string = "SampleData"; -} - export class PortalBackendEndpoints { public static readonly Development: string = "https://localhost:7235"; public static readonly Mpac: string = "https://cdb-ms-mpac-pbe.cosmos.azure.com"; @@ -257,6 +248,7 @@ export class Areas { public static ShareDialog: string = "Share Access Dialog"; public static Notebook: string = "Notebook"; public static Copilot: string = "Copilot"; + public static CloudShell: string = "Cloud Shell"; } export class HttpHeaders { @@ -530,6 +522,9 @@ export class ariaLabelForLearnMoreLink { public static readonly AzureSynapseLink = "Learn more about Azure Synapse Link."; } +export class GlobalSecondaryIndexLabels { + public static readonly NewGlobalSecondaryIndex: string = "New Global Secondary Index"; +} export class FeedbackLabels { public static readonly provideFeedback: string = "Provide feedback"; } @@ -770,3 +765,10 @@ export const ShortenedQueryCopilotSampleContainerSchema = { userPrompt: "find all products", }; + +export enum MongoGuidRepresentation { + Standard = "Standard", + CSharpLegacy = "CSharpLegacy", + JavaLegacy = "JavaLegacy", + PythonLegacy = "PythonLegacy", +} diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index cf34b2279..54444d09e 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -4,12 +4,12 @@ import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract"; import { AuthorizationToken } from "Contracts/FabricMessageTypes"; import { checkDatabaseResourceTokensValidity, isFabricMirroredKey } from "Platform/Fabric/FabricUtil"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; +import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils"; import { AuthType } from "../AuthType"; import { PriorityLevel } from "../Common/Constants"; import * as Logger from "../Common/Logger"; import { Platform, configContext } from "../ConfigContext"; import { FabricArtifactInfo, updateUserContext, userContext } from "../UserContext"; -import { isDataplaneRbacSupported } from "../Utils/APITypeUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils"; import { EmulatorMasterKey, HttpHeaders } from "./Constants"; @@ -20,8 +20,7 @@ const _global = typeof self === "undefined" ? window : self; export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { const { verb, resourceId, resourceType, headers } = requestInfo; - const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType); - if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) { + if (useDataplaneRbacAuthorization(userContext)) { Logger.logInfo( `AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `, "Explorer/tokenProvider", @@ -125,7 +124,11 @@ export const endpoint = () => { const location = _global.parent ? _global.parent.location : _global.location; return configContext.EMULATOR_ENDPOINT || location.origin; } - return userContext.endpoint || userContext?.databaseAccount?.properties?.documentEndpoint; + return ( + userContext.selectedRegionalEndpoint || + userContext.endpoint || + userContext?.databaseAccount?.properties?.documentEndpoint + ); }; export async function getTokenFromAuthService( @@ -203,6 +206,7 @@ export function client(): Cosmos.CosmosClient { userAgentSuffix: "Azure Portal", defaultHeaders: _defaultHeaders, connectionPolicy: { + enableEndpointDiscovery: !userContext.selectedRegionalEndpoint, retryOptions: { maxRetryAttemptCount: LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts), fixedRetryIntervalInMilliseconds: LocalStorageUtility.getEntryNumber(StorageKey.RetryInterval), diff --git a/src/Common/DatabaseAccountUtility.ts b/src/Common/DatabaseAccountUtility.ts index c72d3baf6..50ec0064a 100644 --- a/src/Common/DatabaseAccountUtility.ts +++ b/src/Common/DatabaseAccountUtility.ts @@ -1,5 +1,6 @@ import { TagNames, WorkloadType } from "Common/Constants"; import { Tags } from "Contracts/DataModels"; +import { isFabric } from "Platform/Fabric/FabricUtil"; import { userContext } from "../UserContext"; function isVirtualNetworkFilterEnabled() { @@ -26,3 +27,9 @@ export function getWorkloadType(): WorkloadType { } return workloadType; } + +export function isGlobalSecondaryIndexEnabled(): boolean { + return ( + !isFabric() && userContext.apiType === "SQL" && userContext.databaseAccount?.properties?.enableMaterializedViews + ); +} diff --git a/src/Common/IteratorUtilities.ts b/src/Common/IteratorUtilities.ts index 6283488b8..827033fd7 100644 --- a/src/Common/IteratorUtilities.ts +++ b/src/Common/IteratorUtilities.ts @@ -1,5 +1,7 @@ -import { QueryOperationOptions } from "@azure/cosmos"; +import { Action } from "Shared/Telemetry/TelemetryConstants"; +import * as Constants from "../Common/Constants"; import { QueryResults } from "../Contracts/ViewModels"; +import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; interface QueryResponse { // [Todo] remove any @@ -11,17 +13,15 @@ interface QueryResponse { } export interface MinimalQueryIterator { - fetchNext: (queryOperationOptions?: QueryOperationOptions) => Promise; + fetchNext: () => Promise; } // Pick, "fetchNext">; -export function nextPage( - documentsIterator: MinimalQueryIterator, - firstItemIndex: number, - queryOperationOptions?: QueryOperationOptions, -): Promise { - return documentsIterator.fetchNext(queryOperationOptions).then((response) => { +export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise { + TelemetryProcessor.traceStart(Action.ExecuteQuery); + return documentsIterator.fetchNext().then((response) => { + TelemetryProcessor.traceSuccess(Action.ExecuteQuery, { dataExplorerArea: Constants.Areas.Tab }); const documents = response.resources; // eslint-disable-next-line @typescript-eslint/no-explicit-any const headers = (response as any).headers || {}; // TODO this is a private key. Remove any diff --git a/src/Common/MongoProxyClient.test.ts b/src/Common/MongoProxyClient.test.ts index 0e41a6d35..ddd13b1eb 100644 --- a/src/Common/MongoProxyClient.test.ts +++ b/src/Common/MongoProxyClient.test.ts @@ -65,7 +65,6 @@ describe("MongoProxyClient", () => { }); updateConfigContext({ MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, - globallyEnabledMongoAPIs: [], }); window.fetch = jest.fn().mockImplementation(fetchMock); }); @@ -84,7 +83,6 @@ describe("MongoProxyClient", () => { it("builds the correct proxy URL in development", () => { updateConfigContext({ MONGO_PROXY_ENDPOINT: "https://localhost:1234", - globallyEnabledMongoAPIs: [], }); queryDocuments(databaseId, collection, true, "{}"); expect(window.fetch).toHaveBeenCalledWith( @@ -101,7 +99,6 @@ describe("MongoProxyClient", () => { }); updateConfigContext({ MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, - globallyEnabledMongoAPIs: [], }); window.fetch = jest.fn().mockImplementation(fetchMock); }); @@ -120,7 +117,6 @@ describe("MongoProxyClient", () => { it("builds the correct proxy URL in development", () => { updateConfigContext({ MONGO_PROXY_ENDPOINT: "https://localhost:1234", - globallyEnabledMongoAPIs: [], }); readDocument(databaseId, collection, documentId); expect(window.fetch).toHaveBeenCalledWith( @@ -137,7 +133,6 @@ describe("MongoProxyClient", () => { }); updateConfigContext({ MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, - globallyEnabledMongoAPIs: [], }); window.fetch = jest.fn().mockImplementation(fetchMock); }); @@ -156,7 +151,6 @@ describe("MongoProxyClient", () => { it("builds the correct proxy URL in development", () => { updateConfigContext({ MONGO_PROXY_ENDPOINT: "https://localhost:1234", - globallyEnabledMongoAPIs: [], }); readDocument(databaseId, collection, documentId); expect(window.fetch).toHaveBeenCalledWith( @@ -173,7 +167,6 @@ describe("MongoProxyClient", () => { }); updateConfigContext({ MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, - globallyEnabledMongoAPIs: [], }); window.fetch = jest.fn().mockImplementation(fetchMock); }); @@ -197,7 +190,6 @@ describe("MongoProxyClient", () => { }); updateConfigContext({ MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, - globallyEnabledMongoAPIs: [], }); window.fetch = jest.fn().mockImplementation(fetchMock); }); @@ -216,7 +208,6 @@ describe("MongoProxyClient", () => { it("builds the correct proxy URL in development", () => { updateConfigContext({ MONGO_PROXY_ENDPOINT: "https://localhost:1234", - globallyEnabledMongoAPIs: [], }); deleteDocuments(databaseId, collection, [documentId]); expect(window.fetch).toHaveBeenCalledWith( @@ -233,7 +224,6 @@ describe("MongoProxyClient", () => { }); updateConfigContext({ MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, - globallyEnabledMongoAPIs: [], }); }); diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index 89156e50c..a07572bd3 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -1,4 +1,5 @@ import { Constants as CosmosSDKConstants } from "@azure/cosmos"; +import { getMongoGuidRepresentation } from "Shared/StorageUtility"; import { AuthType } from "../AuthType"; import { configContext } from "../ConfigContext"; import * as DataModels from "../Contracts/DataModels"; @@ -139,6 +140,9 @@ export function readDocument( documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperties?.[0] : "", + clientSettings: { + guidRepresentation: getMongoGuidRepresentation(), + }, }; const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT); @@ -181,6 +185,9 @@ export function createDocument( partitionKey: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "", documentContent: JSON.stringify(documentContent), + clientSettings: { + guidRepresentation: getMongoGuidRepresentation(), + }, }; const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT); @@ -228,6 +235,9 @@ export function updateDocument( ? documentId.partitionKeyProperties?.[0] : "", documentContent, + clientSettings: { + guidRepresentation: getMongoGuidRepresentation(), + }, }; const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT); @@ -274,6 +284,9 @@ export function deleteDocuments( subscriptionID: userContext.subscriptionId, resourceGroup: userContext.resourceGroup, databaseAccountName: databaseAccount.name, + clientSettings: { + guidRepresentation: getMongoGuidRepresentation(), + }, }; const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT); diff --git a/src/Common/QueryError.ts b/src/Common/QueryError.ts index c1bab1c02..de84deee7 100644 --- a/src/Common/QueryError.ts +++ b/src/Common/QueryError.ts @@ -1,5 +1,4 @@ import { monaco } from "Explorer/LazyMonaco"; -import { getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility"; export enum QueryErrorSeverity { Error = "Error", @@ -103,20 +102,9 @@ export interface ErrorEnrichment { learnMoreUrl?: string; } -const REPLACEMENT_MESSAGES: Record string> = { - OPERATION_RU_LIMIT_EXCEEDED: (original) => { - if (ruThresholdEnabled()) { - const threshold = getRUThreshold(); - return `Query exceeded the Request Unit (RU) limit of ${threshold} RUs. You can change this limit in Data Explorer settings.`; - } - return original; - }, -}; +const REPLACEMENT_MESSAGES: Record string> = {}; -const HELP_LINKS: Record = { - OPERATION_RU_LIMIT_EXCEEDED: - "https://learn.microsoft.com/en-us/azure/cosmos-db/data-explorer#configure-request-unit-threshold", -}; +const HELP_LINKS: Record = {}; export default class QueryError { message: string; diff --git a/src/Common/Tooltip/InfoTooltip.tsx b/src/Common/Tooltip/InfoTooltip.tsx index abd12385e..93a3bf6ab 100644 --- a/src/Common/Tooltip/InfoTooltip.tsx +++ b/src/Common/Tooltip/InfoTooltip.tsx @@ -4,13 +4,18 @@ import * as React from "react"; export interface TooltipProps { children: string; className?: string; + ariaLabelForTooltip?: string; } -export const InfoTooltip: React.FunctionComponent = ({ children, className }: TooltipProps) => { +export const InfoTooltip: React.FunctionComponent = ({ + children, + className, + ariaLabelForTooltip = children, +}: TooltipProps) => { return ( - + ); diff --git a/src/Common/dataAccess/__snapshots__/queryDocuments.test.ts.snap b/src/Common/dataAccess/__snapshots__/queryDocuments.test.ts.snap index bcf0034a4..28f3b8064 100644 --- a/src/Common/dataAccess/__snapshots__/queryDocuments.test.ts.snap +++ b/src/Common/dataAccess/__snapshots__/queryDocuments.test.ts.snap @@ -2,7 +2,7 @@ exports[`getCommonQueryOptions builds the correct default options objects 1`] = ` { - "disableNonStreamingOrderByQuery": true, + "enableQueryControl": false, "enableScanInQuery": true, "forceQueryPlan": true, "maxDegreeOfParallelism": 0, @@ -13,7 +13,7 @@ exports[`getCommonQueryOptions builds the correct default options objects 1`] = exports[`getCommonQueryOptions reads from localStorage 1`] = ` { - "disableNonStreamingOrderByQuery": true, + "enableQueryControl": false, "enableScanInQuery": true, "forceQueryPlan": true, "maxDegreeOfParallelism": 17, diff --git a/src/Common/dataAccess/createMaterializedView.ts b/src/Common/dataAccess/createMaterializedView.ts new file mode 100644 index 000000000..659da9c14 --- /dev/null +++ b/src/Common/dataAccess/createMaterializedView.ts @@ -0,0 +1,74 @@ +import { constructRpOptions } from "Common/dataAccess/createCollection"; +import { handleError } from "Common/ErrorHandlingUtils"; +import { Collection, CreateMaterializedViewsParams as CreateGlobalSecondaryIndexParams } from "Contracts/DataModels"; +import { userContext } from "UserContext"; +import { createUpdateSqlContainer } from "Utils/arm/generatedClients/cosmos/sqlResources"; +import { + CreateUpdateOptions, + SqlContainerResource, + SqlDatabaseCreateUpdateParameters, +} from "Utils/arm/generatedClients/cosmos/types"; +import { logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils"; + +export const createGlobalSecondaryIndex = async (params: CreateGlobalSecondaryIndexParams): Promise => { + const clearMessage = logConsoleProgress( + `Creating a new global secondary index ${params.materializedViewId} for database ${params.databaseId}`, + ); + + const options: CreateUpdateOptions = constructRpOptions(params); + + const resource: SqlContainerResource = { + id: params.materializedViewId, + }; + if (params.materializedViewDefinition) { + resource.materializedViewDefinition = params.materializedViewDefinition; + } + if (params.analyticalStorageTtl) { + resource.analyticalStorageTtl = params.analyticalStorageTtl; + } + if (params.indexingPolicy) { + resource.indexingPolicy = params.indexingPolicy; + } + if (params.partitionKey) { + resource.partitionKey = params.partitionKey; + } + if (params.uniqueKeyPolicy) { + resource.uniqueKeyPolicy = params.uniqueKeyPolicy; + } + if (params.vectorEmbeddingPolicy) { + resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy; + } + if (params.fullTextPolicy) { + resource.fullTextPolicy = params.fullTextPolicy; + } + + const rpPayload: SqlDatabaseCreateUpdateParameters = { + properties: { + resource, + options, + }, + }; + + try { + const createResponse = await createUpdateSqlContainer( + userContext.subscriptionId, + userContext.resourceGroup, + userContext.databaseAccount.name, + params.databaseId, + params.materializedViewId, + rpPayload, + ); + logConsoleInfo(`Successfully created global secondary index ${params.materializedViewId}`); + + return createResponse && (createResponse.properties.resource as Collection); + } catch (error) { + handleError( + error, + "CreateGlobalSecondaryIndex", + `Error while creating global secondary index ${params.materializedViewId}`, + ); + throw error; + } finally { + clearMessage(); + } +}; diff --git a/src/Common/dataAccess/deleteDocument.ts b/src/Common/dataAccess/deleteDocument.ts index 1f551ee0e..9d4b8a396 100644 --- a/src/Common/dataAccess/deleteDocument.ts +++ b/src/Common/dataAccess/deleteDocument.ts @@ -42,6 +42,7 @@ export interface IBulkDeleteResult { export const deleteDocuments = async ( collection: CollectionBase, documentIds: DocumentId[], + abortSignal: AbortSignal, ): Promise => { const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`); try { @@ -65,12 +66,16 @@ export const deleteDocuments = async ( operationType: BulkOperationType.Delete, })); - const promise = v2Container.items.bulk(operations).then((bulkResults) => { - return bulkResults.map((bulkResult, index) => { - const documentId = documentIdsChunk[index]; - return { ...bulkResult, documentId }; + const promise = v2Container.items + .bulk(operations, undefined, { + abortSignal, + }) + .then((bulkResults) => { + return bulkResults.map((bulkResult, index) => { + const documentId = documentIdsChunk[index]; + return { ...bulkResult, documentId }; + }); }); - }); promiseArray.push(promise); } diff --git a/src/Common/dataAccess/getCollectionDataUsageSize.ts b/src/Common/dataAccess/getCollectionDataUsageSize.ts index bb53bc6ac..7f8e009c2 100644 --- a/src/Common/dataAccess/getCollectionDataUsageSize.ts +++ b/src/Common/dataAccess/getCollectionDataUsageSize.ts @@ -1,3 +1,4 @@ +import { isFabricNative } from "Platform/Fabric/FabricUtil"; import { AuthType } from "../../AuthType"; import { configContext } from "../../ConfigContext"; import { userContext } from "../../UserContext"; @@ -41,7 +42,7 @@ interface MetricsResponse { } export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise => { - if (userContext.authType !== AuthType.AAD) { + if (userContext.authType !== AuthType.AAD || isFabricNative()) { return undefined; } diff --git a/src/Common/dataAccess/queryDocuments.ts b/src/Common/dataAccess/queryDocuments.ts index 223fe987d..cacbdd867 100644 --- a/src/Common/dataAccess/queryDocuments.ts +++ b/src/Common/dataAccess/queryDocuments.ts @@ -1,5 +1,4 @@ import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; -import { isVectorSearchEnabled } from "Utils/CapabilityUtils"; import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { Queries } from "../Constants"; import { client } from "../CosmosClient"; @@ -26,7 +25,7 @@ export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => { options.maxItemCount || (storedItemPerPageSetting !== undefined && storedItemPerPageSetting) || Queries.itemsPerPage; + options.enableQueryControl = LocalStorageUtility.getEntryBoolean(StorageKey.QueryControlEnabled); options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism); - options.disableNonStreamingOrderByQuery = !isVectorSearchEnabled(); return options; }; diff --git a/src/Common/dataAccess/queryDocumentsPage.ts b/src/Common/dataAccess/queryDocumentsPage.ts index 17e84ba28..556ed290c 100644 --- a/src/Common/dataAccess/queryDocumentsPage.ts +++ b/src/Common/dataAccess/queryDocumentsPage.ts @@ -1,4 +1,3 @@ -import { QueryOperationOptions } from "@azure/cosmos"; import { QueryResults } from "../../Contracts/ViewModels"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { getEntityName } from "../DocumentUtility"; @@ -9,13 +8,12 @@ export const queryDocumentsPage = async ( resourceName: string, documentsIterator: MinimalQueryIterator, firstItemIndex: number, - queryOperationOptions?: QueryOperationOptions, ): Promise => { const entityName = getEntityName(); const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`); try { - const result: QueryResults = await nextPage(documentsIterator, firstItemIndex, queryOperationOptions); + const result: QueryResults = await nextPage(documentsIterator, firstItemIndex); const itemCount = (result.documents && result.documents.length) || 0; logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`); return result; diff --git a/src/Common/dataAccess/readCollectionOffer.ts b/src/Common/dataAccess/readCollectionOffer.ts index 6fb6e9e4b..d3c8e25cd 100644 --- a/src/Common/dataAccess/readCollectionOffer.ts +++ b/src/Common/dataAccess/readCollectionOffer.ts @@ -1,3 +1,4 @@ +import { isFabric } from "Platform/Fabric/FabricUtil"; import { AuthType } from "../../AuthType"; import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels"; import { userContext } from "../../UserContext"; @@ -13,6 +14,11 @@ import { readOfferWithSDK } from "./readOfferWithSDK"; export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise => { const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`); + if (isFabric()) { + // Not exposing offers in Fabric + return undefined; + } + try { if ( userContext.authType === AuthType.AAD && diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index f09e5feb6..da35f7839 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -1,21 +1,15 @@ +import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants"; import { - BackendApi, - CassandraProxyEndpoints, - JunoEndpoints, - MongoProxyEndpoints, - PortalBackendEndpoints, -} from "Common/Constants"; -import { - allowedAadEndpoints, allowedArcadiaEndpoints, allowedEmulatorEndpoints, - allowedGraphEndpoints, allowedHostedExplorerEndpoints, allowedJunoOrigins, allowedMsalRedirectEndpoints, + defaultAllowedAadEndpoints, defaultAllowedArmEndpoints, defaultAllowedBackendEndpoints, defaultAllowedCassandraProxyEndpoints, + defaultAllowedGraphEndpoints, defaultAllowedMongoProxyEndpoints, validateEndpoint, } from "Utils/EndpointUtils"; @@ -29,6 +23,8 @@ export enum Platform { export interface ConfigContext { platform: Platform; + allowedAadEndpoints: ReadonlyArray; + allowedGraphEndpoints: ReadonlyArray; allowedArmEndpoints: ReadonlyArray; allowedBackendEndpoints: ReadonlyArray; allowedCassandraProxyEndpoints: ReadonlyArray; @@ -37,10 +33,8 @@ export interface ConfigContext { gitSha?: string; proxyPath?: string; AAD_ENDPOINT: string; - ARM_AUTH_AREA: string; ARM_ENDPOINT: string; EMULATOR_ENDPOINT?: string; - ARM_API_VERSION: string; GRAPH_ENDPOINT: string; GRAPH_API_VERSION: string; // This is the endpoint to get offering Ids to be used to fetch prices. Refer to this doc: https://learn.microsoft.com/en-us/rest/api/marketplacecatalog/dataplane/skus/list?view=rest-marketplacecatalog-dataplane-2023-05-01-preview&tabs=HTTP @@ -50,27 +44,24 @@ export interface ConfigContext { ARCADIA_ENDPOINT: string; ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string; PORTAL_BACKEND_ENDPOINT: string; - NEW_BACKEND_APIS?: BackendApi[]; MONGO_PROXY_ENDPOINT: string; CASSANDRA_PROXY_ENDPOINT: string; - NEW_CASSANDRA_APIS?: string[]; PROXY_PATH?: string; JUNO_ENDPOINT: string; GITHUB_CLIENT_ID: string; 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; - globallyEnabledCassandraAPIs?: string[]; - globallyEnabledMongoAPIs?: string[]; } // Default configuration let configContext: Readonly = { platform: Platform.Portal, + allowedAadEndpoints: defaultAllowedAadEndpoints, + allowedGraphEndpoints: defaultAllowedGraphEndpoints, allowedArmEndpoints: defaultAllowedArmEndpoints, allowedBackendEndpoints: defaultAllowedBackendEndpoints, allowedCassandraProxyEndpoints: defaultAllowedCassandraProxyEndpoints, @@ -85,17 +76,12 @@ let configContext: Readonly = { `^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`, `^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`, `^https:\\/\\/.*\\.powerbi\\.com$`, - `^https:\\/\\/.*\\.analysis-df\\.net$`, - `^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`, - `^https:\\/\\/.*\\.azure-test\\.net$`, `^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`, ], // Webpack injects this at build time gitSha: process.env.GIT_SHA, hostedExplorerURL: "https://cosmos.azure.com/", AAD_ENDPOINT: "https://login.microsoftonline.com/", - ARM_AUTH_AREA: "https://management.azure.com/", ARM_ENDPOINT: "https://management.azure.com/", - ARM_API_VERSION: "2016-06-01", GRAPH_ENDPOINT: "https://graph.microsoft.com", GRAPH_API_VERSION: "1.6", CATALOG_ENDPOINT: "https://catalogapi.azure.com/", @@ -109,11 +95,7 @@ let configContext: Readonly = { PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod, MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod, - NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"], - isTerminalEnabled: false, isPhoenixEnabled: false, - globallyEnabledCassandraAPIs: [], - globallyEnabledMongoAPIs: [], }; export function resetConfigContext(): void { @@ -128,19 +110,21 @@ export function updateConfigContext(newContext: Partial): void { return; } - if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) { - delete newContext.ARM_ENDPOINT; + if (!validateEndpoint(newContext.AAD_ENDPOINT, configContext.allowedAadEndpoints || defaultAllowedAadEndpoints)) { + delete newContext.AAD_ENDPOINT; } - if (!validateEndpoint(newContext.AAD_ENDPOINT, allowedAadEndpoints)) { - delete newContext.AAD_ENDPOINT; + if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) { + delete newContext.ARM_ENDPOINT; } if (!validateEndpoint(newContext.EMULATOR_ENDPOINT, allowedEmulatorEndpoints)) { delete newContext.EMULATOR_ENDPOINT; } - if (!validateEndpoint(newContext.GRAPH_ENDPOINT, allowedGraphEndpoints)) { + if ( + !validateEndpoint(newContext.GRAPH_ENDPOINT, configContext.allowedGraphEndpoints || defaultAllowedGraphEndpoints) + ) { delete newContext.GRAPH_ENDPOINT; } @@ -148,6 +132,15 @@ export function updateConfigContext(newContext: Partial): void { delete newContext.ARCADIA_ENDPOINT; } + if ( + !validateEndpoint( + newContext.PORTAL_BACKEND_ENDPOINT, + configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints, + ) + ) { + delete newContext.PORTAL_BACKEND_ENDPOINT; + } + if ( !validateEndpoint( newContext.MONGO_PROXY_ENDPOINT, diff --git a/src/Contracts/ActionContracts.ts b/src/Contracts/ActionContracts.ts index 9d6c9f8a3..aa58da20e 100644 --- a/src/Contracts/ActionContracts.ts +++ b/src/Contracts/ActionContracts.ts @@ -23,6 +23,7 @@ export enum PaneKind { GlobalSettings, AdHocAccess, SwitchDirectory, + QuickStart, } /** diff --git a/src/Contracts/DataExplorerMessagesContract.ts b/src/Contracts/DataExplorerMessagesContract.ts index a38940120..c017bffa8 100644 --- a/src/Contracts/DataExplorerMessagesContract.ts +++ b/src/Contracts/DataExplorerMessagesContract.ts @@ -18,10 +18,13 @@ export type DataExploreMessageV3 = | { type: FabricMessageTypes.GetAllResourceTokens; id: string; + } + | { + type: FabricMessageTypes.OpenSettings; + settingsId: string; }; - -export type GetCosmosTokenMessageOptions = { +export interface GetCosmosTokenMessageOptions { verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace"; resourceType: "" | "dbs" | "colls" | "docs" | "sprocs" | "pkranges"; resourceId: string; -}; +} diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index 3b3ab5027..7dfe33bbb 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -7,6 +7,7 @@ export interface ArmEntity { type: string; kind: string; tags?: Tags; + resourceGroup?: string; } export interface DatabaseAccount extends ArmEntity { @@ -32,6 +33,7 @@ export interface DatabaseAccountExtendedProperties { writeLocations?: DatabaseAccountResponseLocation[]; enableFreeTier?: boolean; enableAnalyticalStorage?: boolean; + enableMaterializedViews?: boolean; isVirtualNetworkFilterEnabled?: boolean; ipRules?: IpRule[]; privateEndpointConnections?: unknown[]; @@ -164,6 +166,8 @@ export interface Collection extends Resource { schema?: ISchema; requestSchema?: () => void; computedProperties?: ComputedProperties; + materializedViews?: MaterializedView[]; + materializedViewDefinition?: MaterializedViewDefinition; } export interface CollectionsWithPagination { @@ -207,7 +211,7 @@ export interface IndexingPolicy { export interface VectorIndex { path: string; type: "flat" | "diskANN" | "quantizedFlat"; - diskANNShardKey?: string; + vectorIndexShardKey?: string[]; indexingSearchListSize?: number; quantizationByteSize?: number; } @@ -223,6 +227,17 @@ export interface ComputedProperty { export type ComputedProperties = ComputedProperty[]; +export interface MaterializedView { + id: string; + _rid: string; +} + +export interface MaterializedViewDefinition { + definition: string; + sourceCollectionId: string; + sourceCollectionRid?: string; +} + export interface PartitionKey { paths: string[]; kind: "Hash" | "Range" | "MultiHash"; @@ -345,9 +360,7 @@ export interface CreateDatabaseParams { offerThroughput?: number; } -export interface CreateCollectionParams { - createNewDatabase: boolean; - collectionId: string; +export interface CreateCollectionParamsBase { databaseId: string; databaseLevelThroughput: boolean; offerThroughput?: number; @@ -361,12 +374,22 @@ export interface CreateCollectionParams { fullTextPolicy?: FullTextPolicy; } +export interface CreateCollectionParams extends CreateCollectionParamsBase { + createNewDatabase: boolean; + collectionId: string; +} + +export interface CreateMaterializedViewsParams extends CreateCollectionParamsBase { + materializedViewId: string; + materializedViewDefinition: MaterializedViewDefinition; +} + export interface VectorEmbeddingPolicy { vectorEmbeddings: VectorEmbedding[]; } export interface VectorEmbedding { - dataType: "float16" | "float32" | "uint8" | "int8"; + dataType: "float32" | "uint8" | "int8"; dimensions: number; distanceFunction: "euclidean" | "cosine" | "dotproduct"; path: string; diff --git a/src/Contracts/FabricMessageTypes.ts b/src/Contracts/FabricMessageTypes.ts index 1d4576391..02871ca47 100644 --- a/src/Contracts/FabricMessageTypes.ts +++ b/src/Contracts/FabricMessageTypes.ts @@ -6,6 +6,7 @@ export enum FabricMessageTypes { GetAllResourceTokens = "GetAllResourceTokens", GetAccessToken = "GetAccessToken", Ready = "Ready", + OpenSettings = "OpenSettings", } export interface AuthorizationToken { diff --git a/src/Contracts/FabricMessagesContract.ts b/src/Contracts/FabricMessagesContract.ts index 2cc99c578..8cc198a11 100644 --- a/src/Contracts/FabricMessagesContract.ts +++ b/src/Contracts/FabricMessagesContract.ts @@ -81,6 +81,13 @@ export type FabricMessageV3 = error: string | undefined; data: { accessToken: string }; }; + } + | { + type: "refreshResourceTree"; + message: { + id: string; + error: string | undefined; + }; }; export enum CosmosDbArtifactType { diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index 14c2fee34..264ee22c1 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -1,4 +1,6 @@ import { + ItemDefinition, + JSONObject, QueryMetrics, Resource, StoredProcedureDefinition, @@ -29,8 +31,11 @@ export interface UploadDetailsRecord { numFailed: number; numThrottled: number; errors: string[]; + resources?: ItemDefinition[]; } +export type BulkInsertResult = Omit; + export interface QueryResultsMetadata { hasMoreResults: boolean; firstItemIndex: number; @@ -45,6 +50,7 @@ export interface QueryResults extends QueryResultsMetadata { roundTrips?: number; headers?: any; queryMetrics?: QueryMetrics; + ruThresholdExceeded?: boolean; } export interface Button { @@ -143,6 +149,8 @@ export interface Collection extends CollectionBase { geospatialConfig: ko.Observable; documentIds: ko.ObservableArray; computedProperties: ko.Observable; + materializedViews: ko.Observable; + materializedViewDefinition: ko.Observable; cassandraKeys: CassandraTableKeys; cassandraSchema: CassandraTableKey[]; @@ -204,6 +212,12 @@ export interface Collection extends CollectionBase { onDragOver(source: Collection, event: { originalEvent: DragEvent }): void; onDrop(source: Collection, event: { originalEvent: DragEvent }): void; uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>; + bulkInsertDocuments(documents: JSONObject[]): Promise<{ + numSucceeded: number; + numFailed: number; + numThrottled: number; + errors: string[]; + }>; } /** @@ -429,6 +443,7 @@ export interface DataExplorerInputsFrame { [key: string]: string; }; feedbackPolicies?: any; + aadToken?: string; } export interface SelfServeFrameInputs { diff --git a/src/Controls/Heatmap/Heatmap.html b/src/Controls/Heatmap/Heatmap.html deleted file mode 100644 index c397faf3b..000000000 --- a/src/Controls/Heatmap/Heatmap.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - -
- - diff --git a/src/Controls/Heatmap/Heatmap.less b/src/Controls/Heatmap/Heatmap.less deleted file mode 100644 index 108ade9b4..000000000 --- a/src/Controls/Heatmap/Heatmap.less +++ /dev/null @@ -1,55 +0,0 @@ -@import "../../../less/Common/Constants"; -html { - font-family: @DataExplorerFont; - padding: 0px; - margin: 0px; - border: 0px; - overflow: hidden; - position: fixed; - width: 100%; - height: 100%; -} - -body { - font-family: @DataExplorerFont; - padding: 0px; - margin: 0px; - border: 0px; - overflow: hidden; -} - -#heatmap { - .dark-theme { - color: @BaseLight; - } - - .chartTitle { - position: absolute; - top: 5px; - left: 3px; - font-size: 13px; - } - - .noDataMessage { - display: flex; - justify-content: center; - align-items: center; - position: absolute; - z-index: 10000; - height: 100%; - width: 100%; - top: 0; - left: 0; - opacity: 0.97; - div { - border-color: rgba(204, 204, 204, 0.8); - box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.12); - padding: 15px 10px; - width: calc(55% - 40px); - font-size: 13px; - text-align: center; - border-width: 1px; - border-style: solid; - } - } -} diff --git a/src/Controls/Heatmap/Heatmap.test.ts b/src/Controls/Heatmap/Heatmap.test.ts deleted file mode 100644 index e51c91285..000000000 --- a/src/Controls/Heatmap/Heatmap.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -import dayjs from "dayjs"; -import { handleMessage, Heatmap, isDarkTheme } from "./Heatmap"; -import { PortalTheme } from "./HeatmapDatatypes"; - -describe("The Heatmap Control", () => { - const dataPoints = { - "1": { - "2019-06-19T00:59:10Z": { - "Normalized Throughput": 0.35, - }, - "2019-06-19T00:48:10Z": { - "Normalized Throughput": 0.25, - }, - }, - }; - - const chartCaptions = { - chartTitle: "chart title", - yAxisTitle: "YAxisTitle", - tooltipText: "Tooltip text", - timeWindow: 123456789, - }; - - let heatmap: Heatmap; - const theme: PortalTheme = 1; - const divElement = `
`; - - describe("drawHeatmap rendering", () => { - beforeEach(() => { - heatmap = new Heatmap(dataPoints, chartCaptions, theme); - document.body.innerHTML = divElement; - }); - - afterEach(() => { - document.body.innerHTML = ``; - }); - - it("should call _getChartSettings when drawHeatmap is invoked", () => { - const _getChartSettings = jest.spyOn(heatmap, "_getChartSettings"); - heatmap.drawHeatmap(); - expect(_getChartSettings).toHaveBeenCalled(); - }); - - it("should call _getLayoutSettings when drawHeatmap is invoked", () => { - const _getLayoutSettings = jest.spyOn(heatmap, "_getLayoutSettings"); - heatmap.drawHeatmap(); - expect(_getLayoutSettings).toHaveBeenCalled(); - }); - - it("should call _getChartDisplaySettings when drawHeatmap is invoked", () => { - const _getChartDisplaySettings = jest.spyOn(heatmap, "_getChartDisplaySettings"); - heatmap.drawHeatmap(); - expect(_getChartDisplaySettings).toHaveBeenCalled(); - }); - - it("drawHeatmap should render a Heatmap inside the div element", () => { - heatmap.drawHeatmap(); - expect(document.body.innerHTML).not.toEqual(divElement); - }); - }); - - describe("generateMatrixFromMap", () => { - it("should massage input data to match output expected", () => { - expect(heatmap.generateMatrixFromMap(dataPoints).yAxisPoints).toEqual(["1"]); - expect(heatmap.generateMatrixFromMap(dataPoints).dataPoints).toEqual([[0.25, 0.35]]); - expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints.length).toEqual(2); - }); - - it("should output the date format to ISO8601 string format", () => { - expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints[0].slice(10, 11)).toEqual("T"); - expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints[0].slice(-1)).toEqual("Z"); - }); - - it("should convert the time to the user's local time", () => { - if (dayjs().utcOffset()) { - expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints).not.toEqual([ - "2019-06-19T00:48:10Z", - "2019-06-19T00:59:10Z", - ]); - } else { - expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints).toEqual([ - "2019-06-19T00:48:10Z", - "2019-06-19T00:59:10Z", - ]); - } - }); - }); - - describe("isDarkTheme", () => { - it("isDarkTheme should return the correct result", () => { - expect(isDarkTheme(PortalTheme.dark)).toEqual(true); - expect(isDarkTheme(PortalTheme.azure)).not.toEqual(true); - }); - }); -}); - -describe("iframe rendering when there is no data", () => { - afterEach(() => { - document.body.innerHTML = ``; - }); - - it("should show a no data message with a dark theme", () => { - const data = { - data: { - signature: "pcIframe", - data: { - chartData: {}, - chartSettings: {}, - theme: 4, - }, - }, - origin: "http://localhost", - }; - - const divElement = `
`; - document.body.innerHTML = divElement; - - handleMessage(data as MessageEvent); - expect(document.body.innerHTML).toContain("dark-theme"); - expect(document.body.innerHTML).toContain("noDataMessage"); - }); - - it("should show a no data message with a white theme", () => { - const data = { - data: { - signature: "pcIframe", - data: { - chartData: {}, - chartSettings: {}, - theme: 2, - }, - }, - origin: "http://localhost", - }; - - const divElement = `
`; - document.body.innerHTML = divElement; - - handleMessage(data as MessageEvent); - expect(document.body.innerHTML).not.toContain("dark-theme"); - expect(document.body.innerHTML).toContain("noDataMessage"); - }); -}); diff --git a/src/Controls/Heatmap/Heatmap.ts b/src/Controls/Heatmap/Heatmap.ts deleted file mode 100644 index 533341677..000000000 --- a/src/Controls/Heatmap/Heatmap.ts +++ /dev/null @@ -1,272 +0,0 @@ -import dayjs from "dayjs"; -import * as Plotly from "plotly.js-cartesian-dist-min"; -import { sendCachedDataMessage, sendReadyMessage } from "../../Common/MessageHandler"; -import { StyleConstants } from "../../Common/StyleConstants"; -import { MessageTypes } from "../../Contracts/ExplorerContracts"; -import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation"; -import "./Heatmap.less"; -import { - ChartSettings, - DataPayload, - DisplaySettings, - FontSettings, - HeatmapCaptions, - HeatmapData, - LayoutSettings, - PartitionTimeStampToData, - PortalTheme, -} from "./HeatmapDatatypes"; - -export class Heatmap { - public static readonly elementId: string = "heatmap"; - - private _chartData: HeatmapData; - private _heatmapCaptions: HeatmapCaptions; - private _theme: PortalTheme; - private _defaultFontColor: string; - - constructor(data: DataPayload, heatmapCaptions: HeatmapCaptions, theme: PortalTheme) { - this._theme = theme; - this._defaultFontColor = StyleConstants.BaseDark; - this._setThemeColorForChart(); - this._chartData = this.generateMatrixFromMap(data); - this._heatmapCaptions = heatmapCaptions; - } - - private _setThemeColorForChart() { - if (isDarkTheme(this._theme)) { - this._defaultFontColor = StyleConstants.BaseLight; - } - } - - private _getFontStyles(size: number = StyleConstants.MediumFontSize, color = "#838383"): FontSettings { - return { - family: StyleConstants.DataExplorerFont, - size, - color, - }; - } - - public generateMatrixFromMap(data: DataPayload): HeatmapData { - // all keys in data payload, sorted... - const rows: string[] = Object.keys(data).sort((a: string, b: string) => { - if (parseInt(a) < parseInt(b)) { - return -1; - } else { - if (parseInt(a) > parseInt(b)) { - return 1; - } else { - return 0; - } - } - }); - const output: HeatmapData = { - yAxisPoints: [], - dataPoints: [], - xAxisPoints: Object.keys(data[rows[0]]).sort((a: string, b: string) => { - if (a < b) { - return -1; - } else { - if (a > b) { - return 1; - } else { - return 0; - } - } - }), - }; - // go thru all rows and create 2d matrix for heatmap... - for (let i = 0; i < rows.length; i++) { - output.yAxisPoints.push(rows[i]); - const dataPoints: number[] = []; - for (let a = 0; a < output.xAxisPoints.length; a++) { - const row: PartitionTimeStampToData = data[rows[i]]; - dataPoints.push(row[output.xAxisPoints[a]]["Normalized Throughput"]); - } - output.dataPoints.push(dataPoints); - } - for (let a = 0; a < output.xAxisPoints.length; a++) { - const dateTime = output.xAxisPoints[a]; - // convert to local users timezone... - const day = dayjs(new Date(dateTime)).format("YYYY-MM-DD"); - const hour = dayjs(new Date(dateTime)).format("HH:mm:ss"); - // coerce to ISOString format since that is what plotly wants... - output.xAxisPoints[a] = `${day}T${hour}Z`; - } - return output; - } - - // public for testing purposes - public _getChartSettings(): ChartSettings[] { - return [ - { - z: this._chartData.dataPoints, - type: "heatmap", - zmin: 0, - zmid: 50, - zmax: 100, - colorscale: [ - [0.0, "#1FD338"], - [0.1, "#1CAD2F"], - [0.2, "#50A527"], - [0.3, "#719F21"], - [0.4, "#95991B"], - [0.5, "#CE8F11"], - [0.6, "#E27F0F"], - [0.7, "#E46612"], - [0.8, "#E64914"], - [0.9, "#B80016"], - [1.0, "#B80016"], - ], - name: "", - hovertemplate: this._heatmapCaptions.tooltipText, - colorbar: { - thickness: 15, - outlinewidth: 0, - tickcolor: StyleConstants.BaseDark, - tickfont: this._getFontStyles(10, this._defaultFontColor), - }, - y: this._chartData.yAxisPoints, - x: this._chartData.xAxisPoints, - }, - ]; - } - - // public for testing purposes - public _getLayoutSettings(): LayoutSettings { - return { - margin: { - l: 40, - r: 10, - b: 35, - t: 30, - pad: 0, - }, - paper_bgcolor: "transparent", - plot_bgcolor: "transparent", - width: 462, - height: 240, - yaxis: { - title: this._heatmapCaptions.yAxisTitle, - titlefont: this._getFontStyles(11), - autorange: true, - showgrid: false, - zeroline: false, - showline: false, - autotick: true, - fixedrange: true, - ticks: "", - showticklabels: false, - }, - xaxis: { - fixedrange: true, - title: "*White area in heatmap indicates there is no available data", - titlefont: this._getFontStyles(11), - autorange: true, - showgrid: false, - zeroline: false, - showline: false, - autotick: true, - tickformat: this._heatmapCaptions.timeWindow > 7 ? "%I:%M %p" : "%b %e", - showticklabels: true, - tickfont: this._getFontStyles(10), - }, - title: { - text: this._heatmapCaptions.chartTitle, - x: 0.01, - font: this._getFontStyles(13, this._defaultFontColor), - }, - }; - } - - // public for testing purposes - public _getChartDisplaySettings(): DisplaySettings { - return { - /* heatmap can be fully responsive however the min-height needed in that case is greater than the iframe portal height, hence explicit width + height have been set in _getLayoutSettings - responsive: true,*/ - displayModeBar: false, - }; - } - - public drawHeatmap(): void { - // todo - create random elementId generator so multiple heatmaps can be created - ticket # 431469 - Plotly.plot( - Heatmap.elementId, - this._getChartSettings(), - this._getLayoutSettings(), - this._getChartDisplaySettings(), - ); - const plotDiv: any = document.getElementById(Heatmap.elementId); - plotDiv.on("plotly_click", (data: any) => { - let timeSelected: string = data.points[0].x; - timeSelected = timeSelected.replace(" ", "T"); - timeSelected = `${timeSelected}Z`; - let xAxisIndex = 0; - for (let i = 0; i < this._chartData.xAxisPoints.length; i++) { - if (this._chartData.xAxisPoints[i] === timeSelected) { - xAxisIndex = i; - break; - } - } - const output = []; - for (let i = 0; i < this._chartData.dataPoints.length; i++) { - output.push(this._chartData.dataPoints[i][xAxisIndex]); - } - sendCachedDataMessage(MessageTypes.LogInfo, output); - }); - } -} - -export function isDarkTheme(theme: PortalTheme) { - return theme === PortalTheme.dark; -} - -export function handleMessage(event: MessageEvent) { - if (isInvalidParentFrameOrigin(event)) { - return; - } - - if (typeof event.data !== "object" || event.data["signature"] !== "pcIframe") { - return; - } - if ( - typeof event.data.data !== "object" || - !("chartData" in event.data.data) || - !("chartSettings" in event.data.data) - ) { - return; - } - Plotly.purge(Heatmap.elementId); - - document.getElementById(Heatmap.elementId)!.innerHTML = ""; - const data = event.data.data; - const chartData: DataPayload = data.chartData; - const chartSettings: HeatmapCaptions = data.chartSettings; - const chartTheme: PortalTheme = data.theme; - if (Object.keys(chartData).length) { - new Heatmap(chartData, chartSettings, chartTheme).drawHeatmap(); - } else { - const chartTitleElement = document.createElement("div"); - chartTitleElement.innerHTML = data.chartSettings.chartTitle; - chartTitleElement.classList.add("chartTitle"); - - const noDataMessageElement = document.createElement("div"); - noDataMessageElement.classList.add("noDataMessage"); - const noDataMessageContent = document.createElement("div"); - noDataMessageContent.innerHTML = data.errorMessage; - - noDataMessageElement.appendChild(noDataMessageContent); - - if (isDarkTheme(chartTheme)) { - chartTitleElement.classList.add("dark-theme"); - noDataMessageElement.classList.add("dark-theme"); - noDataMessageContent.classList.add("dark-theme"); - } - - document.getElementById(Heatmap.elementId)!.appendChild(chartTitleElement); - document.getElementById(Heatmap.elementId)!.appendChild(noDataMessageElement); - } -} - -window.addEventListener("message", handleMessage, false); -sendReadyMessage(); diff --git a/src/Controls/Heatmap/HeatmapDatatypes.ts b/src/Controls/Heatmap/HeatmapDatatypes.ts deleted file mode 100644 index 3ef9a7109..000000000 --- a/src/Controls/Heatmap/HeatmapDatatypes.ts +++ /dev/null @@ -1,106 +0,0 @@ -type dataPoint = string | number; - -export interface DataPayload { - [id: string]: PartitionTimeStampToData; -} - -export enum PortalTheme { - blue = 1, - azure, - light, - dark, -} - -export interface HeatmapData { - yAxisPoints: string[]; - xAxisPoints: string[]; - dataPoints: dataPoint[][]; -} - -export interface HeatmapCaptions { - chartTitle: string; - yAxisTitle: string; - tooltipText: string; - timeWindow: number; -} - -export interface FontSettings { - family: string; - size: number; - color: string; -} - -export interface LayoutSettings { - paper_bgcolor?: string; - plot_bgcolor?: string; - margin?: { - l: number; - r: number; - b: number; - t: number; - pad: number; - }; - width?: number; - height?: number; - yaxis?: { - fixedrange: boolean; - title: HeatmapCaptions["yAxisTitle"]; - titlefont: FontSettings; - autorange: boolean; - showgrid: boolean; - zeroline: boolean; - showline: boolean; - autotick: boolean; - ticks: ""; - showticklabels: boolean; - }; - xaxis?: { - fixedrange: boolean; - title: string; - titlefont: FontSettings; - autorange: boolean; - showgrid: boolean; - zeroline: boolean; - showline: boolean; - autotick: boolean; - showticklabels: boolean; - tickformat: string; - tickfont: FontSettings; - }; - title?: { - text: HeatmapCaptions["chartTitle"]; - x: number; - font?: FontSettings; - }; - font?: FontSettings; -} - -export interface ChartSettings { - z: HeatmapData["dataPoints"]; - type: "heatmap"; - zmin: number; - zmid: number; - zmax: number; - colorscale: [number, string][]; - name: string; - hovertemplate: HeatmapCaptions["tooltipText"]; - colorbar: { - thickness: number; - outlinewidth: number; - tickcolor: string; - tickfont: FontSettings; - }; - y: HeatmapData["yAxisPoints"]; - x: HeatmapData["xAxisPoints"]; -} - -export interface DisplaySettings { - displayModeBar: boolean; - responsive?: boolean; -} - -export interface PartitionTimeStampToData { - [timeSeriesDates: string]: { - [NormalizedThroughput: string]: number; - }; -} diff --git a/src/Explorer/ContextMenuButtonFactory.tsx b/src/Explorer/ContextMenuButtonFactory.tsx index 8108cbbb2..3f6a795ee 100644 --- a/src/Explorer/ContextMenuButtonFactory.tsx +++ b/src/Explorer/ContextMenuButtonFactory.tsx @@ -1,5 +1,11 @@ +import { GlobalSecondaryIndexLabels } from "Common/Constants"; +import { isGlobalSecondaryIndexEnabled } from "Common/DatabaseAccountUtility"; import { configContext, Platform } from "ConfigContext"; import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; +import { + AddGlobalSecondaryIndexPanel, + AddGlobalSecondaryIndexPanelProps, +} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel"; import { useDatabases } from "Explorer/useDatabases"; import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil"; import { Action } from "Shared/Telemetry/TelemetryConstants"; @@ -97,17 +103,23 @@ export const createCollectionContextMenuButton = ( iconSrc: HostedTerminalIcon, onClick: () => { const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); - if (useNotebook.getState().isShellEnabled) { + if (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) { container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); } else { selectedCollection && selectedCollection.onNewMongoShellClick(); } }, - label: useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell", + label: + useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell + ? "Open Mongo Shell" + : "New Shell", }); } - if (useNotebook.getState().isShellEnabled && userContext.apiType === "Cassandra") { + if ( + (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) && + userContext.apiType === "Cassandra" + ) { items.push({ iconSrc: HostedTerminalIcon, onClick: () => { @@ -164,6 +176,24 @@ export const createCollectionContextMenuButton = ( }); } + if (isGlobalSecondaryIndexEnabled() && !selectedCollection.materializedViewDefinition()) { + items.push({ + label: GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex, + onClick: () => { + const addMaterializedViewPanelProps: AddGlobalSecondaryIndexPanelProps = { + explorer: container, + sourceContainer: selectedCollection, + }; + useSidePanel + .getState() + .openSidePanel( + GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex, + , + ); + }, + }); + } + return items; }; diff --git a/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx b/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx index b6e847d54..168962312 100644 --- a/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx +++ b/src/Explorer/Controls/CommandButton/CommandButtonComponent.tsx @@ -58,6 +58,26 @@ export interface CommandButtonComponentProps { */ tooltipText?: string; + /** + * Custom styles to apply to the button using Fluent UI theme tokens + */ + styles?: { + root?: { + backgroundColor?: string; + color?: string; + selectors?: { + ":hover"?: { + backgroundColor?: string; + color?: string; + }; + ":active"?: { + backgroundColor?: string; + color?: string; + }; + }; + }; + }; + /** * tabindex for the command button */ @@ -250,6 +270,8 @@ export class CommandButtonComponent extends React.Component ) => this.commandClickCallback(e)} >
diff --git a/src/Explorer/Controls/Dialog.tsx b/src/Explorer/Controls/Dialog.tsx index 9f69d4761..a4c50a3fd 100644 --- a/src/Explorer/Controls/Dialog.tsx +++ b/src/Explorer/Controls/Dialog.tsx @@ -214,8 +214,10 @@ export const Dialog: FC = () => { {contentHtml} {progressIndicatorProps && } - - {secondaryButtonProps && } + + {secondaryButtonProps && ( + + )} ) : ( diff --git a/src/Explorer/Controls/InputDataList/InputDataList.tsx b/src/Explorer/Controls/InputDataList/InputDataList.tsx index cd31db53b..2f483d362 100644 --- a/src/Explorer/Controls/InputDataList/InputDataList.tsx +++ b/src/Explorer/Controls/InputDataList/InputDataList.tsx @@ -193,6 +193,7 @@ export const InputDataList: FC = ({ <> { it("should save throughput bucket changes when Save button is clicked", async () => { updateUserContext({ apiType: "SQL", - features: { enableThroughputBuckets: true } as Features, + throughputBucketsEnabled: true, authType: AuthType.AAD, }); diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index 7aca73f57..7aeb2e285 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -1,18 +1,19 @@ -import { IPivotItemProps, IPivotProps, Pivot, PivotItem, Stack, getTheme } from "@fluentui/react"; +import { IPivotItemProps, IPivotProps, Pivot, PivotItem, Stack } from "@fluentui/react"; import { - ComputedPropertiesComponent, - ComputedPropertiesComponentProps, + ComputedPropertiesComponent, + ComputedPropertiesComponentProps, } from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent"; import { - ContainerPolicyComponent, - ContainerPolicyComponentProps, + ContainerPolicyComponent, + ContainerPolicyComponentProps, } from "Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent"; import { - ThroughputBucketsComponent, - ThroughputBucketsComponentProps, + ThroughputBucketsComponent, + ThroughputBucketsComponentProps, } from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent"; import { useDatabases } from "Explorer/useDatabases"; -import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils"; +import { isFabricNative } from "Platform/Fabric/FabricUtil"; +import { isVectorSearchEnabled } from "Utils/CapabilityUtils"; import { isRunningOnPublicCloud } from "Utils/CloudUtils"; import * as React from "react"; import DiscardIcon from "../../../../images/discard.svg"; @@ -33,37 +34,41 @@ import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { - PartitionKeyComponent, - PartitionKeyComponentProps, + PartitionKeyComponent, + PartitionKeyComponentProps, } from "../../Controls/Settings/SettingsSubComponents/PartitionKeyComponent"; import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter"; import { SettingsTabV2 } from "../../Tabs/SettingsTabV2"; import "./SettingsComponent.less"; import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils"; import { - ConflictResolutionComponent, - ConflictResolutionComponentProps, + ConflictResolutionComponent, + ConflictResolutionComponentProps, } from "./SettingsSubComponents/ConflictResolutionComponent"; +import { + GlobalSecondaryIndexComponent, + GlobalSecondaryIndexComponentProps, +} from "./SettingsSubComponents/GlobalSecondaryIndexComponent"; import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent"; import { - MongoIndexingPolicyComponent, - MongoIndexingPolicyComponentProps, + MongoIndexingPolicyComponent, + MongoIndexingPolicyComponentProps, } from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent"; import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent"; import { SubSettingsComponent, SubSettingsComponentProps } from "./SettingsSubComponents/SubSettingsComponent"; import { - AddMongoIndexProps, - ChangeFeedPolicyState, - GeospatialConfigType, - MongoIndexTypes, - SettingsV2TabTypes, - TtlType, - getMongoNotification, - getTabTitle, - hasDatabaseSharedThroughput, - isDirty, - parseConflictResolutionMode, - parseConflictResolutionProcedure, + AddMongoIndexProps, + ChangeFeedPolicyState, + GeospatialConfigType, + MongoIndexTypes, + SettingsV2TabTypes, + TtlType, + getMongoNotification, + getTabTitle, + hasDatabaseSharedThroughput, + isDirty, + parseConflictResolutionMode, + parseConflictResolutionProcedure, } from "./SettingsUtils"; interface SettingsV2TabInfo { @@ -162,6 +167,7 @@ export class SettingsComponent extends React.Component, }); } + if (this.isGlobalSecondaryIndex) { + tabs.push({ + tab: SettingsV2TabTypes.GlobalSecondaryIndexTab, + content: , + }); + } + const pivotProps: IPivotProps = { onLinkClick: this.onPivotChange, selectedKey: SettingsV2TabTypes[this.state.selectedTab], @@ -1343,95 +1363,102 @@ export class SettingsComponent extends React.Component +
{this.shouldShowKeyspaceSharedThroughputMessage() && (
This table shared throughput is configured at the keyspace
)} -
+
{tabs.map((tab) => { const pivotItemProps: IPivotItemProps = { itemKey: SettingsV2TabTypes[tab.tab], - style: { + style: { marginTop: 20, - backgroundColor: 'var(--colorNeutralBackground1)', - color: 'var(--colorNeutralForeground1)' + backgroundColor: "var(--colorNeutralBackground1)", + color: "var(--colorNeutralForeground1)", }, headerText: getTabTitle(tab.tab), }; return ( - - {tab.content} - + {tab.content} ); })} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.tsx index 34dd1fa5a..bb277bd21 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent.tsx @@ -86,7 +86,7 @@ export class ComputedPropertiesComponent extends React.Component< value: value, language: "json", ariaLabel: "Computed properties", - theme:monacoTheme, + theme: monacoTheme, }); if (this.computedPropertiesEditor) { const computedPropertiesEditorModel = this.computedPropertiesEditor.getModel(); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent.tsx index f4db62ec6..1257d4a65 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent.tsx @@ -22,6 +22,7 @@ export interface ContainerPolicyComponentProps { isFullTextSearchEnabled: boolean; shouldDiscardContainerPolicies: boolean; resetShouldDiscardContainerPolicyChange: () => void; + isGlobalSecondaryIndex?: boolean; } export const ContainerPolicyComponent: React.FC = ({ diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexComponent.test.tsx new file mode 100644 index 000000000..ac290f93c --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexComponent.test.tsx @@ -0,0 +1,46 @@ +import { shallow } from "enzyme"; +import React from "react"; +import { collection, container } from "../TestUtils"; +import { GlobalSecondaryIndexComponent } from "./GlobalSecondaryIndexComponent"; +import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent"; +import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent"; + +describe("GlobalSecondaryIndexComponent", () => { + let testCollection: typeof collection; + let testExplorer: typeof container; + + beforeEach(() => { + testCollection = { ...collection }; + }); + + it("renders only the source component when materializedViewDefinition is missing", () => { + testCollection.materializedViews([ + { id: "view1", _rid: "rid1" }, + { id: "view2", _rid: "rid2" }, + ]); + testCollection.materializedViewDefinition(null); + const wrapper = shallow(); + expect(wrapper.find(GlobalSecondaryIndexSourceComponent).exists()).toBe(true); + expect(wrapper.find(GlobalSecondaryIndexTargetComponent).exists()).toBe(false); + }); + + it("renders only the target component when materializedViews is missing", () => { + testCollection.materializedViews(null); + testCollection.materializedViewDefinition({ + definition: "SELECT * FROM c WHERE c.id = 1", + sourceCollectionId: "source1", + sourceCollectionRid: "rid123", + }); + const wrapper = shallow(); + expect(wrapper.find(GlobalSecondaryIndexSourceComponent).exists()).toBe(false); + expect(wrapper.find(GlobalSecondaryIndexTargetComponent).exists()).toBe(true); + }); + + it("renders neither component when both are missing", () => { + testCollection.materializedViews(null); + testCollection.materializedViewDefinition(null); + const wrapper = shallow(); + expect(wrapper.find(GlobalSecondaryIndexSourceComponent).exists()).toBe(false); + expect(wrapper.find(GlobalSecondaryIndexTargetComponent).exists()).toBe(false); + }); +}); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexComponent.tsx new file mode 100644 index 000000000..66aa3313a --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexComponent.tsx @@ -0,0 +1,41 @@ +import { FontIcon, Link, Stack, Text } from "@fluentui/react"; +import Explorer from "Explorer/Explorer"; +import React from "react"; +import * as ViewModels from "../../../../Contracts/ViewModels"; +import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent"; +import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent"; + +export interface GlobalSecondaryIndexComponentProps { + collection: ViewModels.Collection; + explorer: Explorer; +} + +export const GlobalSecondaryIndexComponent: React.FC = ({ + collection, + explorer, +}) => { + const isTargetContainer = !!collection?.materializedViewDefinition(); + const isSourceContainer = !!collection?.materializedViews(); + + return ( + + + {isSourceContainer && ( + This container has the following indexes defined for it. + )} + + + Learn more + + {" "} + about how to define global secondary indexes and how to use them. + + + {isSourceContainer && } + {isTargetContainer && } + + ); +}; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexSourceComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexSourceComponent.test.tsx new file mode 100644 index 000000000..30ac800b9 --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexSourceComponent.test.tsx @@ -0,0 +1,42 @@ +import { PrimaryButton } from "@fluentui/react"; +import { shallow } from "enzyme"; +import React from "react"; +import { collection, container } from "../TestUtils"; +import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent"; + +describe("GlobalSecondaryIndexSourceComponent", () => { + let testCollection: typeof collection; + let testExplorer: typeof container; + + beforeEach(() => { + testCollection = { ...collection }; + }); + + it("renders without crashing", () => { + const wrapper = shallow( + , + ); + expect(wrapper.exists()).toBe(true); + }); + + it("renders the PrimaryButton", () => { + const wrapper = shallow( + , + ); + expect(wrapper.find(PrimaryButton).exists()).toBe(true); + }); + + it("updates when new global secondary indexes are provided", () => { + const wrapper = shallow( + , + ); + + // Simulating an update by modifying the observable directly + testCollection.materializedViews([{ id: "view3", _rid: "rid3" }]); + + wrapper.setProps({ collection: testCollection }); + wrapper.update(); + + expect(wrapper.find(PrimaryButton).exists()).toBe(true); + }); +}); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexSourceComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexSourceComponent.tsx new file mode 100644 index 000000000..aa0a0edae --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexSourceComponent.tsx @@ -0,0 +1,114 @@ +import { PrimaryButton } from "@fluentui/react"; +import { GlobalSecondaryIndexLabels } from "Common/Constants"; +import { MaterializedView } from "Contracts/DataModels"; +import Explorer from "Explorer/Explorer"; +import { loadMonaco } from "Explorer/LazyMonaco"; +import { AddGlobalSecondaryIndexPanel } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel"; +import { useDatabases } from "Explorer/useDatabases"; +import { useSidePanel } from "hooks/useSidePanel"; +import * as monaco from "monaco-editor"; +import React, { useEffect, useRef } from "react"; +import * as ViewModels from "../../../../Contracts/ViewModels"; + +export interface GlobalSecondaryIndexSourceComponentProps { + collection: ViewModels.Collection; + explorer: Explorer; +} + +export const GlobalSecondaryIndexSourceComponent: React.FC = ({ + collection, + explorer, +}) => { + const editorContainerRef = useRef(null); + const editorRef = useRef(null); + + const globalSecondaryIndexes: MaterializedView[] = collection?.materializedViews() ?? []; + + // Helper function to fetch the definition and partition key of targetContainer by traversing through all collections and matching id from MaterializedView[] with collection id. + const getViewDetails = (viewId: string): { definition: string; partitionKey: string[] } => { + let definition = ""; + let partitionKey: string[] = []; + + useDatabases.getState().databases.find((database) => { + const collection = database.collections().find((collection) => collection.id() === viewId); + if (collection) { + const globalSecondaryIndexDefinition = collection.materializedViewDefinition(); + globalSecondaryIndexDefinition && (definition = globalSecondaryIndexDefinition.definition); + collection.partitionKey?.paths && (partitionKey = collection.partitionKey.paths); + } + }); + + return { definition, partitionKey }; + }; + + //JSON value for the editor using the fetched id and definitions. + const jsonValue = JSON.stringify( + globalSecondaryIndexes.map((view) => { + const { definition, partitionKey } = getViewDetails(view.id); + return { + name: view.id, + partitionKey: partitionKey.join(", "), + definition, + }; + }), + null, + 2, + ); + + // Initialize Monaco editor with the computed JSON value. + useEffect(() => { + let disposed = false; + const initMonaco = async () => { + const monacoInstance = await loadMonaco(); + if (disposed || !editorContainerRef.current) { + return; + } + + editorRef.current = monacoInstance.editor.create(editorContainerRef.current, { + value: jsonValue, + language: "json", + ariaLabel: "Global Secondary Index JSON", + readOnly: true, + }); + }; + + initMonaco(); + return () => { + disposed = true; + editorRef.current?.dispose(); + }; + }, [jsonValue]); + + // Update the editor when the jsonValue changes. + useEffect(() => { + if (editorRef.current) { + editorRef.current.setValue(jsonValue); + } + }, [jsonValue]); + + return ( +
+
+ + useSidePanel + .getState() + .openSidePanel( + GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex, + , + ) + } + /> +
+ ); +}; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexTargetComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexTargetComponent.test.tsx new file mode 100644 index 000000000..6296cdab7 --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexTargetComponent.test.tsx @@ -0,0 +1,32 @@ +import { Text } from "@fluentui/react"; +import { Collection } from "Contracts/ViewModels"; +import { shallow } from "enzyme"; +import React from "react"; +import { collection } from "../TestUtils"; +import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent"; + +describe("GlobalSecondaryIndexTargetComponent", () => { + let testCollection: Collection; + + beforeEach(() => { + testCollection = { + ...collection, + materializedViewDefinition: collection.materializedViewDefinition, + }; + }); + + it("renders without crashing", () => { + const wrapper = shallow(); + expect(wrapper.exists()).toBe(true); + }); + + it("displays the source container ID", () => { + const wrapper = shallow(); + expect(wrapper.find(Text).at(2).dive().text()).toBe("source1"); + }); + + it("displays the global secondary index definition", () => { + const wrapper = shallow(); + expect(wrapper.find(Text).at(4).dive().text()).toBe("SELECT * FROM c WHERE c.id = 1"); + }); +}); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexTargetComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexTargetComponent.tsx new file mode 100644 index 000000000..8fa1171e8 --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexTargetComponent.tsx @@ -0,0 +1,45 @@ +import { Stack, Text } from "@fluentui/react"; +import * as React from "react"; +import * as ViewModels from "../../../../Contracts/ViewModels"; + +export interface GlobalSecondaryIndexTargetComponentProps { + collection: ViewModels.Collection; +} + +export const GlobalSecondaryIndexTargetComponent: React.FC = ({ + collection, +}) => { + const globalSecondaryIndexDefinition = collection?.materializedViewDefinition(); + + const textHeadingStyle = { + root: { fontWeight: "600", fontSize: 16 }, + }; + + const valueBoxStyle = { + root: { + backgroundColor: "#f3f3f3", + padding: "5px 10px", + borderRadius: "4px", + }, + }; + + return ( + + Global Secondary Index Settings + + + Source container + + {globalSecondaryIndexDefinition?.sourceCollectionId} + + + + + Global secondary index definition + + {globalSecondaryIndexDefinition?.definition} + + + + ); +}; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx index 5bb92d7b1..632459fa8 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx @@ -107,50 +107,50 @@ export class IndexingPolicyComponent extends React.Component< } } } -// private async createIndexingPolicyEditor(): Promise { -// const isDarkMode = true; -// const monacoThemeName = "fluent-theme"; + // private async createIndexingPolicyEditor(): Promise { + // const isDarkMode = true; + // const monacoThemeName = "fluent-theme"; -// if (!this.indexingPolicyDiv.current) return; + // if (!this.indexingPolicyDiv.current) return; -// const value: string = JSON.stringify(this.props.indexingPolicyContent, undefined, 4); -// const monaco = await loadMonaco(); + // const value: string = JSON.stringify(this.props.indexingPolicyContent, undefined, 4); + // const monaco = await loadMonaco(); -// // Safely get Fluent UI theme colors -// const bodyStyles = getComputedStyle(document.body); -// const backgroundColor = bodyStyles.getPropertyValue("--colorNeutralBackground1").trim() || "#1b1a19"; -// const foregroundColor = bodyStyles.getPropertyValue("--colorNeutralForeground1").trim() || "#ffffff"; + // // Safely get Fluent UI theme colors + // const bodyStyles = getComputedStyle(document.body); + // const backgroundColor = bodyStyles.getPropertyValue("--colorNeutralBackground1").trim() || "#1b1a19"; + // const foregroundColor = bodyStyles.getPropertyValue("--colorNeutralForeground1").trim() || "#ffffff"; -// // Define Monaco theme using Fluent UI colors -// monaco.editor.defineTheme(monacoThemeName, { -// base: isDarkMode ? "vs-dark" : "vs", -// inherit: true, -// rules: [], -// colors: { -// "editor.background": backgroundColor, -// "editor.foreground": foregroundColor, -// "editorCursor.foreground": "#ffcc00", -// "editorLineNumber.foreground": "#aaaaaa", -// "editor.selectionBackground": "#666666", -// "editor.lineHighlightBackground": "#333333" -// } -// }); + // // Define Monaco theme using Fluent UI colors + // monaco.editor.defineTheme(monacoThemeName, { + // base: isDarkMode ? "vs-dark" : "vs", + // inherit: true, + // rules: [], + // colors: { + // "editor.background": backgroundColor, + // "editor.foreground": foregroundColor, + // "editorCursor.foreground": "#ffcc00", + // "editorLineNumber.foreground": "#aaaaaa", + // "editor.selectionBackground": "#666666", + // "editor.lineHighlightBackground": "#333333" + // } + // }); -// // Create the editor with the custom theme -// this.indexingPolicyEditor = monaco.editor.create(this.indexingPolicyDiv.current, { -// value, -// language: "json", -// readOnly: isIndexTransforming(this.props.indexTransformationProgress), -// ariaLabel: "Indexing Policy", -// theme: monacoThemeName -// }); + // // Create the editor with the custom theme + // this.indexingPolicyEditor = monaco.editor.create(this.indexingPolicyDiv.current, { + // value, + // language: "json", + // readOnly: isIndexTransforming(this.props.indexTransformationProgress), + // ariaLabel: "Indexing Policy", + // theme: monacoThemeName + // }); -// if (this.indexingPolicyEditor) { -// const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel(); -// indexingPolicyEditorModel?.onDidChangeContent(this.onEditorContentChange.bind(this)); -// this.props.logIndexingPolicySuccessMessage(); -// } -// } + // if (this.indexingPolicyEditor) { + // const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel(); + // indexingPolicyEditorModel?.onDidChangeContent(this.onEditorContentChange.bind(this)); + // this.props.logIndexingPolicySuccessMessage(); + // } + // } private onEditorContentChange = (): void => { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.test.tsx new file mode 100644 index 000000000..dfde4ec78 --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.test.tsx @@ -0,0 +1,41 @@ +import { shallow } from "enzyme"; +import { + PartitionKeyComponent, + PartitionKeyComponentProps, +} from "Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent"; +import Explorer from "Explorer/Explorer"; +import React from "react"; + +describe("PartitionKeyComponent", () => { + // Create a test setup function to get fresh instances for each test + const setupTest = () => { + // Create an instance of the mocked Explorer + const explorer = new Explorer(); + // Create minimal mock objects for database and collection + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mockDatabase = {} as any as import("../../../../Contracts/ViewModels").Database; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mockCollection = {} as any as import("../../../../Contracts/ViewModels").Collection; + + // Create props with the mocked Explorer instance + const props: PartitionKeyComponentProps = { + database: mockDatabase, + collection: mockCollection, + explorer, + }; + + return { explorer, props }; + }; + + it("renders default component and matches snapshot", () => { + const { props } = setupTest(); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it("renders read-only component and matches snapshot", () => { + const { props } = setupTest(); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx index 2d74df8f9..7c2c42663 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx @@ -29,16 +29,26 @@ export interface PartitionKeyComponentProps { database: ViewModels.Database; collection: ViewModels.Collection; explorer: Explorer; + isReadOnly?: boolean; // true: cannot change partition key } -export const PartitionKeyComponent: React.FC = ({ database, collection, explorer }) => { +export const PartitionKeyComponent: React.FC = ({ + database, + collection, + explorer, + isReadOnly, +}) => { const { dataTransferJobs } = useDataTransferJobs(); const [portalDataTransferJob, setPortalDataTransferJob] = React.useState(null); React.useEffect(() => { + if (isReadOnly) { + return; + } + const loadDataTransferJobs = refreshDataTransferOperations; loadDataTransferJobs(); - }, []); + }, [isReadOnly]); React.useEffect(() => { const currentJob = findPortalDataTransferJob(); @@ -56,14 +66,14 @@ export const PartitionKeyComponent: React.FC = ({ da const partitionKeyValue = getPartitionKeyValue(); const textHeadingStyle = { - root: { fontWeight: FontWeights.semibold, fontSize: 16, color: 'var(--colorNeutralForeground1)' }, + root: { fontWeight: FontWeights.semibold, fontSize: 16, color: "var(--colorNeutralForeground1)" }, }; const textSubHeadingStyle = { - root: { fontWeight: FontWeights.semibold , color: 'var(--colorNeutralForeground1)' }, + root: { fontWeight: FontWeights.semibold, color: "var(--colorNeutralForeground1)" }, }; const textSubHeadingStyle1 = { - root: {color: 'var(--colorNeutralForeground1)' }, + root: { color: "var(--colorNeutralForeground1)" }, }; const startPollingforUpdate = (currentJob: DataTransferJobGetResults) => { if (isCurrentJobInProgress(currentJob)) { @@ -153,7 +163,7 @@ export const PartitionKeyComponent: React.FC = ({ da return ( - Change {partitionKeyName.toLowerCase()} + {!isReadOnly && Change {partitionKeyName.toLowerCase()}} Current {partitionKeyName.toLowerCase()} @@ -161,60 +171,67 @@ export const PartitionKeyComponent: React.FC = ({ da {partitionKeyValue} - {isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"} + + {isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"} + - - To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to the - source container for the entire duration of the partition key change process. - - Learn more - - - - To change the partition key, a new destination container must be created or an existing destination container - selected. Data will then be copied to the destination container. - - {configContext.platform !== Platform.Emulator && ( - - )} - {portalDataTransferJob && ( - - {partitionKeyName} change job - - - {isCurrentJobInProgress(portalDataTransferJob) && ( - cancelRunningDataTransferJob(portalDataTransferJob)} /> - )} - - + + {!isReadOnly && ( + <> + + To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to + the source container for the entire duration of the partition key change process. + + Learn more + + + + To change the partition key, a new destination container must be created or an existing destination + container selected. Data will then be copied to the destination container. + + {configContext.platform !== Platform.Emulator && ( + + )} + {portalDataTransferJob && ( + + {partitionKeyName} change job + + + {isCurrentJobInProgress(portalDataTransferJob) && ( + cancelRunningDataTransferJob(portalDataTransferJob)} /> + )} + + + )} + )} ); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx index b0a9e1739..e54f1459c 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.test.tsx @@ -9,6 +9,7 @@ describe("ScaleComponent", () => { collection: collection, database: undefined, isFixedContainer: false, + isGlobalSecondaryIndex: false, onThroughputChange: () => { return; }, diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx index 251a3b841..1928a68e6 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx @@ -22,6 +22,7 @@ export interface ScaleComponentProps { collection: ViewModels.Collection; database: ViewModels.Database; isFixedContainer: boolean; + isGlobalSecondaryIndex: boolean; onThroughputChange: (newThroughput: number) => void; throughput: number; throughputBaseline: number; @@ -143,6 +144,7 @@ export class ScaleComponent extends React.Component { throughputError={this.props.throughputError} instantMaximumThroughput={this.offer?.instantMaximumThroughput} softAllowedMaximumThroughput={this.offer?.softAllowedMaximumThroughput} + isGlobalSecondaryIndex={this.props.isGlobalSecondaryIndex} /> ); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.test.tsx index 4658a969a..f9c710cb3 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.test.tsx @@ -143,4 +143,39 @@ describe("SubSettingsComponent", () => { expect(subSettingsComponentInstance.getTtlValue(TtlType.On)).toEqual(TtlOn); expect(subSettingsComponentInstance.getTtlValue(TtlType.Off)).toEqual(TtlOff); }); + + it("uniqueKey is visible", () => { + updateUserContext({ + databaseAccount: { + properties: { + capabilities: [{ name: "EnableSQL" }], + }, + } as DatabaseAccount, + }); + const subSettingsComponent = new SubSettingsComponent(baseProps); + expect(subSettingsComponent.getUniqueKeyVisible()).toEqual(true); + }); + + it("uniqueKey not visible due to no keys", () => { + const props = { + ...baseProps, + ...(baseProps.collection.rawDataModel.uniqueKeyPolicy.uniqueKeys = []), + }; + const subSettingsComponent = new SubSettingsComponent(props); + expect(subSettingsComponent.getUniqueKeyVisible()).toEqual(false); + }); + + it("uniqueKey not visible for API", () => { + const newContainer = new Explorer(); + updateUserContext({ + databaseAccount: { + properties: { + capabilities: [{ name: "EnableMongo" }], + }, + } as DatabaseAccount, + }); + const props = { ...baseProps, container: newContainer }; + const subSettingsComponent = new SubSettingsComponent(props); + expect(subSettingsComponent.getUniqueKeyVisible()).toEqual(false); + }); }); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx index a58b4517f..ed05a8960 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx @@ -1,4 +1,15 @@ -import { ChoiceGroup, IChoiceGroupOption, Label, Link, MessageBar, Stack, Text, TextField, getTheme, mergeStyleSets } from "@fluentui/react"; +import { + ChoiceGroup, + IChoiceGroupOption, + Label, + Link, + MessageBar, + Stack, + Text, + TextField, + getTheme, + mergeStyleSets, +} from "@fluentui/react"; import * as React from "react"; import * as ViewModels from "../../../../Contracts/ViewModels"; import { userContext } from "../../../../UserContext"; @@ -29,7 +40,7 @@ const theme = getTheme(); const classNames = mergeStyleSets({ hintText: { - color: 'var(--colorNeutralForeground1)', // theme-aware + color: "var(--colorNeutralForeground1)", // theme-aware }, }); export interface SubSettingsComponentProps { @@ -70,12 +81,16 @@ export class SubSettingsComponent extends React.Component ) : ( - Hierarchically partitioned container. - ) : ( - Non-hierarchically partitioned container. + ) : ( + Non-hierarchically partitioned container. ))} ); @@ -370,6 +385,28 @@ export class SubSettingsComponent extends React.Component this.props.collection.partitionKey?.version >= 2; public isHierarchicalPartitionedContainer = (): boolean => this.props.collection.partitionKey?.kind === "MultiHash"; + public getUniqueKeyVisible = (): boolean => { + return this.props.collection.rawDataModel.uniqueKeyPolicy?.uniqueKeys.length > 0 && userContext.apiType === "SQL"; + }; + + private getUniqueKeyValue = (): string => { + const paths = this.props.collection.rawDataModel.uniqueKeyPolicy?.uniqueKeys?.[0]?.paths; + return paths?.join(", ") || ""; + }; + + private getUniqueKeyComponent = (): JSX.Element => ( + + {this.getUniqueKeyVisible() && ( + + )} + + ); + public render(): JSX.Element { return ( @@ -382,6 +419,8 @@ export class SubSettingsComponent extends React.Component ); } diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent.test.tsx index 460dfa252..2ad5c819e 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent.test.tsx @@ -26,7 +26,7 @@ describe("ThroughputBucketsComponent", () => { it("renders the correct number of buckets", () => { render(); - expect(screen.getAllByText(/Group \d+/)).toHaveLength(5); + expect(screen.getAllByText(/Bucket \d+/)).toHaveLength(5); }); it("renders buckets in the correct order even if input is unordered", () => { @@ -36,8 +36,14 @@ describe("ThroughputBucketsComponent", () => { ]; render(); - const bucketLabels = screen.getAllByText(/Group \d+/).map((el) => el.textContent); - expect(bucketLabels).toEqual(["Group 1 (Data Explorer Query Bucket)", "Group 2", "Group 3", "Group 4", "Group 5"]); + const bucketLabels = screen.getAllByText(/Bucket \d+/).map((el) => el.textContent); + expect(bucketLabels).toEqual([ + "Bucket 1 (Data Explorer Query Bucket)", + "Bucket 2", + "Bucket 3", + "Bucket 4", + "Bucket 5", + ]); }); it("renders all provided buckets even if they exceed the max default bucket count", () => { @@ -53,7 +59,7 @@ describe("ThroughputBucketsComponent", () => { render(); - expect(screen.getAllByText(/Group \d+/)).toHaveLength(7); + expect(screen.getAllByText(/Bucket \d+/)).toHaveLength(7); expect(screen.getByDisplayValue("50")).toBeInTheDocument(); expect(screen.getByDisplayValue("60")).toBeInTheDocument(); @@ -171,7 +177,7 @@ describe("ThroughputBucketsComponent", () => { it("ensures default buckets are used when no buckets are provided", () => { render(); - expect(screen.getAllByText(/Group \d+/)).toHaveLength(5); + expect(screen.getAllByText(/Bucket \d+/)).toHaveLength(5); expect(screen.getAllByDisplayValue("100")).toHaveLength(5); }); }); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent.tsx index a9408b1e4..a0b85cabf 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent.tsx @@ -76,7 +76,7 @@ export const ThroughputBucketsComponent: FC = ( value={bucket.maxThroughputPercentage} onChange={(newValue) => handleBucketChange(bucket.id, newValue)} showValue={false} - label={`Group ${bucket.id}${bucket.id === 1 ? " (Data Explorer Query Bucket)" : ""}`} + label={`Bucket ${bucket.id}${bucket.id === 1 ? " (Data Explorer Query Bucket)" : ""}`} styles={{ root: { flex: 2, maxWidth: 400 } }} disabled={bucket.maxThroughputPercentage === 100} /> diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx index ff7b715b3..a02d907b3 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx @@ -44,6 +44,7 @@ describe("ThroughputInputAutoPilotV3Component", () => { }, instantMaximumThroughput: 5000, softAllowedMaximumThroughput: 1000000, + isGlobalSecondaryIndex: false, }; it("throughput input visible", () => { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx index 9284454c6..3f086a7d9 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx @@ -80,6 +80,7 @@ export interface ThroughputInputAutoPilotV3Props { throughputError?: string; instantMaximumThroughput: number; softAllowedMaximumThroughput: number; + isGlobalSecondaryIndex: boolean; } interface ThroughputInputAutoPilotV3State { @@ -235,12 +236,12 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< ); return (
- Updated cost per month + Updated cost per month - + {newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)} min - + {newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)} max @@ -253,12 +254,12 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< return ( {newThroughput && newThroughputCostElement()} - Current cost per month - - + Current cost per month + + {prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)} min - + {prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)} max @@ -269,8 +270,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< return getEstimatedSpendingElement(costElement(), newThroughput ?? throughput, numberOfRegions, prices, true); }; settingsAndScaleStyle = { - root: { width: "33%", - color: 'var(--colorNeutralForeground1)' }, + root: { width: "33%", color: "var(--colorNeutralForeground1)" }, }; private getEstimatedManualSpendElement = ( throughput: number, @@ -287,40 +287,40 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< serverId, numberOfRegions, isMultimaster, - true, + false, ); return (
- Updated cost per month + Updated cost per month - + {newPrices.currencySign} {calculateEstimateNumber(newPrices.hourlyPrice)}/hr - + {newPrices.currencySign} {calculateEstimateNumber(newPrices.dailyPrice)}/day - + {newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}/mo
); }; - + const costElement = (): JSX.Element => { const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, false); return ( {newThroughput && newThroughputCostElement()} - Current cost per month + Current cost per month - + {prices.currencySign} {calculateEstimateNumber(prices.hourlyPrice)}/hr - + {prices.currencySign} {calculateEstimateNumber(prices.dailyPrice)}/day - + {prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}/mo @@ -378,22 +378,26 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< toolTipElement={getToolTipContainer(this.props.infoBubbleText)} /> - {this.overrideWithProvisionedThroughputSettings() && ( - - {manualToAutoscaleDisclaimerElement} - + {!this.props.isGlobalSecondaryIndex && ( + <> + {this.overrideWithProvisionedThroughputSettings() && ( + + {manualToAutoscaleDisclaimerElement} + + )} + + )} - ); }; @@ -405,8 +409,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< const capacity: string = this.props.isFixed ? "Fixed" : "Unlimited"; return ( - - {capacity} + + {capacity} ); }; @@ -557,26 +561,81 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< private getThroughputTextField = (): JSX.Element => ( <> {this.props.isAutoPilotSelected ? ( - { - const sanitizedValue = getSanitizedInputValue(value); - return sanitizedValue % 1000 - ? "Throughput value must be in increments of 1000" - : this.props.throughputError; - }} - validateOnLoad={false} - /> + + {/* Column 1: Minimum RU/s */} + + + + Minimum RU/s + + + + + {AutoPilotUtils.getMinRUsBasedOnUserInput(this.props.maxAutoPilotThroughput)} + + + + {/* Column 2: "x 10 =" Text */} + + x 10 = + + + {/* Column 3: Maximum RU/s */} + + + + Maximum RU/s + + + + { + const sanitizedValue = getSanitizedInputValue(value); + return sanitizedValue % 1000 + ? "Throughput value must be in increments of 1000" + : this.props.throughputError; + }} + validateOnLoad={false} + /> + + ) : ( )} {this.props.isAutoPilotSelected ? ( - + Based on usage, your {this.props.collectionName ? "container" : "database"} throughput will scale from{" "} {AutoPilotUtils.getMinRUsBasedOnUserInput(this.props.maxAutoPilotThroughput)} RU/s (10% of max RU/s) -{" "} @@ -633,7 +692,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< )} {!this.overrideWithProvisionedThroughputSettings() && ( - + Estimate your required RU/s with {` capacity calculator`} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap index 202d92fd8..f4e300d91 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap @@ -157,35 +157,148 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = ` } } > - + verticalAlign="end" + > + + + + Minimum RU/s + + + + + 400 + + + + x 10 = + + + + + Maximum RU/s + + + + + +
- {this.props.label && {this.props.label}} + {this.props.label && ( + {this.props.label} + )} {this.props.toolTipElement && ( + + + Change + partition key + + + + + Current + partition key + + + Partitioning + + + + + + Non-hierarchical + + + + + + To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to the source container for the entire duration of the partition key change process. + + Learn more + + + + To change the partition key, a new destination container must be created or an existing destination container selected. Data will then be copied to the destination container. + + + +`; + +exports[`PartitionKeyComponent renders read-only component and matches snapshot 1`] = ` + + + + + + Current + partition key + + + Partitioning + + + + + + Non-hierarchical + + + + + +`; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/SubSettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/SubSettingsComponent.test.tsx.snap index db6960740..9d5471ccc 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/SubSettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/SubSettingsComponent.test.tsx.snap @@ -231,6 +231,34 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = ` Non-hierarchically partitioned container. + + + `; @@ -520,6 +548,34 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = ` Non-hierarchically partitioned container. + + +
`; @@ -769,6 +825,34 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = ` Non-hierarchically partitioned container. + + + `; @@ -1083,6 +1167,34 @@ exports[`SubSettingsComponent renders 1`] = ` Non-hierarchically partitioned container. + + + `; @@ -1371,5 +1483,33 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = ` Non-hierarchically partitioned container. + + + `; diff --git a/src/Explorer/Controls/Settings/SettingsUtils.tsx b/src/Explorer/Controls/Settings/SettingsUtils.tsx index 900ad6ab0..2617af6ac 100644 --- a/src/Explorer/Controls/Settings/SettingsUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsUtils.tsx @@ -1,6 +1,7 @@ import * as Constants from "../../../Common/Constants"; import * as DataModels from "../../../Contracts/DataModels"; import * as ViewModels from "../../../Contracts/ViewModels"; +import { isFabricNative } from "../../../Platform/Fabric/FabricUtil"; import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types"; const zeroValue = 0; @@ -57,6 +58,7 @@ export enum SettingsV2TabTypes { ComputedPropertiesTab, ContainerVectorPolicyTab, ThroughputBucketsTab, + GlobalSecondaryIndexTab, } export enum ContainerPolicyTabTypes { @@ -164,13 +166,15 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => { case SettingsV2TabTypes.IndexingPolicyTab: return "Indexing Policy"; case SettingsV2TabTypes.PartitionKeyTab: - return "Partition Keys (preview)"; + return isFabricNative() ? "Partition Keys" : "Partition Keys (preview)"; case SettingsV2TabTypes.ComputedPropertiesTab: return "Computed Properties"; case SettingsV2TabTypes.ContainerVectorPolicyTab: return "Container Policies"; case SettingsV2TabTypes.ThroughputBucketsTab: return "Throughput Buckets"; + case SettingsV2TabTypes.GlobalSecondaryIndexTab: + return "Global Secondary Index (Preview)"; default: throw new Error(`Unknown tab ${tab}`); } diff --git a/src/Explorer/Controls/Settings/TestUtils.tsx b/src/Explorer/Controls/Settings/TestUtils.tsx index c158e5cba..96dfbddb3 100644 --- a/src/Explorer/Controls/Settings/TestUtils.tsx +++ b/src/Explorer/Controls/Settings/TestUtils.tsx @@ -17,7 +17,15 @@ export const collection = { includedPaths: [], excludedPaths: [], }), - uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy, + rawDataModel: { + uniqueKeyPolicy: { + uniqueKeys: [ + { + paths: ["/id"], + }, + ], + }, + }, usageSizeInKB: ko.observable(100), offer: ko.observable({ autoscaleMaxThroughput: undefined, @@ -48,6 +56,15 @@ export const collection = { ]), vectorEmbeddingPolicy: ko.observable({} as DataModels.VectorEmbeddingPolicy), fullTextPolicy: ko.observable({} as DataModels.FullTextPolicy), + materializedViews: ko.observable([ + { id: "view1", _rid: "rid1" }, + { id: "view2", _rid: "rid2" }, + ]), + materializedViewDefinition: ko.observable({ + definition: "SELECT * FROM c WHERE c.id = 1", + sourceCollectionId: "source1", + sourceCollectionRid: "rid123", + }), readSettings: () => { return; }, diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index ea6fe2864..732bdf3a7 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -60,6 +60,8 @@ exports[`SettingsComponent renders 1`] = ` "getDatabase": [Function], "id": [Function], "indexingPolicy": [Function], + "materializedViewDefinition": [Function], + "materializedViews": [Function], "offer": [Function], "partitionKey": { "kind": "hash", @@ -69,14 +71,25 @@ exports[`SettingsComponent renders 1`] = ` "partitionKeyProperties": [ "partitionKey", ], + "rawDataModel": { + "uniqueKeyPolicy": { + "uniqueKeys": [ + { + "paths": [ + "/id", + ], + }, + ], + }, + }, "readSettings": [Function], - "uniqueKeyPolicy": {}, "usageSizeInKB": [Function], "vectorEmbeddingPolicy": [Function], } } isAutoPilotSelected={false} isFixedContainer={false} + isGlobalSecondaryIndex={true} onAutoPilotSelected={[Function]} onMaxAutoPilotThroughputChange={[Function]} onScaleDiscardableChange={[Function]} @@ -139,6 +152,8 @@ exports[`SettingsComponent renders 1`] = ` "getDatabase": [Function], "id": [Function], "indexingPolicy": [Function], + "materializedViewDefinition": [Function], + "materializedViews": [Function], "offer": [Function], "partitionKey": { "kind": "hash", @@ -148,8 +163,18 @@ exports[`SettingsComponent renders 1`] = ` "partitionKeyProperties": [ "partitionKey", ], + "rawDataModel": { + "uniqueKeyPolicy": { + "uniqueKeys": [ + { + "paths": [ + "/id", + ], + }, + ], + }, + }, "readSettings": [Function], - "uniqueKeyPolicy": {}, "usageSizeInKB": [Function], "vectorEmbeddingPolicy": [Function], } @@ -173,6 +198,32 @@ exports[`SettingsComponent renders 1`] = ` timeToLiveSecondsBaseline={5} /> + + + + + +
diff --git a/src/Explorer/Controls/ThroughputInput/CostEstimateText/CostEstimateText.tsx b/src/Explorer/Controls/ThroughputInput/CostEstimateText/CostEstimateText.tsx index fbc469f47..4667a1a74 100644 --- a/src/Explorer/Controls/ThroughputInput/CostEstimateText/CostEstimateText.tsx +++ b/src/Explorer/Controls/ThroughputInput/CostEstimateText/CostEstimateText.tsx @@ -1,4 +1,4 @@ -import { Text } from "@fluentui/react"; +import { Stack, Text } from "@fluentui/react"; import React, { FunctionComponent } from "react"; import { InfoTooltip } from "../../../../Common/Tooltip/InfoTooltip"; import * as SharedConstants from "../../../../Shared/Constants"; @@ -44,33 +44,42 @@ export const CostEstimateText: FunctionComponent = ({ const currencySign: string = getCurrencySign(serverId); const multiplier = getMultimasterMultiplier(numberOfRegions, multimasterEnabled); const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multiplier) : getPricePerRu(serverId, multiplier); + const estimatedMonthlyCost = "Estimated monthly cost"; - const iconWithEstimatedCostDisclaimer: JSX.Element = {estimatedCostDisclaimer}; + const iconWithEstimatedCostDisclaimer: JSX.Element = ( + + {estimatedCostDisclaimer} + + ); if (isAutoscale) { return ( - - Estimated monthly cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "} - - {currencySign + calculateEstimateNumber(monthlyPrice / 10)} -{" "} - {currencySign + calculateEstimateNumber(monthlyPrice)}{" "} - - ({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits / 10} - {requestUnits}{" "} - RU/s, {currencySign + pricePerRu}/RU) - + + + {estimatedMonthlyCost} ({currency}){iconWithEstimatedCostDisclaimer}:{" "} + + {currencySign + calculateEstimateNumber(monthlyPrice / 10)} -{" "} + {currencySign + calculateEstimateNumber(monthlyPrice)}{" "} + + ({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits / 10} - {requestUnits}{" "} + RU/s, {currencySign + pricePerRu}/RU) + + ); } return ( - - Estimated cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "} - - {currencySign + calculateEstimateNumber(hourlyPrice)} hourly /{" "} - {currencySign + calculateEstimateNumber(dailyPrice)} daily /{" "} - {currencySign + calculateEstimateNumber(monthlyPrice)} monthly{" "} - - ({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits}RU/s,{" "} - {currencySign + pricePerRu}/RU) - + + + Estimated cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "} + + {currencySign + calculateEstimateNumber(hourlyPrice)} hourly /{" "} + {currencySign + calculateEstimateNumber(dailyPrice)} daily /{" "} + {currencySign + calculateEstimateNumber(monthlyPrice)} monthly{" "} + + ({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits}RU/s,{" "} + {currencySign + pricePerRu}/RU) + + ); }; diff --git a/src/Explorer/Controls/ThroughputInput/ThroughputInput.less b/src/Explorer/Controls/ThroughputInput/ThroughputInput.less index 059b210f6..52feab10a 100644 --- a/src/Explorer/Controls/ThroughputInput/ThroughputInput.less +++ b/src/Explorer/Controls/ThroughputInput/ThroughputInput.less @@ -28,3 +28,16 @@ .deleteQuery:focus::after { outline: none !important; } + +// Override Fluent UI TextField focus styles +.throughputInputContainer { + :global { + .ms-TextField { + .ms-TextField-fieldGroup { + &:focus-within { + border-color: var(--colorCompoundBrandStroke1, @SelectionColor) !important; + } + } + } + } +} diff --git a/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx b/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx index 9fb5b28f9..13a0923f3 100644 --- a/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx +++ b/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx @@ -1,15 +1,16 @@ -import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react"; +import { Checkbox, DirectionalHint, Link, Separator, Stack, Text, TextField, TooltipHost } from "@fluentui/react"; import { getWorkloadType } from "Common/DatabaseAccountUtility"; +import { CostEstimateText } from "Explorer/Controls/ThroughputInput/CostEstimateText/CostEstimateText"; import { useDatabases } from "Explorer/useDatabases"; import React, { FunctionComponent, useEffect, useState } from "react"; import * as Constants from "../../../Common/Constants"; import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip"; +import { isFabricNative } from "../../../Platform/Fabric/FabricUtil"; import * as SharedConstants from "../../../Shared/Constants"; import { userContext } from "../../../UserContext"; import { getCollectionName } from "../../../Utils/APITypeUtils"; import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; import * as PricingUtils from "../../../Utils/PricingUtils"; -import { CostEstimateText } from "./CostEstimateText/CostEstimateText"; import "./ThroughputInput.less"; export interface ThroughputInputProps { @@ -18,6 +19,7 @@ export interface ThroughputInputProps { isFreeTier: boolean; showFreeTierExceedThroughputTooltip: boolean; isQuickstart?: boolean; + isGlobalSecondaryIndex?: boolean; setThroughputValue: (throughput: number) => void; setIsAutoscale: (isAutoscale: boolean) => void; setIsThroughputCapExceeded: (isThroughputCapExceeded: boolean) => void; @@ -30,6 +32,7 @@ export const ThroughputInput: FunctionComponent = ({ isFreeTier, showFreeTierExceedThroughputTooltip, isQuickstart, + isGlobalSecondaryIndex, setThroughputValue, setIsAutoscale, setIsThroughputCapExceeded, @@ -38,7 +41,9 @@ export const ThroughputInput: FunctionComponent = ({ let defaultThroughput: number; const workloadType: Constants.WorkloadType = getWorkloadType(); - if ( + if (isFabricNative()) { + defaultThroughput = AutoPilotUtils.autoPilotThroughput5K; + } else if ( isFreeTier || isQuickstart || [Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType) @@ -193,88 +198,127 @@ export const ThroughputInput: FunctionComponent = ({ {PricingUtils.getRuToolTipText()} + {!isGlobalSecondaryIndex && ( + +
+ handleOnChangeMode(e, "Autoscale")} + /> + - -
- handleOnChangeMode(e, "Autoscale")} - /> - - - handleOnChangeMode(e, "Manual")} - /> - -
-
+ handleOnChangeMode(e, "Manual")} + /> + +
+
+ )} {isAutoscaleSelected && ( - - Estimate your required RU/s with{" "} - - capacity calculator - - . + + Your container throughput will automatically scale up to the maximum value you select, from a minimum of 10% + of that value. + + + + + Minimum RU/s + + The minimum RU/s your container will scale to + + + {Math.round(throughput / 10).toString()} + + - - - {isDatabase ? "Database" : getCollectionName()} Max RU/s + + x 10 = - {getAutoScaleTooltip()} + + + + + Maximum RU/s + + {getAutoScaleTooltip()} + + onThroughputValueChange(newInput)} + step={AutoPilotUtils.autoPilotIncrementStep} + min={AutoPilotUtils.autoPilotThroughput1K} + max={isSharded ? Number.MAX_SAFE_INTEGER.toString() : "10000"} + value={throughput.toString()} + ariaLabel={`${isDatabase ? "Database" : getCollectionName()} max RU/s`} + required={true} + errorMessage={throughputError} + /> + - onThroughputValueChange(newInput)} - step={AutoPilotUtils.autoPilotIncrementStep} - min={AutoPilotUtils.autoPilotThroughput1K} - max={isSharded ? Number.MAX_SAFE_INTEGER.toString() : "10000"} - value={throughput.toString()} - ariaLabel={`${isDatabase ? "Database" : getCollectionName()} max RU/s`} - required={true} - errorMessage={throughputError} - /> - - - Your {isDatabase ? "database" : getCollectionName().toLocaleLowerCase()} throughput will automatically scale - from{" "} - - {AutoPilotUtils.getMinRUsBasedOnUserInput(throughput)} RU/s (10% of max RU/s) - {throughput} RU/s - {" "} - based on usage. - + + + + Estimate your required RU/s with  + + capacity calculator + + . + + + )} @@ -298,7 +342,6 @@ export const ThroughputInput: FunctionComponent = ({ {getAutoScaleTooltip()} - = ({ errorMessage={throughputError} /> + )} - - {throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && ( - Estimate your required RU/s with - - - - - capacity calculator - - - - . + Your container throughput will automatically scale up to the maximum value you select, from a minimum of 10% of that value.
- +
+ +
+ + + Minimum RU/s + + + + + + +
+ + + +  + + + + +
+
+
+
+
+
+
+ + + 400 + + +
+ + - Container - Max RU/s + x 10 = - - - + - + + + Maximum RU/s + + + + + + +
+ + + +  + + + + +
+
+
+
+
+
+
+ +
- - - -  - - -
- - - - +
+
+
+
- + +
+ + + Estimate your required RU/s with  + + + + capacity calculator + + + + . + + +
+
+ -
-
- -
-
+ aria-orientation="horizontal" + className="content-135" + role="separator" + />
-
-
- - - Your - container - throughput will automatically scale from - - - 400 - RU/s (10% of max RU/s) - - 4000 - RU/s - - - based on usage. - - + +
-
`; diff --git a/src/Explorer/Controls/V9Components/Button/index.tsx b/src/Explorer/Controls/V9Components/Button/index.tsx new file mode 100644 index 000000000..8c152bbf9 --- /dev/null +++ b/src/Explorer/Controls/V9Components/Button/index.tsx @@ -0,0 +1,56 @@ +import { + Button as FluentButton, + makeStyles, + tokens, +} from "@fluentui/react-components"; +import * as React from "react"; + +export type CustomButtonProps = { + primary?: boolean; + className?: string; + children?: React.ReactNode; + onClick?: (ev: React.MouseEvent) => void; + disabled?: boolean; + type?: "button" | "submit" | "reset"; +}; + +const useStyles = makeStyles({ + button: { + backgroundColor: tokens.colorNeutralBackground1, + color: tokens.colorNeutralForeground1, + "&:hover": { + backgroundColor: tokens.colorNeutralBackground1Hover, + color: tokens.colorNeutralForeground1Hover, + }, + "&:active": { + backgroundColor: tokens.colorNeutralBackground1Pressed, + color: tokens.colorNeutralForeground1Pressed, + }, + }, + primary: { + backgroundColor: tokens.colorBrandBackground, + color: tokens.colorNeutralForegroundOnBrand, + "&:hover": { + backgroundColor: tokens.colorBrandBackgroundHover, + }, + "&:active": { + backgroundColor: tokens.colorBrandBackgroundPressed, + }, + }, +}); + +export const Button = React.forwardRef( + ({ primary, className, ...props }, ref) => { + const baseStyles = useStyles(); + const buttonClassName = primary ? baseStyles.primary : baseStyles.button; + + return ( + + ); + } +); diff --git a/src/Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent.tsx b/src/Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent.tsx index 03d1ab1e6..ba0c5b62b 100644 --- a/src/Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent.tsx +++ b/src/Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent.tsx @@ -9,6 +9,7 @@ import { Stack, TextField, } from "@fluentui/react"; +import { InfoTooltip } from "Common/Tooltip/InfoTooltip"; import { VectorEmbedding, VectorIndex } from "Contracts/DataModels"; import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent"; import { @@ -29,6 +30,7 @@ export interface IVectorEmbeddingPoliciesComponentProps { discardChanges?: boolean; onChangesDiscarded?: () => void; disabled?: boolean; + isGlobalSecondaryIndex?: boolean; } export interface VectorEmbeddingPolicyData { @@ -39,8 +41,7 @@ export interface VectorEmbeddingPolicyData { indexType: VectorIndex["type"] | "none"; pathError: string; dimensionsError: string; - diskANNShardKey?: string; - diskANNShardKeyError?: string; + vectorIndexShardKey?: string[]; indexingSearchListSize?: number; indexingSearchListSizeError?: string; quantizationByteSize?: number; @@ -87,6 +88,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent { const onVectorEmbeddingPathError = (path: string, index?: number): string => { let error = ""; @@ -132,12 +134,6 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent { - // return ""; - // }; - const initializeData = (vectorEmbeddings: VectorEmbedding[], vectorIndexes: VectorIndex[]) => { const mergedData: VectorEmbeddingPolicyData[] = []; vectorEmbeddings.forEach((embedding) => { @@ -147,6 +143,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent) => { - // const value = event.target.value.trim(); - // const vectorEmbeddings = [...vectorEmbeddingPolicyData]; - // if (!vectorEmbeddings[index]?.diskANNShardKey && !value.startsWith("/")) { - // vectorEmbeddings[index].diskANNShardKey = "/" + value; - // } else { - // vectorEmbeddings[index].diskANNShardKey = value; - // } - // const error = onDiskANNShardKeyError(value); - // vectorEmbeddings[index].diskANNShardKeyError = error; - // setVectorEmbeddingPolicyData(vectorEmbeddings); - // } + const onShardKeyChange = (index: number, event: React.ChangeEvent) => { + const value = event.target.value.trim(); + const vectorEmbeddings = [...vectorEmbeddingPolicyData]; + if (!vectorEmbeddings[index]?.vectorIndexShardKey?.[0] && !value.startsWith("/")) { + vectorEmbeddings[index].vectorIndexShardKey = ["/" + value]; + } else { + vectorEmbeddings[index].vectorIndexShardKey = [value]; + } + setVectorEmbeddingPolicyData(vectorEmbeddings); + }; const onVectorEmbeddingPolicyChange = ( index: number, @@ -292,6 +286,11 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent { + const containerName: string = isGlobalSecondaryIndex ? "global secondary index" : "container"; + return `This is dynamically set by the ${containerName} if left blank, or it can be set to a fixed number`; + }; + return ( {vectorEmbeddingPolicyData && @@ -402,6 +401,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent Quantization byte size + {getQuantizationByteSizeTooltipContent()} - {/*TODO: uncomment after Ignite */} - {/* DiskANNShardKey was removed for Ignite due to backend problems. Leaving this here as it will be reinstated immediately after Ignite - - + + ) => - onDiskANNShardKeyChange(index, event) - } + value={String(vectorEmbeddingPolicy.vectorIndexShardKey?.[0] ?? "")} + onChange={(event: React.ChangeEvent) => onShardKeyChange(index, event)} /> - */} )} diff --git a/src/Explorer/DataExplorer.tsx b/src/Explorer/DataExplorer.tsx index 18c112377..0e5b66517 100644 --- a/src/Explorer/DataExplorer.tsx +++ b/src/Explorer/DataExplorer.tsx @@ -1,10 +1,9 @@ import { makeStyles } from "@fluentui/react-components"; import React from "react"; import type { Explorer } from "../Contracts/ViewModels"; -import { useTheme } from "../hooks/useTheme"; interface DataExplorerProps { - dataExplorer: Explorer; + dataExplorer?: Explorer; } const useStyles = makeStyles({ @@ -12,15 +11,13 @@ const useStyles = makeStyles({ backgroundColor: "var(--colorNeutralBackground1)", color: "var(--colorNeutralForeground1)", height: "100%", - width: "100%" - } + width: "100%", + }, }); export const DataExplorer: React.FC = ({ dataExplorer }) => { - const { isDarkMode } = useTheme(); const styles = useStyles(); - return (
Data Explorer Content
diff --git a/src/Explorer/DataSamples/ContainerSampleGenerator.ts b/src/Explorer/DataSamples/ContainerSampleGenerator.ts index ace021718..7613b7a07 100644 --- a/src/Explorer/DataSamples/ContainerSampleGenerator.ts +++ b/src/Explorer/DataSamples/ContainerSampleGenerator.ts @@ -1,3 +1,4 @@ +import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils"; import { createCollection } from "../../Common/dataAccess/createCollection"; import { createDocument } from "../../Common/dataAccess/createDocument"; import { createDocument as createMongoDocument } from "../../Common/MongoProxyClient"; @@ -90,12 +91,13 @@ export class ContainerSampleGenerator { } const { databaseAccount: account } = userContext; const databaseId = collection.databaseId; + const gremlinClient = new GremlinClient(); gremlinClient.initialize({ endpoint: `wss://${GraphTab.getGremlinEndpoint(account)}`, databaseId: databaseId, collectionId: collection.id(), - masterKey: userContext.masterKey || "", + password: useDataplaneRbacAuthorization(userContext) ? userContext.aadToken : userContext.masterKey || "", maxResultSize: 100, }); diff --git a/src/Explorer/DataSamples/DataSamplesUtil.ts b/src/Explorer/DataSamples/DataSamplesUtil.ts index d28ef0426..514322fed 100644 --- a/src/Explorer/DataSamples/DataSamplesUtil.ts +++ b/src/Explorer/DataSamples/DataSamplesUtil.ts @@ -6,6 +6,7 @@ import Explorer from "../Explorer"; import { useDatabases } from "../useDatabases"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; +// TODO: this does not seem to be used. Remove? export class DataSamplesUtil { private static readonly DialogTitle = "Create Sample Container"; constructor(private container: Explorer) {} diff --git a/src/Explorer/ErrorBoundary.tsx b/src/Explorer/ErrorBoundary.tsx index 68bd62b00..d92325b59 100644 --- a/src/Explorer/ErrorBoundary.tsx +++ b/src/Explorer/ErrorBoundary.tsx @@ -1,4 +1,4 @@ -import React, { Component, ErrorInfo, ReactNode } from "react"; +import React, { Component, ReactNode } from "react"; interface Props { children: ReactNode; @@ -19,7 +19,8 @@ export class ErrorBoundary extends Component { return { hasError: true, error }; } - public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + public componentDidCatch(error: Error): void { + console.error("Error caught in boundary:", error); } public render() { diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index ace0aaffe..91ad1b20b 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -8,10 +8,16 @@ import { MessageTypes } from "Contracts/ExplorerContracts"; import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane"; import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { IGalleryItem } from "Juno/JunoClient"; -import { isFabricMirrored, isFabricMirroredKey, scheduleRefreshFabricToken } from "Platform/Fabric/FabricUtil"; +import { + isFabricMirrored, + isFabricMirroredKey, + isFabricNative, + scheduleRefreshFabricToken, +} from "Platform/Fabric/FabricUtil"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils"; import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils"; +import { featureRegistered } from "Utils/FeatureRegistrationUtils"; import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import * as ko from "knockout"; @@ -30,6 +36,7 @@ import { readDatabases } from "../Common/dataAccess/readDatabases"; import * as DataModels from "../Contracts/DataModels"; import { ContainerConnectionInfo, IPhoenixServiceInfo, IProvisionData, IResponse } from "../Contracts/DataModels"; import * as ViewModels from "../Contracts/ViewModels"; +import { UploadDetailsRecord } from "../Contracts/ViewModels"; import { GitHubOAuthService } from "../GitHub/GitHubOAuthService"; import { PhoenixClient } from "../Phoenix/PhoenixClient"; import * as ExplorerSettings from "../Shared/ExplorerSettings"; @@ -55,7 +62,7 @@ import type NotebookManager from "./Notebook/NotebookManager"; import { NotebookPaneContent } from "./Notebook/NotebookManager"; import { NotebookUtil } from "./Notebook/NotebookUtil"; import { useNotebook } from "./Notebook/useNotebook"; -import { AddCollectionPanel } from "./Panes/AddCollectionPanel"; +import { AddCollectionPanel } from "./Panes/AddCollectionPanel/AddCollectionPanel"; import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane"; import { StringInputPane } from "./Panes/StringInputPane/StringInputPane"; @@ -282,6 +289,68 @@ export default class Explorer { } } + /** + * Generates a VS Code DocumentDB connection URL using the current user's MongoDB connection parameters. + * Double-encodes the updated connection string for safe usage in VS Code URLs. + * + * The DocumentDB VS Code extension requires double encoding for connection strings. + * See: https://microsoft.github.io/vscode-documentdb/manual/how-to-construct-url.html#double-encoding + * + * @returns {string} The encoded VS Code DocumentDB connection URL. + */ + private getDocumentDbUrl() { + const { adminLogin: adminLoginuserName = "", connectionString = "" } = userContext.vcoreMongoConnectionParams; + const updatedConnectionString = connectionString.replace(/<(user|username)>:/i, adminLoginuserName); + const encodedUpdatedConnectionString = encodeURIComponent(encodeURIComponent(updatedConnectionString)); + const documentDbUrl = `vscode://ms-azuretools.vscode-documentdb?connectionString=${encodedUpdatedConnectionString}`; + return documentDbUrl; + } + + private getCosmosDbUrl() { + const activeTab = useTabs.getState().activeTab; + const resourceId = encodeURIComponent(userContext.databaseAccount.id); + const database = encodeURIComponent(activeTab?.collection?.databaseId); + const container = encodeURIComponent(activeTab?.collection?.id()); + const baseUrl = `vscode://ms-azuretools.vscode-cosmosdb?resourceId=${resourceId}`; + const vscodeUrl = activeTab ? `${baseUrl}&database=${database}&container=${container}` : baseUrl; + return vscodeUrl; + } + + private getVSCodeUrl(): string { + const isvCore = (userContext.apiType || userContext.databaseAccount.kind) === "VCoreMongo"; + return isvCore ? this.getDocumentDbUrl() : this.getCosmosDbUrl(); + } + + public openInVsCode(): void { + const vscodeUrl = this.getVSCodeUrl(); + const openVSCodeDialogProps: DialogProps = { + linkProps: { + linkText: "Download Visual Studio Code", + linkUrl: "https://code.visualstudio.com/download", + }, + isModal: true, + title: `Open your Azure Cosmos DB account in Visual Studio Code`, + subText: `Please ensure Visual Studio Code is installed on your device. + If you don't have it installed, please download it from the link below.`, + primaryButtonText: "Open in VS Code", + secondaryButtonText: "Cancel", + + onPrimaryButtonClick: () => { + try { + window.location.href = vscodeUrl; + TelemetryProcessor.traceStart(Action.OpenVSCode); + } catch (error) { + logConsoleError(`Failed to open VS Code: ${getErrorMessage(error)}`); + } + }, + onSecondaryButtonClick: () => { + useDialog.getState().closeDialog(); + TelemetryProcessor.traceCancel(Action.OpenVSCode); + }, + }; + useDialog.getState().openDialog(openVSCodeDialogProps); + } + public async openCESCVAFeedbackBlade(): Promise { sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade }); Logger.logInfo( @@ -910,7 +979,9 @@ export default class Explorer { } public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise { - if (useNotebook.getState().isPhoenixFeatures) { + if (userContext.features.enableCloudShell) { + this.connectToNotebookTerminal(kind); + } else if (useNotebook.getState().isPhoenixFeatures) { await this.allocateContainer(PoolIdType.DefaultPoolId); const notebookServerInfo = useNotebook.getState().notebookServerInfo; if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) { @@ -1076,8 +1147,8 @@ export default class Explorer { } } - public openUploadItemsPane(): void { - useSidePanel.getState().openSidePanel("Upload " + getUploadName(), ); + public openUploadItemsPane(onUpload?: (data: UploadDetailsRecord[]) => void): void { + useSidePanel.getState().openSidePanel("Upload " + getUploadName(), ); } public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void { useSidePanel @@ -1085,7 +1156,7 @@ export default class Explorer { .openSidePanel("Input parameters", ); } - public getDownloadModalConent(fileName: string): JSX.Element { + public getDownloadModalContent(fileName: string): JSX.Element { if (useNotebook.getState().isPhoenixNotebooks) { return ( <> @@ -1109,7 +1180,10 @@ export default class Explorer { ? this.refreshDatabaseForResourceToken() : await this.refreshAllDatabases(); // await: we rely on the databases to be loaded before restoring the tabs further in the flow } - await useNotebook.getState().refreshNotebooksEnabledStateForAccount(); + + if (!isFabricNative()) { + await useNotebook.getState().refreshNotebooksEnabledStateForAccount(); + } // TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount const isNotebookEnabled = @@ -1131,6 +1205,11 @@ export default class Explorer { await this.initNotebooks(userContext.databaseAccount); } + if (userContext.authType === AuthType.AAD && userContext.apiType === "SQL" && !isFabricNative()) { + const throughputBucketsEnabled = await featureRegistered(userContext.subscriptionId, "ThroughputBucketing"); + updateUserContext({ throughputBucketsEnabled }); + } + this.refreshSampleData(); } diff --git a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx index e408adac7..ea9be4c72 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx @@ -163,8 +163,7 @@ describe("GraphExplorer", () => { graphBackendEndpoint: "graphBackendEndpoint", databaseId: "databaseId", collectionId: "collectionId", - masterKey: "masterKey", - + password: "password", onLoadStartKey: 0, onLoadStartKeyChange: (newKey: number): void => {}, resourceId: "resourceId", diff --git a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx index 5ca296b57..5077bc189 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx @@ -16,7 +16,12 @@ import * as StorageUtility from "../../../Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "../../../Shared/StorageUtility"; import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; -import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils"; +import { + logConsoleError, + logConsoleInfo, + logConsoleProgress, + logConsoleWarning, +} from "../../../Utils/NotificationConsoleUtils"; import { EditorReact } from "../../Controls/Editor/EditorReact"; import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent"; import * as TabComponent from "../../Controls/Tabs/TabComponent"; @@ -54,7 +59,7 @@ export interface GraphExplorerProps { graphBackendEndpoint: string; databaseId: string; collectionId: string; - masterKey: string; + password: string; onLoadStartKey: number; onLoadStartKeyChange: (newKey: number) => void; @@ -1083,6 +1088,7 @@ export class GraphExplorer extends React.Component void; public static reportToConsole(type: ConsoleDataType.Info, msg: string, ...errorData: any[]): void; public static reportToConsole(type: ConsoleDataType.Error, msg: string, ...errorData: any[]): void; + public static reportToConsole(type: ConsoleDataType.Warning, msg: string, ...errorData: any[]): void; public static reportToConsole(type: ConsoleDataType, msg: string, ...errorData: any[]): void | (() => void) { let errorDataStr = ""; if (errorData && errorData.length > 0) { @@ -1099,6 +1105,8 @@ export class GraphExplorer extends React.Component { endpoint: null, collectionId: null, databaseId: null, - masterKey: null, maxResultSize: 10000, + password: null, }; - it("should use databaseId, collectionId and masterKey to authenticate", () => { + it("should use databaseId, collectionId and password to authenticate", () => { const collectionId = "collectionId"; const databaseId = "databaseId"; - const masterKey = "masterKey"; + const testPassword = "password"; const gremlinClient = new GremlinClient(); gremlinClient.initialize({ endpoint: null, collectionId, databaseId, - masterKey, maxResultSize: 0, + password: testPassword, }); // User must includes these values expect(gremlinClient.client.params.user.indexOf(collectionId)).not.toBe(-1); expect(gremlinClient.client.params.user.indexOf(databaseId)).not.toBe(-1); - expect(gremlinClient.client.params.password).toEqual(masterKey); + expect(gremlinClient.client.params.password).toEqual(testPassword); }); it("should aggregate RU charges across multiple responses", (done) => { diff --git a/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts b/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts index 764025e82..ad0c177ac 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts +++ b/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts @@ -11,8 +11,8 @@ export interface GremlinClientParameters { endpoint: string; databaseId: string; collectionId: string; - masterKey: string; maxResultSize: number; + password: string; } export interface GremlinRequestResult { @@ -43,7 +43,7 @@ export class GremlinClient { this.client = new GremlinSimpleClient({ endpoint: params.endpoint, user: `/dbs/${params.databaseId}/colls/${params.collectionId}`, - password: params.masterKey, + password: params.password, successCallback: (result: Result) => { this.storePendingResult(result); this.flushResult(result.requestId); diff --git a/src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.ts b/src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.ts index 09a9ff927..db86485ad 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.ts +++ b/src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.ts @@ -5,11 +5,11 @@ import * as sinon from "sinon"; import { + GremlinRequestMessage, + GremlinResponseMessage, GremlinSimpleClient, GremlinSimpleClientParameters, Result, - GremlinRequestMessage, - GremlinResponseMessage, } from "./GremlinSimpleClient"; describe("Gremlin Simple Client", () => { diff --git a/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.less b/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.less index 6993cbe39..25723ace3 100644 --- a/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.less +++ b/src/Explorer/Graph/NewVertexComponent/NewVertexComponent.less @@ -95,3 +95,10 @@ white-space: nowrap; } } + +@media (max-width: 768px) { + .newVertexComponent { + padding: 0; + width: 100%; + } +} \ No newline at end of file diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index 66b480723..abb986fe3 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -38,8 +38,8 @@ export const useCommandBar: UseStore = create((set) => ({ const useStyles = makeStyles({ commandBarContainer: { - borderBottom: "1px solid var(--colorNeutralStroke1)" - } + borderBottom: "1px solid var(--colorNeutralStroke1)", + }, }); export const CommandBar: React.FC = ({ container }: Props) => { @@ -62,10 +62,10 @@ export const CommandBar: React.FC = ({ container }: Props) => { ariaLabel="Use left and right arrow keys to navigate between commands" items={CommandBarUtil.convertButton(buttons, "var(--colorNeutralBackground1)")} styles={{ - root: { + root: { backgroundColor: "var(--colorNeutralBackground1)", - color: "var(--colorNeutralForeground1)" - } + color: "var(--colorNeutralForeground1)", + }, }} overflowButtonProps={{ ariaLabel: "More commands" }} /> @@ -84,7 +84,10 @@ export const CommandBar: React.FC = ({ container }: Props) => { uiFabricStaticButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true)); } - const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(contextButtons, "var(--colorNeutralBackground1)"); + const uiFabricTabsButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton( + contextButtons, + "var(--colorNeutralBackground1)", + ); if (uiFabricTabsButtons.length > 0) { uiFabricStaticButtons.push(CommandBarUtil.createDivider("commandBarDivider")); @@ -109,14 +112,14 @@ export const CommandBar: React.FC = ({ container }: Props) => { root: { backgroundColor: "var(--colorNeutralBackground1)", padding: "2px 8px 0px 8px", - color: "var(--colorNeutralForeground1)" - } + color: "var(--colorNeutralForeground1)", + }, } : { root: { backgroundColor: "var(--colorNeutralBackground1)", - color: "var(--colorNeutralForeground1)" - } + color: "var(--colorNeutralForeground1)", + }, }; const allButtons = staticButtons.concat(contextButtons).concat(controlButtons); diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 49c513a0a..ad21c0932 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -14,6 +14,7 @@ import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg"; import OpenInTabIcon from "../../../../images/open-in-tab.svg"; import SettingsIcon from "../../../../images/settings_15x15.svg"; import SynapseIcon from "../../../../images/synapse-link.svg"; +import VSCodeIcon from "../../../../images/vscode.svg"; import { AuthType } from "../../../AuthType"; import * as Constants from "../../../Common/Constants"; import { Platform, configContext } from "../../../ConfigContext"; @@ -60,6 +61,10 @@ export function createStaticCommandBarButtons( addDivider(); buttons.push(addSynapseLink); } + if (userContext.apiType !== "Gremlin") { + const addVsCode = createOpenVsCodeDialogButton(container); + buttons.push(addVsCode); + } } if (isDataplaneRbacSupported(userContext.apiType)) { @@ -126,13 +131,14 @@ export function createContextCommandBarButtons( const buttons: CommandButtonComponentProps[] = []; if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") { - const label = useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell"; + const label = + useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell ? "Open Mongo Shell" : "New Shell"; const newMongoShellBtn: CommandButtonComponentProps = { iconSrc: HostedTerminalIcon, iconAlt: label, onCommandClick: () => { const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); - if (useNotebook.getState().isShellEnabled) { + if (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) { container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); } else { selectedCollection && selectedCollection.onNewMongoShellClick(); @@ -146,7 +152,7 @@ export function createContextCommandBarButtons( } if ( - useNotebook.getState().isShellEnabled && + (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) && !selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Cassandra" ) { @@ -267,6 +273,18 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo }; } +function createOpenVsCodeDialogButton(container: Explorer): CommandButtonComponentProps { + const label = "Visual Studio Code"; + return { + iconSrc: VSCodeIcon, + iconAlt: label, + onCommandClick: () => container.openInVsCode(), + commandButtonLabel: label, + hasPopup: false, + ariaLabel: label, + }; +} + function createLoginForEntraIDButton(container: Explorer): CommandButtonComponentProps { if (configContext.platform !== Platform.Portal) { return undefined; @@ -352,6 +370,22 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState) disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected() || selectedNodeState.isDatabaseNodeOrNoneSelected(), + styles: { + root: { + backgroundColor: "var(--colorNeutralBackground1)", + color: "var(--colorNeutralForeground1)", + selectors: { + ":hover": { + backgroundColor: "var(--colorNeutralBackground1Hover)", + color: "var(--colorNeutralForeground1Hover)", + }, + ":active": { + backgroundColor: "var(--colorNeutralBackground1Pressed)", + color: "var(--colorNeutralForeground1Pressed)", + }, + }, + }, + }, }; buttons.push(newStoredProcedureBtn); } @@ -372,6 +406,22 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState) disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected() || selectedNodeState.isDatabaseNodeOrNoneSelected(), + styles: { + root: { + backgroundColor: "var(--colorNeutralBackground1)", + color: "var(--colorNeutralForeground1)", + selectors: { + ":hover": { + backgroundColor: "var(--colorNeutralBackground1Hover)", + color: "var(--colorNeutralForeground1Hover)", + }, + ":active": { + backgroundColor: "var(--colorNeutralBackground1Pressed)", + color: "var(--colorNeutralForeground1Pressed)", + }, + }, + }, + }, }; buttons.push(newUserDefinedFunctionBtn); } @@ -392,6 +442,22 @@ export function createScriptCommandButtons(selectedNodeState: SelectedNodeState) disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected() || selectedNodeState.isDatabaseNodeOrNoneSelected(), + styles: { + root: { + backgroundColor: "var(--colorNeutralBackground1)", + color: "var(--colorNeutralForeground1)", + selectors: { + ":hover": { + backgroundColor: "var(--colorNeutralBackground1Hover)", + color: "var(--colorNeutralForeground1Hover)", + }, + ":active": { + backgroundColor: "var(--colorNeutralBackground1Pressed)", + color: "var(--colorNeutralForeground1Pressed)", + }, + }, + }, + }, }; buttons.push(newTriggerBtn); } @@ -455,7 +521,7 @@ function createOpenTerminalButtonByKind( iconSrc: HostedTerminalIcon, iconAlt: label, onCommandClick: () => { - if (useNotebook.getState().isNotebookEnabled) { + if (useNotebook.getState().isNotebookEnabled || userContext.features.enableCloudShell) { container.openNotebookTerminal(terminalKind); } }, @@ -499,6 +565,6 @@ export function createPostgreButtons(container: Explorer): CommandButtonComponen export function createVCoreMongoButtons(container: Explorer): CommandButtonComponentProps[] { const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.VCoreMongo); - - return [openVCoreMongoTerminalButton]; + const addVsCode = createOpenVsCodeDialogButton(container); + return [openVCoreMongoTerminalButton, addVsCode]; } diff --git a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx index 77feb91bd..1f061b913 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx @@ -1,10 +1,10 @@ import { - Dropdown, - ICommandBarItemProps, - IComponentAsProps, - IconType, - IDropdownOption, - IDropdownStyles, + Dropdown, + ICommandBarItemProps, + IComponentAsProps, + IconType, + IDropdownOption, + IDropdownStyles, } from "@fluentui/react"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import { KeyboardHandlerMap } from "KeyboardShortcuts"; @@ -91,24 +91,24 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol selectors: { "&:hover": { backgroundColor: "var(--colorNeutralBackground1Hover)", - color: "var(--colorNeutralForeground1)" + color: "var(--colorNeutralForeground1)", }, "&:active": { backgroundColor: "var(--colorNeutralBackground1Pressed)", - color: "var(--colorNeutralForeground1)" - } - } + color: "var(--colorNeutralForeground1)", + }, + }, }, rootDisabled: { backgroundColor: "var(--colorNeutralBackground1)", pointerEvents: "auto", - color: "var(--colorNeutralForegroundDisabled)" + color: "var(--colorNeutralForegroundDisabled)", }, splitButtonMenuButton: { backgroundColor: "var(--colorNeutralBackground1)", selectors: { - ":hover": { - backgroundColor: "var(--colorNeutralBackground1Hover)" + ":hover": { + backgroundColor: "var(--colorNeutralBackground1Hover)", }, }, width: 16, @@ -118,21 +118,21 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol configContext.platform == Platform.Fabric ? StyleConstants.DefaultFontSize : StyleConstants.mediumFontSize, - color: "var(--colorNeutralForeground1)" + color: "var(--colorNeutralForeground1)", }, - rootHovered: { + rootHovered: { backgroundColor: "var(--colorNeutralBackground1Hover)", - color: "var(--colorNeutralForeground1)" + color: "var(--colorNeutralForeground1)", }, - rootPressed: { + rootPressed: { backgroundColor: "var(--colorNeutralBackground1Pressed)", - color: "var(--colorNeutralForeground1)" + color: "var(--colorNeutralForeground1)", }, splitButtonMenuButtonExpanded: { backgroundColor: "var(--colorNeutralBackground1Pressed)", selectors: { - ":hover": { - backgroundColor: "var(--colorNeutralBackground1Hover)" + ":hover": { + backgroundColor: "var(--colorNeutralBackground1Hover)", }, }, }, @@ -142,7 +142,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol icon: { paddingLeft: 0, paddingRight: 0, - color: "var(--colorNeutralForeground1)" + color: "var(--colorNeutralForeground1)", }, splitButtonContainer: { marginLeft: 5, diff --git a/src/Explorer/Menus/NotificationConsole/ConsoleData.tsx b/src/Explorer/Menus/NotificationConsole/ConsoleData.tsx index 2ede2f003..9307c164f 100644 --- a/src/Explorer/Menus/NotificationConsole/ConsoleData.tsx +++ b/src/Explorer/Menus/NotificationConsole/ConsoleData.tsx @@ -13,4 +13,5 @@ export enum ConsoleDataType { Info = 0, Error = 1, InProgress = 2, + Warning = 3, } diff --git a/src/Explorer/Menus/NotificationConsole/NotificationConsole.less b/src/Explorer/Menus/NotificationConsole/NotificationConsole.less index a03573a72..fb5aed51c 100644 --- a/src/Explorer/Menus/NotificationConsole/NotificationConsole.less +++ b/src/Explorer/Menus/NotificationConsole/NotificationConsole.less @@ -173,8 +173,20 @@ .message { flex-grow: 1; white-space:pre-wrap; + overflow-wrap: break-word; + word-break: break-word; } } } } + + @media (max-width: 768px) { + .notificationConsoleContents { + overflow-y: auto; + + .notificationConsoleData { + overflow: visible; + } + } + } } \ No newline at end of file diff --git a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx index e2f63926f..1288ee5da 100644 --- a/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx +++ b/src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx @@ -14,6 +14,7 @@ import ErrorRedIcon from "../../../../images/error_red.svg"; import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg"; import InfoIcon from "../../../../images/info_color.svg"; import LoadingIcon from "../../../../images/loading.svg"; +import WarningIcon from "../../../../images/warning.svg"; import { ClientDefaults, KeyCodes } from "../../../Common/Constants"; import { userContext } from "../../../UserContext"; import { useNotificationConsole } from "../../../hooks/useNotificationConsole"; @@ -91,6 +92,9 @@ export class NotificationConsoleComponent extends React.Component< const numInfoItems = this.state.allConsoleData.filter( (data: ConsoleData) => data.type === ConsoleDataType.Info, ).length; + const numWarningItems = this.state.allConsoleData.filter( + (data: ConsoleData) => data.type === ConsoleDataType.Warning, + ).length; return (
@@ -118,6 +122,10 @@ export class NotificationConsoleComponent extends React.Component< Info items {numInfoItems} + + Warning items + {numWarningItems} + {userContext.features.pr && } @@ -198,6 +206,7 @@ export class NotificationConsoleComponent extends React.Component< {item.type === ConsoleDataType.Info && info} {item.type === ConsoleDataType.Error && error} {item.type === ConsoleDataType.InProgress && in progress} + {item.type === ConsoleDataType.Warning && warning} {item.date} {item.message} diff --git a/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap b/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap index b2672aeec..5d471f177 100644 --- a/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap +++ b/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap @@ -59,6 +59,19 @@ exports[`NotificationConsoleComponent renders the console 1`] = ` 0 + + Warning items + + 0 + + + + Warning items + + 0 + + )} - {!this.state.errorMessage && this.isFreeTierAccount() && ( + {!this.state.errorMessage && isFreeTierAccount() && ( {!(isFabricNative() && this.props.databaseId !== undefined) && ( -