mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-27 12:51:41 +00:00
Compare commits
78 Commits
users/aisa
...
release/ma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7224dd26c1 | ||
|
|
4c73a1cc47 | ||
|
|
610da6a9a5 | ||
|
|
7812ca4914 | ||
|
|
bc4f18ba79 | ||
|
|
fd2551423d | ||
|
|
508abcd21c | ||
|
|
7774589d60 | ||
|
|
e23ba5ec8c | ||
|
|
75719b3cf0 | ||
|
|
f3f8fd241a | ||
|
|
2dc2e59162 | ||
|
|
4b65760a1d | ||
|
|
ced2725476 | ||
|
|
6b811b5e76 | ||
|
|
69cf523274 | ||
|
|
2e45d8a2a4 | ||
|
|
b5d7423849 | ||
|
|
1529303107 | ||
|
|
8624bf0423 | ||
|
|
db1600d81b | ||
|
|
176bb47cb5 | ||
|
|
b6d17284b5 | ||
|
|
7b7a2817b6 | ||
|
|
083bccfda9 | ||
|
|
14c9874e5e | ||
|
|
a04eaff6be | ||
|
|
51a412e2c0 | ||
|
|
f0e32491d7 | ||
|
|
3fcbdf6152 | ||
|
|
8da078579e | ||
|
|
4ac41031e6 | ||
|
|
c33c497fd9 | ||
|
|
d7923db108 | ||
|
|
cec621443d | ||
|
|
b8017763b7 | ||
|
|
59619a856e | ||
|
|
0170c9e1cc | ||
|
|
b1f016a796 | ||
|
|
ae3912cbf2 | ||
|
|
2730da7ab6 | ||
|
|
de2449ee25 | ||
|
|
99378582ce | ||
|
|
bd592d07af | ||
|
|
644f5941ec | ||
|
|
9fb006a996 | ||
|
|
c2b98c3e23 | ||
|
|
76d49d86d4 | ||
|
|
7893b89bf7 | ||
|
|
5945e3cb6b | ||
|
|
213d1c68fe | ||
|
|
c26f9a1ebb | ||
|
|
bd7cd7ae8f | ||
|
|
6504358580 | ||
|
|
ce88659fca | ||
|
|
642c708e9c | ||
|
|
4156009d09 | ||
|
|
5c6abbd635 | ||
|
|
881726e9af | ||
|
|
7015590d1a | ||
|
|
1d952a4ea2 | ||
|
|
2a81551a60 | ||
|
|
eceee36913 | ||
|
|
96faf92c12 | ||
|
|
2fdb3df4ae | ||
|
|
7c9802c07d | ||
|
|
e5609bd91e | ||
|
|
4b75e86b74 | ||
|
|
abf061089d | ||
|
|
ec25586a6e | ||
|
|
c15d1432b2 | ||
|
|
73d2686025 | ||
|
|
80b926214b | ||
|
|
070b7a4ca7 | ||
|
|
d61ff5dcb5 | ||
|
|
d42eebaa5a | ||
|
|
056be2a74d | ||
|
|
b93c90e7d1 |
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1 +1 @@
|
||||
[Preview this branch](https://cosmos-explorer-preview.azurewebsites.net/pull/EDIT_THIS_NUMBER_IN_THE_PR_DESCRIPTION?feature.someFeatureFlagYouMightNeed=true)
|
||||
[Preview this branch](https://dataexplorer-preview.azurewebsites.net/pull/EDIT_THIS_NUMBER_IN_THE_PR_DESCRIPTION?feature.someFeatureFlagYouMightNeed=true)
|
||||
|
||||
103
.github/workflows/ci.yml
vendored
103
.github/workflows/ci.yml
vendored
@@ -83,7 +83,7 @@ jobs:
|
||||
- run: npm ci
|
||||
- run: npm run build:contracts
|
||||
- name: Restore Build Cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .cache
|
||||
key: ${{ runner.os }}-build-cache
|
||||
@@ -92,18 +92,20 @@ jobs:
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
- run: cp -r ./Contracts ./dist/contracts
|
||||
- run: cp -r ./configs ./dist/configs
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
- name: "Az CLI login"
|
||||
uses: azure/login@v1
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.PREVIEW_SUBSCRIPTION_ID }}
|
||||
- name: Upload build to preview blob storage
|
||||
run: az storage blob upload-batch -d '$web' -s 'dist' --account-name cosmosexplorerpreview --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --account-key="${PREVIEW_STORAGE_KEY}" --overwrite true
|
||||
env:
|
||||
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
|
||||
run: az storage blob upload-batch -d '$web' -s 'dist' --account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --auth-mode login --overwrite true
|
||||
- name: Upload preview config to blob storage
|
||||
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name cosmosexplorerpreview --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}" --overwrite true
|
||||
env:
|
||||
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
|
||||
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --auth-mode login --overwrite true
|
||||
nuget:
|
||||
name: Publish Nuget
|
||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||
@@ -113,21 +115,21 @@ jobs:
|
||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
|
||||
steps:
|
||||
- uses: nuget/setup-nuget@v1
|
||||
with:
|
||||
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
||||
- name: Download Dist Folder
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
- run: cp ./configs/prod.json config.json
|
||||
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
|
||||
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||
- uses: actions/upload-artifact@v3
|
||||
name: packages
|
||||
- run: dotnet nuget add source "$NUGET_SOURCE" --name "ADO" --username "jawelton@microsoft.com" --password "$AZURE_DEVOPS_PAT" --store-password-in-clear-text
|
||||
- run: dotnet pack DataExplorer.proj /p:PackageVersion="2.0.0-github-${GITHUB_SHA}"
|
||||
- run: dotnet nuget push "bin/Release/*.nupkg" --skip-duplicate --api-key Az --source="$NUGET_SOURCE"
|
||||
- run: dotnet nuget remove source "ADO"
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: Upload package to Artifacts
|
||||
with:
|
||||
path: "*.nupkg"
|
||||
name: prod-package
|
||||
path: "bin/Release/*.nupkg"
|
||||
|
||||
nugetmpac:
|
||||
name: Publish Nuget MPAC
|
||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||
@@ -137,22 +139,21 @@ jobs:
|
||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
|
||||
steps:
|
||||
- uses: nuget/setup-nuget@v1
|
||||
with:
|
||||
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
||||
- name: Download Dist Folder
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
- run: cp ./configs/mpac.json config.json
|
||||
- run: sed -i 's/Azure.Cosmos.DB.Data.Explorer/Azure.Cosmos.DB.Data.Explorer.MPAC/g' DataExplorer.nuspec
|
||||
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
|
||||
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||
- uses: actions/upload-artifact@v3
|
||||
name: packages
|
||||
- run: dotnet nuget add source "$NUGET_SOURCE" --name "ADO" --username "jawelton@microsoft.com" --password "$AZURE_DEVOPS_PAT" --store-password-in-clear-text
|
||||
- run: dotnet pack DataExplorer.proj /p:PackageVersion="2.0.0-github-${GITHUB_SHA}"
|
||||
- run: dotnet nuget push "bin/Release/*.nupkg" --skip-duplicate --api-key Az --source="$NUGET_SOURCE"
|
||||
- run: dotnet nuget remove source "ADO"
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: Upload package to Artifacts
|
||||
with:
|
||||
path: "*.nupkg"
|
||||
name: mpac-package
|
||||
path: "bin/Release/*.nupkg"
|
||||
|
||||
playwright-tests:
|
||||
name: "Run Playwright Tests (Shard ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }})"
|
||||
@@ -185,9 +186,9 @@ jobs:
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: blob-report-${{ matrix.shardIndex }}
|
||||
path: blob-report
|
||||
retention-days: 1
|
||||
name: blob-report-${{ matrix.shardIndex }}
|
||||
path: blob-report
|
||||
retention-days: 1
|
||||
|
||||
merge-playwright-reports:
|
||||
name: "Merge Playwright Reports"
|
||||
@@ -197,26 +198,26 @@ jobs:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: all-blob-reports
|
||||
pattern: blob-report-*
|
||||
merge-multiple: true
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: all-blob-reports
|
||||
pattern: blob-report-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Merge into HTML Report
|
||||
run: npx playwright merge-reports --reporter html ./all-blob-reports
|
||||
- name: Merge into HTML Report
|
||||
run: npx playwright merge-reports --reporter html ./all-blob-reports
|
||||
|
||||
- name: Upload HTML report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: html-report--attempt-${{ github.run_attempt }}
|
||||
path: playwright-report
|
||||
retention-days: 14
|
||||
- name: Upload HTML report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: html-report--attempt-${{ github.run_attempt }}
|
||||
path: playwright-report
|
||||
retention-days: 14
|
||||
|
||||
9
DataExplorer.proj
Normal file
9
DataExplorer.proj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<NoBuild>true</NoBuild>
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<NuspecFile>DataExplorer.nuspec</NuspecFile>
|
||||
<NuspecProperties>version=$(PackageVersion)</NuspecProperties>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -174,7 +174,11 @@ module.exports = {
|
||||
},
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
transformIgnorePatterns: ["/node_modules/(?!@fluentui/react-icons)", "/externals/"],
|
||||
transformIgnorePatterns: [
|
||||
"/node_modules/(?!@fluentui/react-icons|(.*)/dist/browser)/",
|
||||
"/node_modules/plotly.js-cartesian-dist-min",
|
||||
"/externals/",
|
||||
],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
|
||||
@GalleryBackgroundColor: #fdfdfd;
|
||||
|
||||
@LinkColor: #2d6da4;
|
||||
|
||||
//Icons
|
||||
@InfoIconColor: #0072c6;
|
||||
@WarningIconColor: #db7500;
|
||||
@@ -246,6 +248,10 @@
|
||||
outline: 1px dashed @FocusColor;
|
||||
}
|
||||
|
||||
.focusedBorder() {
|
||||
border: 1px dashed @FocusColor;
|
||||
}
|
||||
|
||||
/************************************************************************************************
|
||||
Common Toggle Switch
|
||||
*************************************************************************************************/
|
||||
|
||||
@@ -1830,6 +1830,14 @@ input::-webkit-calendar-picker-indicator::after {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.customAccordion button:focus {
|
||||
.focus();
|
||||
}
|
||||
|
||||
.customAccordion {
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.datalist-arrow:after:hover {
|
||||
content: "\276F";
|
||||
position: absolute;
|
||||
|
||||
@@ -26,7 +26,6 @@ a:focus {
|
||||
|
||||
#divExplorer {
|
||||
background-color: #f5f5f5;
|
||||
padding: @FabricBoxMargin;
|
||||
}
|
||||
|
||||
.resourceTreeAndTabs {
|
||||
@@ -38,12 +37,12 @@ a:focus {
|
||||
}
|
||||
|
||||
.tabsManagerContainer {
|
||||
background-color: #ffffff
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.nav-tabs-margin {
|
||||
padding-top: 5px;
|
||||
background-color: #ffffff
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.commandBarContainer {
|
||||
@@ -68,17 +67,16 @@ a:focus {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.nav-tabs>li>.tabNavContentContainer>.tab_Content:hover {
|
||||
.nav-tabs > li > .tabNavContentContainer > .tab_Content:hover {
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content,
|
||||
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content:hover {
|
||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content,
|
||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content:hover {
|
||||
border-bottom: 2px solid @FabricAccentMedium;
|
||||
}
|
||||
|
||||
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content>.contentWrapper>.tabNavText {
|
||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText {
|
||||
border-bottom: 0px none transparent;
|
||||
}
|
||||
|
||||
@@ -97,10 +95,10 @@ a:focus {
|
||||
padding-bottom: @SmallSpace;
|
||||
|
||||
.contentWrapper {
|
||||
.statusIconContainer {
|
||||
margin-left: 0px;
|
||||
}
|
||||
.statusIconContainer {
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.tabIconSection {
|
||||
.cancelButton {
|
||||
@@ -122,7 +120,6 @@ a:focus {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.resourceTree {
|
||||
padding: 12px;
|
||||
}
|
||||
@@ -159,24 +156,21 @@ a:focus {
|
||||
}
|
||||
|
||||
.selected {
|
||||
&>.treeNodeHeader {
|
||||
& > .treeNodeHeader {
|
||||
background-color: @FabricAccentExtra;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.dataExplorerErrorConsoleContainer {
|
||||
border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius;
|
||||
border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius;
|
||||
box-shadow: @FabricBoxBorderShadow;
|
||||
margin-top: 0px;
|
||||
width: auto;
|
||||
align-self: auto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.filterbtnstyle {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
@@ -202,12 +196,10 @@ a:focus {
|
||||
border: solid 1px #d1d1d1;
|
||||
}
|
||||
|
||||
|
||||
.gridRowSelected .tabdocumentsGridElement:hover {
|
||||
background-color: @FabricAccentLight !important;
|
||||
}
|
||||
|
||||
|
||||
.refreshcol {
|
||||
filter: brightness(0) saturate(100%);
|
||||
}
|
||||
@@ -218,4 +210,4 @@ a:focus {
|
||||
|
||||
.fileImportImg img {
|
||||
filter: brightness(0) saturate(100%);
|
||||
}
|
||||
}
|
||||
|
||||
765
package-lock.json
generated
765
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -5,10 +5,9 @@
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/cosmos": "4.0.1-beta.3",
|
||||
"@azure/cosmos": "4.2.0-beta.1",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@azure/identity": "1.5.2",
|
||||
"@azure/ms-rest-nodeauth": "3.1.1",
|
||||
"@azure/identity": "4.5.0",
|
||||
"@azure/msal-browser": "2.14.2",
|
||||
"@babel/plugin-proposal-class-properties": "7.12.1",
|
||||
"@babel/plugin-proposal-decorators": "7.12.12",
|
||||
@@ -117,7 +116,7 @@
|
||||
"@babel/preset-env": "7.24.7",
|
||||
"@babel/preset-react": "7.24.7",
|
||||
"@babel/preset-typescript": "7.24.7",
|
||||
"@playwright/test": "1.44.0",
|
||||
"@playwright/test": "1.49.1",
|
||||
"@testing-library/react": "11.2.3",
|
||||
"@types/applicationinsights-js": "1.0.7",
|
||||
"@types/codemirror": "0.0.56",
|
||||
@@ -170,10 +169,10 @@
|
||||
"jest": "29.7.0",
|
||||
"jest-canvas-mock": "2.5.2",
|
||||
"jest-circus": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"jest-html-loader": "1.0.0",
|
||||
"jest-react-hooks-shallow": "1.5.1",
|
||||
"jest-trx-results-processor": "3.0.2",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"less": "3.8.1",
|
||||
"less-loader": "11.1.3",
|
||||
"less-vars-loader": "1.1.0",
|
||||
@@ -247,4 +246,4 @@
|
||||
"printWidth": 120,
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[defaults]
|
||||
group = stfaul
|
||||
sku = P1v2
|
||||
appserviceplan = stfaul_asp_Linux_centralus_0
|
||||
location = centralus
|
||||
web = cosmos-explorer-preview
|
||||
group = dataexplorer-preview
|
||||
sku = P1V2
|
||||
appserviceplan = dataexplorer-preview
|
||||
location = westus2
|
||||
web = dataexplorer-preview
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ Cosmos Explorer Preview makes it possible to try a working version of any commit
|
||||
|
||||
Initial support is for Hosted (Connection string only) or the Azure Portal. Examples:
|
||||
|
||||
Connection string URLs: https://cosmos-explorer-preview.azurewebsites.net/commit/COMMIT_SHA/hostedExplorer.html
|
||||
Portal URLs: https://ms.portal.azure.com/?dataExplorerSource=https://cosmos-explorer-preview.azurewebsites.net/commit/COMMIT_SHA/explorer.html#home
|
||||
Connection string URLs: https://dataexplorer-preview.azurewebsites.net/commit/COMMIT_SHA/hostedExplorer.html
|
||||
Portal URLs: https://ms.portal.azure.com/?dataExplorerSource=https://dataexplorer-preview.azurewebsites.net/commit/COMMIT_SHA/explorer.html#home
|
||||
|
||||
In both cases replace `COMMIT_SHA` with the commit you want to view. It must have already completed its build on GitHub Actions.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"PROXY_PATH": "/proxy",
|
||||
"msalRedirectURI": "https://cosmos-explorer-preview.azurewebsites.net/"
|
||||
"msalRedirectURI": "https://dataexplorer-preview.azurewebsites.net/"
|
||||
}
|
||||
|
||||
@@ -3,8 +3,15 @@ const { createProxyMiddleware } = require("http-proxy-middleware");
|
||||
const port = process.env.PORT || 3000;
|
||||
const fetch = require("node-fetch");
|
||||
|
||||
const api = createProxyMiddleware("/api", {
|
||||
target: "https://cdb-ms-mpac-pbe.cosmos.azure.com",
|
||||
const backendEndpoint = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
|
||||
const previewSiteEndpoint = "https://dataexplorer-preview.azurewebsites.net";
|
||||
const previewStorageWebsiteEndpoint = "https://dataexplorerpreview.z5.web.core.windows.net/";
|
||||
const githubApiUrl = "https://api.github.com/repos/Azure/cosmos-explorer";
|
||||
const githubPullRequestUrl = "https://github.com/Azure/cosmos-explorer/pull";
|
||||
const azurePortalMpacEndpoint = "https://ms.portal.azure.com/";
|
||||
|
||||
const api = createProxyMiddleware({
|
||||
target: backendEndpoint,
|
||||
changeOrigin: true,
|
||||
logLevel: "debug",
|
||||
bypass: (req, res) => {
|
||||
@@ -15,8 +22,8 @@ const api = createProxyMiddleware("/api", {
|
||||
},
|
||||
});
|
||||
|
||||
const proxy = createProxyMiddleware("/proxy", {
|
||||
target: "https://cdb-ms-mpac-pbe.cosmos.azure.com",
|
||||
const proxy = createProxyMiddleware({
|
||||
target: backendEndpoint,
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
logLevel: "debug",
|
||||
@@ -27,35 +34,38 @@ const proxy = createProxyMiddleware("/proxy", {
|
||||
},
|
||||
});
|
||||
|
||||
const commit = createProxyMiddleware("/commit", {
|
||||
target: "https://cosmosexplorerpreview.blob.core.windows.net",
|
||||
const commit = createProxyMiddleware({
|
||||
target: previewStorageWebsiteEndpoint,
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
logLevel: "debug",
|
||||
pathRewrite: { "^/commit": "$web/" },
|
||||
pathRewrite: { "^/commit": "/" },
|
||||
});
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(api);
|
||||
app.use(proxy);
|
||||
app.use(commit);
|
||||
app.use("/api", api);
|
||||
app.use("/proxy", proxy);
|
||||
app.use("/commit", commit);
|
||||
app.get("/pull/:pr(\\d+)", (req, res) => {
|
||||
const pr = req.params.pr;
|
||||
if (!/^\d+$/.test(pr)) {
|
||||
return res.status(400).send("Invalid pull request number");
|
||||
}
|
||||
const [, query] = req.originalUrl.split("?");
|
||||
const search = new URLSearchParams(query);
|
||||
|
||||
fetch("https://api.github.com/repos/Azure/cosmos-explorer/pulls/" + pr)
|
||||
fetch(`${githubApiUrl}/pulls/${pr}`)
|
||||
.then((response) => response.json())
|
||||
.then(({ head: { ref, sha } }) => {
|
||||
const prUrl = new URL("https://github.com/Azure/cosmos-explorer/pull/" + pr);
|
||||
const prUrl = new URL(`${githubPullRequestUrl}/${pr}`);
|
||||
prUrl.hash = ref;
|
||||
search.set("feature.pr", prUrl.href);
|
||||
|
||||
const explorer = new URL("https://cosmos-explorer-preview.azurewebsites.net/commit/" + sha + "/explorer.html");
|
||||
const explorer = new URL(`${previewSiteEndpoint}/commit/${sha}/explorer.html`);
|
||||
explorer.search = search.toString();
|
||||
|
||||
const portal = new URL("https://ms.portal.azure.com/");
|
||||
const portal = new URL(azurePortalMpacEndpoint);
|
||||
portal.searchParams.set("dataExplorerSource", explorer.href);
|
||||
|
||||
return res.redirect(portal.href);
|
||||
@@ -63,12 +73,10 @@ app.get("/pull/:pr(\\d+)", (req, res) => {
|
||||
.catch(() => res.sendStatus(500));
|
||||
});
|
||||
app.get("/", (req, res) => {
|
||||
fetch("https://api.github.com/repos/Azure/cosmos-explorer/branches/master")
|
||||
fetch(`${githubApiUrl}/branches/master`)
|
||||
.then((response) => response.json())
|
||||
.then(({ commit: { sha } }) => {
|
||||
const explorer = new URL(
|
||||
"https://cosmos-explorer-preview.azurewebsites.net/commit/" + sha + "/hostedExplorer.html"
|
||||
);
|
||||
const explorer = new URL(`${previewSiteEndpoint}/commit/${sha}/hostedExplorer.html`);
|
||||
return res.redirect(explorer.href);
|
||||
})
|
||||
.catch(() => res.sendStatus(500));
|
||||
|
||||
1360
preview/package-lock.json
generated
1360
preview/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"deploy": "az webapp up --name \"cosmos-explorer-preview\" --subscription \"cosmosdb-portalteam-generaltest-msft\" --resource-group \"stfaul\"",
|
||||
"deploy": "az webapp up --name \"dataexplorer-preview\" --subscription \"cosmosdb-portalteam-runners\" --resource-group \"dataexplorer-preview\" --runtime \"NODE:18-lts\" --sku P1V2",
|
||||
"start": "node index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
@@ -12,7 +12,8 @@
|
||||
"author": "Microsoft Corporation",
|
||||
"dependencies": {
|
||||
"express": "^4.17.1",
|
||||
"http-proxy-middleware": "^1.1.0",
|
||||
"http-proxy-middleware": "^3.0.3",
|
||||
"node": "^18.20.6",
|
||||
"node-fetch": "^2.6.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ export class CapabilityNames {
|
||||
public static readonly EnableMongo: string = "EnableMongo";
|
||||
public static readonly EnableServerless: string = "EnableServerless";
|
||||
public static readonly EnableNoSQLVectorSearch: string = "EnableNoSQLVectorSearch";
|
||||
public static readonly EnableNoSQLFullTextSearch: string = "EnableNoSQLFullTextSearch";
|
||||
}
|
||||
|
||||
export enum CapacityMode {
|
||||
@@ -96,6 +97,12 @@ export enum CapacityMode {
|
||||
Serverless = "Serverless",
|
||||
}
|
||||
|
||||
export enum WorkloadType {
|
||||
Learning = "Learning",
|
||||
DevelopmentTesting = "Development/Testing",
|
||||
Production = "Production",
|
||||
None = "None",
|
||||
}
|
||||
// flight names returned from the portal are always lowercase
|
||||
export class Flights {
|
||||
public static readonly SettingsV2 = "settingsv2";
|
||||
@@ -118,6 +125,7 @@ export class AfecFeatures {
|
||||
|
||||
export class TagNames {
|
||||
public static defaultExperience: string = "defaultExperience";
|
||||
public static WorkloadType: string = "hidden-workload-type";
|
||||
}
|
||||
|
||||
export class MongoDBAccounts {
|
||||
@@ -130,6 +138,15 @@ 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";
|
||||
@@ -146,6 +163,18 @@ export class MongoProxyEndpoints {
|
||||
public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn";
|
||||
}
|
||||
|
||||
export class MongoProxyApi {
|
||||
public static readonly ResourceList: string = "ResourceList";
|
||||
public static readonly QueryDocuments: string = "QueryDocuments";
|
||||
public static readonly CreateDocument: string = "CreateDocument";
|
||||
public static readonly ReadDocument: string = "ReadDocument";
|
||||
public static readonly UpdateDocument: string = "UpdateDocument";
|
||||
public static readonly DeleteDocument: string = "DeleteDocument";
|
||||
public static readonly CreateCollectionWithProxy: string = "CreateCollectionWithProxy";
|
||||
public static readonly LegacyMongoShell: string = "LegacyMongoShell";
|
||||
public static readonly BulkDelete: string = "BulkDelete";
|
||||
}
|
||||
|
||||
export class CassandraProxyEndpoints {
|
||||
public static readonly Development: string = "https://localhost:7240";
|
||||
public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com";
|
||||
@@ -154,6 +183,18 @@ export class CassandraProxyEndpoints {
|
||||
public static readonly Mooncake: string = "https://cdb-mc-prod-cp.cosmos.azure.cn";
|
||||
}
|
||||
|
||||
//TODO: Remove this when new backend is migrated over
|
||||
export class CassandraBackend {
|
||||
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
||||
public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete";
|
||||
public static readonly queryApi: string = "api/cassandra";
|
||||
public static readonly guestQueryApi: string = "api/guest/cassandra";
|
||||
public static readonly keysApi: string = "api/cassandra/keys";
|
||||
public static readonly guestKeysApi: string = "api/guest/cassandra/keys";
|
||||
public static readonly schemaApi: string = "api/cassandra/schema";
|
||||
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
|
||||
}
|
||||
|
||||
export class CassandraProxyAPIs {
|
||||
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
||||
public static readonly connectionStringCreateOrDeleteApi: string = "api/connectionstring/cassandra/createordelete";
|
||||
@@ -484,6 +525,18 @@ export class PriorityLevel {
|
||||
public static readonly Default = "low";
|
||||
}
|
||||
|
||||
export class ariaLabelForLearnMoreLink {
|
||||
public static readonly AnalyticalStore = "Learn more about analytical store.";
|
||||
public static readonly AzureSynapseLink = "Learn more about Azure Synapse Link.";
|
||||
}
|
||||
|
||||
export class MaterializedViewsLabels {
|
||||
public static readonly NewMaterializedView: string = "New Materialized View";
|
||||
}
|
||||
export class FeedbackLabels {
|
||||
public static readonly provideFeedback: string = "Provide feedback";
|
||||
}
|
||||
|
||||
export const QueryCopilotSampleDatabaseId = "CopilotSampleDB";
|
||||
export const QueryCopilotSampleContainerId = "SampleContainer";
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import * as Cosmos from "@azure/cosmos";
|
||||
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
|
||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
||||
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
|
||||
import { checkDatabaseResourceTokensValidity, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { PriorityLevel } from "../Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { Platform, configContext } from "../ConfigContext";
|
||||
import { userContext } from "../UserContext";
|
||||
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";
|
||||
@@ -18,7 +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 && userContext.apiType === "SQL";
|
||||
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType);
|
||||
if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) {
|
||||
Logger.logInfo(
|
||||
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
|
||||
@@ -41,7 +43,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
return decodeURIComponent(headers.authorization);
|
||||
}
|
||||
|
||||
if (configContext.platform === Platform.Fabric) {
|
||||
if (isFabricMirroredKey()) {
|
||||
switch (requestInfo.resourceType) {
|
||||
case Cosmos.ResourceType.conflicts:
|
||||
case Cosmos.ResourceType.container:
|
||||
@@ -53,8 +55,13 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
// User resource tokens
|
||||
// TODO userContext.fabricContext.databaseConnectionInfo can be undefined
|
||||
headers[HttpHeaders.msDate] = new Date().toUTCString();
|
||||
const resourceTokens = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
||||
checkDatabaseResourceTokensValidity(userContext.fabricContext.databaseConnectionInfo.resourceTokensTimestamp);
|
||||
const resourceTokens = (
|
||||
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
|
||||
).resourceTokenInfo.resourceTokens;
|
||||
checkDatabaseResourceTokensValidity(
|
||||
(userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY])
|
||||
.resourceTokenInfo.resourceTokensTimestamp,
|
||||
);
|
||||
return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
|
||||
|
||||
case Cosmos.ResourceType.none:
|
||||
@@ -65,7 +72,9 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
// For now, these operations aren't used, so fetching the authorization token is commented out.
|
||||
// This provider must return a real token to pass validation by the client, so we return the cached resource token
|
||||
// (which is a valid token, but won't work for these operations).
|
||||
const resourceTokens2 = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
||||
const resourceTokens2 = (
|
||||
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
|
||||
).resourceTokenInfo.resourceTokens;
|
||||
return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId);
|
||||
|
||||
/* ************** TODO: Uncomment this code if we need to support these operations **************
|
||||
@@ -156,13 +165,24 @@ let _client: Cosmos.CosmosClient;
|
||||
|
||||
export function client(): Cosmos.CosmosClient {
|
||||
if (_client) {
|
||||
if (!userContext.hasDataPlaneRbacSettingChanged) {
|
||||
if (!userContext.refreshCosmosClient) {
|
||||
return _client;
|
||||
}
|
||||
_client.dispose();
|
||||
_client = null;
|
||||
}
|
||||
|
||||
if (userContext.refreshCosmosClient) {
|
||||
updateUserContext({
|
||||
refreshCosmosClient: false,
|
||||
});
|
||||
}
|
||||
|
||||
let _defaultHeaders: Cosmos.CosmosHeaders = {};
|
||||
|
||||
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
|
||||
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
||||
_defaultHeaders["x-ms-cosmos-throughput-bucket"] = 1;
|
||||
|
||||
if (
|
||||
userContext.authType === AuthType.ConnectionString ||
|
||||
|
||||
34
src/Common/DatabaseAccountUtility.test.ts
Normal file
34
src/Common/DatabaseAccountUtility.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { WorkloadType } from "Common/Constants";
|
||||
import { getWorkloadType } from "Common/DatabaseAccountUtility";
|
||||
import { DatabaseAccount, Tags } from "Contracts/DataModels";
|
||||
import { updateUserContext } from "UserContext";
|
||||
|
||||
describe("Database Account Utility", () => {
|
||||
describe("Workload Type", () => {
|
||||
beforeEach(() => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
tags: {} as Tags,
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
});
|
||||
|
||||
it("Workload Type should return Learning", () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
tags: {
|
||||
"hidden-workload-type": WorkloadType.Learning,
|
||||
} as Tags,
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
const workloadType: WorkloadType = getWorkloadType();
|
||||
expect(workloadType).toBe(WorkloadType.Learning);
|
||||
});
|
||||
|
||||
it("Workload Type should return None", () => {
|
||||
const workloadType: WorkloadType = getWorkloadType();
|
||||
expect(workloadType).toBe(WorkloadType.None);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
import { TagNames, WorkloadType } from "Common/Constants";
|
||||
import { Tags } from "Contracts/DataModels";
|
||||
import { userContext } from "../UserContext";
|
||||
|
||||
function isVirtualNetworkFilterEnabled() {
|
||||
@@ -15,3 +17,16 @@ function isPrivateEndpointConnectionsEnabled() {
|
||||
export function isPublicInternetAccessAllowed(): boolean {
|
||||
return !isVirtualNetworkFilterEnabled() && !isIpRulesEnabled() && !isPrivateEndpointConnectionsEnabled();
|
||||
}
|
||||
|
||||
export function getWorkloadType(): WorkloadType {
|
||||
const tags: Tags = userContext?.databaseAccount?.tags;
|
||||
const workloadType: WorkloadType = tags && (tags[TagNames.WorkloadType] as WorkloadType);
|
||||
if (!workloadType) {
|
||||
return WorkloadType.None;
|
||||
}
|
||||
return workloadType;
|
||||
}
|
||||
|
||||
export function isMaterializedViewsEnabled(): boolean {
|
||||
return userContext.apiType === "SQL" && userContext.databaseAccount?.properties?.enableMaterializedViews;
|
||||
}
|
||||
|
||||
@@ -4,16 +4,8 @@ import { configContext, resetConfigContext, updateConfigContext } from "../Confi
|
||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||
import { Collection } from "../Contracts/ViewModels";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
||||
import { updateUserContext } from "../UserContext";
|
||||
import {
|
||||
deleteDocument,
|
||||
getEndpoint,
|
||||
getFeatureEndpointOrDefault,
|
||||
queryDocuments,
|
||||
readDocument,
|
||||
updateDocument,
|
||||
} from "./MongoProxyClient";
|
||||
import { deleteDocuments, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
|
||||
|
||||
const databaseId = "testDB";
|
||||
|
||||
@@ -196,20 +188,8 @@ describe("MongoProxyClient", () => {
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("builds the correct proxy URL in development", () => {
|
||||
updateConfigContext({
|
||||
MONGO_BACKEND_ENDPOINT: "https://localhost:1234",
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
updateDocument(databaseId, collection, documentId, "{}");
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("deleteDocument", () => {
|
||||
describe("deleteDocuments", () => {
|
||||
beforeEach(() => {
|
||||
resetConfigContext();
|
||||
updateUserContext({
|
||||
@@ -226,9 +206,9 @@ describe("MongoProxyClient", () => {
|
||||
});
|
||||
|
||||
it("builds the correct URL", () => {
|
||||
deleteDocument(databaseId, collection, documentId);
|
||||
deleteDocuments(databaseId, collection, [documentId]);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/bulkdelete`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
@@ -238,9 +218,9 @@ describe("MongoProxyClient", () => {
|
||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
deleteDocument(databaseId, collection, documentId);
|
||||
deleteDocuments(databaseId, collection, [documentId]);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/bulkdelete`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
@@ -275,33 +255,4 @@ describe("MongoProxyClient", () => {
|
||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/connectionstring/mongo/explorer`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFeatureEndpointOrDefault", () => {
|
||||
beforeEach(() => {
|
||||
resetConfigContext();
|
||||
updateConfigContext({
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
"feature.mongoProxyEndpoint": MongoProxyEndpoints.Prod,
|
||||
"feature.mongoProxyAPIs": "readDocument|createDocument",
|
||||
});
|
||||
const features = extractFeatures(params);
|
||||
updateUserContext({
|
||||
authType: AuthType.AAD,
|
||||
features: features,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a local endpoint", () => {
|
||||
const endpoint = getFeatureEndpointOrDefault();
|
||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
||||
});
|
||||
|
||||
it("returns a production endpoint", () => {
|
||||
const endpoint = getFeatureEndpointOrDefault();
|
||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,7 +78,7 @@ export function queryDocuments(
|
||||
query,
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault();
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT) || "";
|
||||
|
||||
const headers = {
|
||||
...defaultHeaders,
|
||||
@@ -141,7 +141,7 @@ export function readDocument(
|
||||
: "",
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault();
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
|
||||
return window
|
||||
.fetch(endpoint, {
|
||||
@@ -183,7 +183,7 @@ export function createDocument(
|
||||
documentContent: JSON.stringify(documentContent),
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault();
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/createDocument`, {
|
||||
@@ -229,7 +229,7 @@ export function updateDocument(
|
||||
: "",
|
||||
documentContent,
|
||||
};
|
||||
const endpoint = getFeatureEndpointOrDefault();
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
|
||||
return window
|
||||
.fetch(endpoint, {
|
||||
@@ -250,46 +250,6 @@ export function updateDocument(
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
const idComponents = documentId.self.split("/");
|
||||
const path = idComponents.slice(0, 5).join("/");
|
||||
const rid = encodeURIComponent(idComponents[5]);
|
||||
const params = {
|
||||
databaseID: databaseId,
|
||||
collectionID: collection.id(),
|
||||
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
||||
resourceID: rid,
|
||||
resourceType: "docs",
|
||||
subscriptionID: userContext.subscriptionId,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
databaseAccountName: databaseAccount.name,
|
||||
partitionKey:
|
||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||
? documentId.partitionKeyProperties?.[0]
|
||||
: "",
|
||||
};
|
||||
const endpoint = getFeatureEndpointOrDefault();
|
||||
|
||||
return window
|
||||
.fetch(endpoint, {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify(params),
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
...authHeaders(),
|
||||
[HttpHeaders.contentType]: ContentType.applicationJson,
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (response.ok) {
|
||||
return undefined;
|
||||
}
|
||||
return await errorHandling(response, "deleting document", params);
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteDocuments(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
@@ -315,7 +275,7 @@ export function deleteDocuments(
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
databaseAccountName: databaseAccount.name,
|
||||
};
|
||||
const endpoint = getFeatureEndpointOrDefault();
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/bulkdelete`, {
|
||||
@@ -359,7 +319,7 @@ export function createMongoCollectionWithProxy(
|
||||
isSharded: !!shardKey,
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault();
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/createCollection`, {
|
||||
@@ -379,11 +339,6 @@ export function createMongoCollectionWithProxy(
|
||||
});
|
||||
}
|
||||
|
||||
export function getFeatureEndpointOrDefault(): string {
|
||||
const endpoint: string = configContext.MONGO_PROXY_ENDPOINT;
|
||||
return getEndpoint(endpoint);
|
||||
}
|
||||
|
||||
export function getEndpoint(endpoint: string): string {
|
||||
let url = endpoint + "/api/mongo/explorer";
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ describe("QueryError.tryParse", () => {
|
||||
code: "BadRequest",
|
||||
message: "Your query is bad, and you should feel bad",
|
||||
};
|
||||
const message = JSON.stringify(innerError);
|
||||
const message = `Message: ${JSON.stringify(innerError)}\r\nActivity ID: 42`;
|
||||
const outerError = {
|
||||
code: "BadRequest",
|
||||
message,
|
||||
@@ -48,7 +48,7 @@ describe("QueryError.tryParse", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
// Imitate the value coming from the backend, which has the syntax errors serialized as JSON in the message.
|
||||
// Imitate the value coming from the backend, which has the syntax errors serialized as JSON in the message, along with a prefix and activity id.
|
||||
it("handles single-nested error", () => {
|
||||
const errors = [
|
||||
{
|
||||
@@ -69,7 +69,7 @@ describe("QueryError.tryParse", () => {
|
||||
message: "Your query is bad, and you should feel bad",
|
||||
errors,
|
||||
};
|
||||
const message = JSON.stringify(innerError);
|
||||
const message = `Message: ${JSON.stringify(innerError)}\r\nActivity ID: 42`;
|
||||
const outerError = {
|
||||
code: "BadRequest",
|
||||
message,
|
||||
@@ -91,4 +91,23 @@ describe("QueryError.tryParse", () => {
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
// Imitate another value we've gotten from the backend, which has a doubly-nested JSON payload.
|
||||
it("handles double-nested error", () => {
|
||||
const outerError = {
|
||||
code: "BadRequest",
|
||||
message:
|
||||
'{"code":"BadRequest","message":"{\\"errors\\":[{\\"severity\\":\\"Error\\",\\"location\\":{\\"start\\":7,\\"end\\":18},\\"code\\":\\"SC2005\\",\\"message\\":\\"\'nonexistent\' is not a recognized built-in function name.\\"}]}\\r\\nActivityId: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa, Windows/10.0.20348 cosmos-netstandard-sdk/3.18.0"}',
|
||||
};
|
||||
|
||||
const result = QueryError.tryParse(outerError, testErrorLocationResolver);
|
||||
expect(result).toEqual([
|
||||
new QueryError(
|
||||
"'nonexistent' is not a recognized built-in function name.",
|
||||
QueryErrorSeverity.Error,
|
||||
"SC2005",
|
||||
new QueryErrorLocation({ offset: 7, lineNumber: 7, column: 7 }, { offset: 18, lineNumber: 18, column: 18 }),
|
||||
),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -214,16 +214,28 @@ export default class QueryError {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Assign to a new variable because of a TypeScript flow typing quirk, see below.
|
||||
if (message.startsWith("Message: ")) {
|
||||
// Reassigning this to 'error' restores the original type of 'error', which is 'unknown'.
|
||||
// So we use a separate variable to avoid this.
|
||||
message = message.substring("Message: ".length);
|
||||
// Some newer backends produce a message that contains a doubly-nested JSON payload.
|
||||
// In this case, the message we get is a fully-complete JSON object we can parse.
|
||||
// So let's try that first
|
||||
if (message.startsWith("{") && message.endsWith("}")) {
|
||||
let outer: unknown = undefined;
|
||||
try {
|
||||
outer = JSON.parse(message);
|
||||
if (typeof outer === "object" && "message" in outer && typeof outer.message === "string") {
|
||||
message = outer.message;
|
||||
}
|
||||
} catch (e) {
|
||||
// Just continue if the parsing fails. We'll use the fallback logic below.
|
||||
}
|
||||
}
|
||||
|
||||
const lines = message.split("\n");
|
||||
message = lines[0].trim();
|
||||
|
||||
if (message.startsWith("Message: ")) {
|
||||
message = message.substring("Message: ".length);
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(message);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Platform, configContext } from "../ConfigContext";
|
||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
|
||||
|
||||
export function updateStyles(): void {
|
||||
if (configContext.platform === Platform.Fabric) {
|
||||
if (isFabric()) {
|
||||
StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh;
|
||||
StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium;
|
||||
StyleConstants.AccentLight = StyleConstants.FabricAccentLight;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos";
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { useDatabases } from "../../Explorer/useDatabases";
|
||||
@@ -24,7 +25,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
|
||||
);
|
||||
try {
|
||||
let collection: DataModels.Collection;
|
||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||
if (!isFabricNative() && userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||
if (params.createNewDatabase) {
|
||||
const createDatabaseParams: DataModels.CreateDatabaseParams = {
|
||||
autoPilotMaxThroughput: params.autoPilotMaxThroughput,
|
||||
@@ -99,6 +100,9 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
|
||||
if (params.vectorEmbeddingPolicy) {
|
||||
resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy;
|
||||
}
|
||||
if (params.fullTextPolicy) {
|
||||
resource.fullTextPolicy = params.fullTextPolicy;
|
||||
}
|
||||
|
||||
const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = {
|
||||
properties: {
|
||||
@@ -270,6 +274,7 @@ const createCollectionWithSDK = async (params: DataModels.CreateCollectionParams
|
||||
uniqueKeyPolicy: params.uniqueKeyPolicy || undefined,
|
||||
analyticalStorageTtl: params.analyticalStorageTtl,
|
||||
vectorEmbeddingPolicy: params.vectorEmbeddingPolicy,
|
||||
fullTextPolicy: params.fullTextPolicy,
|
||||
} as ContainerRequest; // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed
|
||||
const collectionOptions: RequestOptions = {};
|
||||
const createDatabaseBody: DatabaseRequest = { id: params.databaseId };
|
||||
|
||||
70
src/Common/dataAccess/createMaterializedView.ts
Normal file
70
src/Common/dataAccess/createMaterializedView.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { constructRpOptions } from "Common/dataAccess/createCollection";
|
||||
import { handleError } from "Common/ErrorHandlingUtils";
|
||||
import { Collection, CreateMaterializedViewsParams } 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 createMaterializedView = async (params: CreateMaterializedViewsParams): Promise<Collection> => {
|
||||
const clearMessage = logConsoleProgress(
|
||||
`Creating a new materialized view ${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 materialized view ${params.materializedViewId}`);
|
||||
|
||||
return createResponse && (createResponse.properties.resource as Collection);
|
||||
} catch (error) {
|
||||
handleError(error, "CreateMaterializedView", `Error while creating materialized view ${params.materializedViewId}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { deleteCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||
@@ -12,7 +13,7 @@ import { handleError } from "../ErrorHandlingUtils";
|
||||
export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
|
||||
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
|
||||
try {
|
||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) {
|
||||
await deleteCollectionWithARM(databaseId, collectionId);
|
||||
} else {
|
||||
await client().database(databaseId).container(collectionId).delete();
|
||||
|
||||
@@ -105,6 +105,8 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
||||
? parseInt(resource.softAllowedMaximumThroughput)
|
||||
: resource.softAllowedMaximumThroughput;
|
||||
|
||||
const throughputBuckets = resource?.throughputBuckets;
|
||||
|
||||
if (autoscaleSettings) {
|
||||
return {
|
||||
id: offerId,
|
||||
@@ -114,6 +116,7 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
||||
offerReplacePending: resource.offerReplacePending === "true",
|
||||
instantMaximumThroughput,
|
||||
softAllowedMaximumThroughput,
|
||||
throughputBuckets,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -125,6 +128,7 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
||||
offerReplacePending: resource.offerReplacePending === "true",
|
||||
instantMaximumThroughput,
|
||||
softAllowedMaximumThroughput,
|
||||
throughputBuckets,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ContainerResponse } from "@azure/cosmos";
|
||||
import { Queries } from "Common/Constants";
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
||||
import { isFabric, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { FabricArtifactInfo, userContext } from "../../UserContext";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||
@@ -16,15 +17,13 @@ import { handleError } from "../ErrorHandlingUtils";
|
||||
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
|
||||
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
|
||||
|
||||
if (
|
||||
configContext.platform === Platform.Fabric &&
|
||||
userContext.fabricContext &&
|
||||
userContext.fabricContext.databaseConnectionInfo.databaseId === databaseId
|
||||
) {
|
||||
if (isFabricMirroredKey() && userContext.fabricContext?.databaseName === databaseId) {
|
||||
const collections: DataModels.Collection[] = [];
|
||||
const promises: Promise<ContainerResponse>[] = [];
|
||||
|
||||
for (const collectionResourceId in userContext.fabricContext.databaseConnectionInfo.resourceTokens) {
|
||||
for (const collectionResourceId in (
|
||||
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
|
||||
).resourceTokenInfo.resourceTokens) {
|
||||
// Dictionary key looks like this: dbs/SampleDB/colls/Container
|
||||
const resourceIdObj = collectionResourceId.split("/");
|
||||
const tokenDatabaseId = resourceIdObj[1];
|
||||
@@ -56,7 +55,8 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
|
||||
if (
|
||||
userContext.authType === AuthType.AAD &&
|
||||
!userContext.features.enableSDKoperations &&
|
||||
userContext.apiType !== "Tables"
|
||||
userContext.apiType !== "Tables" &&
|
||||
!isFabric()
|
||||
) {
|
||||
return await readCollectionsWithARM(databaseId);
|
||||
}
|
||||
@@ -126,5 +126,12 @@ async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Co
|
||||
throw new Error(`Unsupported default experience type: ${apiType}`);
|
||||
}
|
||||
|
||||
return rpResponse?.value?.map((collection) => collection.properties?.resource as DataModels.Collection);
|
||||
// TO DO: Remove when we get RP API Spec with materializedViews
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
return rpResponse?.value?.map((collection: any) => {
|
||||
const collectionDataModel: DataModels.Collection = collection.properties?.resource as DataModels.Collection;
|
||||
collectionDataModel.materializedViews = collection.properties?.resource?.materializedViews;
|
||||
collectionDataModel.materializedViewDefinition = collection.properties?.resource?.materializedViewDefinition;
|
||||
return collectionDataModel;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
@@ -11,8 +11,9 @@ import { handleError } from "../ErrorHandlingUtils";
|
||||
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||
|
||||
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
|
||||
if (configContext.platform === Platform.Fabric) {
|
||||
// TODO This works, but is very slow, because it requests the token, so we skip for now
|
||||
if (isFabricMirroredKey() || isFabricNative()) {
|
||||
// For Fabric Mirroring, it is slow, because it requests the token and we don't need it.
|
||||
// For Fabric Native, it is not supported.
|
||||
console.error("Skiping readDatabaseOffer for Fabric");
|
||||
return undefined;
|
||||
}
|
||||
@@ -23,7 +24,8 @@ export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promis
|
||||
if (
|
||||
userContext.authType === AuthType.AAD &&
|
||||
!userContext.features.enableSDKoperations &&
|
||||
userContext.apiType !== "Tables"
|
||||
userContext.apiType !== "Tables" &&
|
||||
!isFabric()
|
||||
) {
|
||||
return await readDatabaseOfferWithARM(params.databaseId);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
||||
import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { FabricArtifactInfo, userContext } from "../../UserContext";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||
@@ -14,8 +15,13 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
||||
let databases: DataModels.Database[];
|
||||
const clearMessage = logConsoleProgress(`Querying databases`);
|
||||
|
||||
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.databaseConnectionInfo.resourceTokens) {
|
||||
const tokensData = userContext.fabricContext.databaseConnectionInfo;
|
||||
if (
|
||||
isFabricMirroredKey() &&
|
||||
(userContext.fabricContext?.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]).resourceTokenInfo
|
||||
.resourceTokens
|
||||
) {
|
||||
const tokensData = (userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY])
|
||||
.resourceTokenInfo;
|
||||
|
||||
const databaseIdsSet = new Set<string>(); // databaseId
|
||||
|
||||
@@ -46,13 +52,28 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
||||
}));
|
||||
clearMessage();
|
||||
return databases;
|
||||
} else if (isFabricNative() && userContext.fabricContext?.databaseName) {
|
||||
const databaseId = userContext.fabricContext.databaseName;
|
||||
databases = [
|
||||
{
|
||||
_rid: "",
|
||||
_self: "",
|
||||
_etag: "",
|
||||
_ts: 0,
|
||||
id: databaseId,
|
||||
collections: [],
|
||||
},
|
||||
];
|
||||
clearMessage();
|
||||
return databases;
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
userContext.authType === AuthType.AAD &&
|
||||
!userContext.features.enableSDKoperations &&
|
||||
userContext.apiType !== "Tables"
|
||||
userContext.apiType !== "Tables" &&
|
||||
!isFabric()
|
||||
) {
|
||||
databases = await readDatabasesWithARM();
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ContainerDefinition, RequestOptions } from "@azure/cosmos";
|
||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Collection } from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
@@ -36,7 +37,8 @@ export async function updateCollection(
|
||||
if (
|
||||
userContext.authType === AuthType.AAD &&
|
||||
!userContext.features.enableSDKoperations &&
|
||||
userContext.apiType !== "Tables"
|
||||
userContext.apiType !== "Tables" &&
|
||||
!isFabric()
|
||||
) {
|
||||
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { OfferDefinition, RequestOptions } from "@azure/cosmos";
|
||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||
import { Offer, SDKOfferDefinition, ThroughputBucket, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import {
|
||||
migrateCassandraKeyspaceToAutoscale,
|
||||
@@ -56,7 +57,7 @@ export const updateOffer = async (params: UpdateOfferParams): Promise<Offer> =>
|
||||
const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`);
|
||||
|
||||
try {
|
||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) {
|
||||
if (params.collectionId) {
|
||||
updatedOffer = await updateCollectionOfferWithARM(params);
|
||||
} else if (userContext.apiType === "Tables") {
|
||||
@@ -359,6 +360,13 @@ const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpd
|
||||
body.properties.resource.throughput = params.manualThroughput;
|
||||
}
|
||||
|
||||
if (params.throughputBuckets) {
|
||||
const throughputBuckets = params.throughputBuckets.filter(
|
||||
(bucket: ThroughputBucket) => bucket.maxThroughputPercentage !== 100,
|
||||
);
|
||||
body.properties.resource.throughputBuckets = throughputBuckets;
|
||||
}
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
|
||||
import {
|
||||
BackendApi,
|
||||
CassandraProxyEndpoints,
|
||||
JunoEndpoints,
|
||||
MongoProxyEndpoints,
|
||||
PortalBackendEndpoints,
|
||||
} from "Common/Constants";
|
||||
import {
|
||||
allowedAadEndpoints,
|
||||
allowedArcadiaEndpoints,
|
||||
@@ -6,12 +12,11 @@ import {
|
||||
allowedGraphEndpoints,
|
||||
allowedHostedExplorerEndpoints,
|
||||
allowedJunoOrigins,
|
||||
allowedMongoBackendEndpoints,
|
||||
allowedMsalRedirectEndpoints,
|
||||
defaultAllowedArmEndpoints,
|
||||
defaultAllowedBackendEndpoints,
|
||||
defaultAllowedCassandraProxyEndpoints,
|
||||
defaultAllowedMongoProxyEndpoints,
|
||||
defaultAllowedPortalBackendEndpoints,
|
||||
validateEndpoint,
|
||||
} from "Utils/EndpointUtils";
|
||||
|
||||
@@ -25,7 +30,7 @@ export enum Platform {
|
||||
export interface ConfigContext {
|
||||
platform: Platform;
|
||||
allowedArmEndpoints: ReadonlyArray<string>;
|
||||
allowedPortalBackendEndpoints: ReadonlyArray<string>;
|
||||
allowedBackendEndpoints: ReadonlyArray<string>;
|
||||
allowedCassandraProxyEndpoints: ReadonlyArray<string>;
|
||||
allowedMongoProxyEndpoints: ReadonlyArray<string>;
|
||||
allowedParentFrameOrigins: ReadonlyArray<string>;
|
||||
@@ -45,9 +50,10 @@ export interface ConfigContext {
|
||||
ARCADIA_ENDPOINT: string;
|
||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||
PORTAL_BACKEND_ENDPOINT: string;
|
||||
MONGO_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;
|
||||
@@ -66,7 +72,7 @@ export interface ConfigContext {
|
||||
let configContext: Readonly<ConfigContext> = {
|
||||
platform: Platform.Portal,
|
||||
allowedArmEndpoints: defaultAllowedArmEndpoints,
|
||||
allowedPortalBackendEndpoints: defaultAllowedPortalBackendEndpoints,
|
||||
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
|
||||
allowedCassandraProxyEndpoints: defaultAllowedCassandraProxyEndpoints,
|
||||
allowedMongoProxyEndpoints: defaultAllowedMongoProxyEndpoints,
|
||||
allowedParentFrameOrigins: [
|
||||
@@ -82,7 +88,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
||||
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
||||
`^https:\\/\\/.*\\.azure-test\\.net$`,
|
||||
`^https:\\/\\/cosmos-explorer-preview\\.azurewebsites\\.net$`,
|
||||
`^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`,
|
||||
], // Webpack injects this at build time
|
||||
gitSha: process.env.GIT_SHA,
|
||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||
@@ -103,6 +109,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||
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: [],
|
||||
@@ -141,15 +148,6 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
delete newContext.ARCADIA_ENDPOINT;
|
||||
}
|
||||
|
||||
if (
|
||||
!validateEndpoint(
|
||||
newContext.PORTAL_BACKEND_ENDPOINT,
|
||||
configContext.allowedPortalBackendEndpoints || defaultAllowedPortalBackendEndpoints,
|
||||
)
|
||||
) {
|
||||
delete newContext.PORTAL_BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
if (
|
||||
!validateEndpoint(
|
||||
newContext.MONGO_PROXY_ENDPOINT,
|
||||
@@ -159,10 +157,6 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
delete newContext.MONGO_PROXY_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) {
|
||||
delete newContext.MONGO_BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
if (
|
||||
!validateEndpoint(
|
||||
newContext.CASSANDRA_PROXY_ENDPOINT,
|
||||
|
||||
@@ -9,6 +9,7 @@ export enum TabKind {
|
||||
Graph,
|
||||
SQLQuery,
|
||||
ScaleSettings,
|
||||
MongoQuery,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,6 +52,8 @@ export interface OpenCollectionTab extends OpenTab {
|
||||
*/
|
||||
export interface OpenQueryTab extends OpenCollectionTab {
|
||||
query: QueryInfo;
|
||||
splitterDirection?: "vertical" | "horizontal";
|
||||
queryViewSizePercent?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface ArmEntity {
|
||||
location: string;
|
||||
type: string;
|
||||
kind: string;
|
||||
tags?: Tags;
|
||||
}
|
||||
|
||||
export interface DatabaseAccount extends ArmEntity {
|
||||
@@ -31,6 +32,7 @@ export interface DatabaseAccountExtendedProperties {
|
||||
writeLocations?: DatabaseAccountResponseLocation[];
|
||||
enableFreeTier?: boolean;
|
||||
enableAnalyticalStorage?: boolean;
|
||||
enableMaterializedViews?: boolean;
|
||||
isVirtualNetworkFilterEnabled?: boolean;
|
||||
ipRules?: IpRule[];
|
||||
privateEndpointConnections?: unknown[];
|
||||
@@ -159,9 +161,12 @@ export interface Collection extends Resource {
|
||||
analyticalStorageTtl?: number;
|
||||
geospatialConfig?: GeospatialConfig;
|
||||
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
|
||||
fullTextPolicy?: FullTextPolicy;
|
||||
schema?: ISchema;
|
||||
requestSchema?: () => void;
|
||||
computedProperties?: ComputedProperties;
|
||||
materializedViews?: MaterializedView[];
|
||||
materializedViewDefinition?: MaterializedViewDefinition;
|
||||
}
|
||||
|
||||
export interface CollectionsWithPagination {
|
||||
@@ -199,11 +204,19 @@ export interface IndexingPolicy {
|
||||
compositeIndexes?: any[];
|
||||
spatialIndexes?: any[];
|
||||
vectorIndexes?: VectorIndex[];
|
||||
fullTextIndexes?: FullTextIndex[];
|
||||
}
|
||||
|
||||
export interface VectorIndex {
|
||||
path: string;
|
||||
type: "flat" | "diskANN" | "quantizedFlat";
|
||||
diskANNShardKey?: string;
|
||||
indexingSearchListSize?: number;
|
||||
quantizationByteSize?: number;
|
||||
}
|
||||
|
||||
export interface FullTextIndex {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface ComputedProperty {
|
||||
@@ -213,6 +226,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";
|
||||
@@ -265,6 +289,12 @@ export interface Offer {
|
||||
offerReplacePending: boolean;
|
||||
instantMaximumThroughput?: number;
|
||||
softAllowedMaximumThroughput?: number;
|
||||
throughputBuckets?: ThroughputBucket[];
|
||||
}
|
||||
|
||||
export interface ThroughputBucket {
|
||||
id: number;
|
||||
maxThroughputPercentage: number;
|
||||
}
|
||||
|
||||
export interface SDKOfferDefinition extends Resource {
|
||||
@@ -329,9 +359,7 @@ export interface CreateDatabaseParams {
|
||||
offerThroughput?: number;
|
||||
}
|
||||
|
||||
export interface CreateCollectionParams {
|
||||
createNewDatabase: boolean;
|
||||
collectionId: string;
|
||||
export interface CreateCollectionParamsBase {
|
||||
databaseId: string;
|
||||
databaseLevelThroughput: boolean;
|
||||
offerThroughput?: number;
|
||||
@@ -342,6 +370,17 @@ export interface CreateCollectionParams {
|
||||
uniqueKeyPolicy?: UniqueKeyPolicy;
|
||||
createMongoWildcardIndex?: boolean;
|
||||
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
|
||||
fullTextPolicy?: FullTextPolicy;
|
||||
}
|
||||
|
||||
export interface CreateCollectionParams extends CreateCollectionParamsBase {
|
||||
createNewDatabase: boolean;
|
||||
collectionId: string;
|
||||
}
|
||||
|
||||
export interface CreateMaterializedViewsParams extends CreateCollectionParamsBase {
|
||||
materializedViewId: string;
|
||||
materializedViewDefinition: MaterializedViewDefinition;
|
||||
}
|
||||
|
||||
export interface VectorEmbeddingPolicy {
|
||||
@@ -355,6 +394,16 @@ export interface VectorEmbedding {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface FullTextPolicy {
|
||||
defaultLanguage: string;
|
||||
fullTextPaths: FullTextPath[];
|
||||
}
|
||||
|
||||
export interface FullTextPath {
|
||||
path: string;
|
||||
language: string;
|
||||
}
|
||||
|
||||
export interface ReadDatabaseOfferParams {
|
||||
databaseId: string;
|
||||
databaseResourceId?: string;
|
||||
@@ -376,6 +425,7 @@ export interface UpdateOfferParams {
|
||||
collectionId?: string;
|
||||
migrateToAutoPilot?: boolean;
|
||||
migrateToManual?: boolean;
|
||||
throughputBuckets?: ThroughputBucket[];
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
@@ -643,3 +693,5 @@ export interface FeatureRegistration {
|
||||
state: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Tags = { [key: string]: string };
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
export enum FabricMessageTypes {
|
||||
GetAuthorizationToken = "GetAuthorizationToken",
|
||||
GetAllResourceTokens = "GetAllResourceTokens",
|
||||
GetAccessToken = "GetAccessToken",
|
||||
Ready = "Ready",
|
||||
}
|
||||
|
||||
|
||||
@@ -1,47 +1,9 @@
|
||||
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||
import { AuthorizationToken } from "./FabricMessageTypes";
|
||||
|
||||
// This is the version of these messages
|
||||
export const FABRIC_RPC_VERSION = "2";
|
||||
export const FABRIC_RPC_VERSION = "FabricMessageV3";
|
||||
|
||||
// Fabric to Data Explorer
|
||||
|
||||
// TODO Deprecated. Remove this section once DE is updated
|
||||
export type FabricMessageV1 =
|
||||
| {
|
||||
type: "newContainer";
|
||||
databaseName: string;
|
||||
}
|
||||
| {
|
||||
type: "initialize";
|
||||
message: {
|
||||
endpoint: string | undefined;
|
||||
databaseId: string | undefined;
|
||||
resourceTokens: unknown | undefined;
|
||||
resourceTokensTimestamp: number | undefined;
|
||||
error: string | undefined;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "authorizationToken";
|
||||
message: {
|
||||
id: string;
|
||||
error: string | undefined;
|
||||
data: AuthorizationToken | undefined;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "allResourceTokens";
|
||||
message: {
|
||||
id: string;
|
||||
error: string | undefined;
|
||||
endpoint: string | undefined;
|
||||
databaseId: string | undefined;
|
||||
resourceTokens: unknown | undefined;
|
||||
resourceTokensTimestamp: number | undefined;
|
||||
};
|
||||
};
|
||||
// -----------------------------
|
||||
|
||||
export type FabricMessageV2 =
|
||||
| {
|
||||
type: "newContainer";
|
||||
@@ -69,7 +31,7 @@ export type FabricMessageV2 =
|
||||
message: {
|
||||
id: string;
|
||||
error: string | undefined;
|
||||
data: FabricDatabaseConnectionInfo | undefined;
|
||||
data: ResourceTokenInfo | undefined;
|
||||
};
|
||||
}
|
||||
| {
|
||||
@@ -79,17 +41,81 @@ export type FabricMessageV2 =
|
||||
};
|
||||
};
|
||||
|
||||
export type CosmosDBTokenResponse = {
|
||||
token: string;
|
||||
date: string;
|
||||
};
|
||||
export type FabricMessageV3 =
|
||||
| {
|
||||
type: "newContainer";
|
||||
databaseName: string;
|
||||
}
|
||||
| {
|
||||
type: "initialize";
|
||||
version: string;
|
||||
id: string;
|
||||
message: InitializeMessageV3<CosmosDbArtifactType>;
|
||||
}
|
||||
| {
|
||||
type: "authorizationToken";
|
||||
message: {
|
||||
id: string;
|
||||
error: string | undefined;
|
||||
data: AuthorizationToken | undefined;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "allResourceTokens_v2";
|
||||
message: {
|
||||
id: string;
|
||||
error: string | undefined;
|
||||
data: ResourceTokenInfo | undefined;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "explorerVisible";
|
||||
message: {
|
||||
visible: boolean;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "accessToken";
|
||||
message: {
|
||||
id: string;
|
||||
error: string | undefined;
|
||||
data: { accessToken: string };
|
||||
};
|
||||
};
|
||||
|
||||
export type CosmosDBConnectionInfoResponse = {
|
||||
export enum CosmosDbArtifactType {
|
||||
MIRRORED_KEY = "MIRRORED_KEY",
|
||||
MIRRORED_AAD = "MIRRORED_AAD",
|
||||
NATIVE = "NATIVE",
|
||||
}
|
||||
export interface ArtifactConnectionInfo {
|
||||
[CosmosDbArtifactType.MIRRORED_KEY]: { connectionId: string };
|
||||
[CosmosDbArtifactType.MIRRORED_AAD]: AccessTokenConnectionInfo;
|
||||
[CosmosDbArtifactType.NATIVE]: AccessTokenConnectionInfo;
|
||||
}
|
||||
|
||||
export interface AccessTokenConnectionInfo {
|
||||
accessToken: string;
|
||||
databaseName: string;
|
||||
accountEndpoint: string;
|
||||
}
|
||||
|
||||
export interface InitializeMessageV3<T extends CosmosDbArtifactType> {
|
||||
connectionId: string;
|
||||
isVisible: boolean;
|
||||
isReadOnly: boolean;
|
||||
artifactType: T;
|
||||
artifactConnectionInfo: ArtifactConnectionInfo[T];
|
||||
}
|
||||
export interface CosmosDBConnectionInfoResponse {
|
||||
endpoint: string;
|
||||
databaseId: string;
|
||||
resourceTokens: { [resourceId: string]: string };
|
||||
};
|
||||
resourceTokens: Record<string, string> | undefined;
|
||||
accessToken: string | undefined;
|
||||
isReadOnly: boolean;
|
||||
credentialType: "Key" | "OAuth2" | undefined;
|
||||
}
|
||||
|
||||
export interface FabricDatabaseConnectionInfo extends CosmosDBConnectionInfoResponse {
|
||||
export interface ResourceTokenInfo extends CosmosDBConnectionInfoResponse {
|
||||
resourceTokensTimestamp: number;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export enum MessageTypes {
|
||||
OpenPostgreSQLPasswordReset,
|
||||
OpenPostgresNetworkingBlade,
|
||||
OpenCosmosDBNetworkingBlade,
|
||||
DisplayNPSSurvey,
|
||||
DisplayNPSSurvey, // unused
|
||||
OpenVCoreMongoNetworkingBlade,
|
||||
OpenVCoreMongoConnectionStringsBlade,
|
||||
GetAuthorizationToken, // unused. Can be removed if the portal uses the same list of enums.
|
||||
|
||||
@@ -115,7 +115,13 @@ export interface CollectionBase extends TreeNode {
|
||||
isSampleCollection?: boolean;
|
||||
|
||||
onDocumentDBDocumentsClick(): void;
|
||||
onNewQueryClick(source: any, event?: MouseEvent, queryText?: string): void;
|
||||
onNewQueryClick(
|
||||
source: any,
|
||||
event?: MouseEvent,
|
||||
queryText?: string,
|
||||
splitterDirection?: "horizontal" | "vertical",
|
||||
queryViewSizePercent?: number,
|
||||
): void;
|
||||
expandCollection(): void;
|
||||
collapseCollection(): void;
|
||||
getDatabase(): Database;
|
||||
@@ -126,6 +132,8 @@ export interface Collection extends CollectionBase {
|
||||
analyticalStorageTtl: ko.Observable<number>;
|
||||
schema?: DataModels.ISchema;
|
||||
requestSchema?: () => void;
|
||||
vectorEmbeddingPolicy: ko.Observable<DataModels.VectorEmbeddingPolicy>;
|
||||
fullTextPolicy: ko.Observable<DataModels.FullTextPolicy>;
|
||||
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
||||
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||
usageSizeInKB: ko.Observable<number>;
|
||||
@@ -135,6 +143,8 @@ export interface Collection extends CollectionBase {
|
||||
geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
|
||||
documentIds: ko.ObservableArray<DocumentId>;
|
||||
computedProperties: ko.Observable<DataModels.ComputedProperties>;
|
||||
materializedViews: ko.Observable<DataModels.MaterializedView[]>;
|
||||
materializedViewDefinition: ko.Observable<DataModels.MaterializedViewDefinition>;
|
||||
|
||||
cassandraKeys: CassandraTableKeys;
|
||||
cassandraSchema: CassandraTableKey[];
|
||||
@@ -149,7 +159,13 @@ export interface Collection extends CollectionBase {
|
||||
onSettingsClick: () => Promise<void>;
|
||||
|
||||
onNewGraphClick(): void;
|
||||
onNewMongoQueryClick(source: any, event?: MouseEvent, queryText?: string): void;
|
||||
onNewMongoQueryClick(
|
||||
source: any,
|
||||
event?: MouseEvent,
|
||||
queryText?: string,
|
||||
splitterDirection?: "horizontal" | "vertical",
|
||||
queryViewSizePercent?: number,
|
||||
): void;
|
||||
onNewMongoShellClick(): void;
|
||||
onNewStoredProcedureClick(source: Collection, event?: MouseEvent): void;
|
||||
onNewUserDefinedFunctionClick(source: Collection, event?: MouseEvent): void;
|
||||
@@ -309,6 +325,8 @@ export interface QueryTabOptions extends TabOptions {
|
||||
partitionKey?: DataModels.PartitionKey;
|
||||
queryText?: string;
|
||||
resourceTokenPartitionKey?: string;
|
||||
splitterDirection?: "horizontal" | "vertical";
|
||||
queryViewSizePercent?: number;
|
||||
}
|
||||
|
||||
export interface ScriptTabOption extends TabOptions {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { MaterializedViewsLabels } from "Common/Constants";
|
||||
import { isMaterializedViewsEnabled } from "Common/DatabaseAccountUtility";
|
||||
import { configContext, Platform } from "ConfigContext";
|
||||
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||
import {
|
||||
AddMaterializedViewPanel,
|
||||
AddMaterializedViewPanelProps,
|
||||
} from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
@@ -19,7 +27,6 @@ import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { Platform, configContext } from "./../ConfigContext";
|
||||
import Explorer from "./Explorer";
|
||||
import { useNotebook } from "./Notebook/useNotebook";
|
||||
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
|
||||
@@ -41,7 +48,7 @@ export interface DatabaseContextMenuButtonParams {
|
||||
* New resource tree (in ReactJS)
|
||||
*/
|
||||
export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => {
|
||||
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) {
|
||||
if (isFabric() && userContext.fabricContext?.isReadOnly) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -53,7 +60,7 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
|
||||
},
|
||||
];
|
||||
|
||||
if (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations) {
|
||||
if (!isFabricNative() && (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations)) {
|
||||
items.push({
|
||||
iconSrc: DeleteDatabaseIcon,
|
||||
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||
@@ -145,7 +152,7 @@ export const createCollectionContextMenuButton = (
|
||||
});
|
||||
}
|
||||
|
||||
if (configContext.platform !== Platform.Fabric) {
|
||||
if (!isFabric() || (isFabric() && !userContext.fabricContext?.isReadOnly)) {
|
||||
items.push({
|
||||
iconSrc: DeleteCollectionIcon,
|
||||
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||
@@ -163,6 +170,24 @@ export const createCollectionContextMenuButton = (
|
||||
});
|
||||
}
|
||||
|
||||
if (isMaterializedViewsEnabled() && !selectedCollection.materializedViewDefinition()) {
|
||||
items.push({
|
||||
label: MaterializedViewsLabels.NewMaterializedView,
|
||||
onClick: () => {
|
||||
const addMaterializedViewPanelProps: AddMaterializedViewPanelProps = {
|
||||
explorer: container,
|
||||
sourceContainer: selectedCollection,
|
||||
};
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
MaterializedViewsLabels.NewMaterializedView,
|
||||
<AddMaterializedViewPanel {...addMaterializedViewPanelProps} />,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DirectionalHint, Icon, Label, Stack, TooltipHost } from "@fluentui/react";
|
||||
import { DirectionalHint, Icon, IconButton, Label, Stack, TooltipHost } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { NormalizedEventKey } from "../../../Common/Constants";
|
||||
import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
|
||||
@@ -9,6 +9,9 @@ export interface CollapsibleSectionProps {
|
||||
onExpand?: () => void;
|
||||
children: JSX.Element;
|
||||
tooltipContent?: string | JSX.Element | JSX.Element[];
|
||||
showDelete?: boolean;
|
||||
onDelete?: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface CollapsibleSectionState {
|
||||
@@ -69,6 +72,20 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
)}
|
||||
{this.props.showDelete && (
|
||||
<Stack.Item style={{ marginLeft: "auto" }}>
|
||||
<IconButton
|
||||
disabled={this.props.disabled}
|
||||
id={`delete-${this.props.title.split(" ").join("-")}`}
|
||||
iconProps={{ iconName: "Delete" }}
|
||||
style={{ height: 27, marginRight: "20px" }}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
this.props.onDelete();
|
||||
}}
|
||||
/>
|
||||
</Stack.Item>
|
||||
)}
|
||||
</Stack>
|
||||
{this.state.isExpanded && this.props.children}
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import "@testing-library/jest-dom";
|
||||
|
||||
describe("AddFullTextPolicyForm", () => {
|
||||
//CTODO: add tests
|
||||
it.skip("should render correctly", () => {});
|
||||
});
|
||||
@@ -0,0 +1,239 @@
|
||||
import {
|
||||
DefaultButton,
|
||||
Dropdown,
|
||||
IDropdownOption,
|
||||
IStyleFunctionOrObject,
|
||||
ITextFieldStyleProps,
|
||||
ITextFieldStyles,
|
||||
Label,
|
||||
Stack,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import { FullTextIndex, FullTextPath, FullTextPolicy } from "Contracts/DataModels";
|
||||
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import * as React from "react";
|
||||
|
||||
export interface FullTextPoliciesComponentProps {
|
||||
fullTextPolicy: FullTextPolicy;
|
||||
onFullTextPathChange: (
|
||||
fullTextPolicy: FullTextPolicy,
|
||||
fullTextIndexes: FullTextIndex[],
|
||||
validationPassed: boolean,
|
||||
) => void;
|
||||
discardChanges?: boolean;
|
||||
onChangesDiscarded?: () => void;
|
||||
}
|
||||
|
||||
export interface FullTextPolicyData {
|
||||
path: string;
|
||||
language: string;
|
||||
pathError: string;
|
||||
}
|
||||
|
||||
const labelStyles = {
|
||||
root: {
|
||||
fontSize: 12,
|
||||
},
|
||||
};
|
||||
|
||||
const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldStyles> = {
|
||||
fieldGroup: {
|
||||
height: 27,
|
||||
},
|
||||
field: {
|
||||
fontSize: 12,
|
||||
padding: "0 8px",
|
||||
},
|
||||
};
|
||||
|
||||
const dropdownStyles = {
|
||||
title: {
|
||||
height: 27,
|
||||
lineHeight: "24px",
|
||||
fontSize: 12,
|
||||
},
|
||||
dropdown: {
|
||||
height: 27,
|
||||
lineHeight: "24px",
|
||||
},
|
||||
dropdownItem: {
|
||||
fontSize: 12,
|
||||
},
|
||||
};
|
||||
|
||||
export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPoliciesComponentProps> = ({
|
||||
fullTextPolicy,
|
||||
onFullTextPathChange,
|
||||
discardChanges,
|
||||
onChangesDiscarded,
|
||||
}): JSX.Element => {
|
||||
const getFullTextPathError = (path: string, index?: number): string => {
|
||||
let error = "";
|
||||
if (!path) {
|
||||
error = "Full text path should not be empty";
|
||||
}
|
||||
if (
|
||||
index >= 0 &&
|
||||
fullTextPathData?.find(
|
||||
(fullTextPath: FullTextPolicyData, dataIndex: number) => dataIndex !== index && fullTextPath.path === path,
|
||||
)
|
||||
) {
|
||||
error = "Full text path is already defined";
|
||||
}
|
||||
return error;
|
||||
};
|
||||
|
||||
const initializeData = (fullTextPolicy: FullTextPolicy): FullTextPolicyData[] => {
|
||||
if (!fullTextPolicy) {
|
||||
fullTextPolicy = { defaultLanguage: getFullTextLanguageOptions()[0].key as never, fullTextPaths: [] };
|
||||
}
|
||||
return fullTextPolicy.fullTextPaths.map((fullTextPath: FullTextPath) => ({
|
||||
...fullTextPath,
|
||||
pathError: getFullTextPathError(fullTextPath.path),
|
||||
}));
|
||||
};
|
||||
|
||||
const [fullTextPathData, setFullTextPathData] = React.useState<FullTextPolicyData[]>(initializeData(fullTextPolicy));
|
||||
const [defaultLanguage, setDefaultLanguage] = React.useState<string>(
|
||||
fullTextPolicy ? fullTextPolicy.defaultLanguage : (getFullTextLanguageOptions()[0].key as never),
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
propagateData();
|
||||
}, [fullTextPathData, defaultLanguage]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (discardChanges) {
|
||||
setFullTextPathData(initializeData(fullTextPolicy));
|
||||
setDefaultLanguage(fullTextPolicy.defaultLanguage);
|
||||
onChangesDiscarded();
|
||||
}
|
||||
}, [discardChanges]);
|
||||
|
||||
const propagateData = () => {
|
||||
const newFullTextPolicy: FullTextPolicy = {
|
||||
defaultLanguage: defaultLanguage,
|
||||
fullTextPaths: fullTextPathData.map((policy: FullTextPolicyData) => ({
|
||||
path: policy.path,
|
||||
language: policy.language,
|
||||
})),
|
||||
};
|
||||
const fullTextIndexes: FullTextIndex[] = fullTextPathData.map((policy) => ({
|
||||
path: policy.path,
|
||||
}));
|
||||
const validationPassed = fullTextPathData.every((policy: FullTextPolicyData) => policy.pathError === "");
|
||||
onFullTextPathChange(newFullTextPolicy, fullTextIndexes, validationPassed);
|
||||
};
|
||||
|
||||
const onFullTextPathValueChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value.trim();
|
||||
const fullTextPaths = [...fullTextPathData];
|
||||
if (!fullTextPaths[index]?.path && !value.startsWith("/")) {
|
||||
fullTextPaths[index].path = "/" + value;
|
||||
} else {
|
||||
fullTextPaths[index].path = value;
|
||||
}
|
||||
fullTextPaths[index].pathError = getFullTextPathError(value, index);
|
||||
setFullTextPathData(fullTextPaths);
|
||||
};
|
||||
|
||||
const onFullTextPathPolicyChange = (index: number, option: IDropdownOption): void => {
|
||||
const policies = [...fullTextPathData];
|
||||
policies[index].language = option.key as never;
|
||||
setFullTextPathData(policies);
|
||||
};
|
||||
|
||||
const onAdd = () => {
|
||||
setFullTextPathData([
|
||||
...fullTextPathData,
|
||||
{
|
||||
path: "",
|
||||
language: defaultLanguage,
|
||||
pathError: getFullTextPathError(""),
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const onDelete = (index: number) => {
|
||||
const policies = fullTextPathData.filter((_uniqueKey, j) => index !== j);
|
||||
setFullTextPathData(policies);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: 4 }}>
|
||||
<Stack style={{ marginBottom: 10 }}>
|
||||
<Label styles={labelStyles}>Default language</Label>
|
||||
<Dropdown
|
||||
required={true}
|
||||
styles={dropdownStyles}
|
||||
options={getFullTextLanguageOptions()}
|
||||
selectedKey={defaultLanguage}
|
||||
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||
setDefaultLanguage(option.key as never)
|
||||
}
|
||||
></Dropdown>
|
||||
</Stack>
|
||||
{fullTextPathData &&
|
||||
fullTextPathData.length > 0 &&
|
||||
fullTextPathData.map((fullTextPolicy: FullTextPolicyData, index: number) => (
|
||||
<CollapsibleSectionComponent
|
||||
key={index}
|
||||
isExpandedByDefault={true}
|
||||
title={`Full text path ${index + 1}`}
|
||||
showDelete={true}
|
||||
onDelete={() => onDelete(index)}
|
||||
>
|
||||
<Stack horizontal tokens={{ childrenGap: 4 }}>
|
||||
<Stack
|
||||
styles={{
|
||||
root: {
|
||||
margin: "0 0 6px 20px !important",
|
||||
paddingLeft: 20,
|
||||
width: "80%",
|
||||
borderLeft: "1px solid",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack>
|
||||
<Label styles={labelStyles}>Path</Label>
|
||||
<TextField
|
||||
id={`full-text-policy-path-${index + 1}`}
|
||||
required={true}
|
||||
placeholder="/fullTextPath1"
|
||||
styles={textFieldStyles}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onFullTextPathValueChange(index, event)}
|
||||
value={fullTextPolicy.path || ""}
|
||||
errorMessage={fullTextPolicy.pathError}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Label styles={labelStyles}>Language</Label>
|
||||
<Dropdown
|
||||
required={true}
|
||||
styles={dropdownStyles}
|
||||
options={getFullTextLanguageOptions()}
|
||||
selectedKey={fullTextPolicy.language}
|
||||
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||
onFullTextPathPolicyChange(index, option)
|
||||
}
|
||||
></Dropdown>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CollapsibleSectionComponent>
|
||||
))}
|
||||
<DefaultButton id={`add-vector-policy`} styles={{ root: { maxWidth: 170, fontSize: 12 } }} onClick={onAdd}>
|
||||
Add full text path
|
||||
</DefaultButton>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const getFullTextLanguageOptions = (): IDropdownOption[] => {
|
||||
return [
|
||||
{
|
||||
key: "en-US",
|
||||
text: "English (US)",
|
||||
},
|
||||
];
|
||||
};
|
||||
314
src/Explorer/Controls/InputDataList/InputDataList.tsx
Normal file
314
src/Explorer/Controls/InputDataList/InputDataList.tsx
Normal file
@@ -0,0 +1,314 @@
|
||||
// This component is used to create a dropdown list of options for the user to select from.
|
||||
// The options are displayed in a dropdown list when the user clicks on the input field.
|
||||
// The user can then select an option from the list. The selected option is then displayed in the input field.
|
||||
|
||||
import { getTheme } from "@fluentui/react";
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Input,
|
||||
Link,
|
||||
makeStyles,
|
||||
Popover,
|
||||
PopoverProps,
|
||||
PopoverSurface,
|
||||
PositioningImperativeRef,
|
||||
} from "@fluentui/react-components";
|
||||
import { ArrowDownRegular, DismissRegular } from "@fluentui/react-icons";
|
||||
import { NormalizedEventKey } from "Common/Constants";
|
||||
import { tokens } from "Explorer/Theme/ThemeUtil";
|
||||
import React, { FC, useEffect, useRef } from "react";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
container: {
|
||||
padding: 0,
|
||||
},
|
||||
input: {
|
||||
flexGrow: 1,
|
||||
paddingRight: 0,
|
||||
outline: "none",
|
||||
"& input:focus": {
|
||||
outline: "none", // Undo body :focus dashed outline
|
||||
},
|
||||
},
|
||||
inputButton: {
|
||||
border: 0,
|
||||
},
|
||||
dropdownHeader: {
|
||||
width: "100%",
|
||||
fontSize: tokens.fontSizeBase300,
|
||||
fontWeight: 600,
|
||||
padding: `${tokens.spacingVerticalM} 0 0 ${tokens.spacingVerticalM}`,
|
||||
},
|
||||
dropdownStack: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: tokens.spacingVerticalS,
|
||||
marginTop: tokens.spacingVerticalS,
|
||||
marginBottom: "1px",
|
||||
},
|
||||
dropdownOption: {
|
||||
fontSize: tokens.fontSizeBase300,
|
||||
fontWeight: 400,
|
||||
justifyContent: "left",
|
||||
padding: `${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalS} ${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalL}`,
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
border: 0,
|
||||
":hover": {
|
||||
outline: `1px dashed ${tokens.colorNeutralForeground1Hover}`,
|
||||
backgroundColor: tokens.colorNeutralBackground2Hover,
|
||||
color: tokens.colorNeutralForeground1,
|
||||
},
|
||||
},
|
||||
bottomSection: {
|
||||
fontSize: tokens.fontSizeBase300,
|
||||
fontWeight: 400,
|
||||
padding: `${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalS} ${tokens.spacingHorizontalXS} ${tokens.spacingHorizontalL}`,
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
},
|
||||
});
|
||||
|
||||
export interface InputDatalistDropdownOptionSection {
|
||||
label: string;
|
||||
options: string[];
|
||||
}
|
||||
|
||||
export interface InputDataListProps {
|
||||
dropdownOptions: InputDatalistDropdownOptionSection[];
|
||||
placeholder?: string;
|
||||
title?: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
autofocus?: boolean; // true: acquire focus on first render
|
||||
bottomLink?: {
|
||||
text: string;
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const InputDataList: FC<InputDataListProps> = ({
|
||||
dropdownOptions,
|
||||
placeholder,
|
||||
title,
|
||||
value,
|
||||
onChange,
|
||||
onKeyDown,
|
||||
autofocus,
|
||||
bottomLink,
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
const [showDropdown, setShowDropdown] = React.useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const positioningRef = React.useRef<PositioningImperativeRef>(null);
|
||||
const [isInputFocused, setIsInputFocused] = React.useState(autofocus);
|
||||
const [autofocusFirstDropdownItem, setAutofocusFirstDropdownItem] = React.useState(false);
|
||||
|
||||
const theme = getTheme();
|
||||
const itemRefs = useRef([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
positioningRef.current?.setTarget(inputRef.current);
|
||||
}
|
||||
}, [inputRef, positioningRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInputFocused) {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}, [isInputFocused]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autofocusFirstDropdownItem && showDropdown) {
|
||||
// Autofocus on first item if input isn't focused
|
||||
itemRefs.current[0]?.focus();
|
||||
setAutofocusFirstDropdownItem(false);
|
||||
}
|
||||
}, [autofocusFirstDropdownItem, showDropdown]);
|
||||
|
||||
const handleOpenChange: PopoverProps["onOpenChange"] = (e, data) => {
|
||||
if (isInputFocused && !data.open) {
|
||||
// Don't close if input is focused and we're opening the dropdown (which will steal the focus)
|
||||
return;
|
||||
}
|
||||
|
||||
setShowDropdown(data.open || false);
|
||||
if (data.open) {
|
||||
setIsInputFocused(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === NormalizedEventKey.Escape) {
|
||||
setShowDropdown(false);
|
||||
} else if (e.key === NormalizedEventKey.DownArrow) {
|
||||
setShowDropdown(true);
|
||||
setAutofocusFirstDropdownItem(true);
|
||||
}
|
||||
onKeyDown(e);
|
||||
};
|
||||
|
||||
const handleDownDropdownItemKeyDown = (
|
||||
e: React.KeyboardEvent<HTMLButtonElement | HTMLAnchorElement>,
|
||||
index: number,
|
||||
) => {
|
||||
if (e.key === NormalizedEventKey.Enter) {
|
||||
e.currentTarget.click();
|
||||
} else if (e.key === NormalizedEventKey.Escape) {
|
||||
setShowDropdown(false);
|
||||
inputRef.current?.focus();
|
||||
} else if (e.key === NormalizedEventKey.DownArrow) {
|
||||
if (index + 1 < itemRefs.current.length) {
|
||||
itemRefs.current[index + 1].focus();
|
||||
} else {
|
||||
setIsInputFocused(true);
|
||||
}
|
||||
} else if (e.key === NormalizedEventKey.UpArrow) {
|
||||
if (index - 1 >= 0) {
|
||||
itemRefs.current[index - 1].focus();
|
||||
} else {
|
||||
// Last item, focus back to input
|
||||
setIsInputFocused(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Flatten dropdownOptions to better manage refs and focus
|
||||
let flatIndex = 0;
|
||||
const indexMap = new Map<string, number>();
|
||||
for (let sectionIndex = 0; sectionIndex < dropdownOptions.length; sectionIndex++) {
|
||||
const section = dropdownOptions[sectionIndex];
|
||||
for (let optionIndex = 0; optionIndex < section.options.length; optionIndex++) {
|
||||
indexMap.set(`${sectionIndex}-${optionIndex}`, flatIndex);
|
||||
flatIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
id="filterInput"
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
size="small"
|
||||
autoComplete="off"
|
||||
className={`filterInput ${styles.input}`}
|
||||
title={title}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
autoFocus
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
// Don't show dropdown if there is already a value in the input field (when user is typing)
|
||||
setShowDropdown(!(newValue.length > 0));
|
||||
onChange(newValue);
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onFocus={() => {
|
||||
// Don't show dropdown if there is already a value in the input field
|
||||
// or isInputFocused is undefined which means component is mounting
|
||||
setShowDropdown(!(value.length > 0) && isInputFocused !== undefined);
|
||||
|
||||
setIsInputFocused(true);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsInputFocused(false);
|
||||
}}
|
||||
contentAfter={
|
||||
value.length > 0 ? (
|
||||
<Button
|
||||
aria-label="Clear filter"
|
||||
className={styles.inputButton}
|
||||
size="small"
|
||||
icon={<DismissRegular />}
|
||||
onClick={() => {
|
||||
onChange("");
|
||||
setIsInputFocused(true);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
aria-label="Open dropdown"
|
||||
className={styles.inputButton}
|
||||
size="small"
|
||||
icon={<ArrowDownRegular />}
|
||||
onClick={() => {
|
||||
setShowDropdown(true);
|
||||
setAutofocusFirstDropdownItem(true);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Popover
|
||||
inline
|
||||
unstable_disableAutoFocus
|
||||
// trapFocus
|
||||
open={showDropdown}
|
||||
onOpenChange={handleOpenChange}
|
||||
positioning={{ positioningRef, position: "below", align: "start", offset: 4 }}
|
||||
>
|
||||
<PopoverSurface className={styles.container}>
|
||||
{dropdownOptions.map((section, sectionIndex) => (
|
||||
<div key={section.label}>
|
||||
<div className={styles.dropdownHeader} style={{ color: theme.palette.themePrimary }}>
|
||||
{section.label}
|
||||
</div>
|
||||
<div className={styles.dropdownStack}>
|
||||
{section.options.map((option, index) => (
|
||||
<Button
|
||||
key={option}
|
||||
ref={(el) => (itemRefs.current[indexMap.get(`${sectionIndex}-${index}`)] = el)}
|
||||
appearance="transparent"
|
||||
shape="square"
|
||||
className={styles.dropdownOption}
|
||||
onClick={() => {
|
||||
onChange(option);
|
||||
setShowDropdown(false);
|
||||
setIsInputFocused(true);
|
||||
}}
|
||||
onBlur={() =>
|
||||
!bottomLink &&
|
||||
sectionIndex === dropdownOptions.length - 1 &&
|
||||
index === section.options.length - 1 &&
|
||||
setShowDropdown(false)
|
||||
}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLButtonElement>) =>
|
||||
handleDownDropdownItemKeyDown(e, indexMap.get(`${sectionIndex}-${index}`))
|
||||
}
|
||||
>
|
||||
{option}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{bottomLink && (
|
||||
<>
|
||||
<Divider />
|
||||
<div className={styles.bottomSection}>
|
||||
<Link
|
||||
ref={(el) => (itemRefs.current[flatIndex] = el)}
|
||||
href={bottomLink.url}
|
||||
target="_blank"
|
||||
onBlur={() => setShowDropdown(false)}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLAnchorElement>) => handleDownDropdownItemKeyDown(e, flatIndex)}
|
||||
>
|
||||
{bottomLink.text}
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</PopoverSurface>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
import { AuthType } from "AuthType";
|
||||
import { shallow } from "enzyme";
|
||||
import ko from "knockout";
|
||||
import { Features } from "Platform/Hosted/extractFeatures";
|
||||
import React from "react";
|
||||
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
|
||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||
@@ -247,4 +249,42 @@ describe("SettingsComponent", () => {
|
||||
expect(conflictResolutionPolicy.mode).toEqual(DataModels.ConflictResolutionMode.Custom);
|
||||
expect(conflictResolutionPolicy.conflictResolutionProcedure).toEqual(expectSprocPath);
|
||||
});
|
||||
|
||||
it("should save throughput bucket changes when Save button is clicked", async () => {
|
||||
updateUserContext({
|
||||
apiType: "SQL",
|
||||
features: { enableThroughputBuckets: true } as Features,
|
||||
authType: AuthType.AAD,
|
||||
});
|
||||
|
||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
||||
|
||||
const settingsComponentInstance = wrapper.instance() as SettingsComponent;
|
||||
const isEnabled = settingsComponentInstance["throughputBucketsEnabled"];
|
||||
expect(isEnabled).toBe(true);
|
||||
|
||||
wrapper.setState({
|
||||
isThroughputBucketsSaveable: true,
|
||||
throughputBuckets: [
|
||||
{ id: 1, maxThroughputPercentage: 70 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
],
|
||||
});
|
||||
|
||||
await settingsComponentInstance.onSaveClick();
|
||||
|
||||
expect(updateOffer).toHaveBeenCalledWith({
|
||||
databaseId: collection.databaseId,
|
||||
collectionId: collection.id(),
|
||||
currentOffer: expect.any(Object),
|
||||
autopilotThroughput: collection.offer().autoscaleMaxThroughput,
|
||||
manualThroughput: collection.offer().manualThroughput,
|
||||
throughputBuckets: [
|
||||
{ id: 1, maxThroughputPercentage: 70 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
],
|
||||
});
|
||||
|
||||
expect(wrapper.state("isThroughputBucketsSaveable")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,11 +4,15 @@ import {
|
||||
ComputedPropertiesComponentProps,
|
||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent";
|
||||
import {
|
||||
ContainerVectorPolicyComponent,
|
||||
ContainerVectorPolicyComponentProps,
|
||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent";
|
||||
ContainerPolicyComponent,
|
||||
ContainerPolicyComponentProps,
|
||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent";
|
||||
import {
|
||||
ThroughputBucketsComponent,
|
||||
ThroughputBucketsComponentProps,
|
||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
||||
import * as React from "react";
|
||||
import DiscardIcon from "../../../../images/discard.svg";
|
||||
@@ -41,6 +45,10 @@ import {
|
||||
ConflictResolutionComponentProps,
|
||||
} from "./SettingsSubComponents/ConflictResolutionComponent";
|
||||
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
|
||||
import {
|
||||
MaterializedViewComponent,
|
||||
MaterializedViewComponentProps,
|
||||
} from "./SettingsSubComponents/MaterializedViewComponent";
|
||||
import {
|
||||
MongoIndexingPolicyComponent,
|
||||
MongoIndexingPolicyComponentProps,
|
||||
@@ -86,6 +94,8 @@ export interface SettingsComponentState {
|
||||
wasAutopilotOriginallySet: boolean;
|
||||
isScaleSaveable: boolean;
|
||||
isScaleDiscardable: boolean;
|
||||
throughputBuckets: DataModels.ThroughputBucket[];
|
||||
throughputBucketsBaseline: DataModels.ThroughputBucket[];
|
||||
throughputError: string;
|
||||
|
||||
timeToLive: TtlType;
|
||||
@@ -104,6 +114,14 @@ export interface SettingsComponentState {
|
||||
changeFeedPolicyBaseline: ChangeFeedPolicyState;
|
||||
isSubSettingsSaveable: boolean;
|
||||
isSubSettingsDiscardable: boolean;
|
||||
isThroughputBucketsSaveable: boolean;
|
||||
|
||||
vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
|
||||
vectorEmbeddingPolicyBaseline: DataModels.VectorEmbeddingPolicy;
|
||||
fullTextPolicy: DataModels.FullTextPolicy;
|
||||
fullTextPolicyBaseline: DataModels.FullTextPolicy;
|
||||
shouldDiscardContainerPolicies: boolean;
|
||||
isContainerPolicyDirty: boolean;
|
||||
|
||||
indexingPolicyContent: DataModels.IndexingPolicy;
|
||||
indexingPolicyContentBaseline: DataModels.IndexingPolicy;
|
||||
@@ -148,8 +166,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private shouldShowComputedPropertiesEditor: boolean;
|
||||
private shouldShowIndexingPolicyEditor: boolean;
|
||||
private shouldShowPartitionKeyEditor: boolean;
|
||||
private isMaterializedView: boolean;
|
||||
private isVectorSearchEnabled: boolean;
|
||||
private isFullTextSearchEnabled: boolean;
|
||||
private totalThroughputUsed: number;
|
||||
private throughputBucketsEnabled: boolean;
|
||||
public mongoDBCollectionResource: MongoDBCollectionResource;
|
||||
|
||||
constructor(props: SettingsComponentProps) {
|
||||
@@ -163,9 +184,16 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
|
||||
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
||||
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
|
||||
this.isMaterializedView =
|
||||
!!this.collection?.materializedViewDefinition() || !!this.collection?.materializedViews();
|
||||
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
||||
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
||||
|
||||
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
||||
this.throughputBucketsEnabled =
|
||||
userContext.apiType === "SQL" &&
|
||||
userContext.features.enableThroughputBuckets &&
|
||||
userContext.authType === AuthType.AAD;
|
||||
|
||||
// Mongo container with system partition key still treat as "Fixed"
|
||||
this.isFixedContainer =
|
||||
@@ -184,6 +212,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
wasAutopilotOriginallySet: false,
|
||||
isScaleSaveable: false,
|
||||
isScaleDiscardable: false,
|
||||
throughputBuckets: undefined,
|
||||
throughputBucketsBaseline: undefined,
|
||||
throughputError: undefined,
|
||||
|
||||
timeToLive: undefined,
|
||||
@@ -202,6 +232,14 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
changeFeedPolicyBaseline: undefined,
|
||||
isSubSettingsSaveable: false,
|
||||
isSubSettingsDiscardable: false,
|
||||
isThroughputBucketsSaveable: false,
|
||||
|
||||
vectorEmbeddingPolicy: undefined,
|
||||
vectorEmbeddingPolicyBaseline: undefined,
|
||||
fullTextPolicy: undefined,
|
||||
fullTextPolicyBaseline: undefined,
|
||||
shouldDiscardContainerPolicies: false,
|
||||
isContainerPolicyDirty: false,
|
||||
|
||||
indexingPolicyContent: undefined,
|
||||
indexingPolicyContentBaseline: undefined,
|
||||
@@ -307,10 +345,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
return (
|
||||
this.state.isScaleSaveable ||
|
||||
this.state.isSubSettingsSaveable ||
|
||||
this.state.isContainerPolicyDirty ||
|
||||
this.state.isIndexingPolicyDirty ||
|
||||
this.state.isConflictResolutionDirty ||
|
||||
this.state.isComputedPropertiesDirty ||
|
||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable)
|
||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable) ||
|
||||
this.state.isThroughputBucketsSaveable
|
||||
);
|
||||
};
|
||||
|
||||
@@ -318,10 +358,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
return (
|
||||
this.state.isScaleDiscardable ||
|
||||
this.state.isSubSettingsDiscardable ||
|
||||
this.state.isContainerPolicyDirty ||
|
||||
this.state.isIndexingPolicyDirty ||
|
||||
this.state.isConflictResolutionDirty ||
|
||||
this.state.isComputedPropertiesDirty ||
|
||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable)
|
||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable) ||
|
||||
this.state.isThroughputBucketsSaveable
|
||||
);
|
||||
};
|
||||
|
||||
@@ -401,10 +443,14 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
|
||||
this.setState({
|
||||
throughput: this.state.throughputBaseline,
|
||||
throughputBuckets: this.state.throughputBucketsBaseline,
|
||||
throughputBucketsBaseline: this.state.throughputBucketsBaseline,
|
||||
timeToLive: this.state.timeToLiveBaseline,
|
||||
timeToLiveSeconds: this.state.timeToLiveSecondsBaseline,
|
||||
displayedTtlSeconds: this.state.displayedTtlSecondsBaseline,
|
||||
geospatialConfigType: this.state.geospatialConfigTypeBaseline,
|
||||
vectorEmbeddingPolicy: this.state.vectorEmbeddingPolicyBaseline,
|
||||
fullTextPolicy: this.state.fullTextPolicyBaseline,
|
||||
indexingPolicyContent: this.state.indexingPolicyContentBaseline,
|
||||
indexesToAdd: [],
|
||||
indexesToDrop: [],
|
||||
@@ -416,11 +462,14 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
changeFeedPolicy: this.state.changeFeedPolicyBaseline,
|
||||
autoPilotThroughput: this.state.autoPilotThroughputBaseline,
|
||||
isAutoPilotSelected: this.state.wasAutopilotOriginallySet,
|
||||
shouldDiscardContainerPolicies: true,
|
||||
shouldDiscardIndexingPolicy: true,
|
||||
isScaleSaveable: false,
|
||||
isScaleDiscardable: false,
|
||||
isSubSettingsSaveable: false,
|
||||
isThroughputBucketsSaveable: false,
|
||||
isSubSettingsDiscardable: false,
|
||||
isContainerPolicyDirty: false,
|
||||
isIndexingPolicyDirty: false,
|
||||
isMongoIndexingPolicySaveable: false,
|
||||
isMongoIndexingPolicyDiscardable: false,
|
||||
@@ -448,9 +497,21 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private onScaleDiscardableChange = (isScaleDiscardable: boolean): void =>
|
||||
this.setState({ isScaleDiscardable: isScaleDiscardable });
|
||||
|
||||
private onVectorEmbeddingPolicyChange = (newVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy): void =>
|
||||
this.setState({ vectorEmbeddingPolicy: newVectorEmbeddingPolicy });
|
||||
|
||||
private onFullTextPolicyChange = (newFullTextPolicy: DataModels.FullTextPolicy): void =>
|
||||
this.setState({ fullTextPolicy: newFullTextPolicy });
|
||||
|
||||
private onIndexingPolicyContentChange = (newIndexingPolicy: DataModels.IndexingPolicy): void =>
|
||||
this.setState({ indexingPolicyContent: newIndexingPolicy });
|
||||
|
||||
private onThroughputBucketsSaveableChange = (isSaveable: boolean): void => {
|
||||
this.setState({ isThroughputBucketsSaveable: isSaveable });
|
||||
};
|
||||
|
||||
private resetShouldDiscardContainerPolicies = (): void => this.setState({ shouldDiscardContainerPolicies: false });
|
||||
|
||||
private resetShouldDiscardIndexingPolicy = (): void => this.setState({ shouldDiscardIndexingPolicy: false });
|
||||
|
||||
private logIndexingPolicySuccessMessage = (): void => {
|
||||
@@ -538,6 +599,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private onSubSettingsDiscardableChange = (isSubSettingsDiscardable: boolean): void =>
|
||||
this.setState({ isSubSettingsDiscardable: isSubSettingsDiscardable });
|
||||
|
||||
private onVectorEmbeddingPolicyDirtyChange = (isVectorEmbeddingPolicyDirty: boolean): void =>
|
||||
this.setState({ isContainerPolicyDirty: isVectorEmbeddingPolicyDirty });
|
||||
|
||||
private onFullTextPolicyDirtyChange = (isFullTextPolicyDirty: boolean): void =>
|
||||
this.setState({ isContainerPolicyDirty: isFullTextPolicyDirty });
|
||||
|
||||
private onIndexingPolicyDirtyChange = (isIndexingPolicyDirty: boolean): void =>
|
||||
this.setState({ isIndexingPolicyDirty: isIndexingPolicyDirty });
|
||||
|
||||
@@ -691,6 +758,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
||||
? ChangeFeedPolicyState.On
|
||||
: ChangeFeedPolicyState.Off;
|
||||
const vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy =
|
||||
this.collection.vectorEmbeddingPolicy && this.collection.vectorEmbeddingPolicy();
|
||||
const fullTextPolicy: DataModels.FullTextPolicy =
|
||||
this.collection.fullTextPolicy && this.collection.fullTextPolicy();
|
||||
const indexingPolicyContent = this.collection.indexingPolicy();
|
||||
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
|
||||
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
|
||||
@@ -709,9 +780,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
] as DataModels.ComputedProperties;
|
||||
}
|
||||
|
||||
const throughputBuckets = this.offer?.throughputBuckets;
|
||||
|
||||
return {
|
||||
throughput: offerThroughput,
|
||||
throughputBaseline: offerThroughput,
|
||||
throughputBuckets,
|
||||
throughputBucketsBaseline: throughputBuckets,
|
||||
changeFeedPolicy: changeFeedPolicy,
|
||||
changeFeedPolicyBaseline: changeFeedPolicy,
|
||||
timeToLive: timeToLive,
|
||||
@@ -724,6 +799,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
analyticalStorageTtlSelectionBaseline: analyticalStorageTtlSelection,
|
||||
analyticalStorageTtlSeconds: analyticalStorageTtlSeconds,
|
||||
analyticalStorageTtlSecondsBaseline: analyticalStorageTtlSeconds,
|
||||
vectorEmbeddingPolicy: vectorEmbeddingPolicy,
|
||||
vectorEmbeddingPolicyBaseline: vectorEmbeddingPolicy,
|
||||
fullTextPolicy: fullTextPolicy,
|
||||
fullTextPolicyBaseline: fullTextPolicy,
|
||||
indexingPolicyContent: indexingPolicyContent,
|
||||
indexingPolicyContentBaseline: indexingPolicyContent,
|
||||
conflictResolutionPolicyMode: conflictResolutionPolicyMode,
|
||||
@@ -795,6 +874,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.setState({ throughput: newThroughput, throughputError });
|
||||
};
|
||||
|
||||
private onThroughputBucketChange = (throughputBuckets: DataModels.ThroughputBucket[]): void => {
|
||||
this.setState({ throughputBuckets });
|
||||
};
|
||||
|
||||
private onAutoPilotSelected = (isAutoPilotSelected: boolean): void =>
|
||||
this.setState({ isAutoPilotSelected: isAutoPilotSelected });
|
||||
|
||||
@@ -854,6 +937,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
|
||||
if (
|
||||
this.state.isSubSettingsSaveable ||
|
||||
this.state.isContainerPolicyDirty ||
|
||||
this.state.isIndexingPolicyDirty ||
|
||||
this.state.isConflictResolutionDirty ||
|
||||
this.state.isComputedPropertiesDirty
|
||||
@@ -875,6 +959,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
const wasIndexingPolicyModified = this.state.isIndexingPolicyDirty;
|
||||
newCollection.defaultTtl = defaultTtl;
|
||||
|
||||
newCollection.vectorEmbeddingPolicy = this.state.vectorEmbeddingPolicy;
|
||||
|
||||
newCollection.fullTextPolicy = this.state.fullTextPolicy;
|
||||
|
||||
newCollection.indexingPolicy = this.state.indexingPolicyContent;
|
||||
|
||||
newCollection.changeFeedPolicy =
|
||||
@@ -913,6 +1001,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
|
||||
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
|
||||
this.collection.computedProperties(updatedCollection.computedProperties);
|
||||
this.collection.vectorEmbeddingPolicy(updatedCollection.vectorEmbeddingPolicy);
|
||||
this.collection.fullTextPolicy(updatedCollection.fullTextPolicy);
|
||||
|
||||
if (wasIndexingPolicyModified) {
|
||||
await this.refreshIndexTransformationProgress();
|
||||
@@ -921,6 +1011,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.setState({
|
||||
isSubSettingsSaveable: false,
|
||||
isSubSettingsDiscardable: false,
|
||||
isContainerPolicyDirty: false,
|
||||
isIndexingPolicyDirty: false,
|
||||
isConflictResolutionDirty: false,
|
||||
isComputedPropertiesDirty: false,
|
||||
@@ -977,6 +1068,24 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
}
|
||||
}
|
||||
|
||||
if (this.throughputBucketsEnabled && this.state.isThroughputBucketsSaveable) {
|
||||
const updatedOffer: DataModels.Offer = await updateOffer({
|
||||
databaseId: this.collection.databaseId,
|
||||
collectionId: this.collection.id(),
|
||||
currentOffer: this.collection.offer(),
|
||||
autopilotThroughput: this.collection.offer().autoscaleMaxThroughput
|
||||
? this.collection.offer().autoscaleMaxThroughput
|
||||
: undefined,
|
||||
manualThroughput: this.collection.offer().manualThroughput
|
||||
? this.collection.offer().manualThroughput
|
||||
: undefined,
|
||||
throughputBuckets: this.state.throughputBuckets,
|
||||
});
|
||||
this.collection.offer(updatedOffer);
|
||||
this.offer = updatedOffer;
|
||||
this.setState({ isThroughputBucketsSaveable: false });
|
||||
}
|
||||
|
||||
if (this.state.isScaleSaveable) {
|
||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||
databaseId: this.collection.databaseId,
|
||||
@@ -1091,6 +1200,21 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
onSubSettingsDiscardableChange: this.onSubSettingsDiscardableChange,
|
||||
};
|
||||
|
||||
const containerPolicyComponentProps: ContainerPolicyComponentProps = {
|
||||
vectorEmbeddingPolicy: this.state.vectorEmbeddingPolicy,
|
||||
vectorEmbeddingPolicyBaseline: this.state.vectorEmbeddingPolicyBaseline,
|
||||
onVectorEmbeddingPolicyChange: this.onVectorEmbeddingPolicyChange,
|
||||
onVectorEmbeddingPolicyDirtyChange: this.onVectorEmbeddingPolicyDirtyChange,
|
||||
isVectorSearchEnabled: this.isVectorSearchEnabled,
|
||||
fullTextPolicy: this.state.fullTextPolicy,
|
||||
fullTextPolicyBaseline: this.state.fullTextPolicyBaseline,
|
||||
onFullTextPolicyChange: this.onFullTextPolicyChange,
|
||||
onFullTextPolicyDirtyChange: this.onFullTextPolicyDirtyChange,
|
||||
isFullTextSearchEnabled: this.isFullTextSearchEnabled,
|
||||
shouldDiscardContainerPolicies: this.state.shouldDiscardContainerPolicies,
|
||||
resetShouldDiscardContainerPolicyChange: this.resetShouldDiscardContainerPolicies,
|
||||
};
|
||||
|
||||
const indexingPolicyComponentProps: IndexingPolicyComponentProps = {
|
||||
shouldDiscardIndexingPolicy: this.state.shouldDiscardIndexingPolicy,
|
||||
resetShouldDiscardIndexingPolicy: this.resetShouldDiscardIndexingPolicy,
|
||||
@@ -1142,14 +1266,22 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange,
|
||||
};
|
||||
|
||||
const throughputBucketsComponentProps: ThroughputBucketsComponentProps = {
|
||||
currentBuckets: this.state.throughputBuckets,
|
||||
throughputBucketsBaseline: this.state.throughputBucketsBaseline,
|
||||
onBucketsChange: this.onThroughputBucketChange,
|
||||
onSaveableChange: this.onThroughputBucketsSaveableChange,
|
||||
};
|
||||
|
||||
const partitionKeyComponentProps: PartitionKeyComponentProps = {
|
||||
database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId),
|
||||
collection: this.collection,
|
||||
explorer: this.props.settingsTab.getContainer(),
|
||||
};
|
||||
|
||||
const containerVectorPolicyProps: ContainerVectorPolicyComponentProps = {
|
||||
vectorEmbeddingPolicy: this.collection.rawDataModel?.vectorEmbeddingPolicy,
|
||||
const materializedViewComponentProps: MaterializedViewComponentProps = {
|
||||
collection: this.collection,
|
||||
explorer: this.props.settingsTab.getContainer(),
|
||||
};
|
||||
|
||||
const tabs: SettingsV2TabInfo[] = [];
|
||||
@@ -1165,10 +1297,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
content: <SubSettingsComponent {...subSettingsComponentProps} />,
|
||||
});
|
||||
|
||||
if (this.isVectorSearchEnabled) {
|
||||
if (this.isVectorSearchEnabled || this.isFullTextSearchEnabled) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.ContainerVectorPolicyTab,
|
||||
content: <ContainerVectorPolicyComponent {...containerVectorPolicyProps} />,
|
||||
content: <ContainerPolicyComponent {...containerPolicyComponentProps} />,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1208,6 +1340,20 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
});
|
||||
}
|
||||
|
||||
if (this.throughputBucketsEnabled) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.ThroughputBucketsTab,
|
||||
content: <ThroughputBucketsComponent {...throughputBucketsComponentProps} />,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.isMaterializedView) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.MaterializedViewTab,
|
||||
content: <MaterializedViewComponent {...materializedViewComponentProps} />,
|
||||
});
|
||||
}
|
||||
|
||||
const pivotProps: IPivotProps = {
|
||||
onLinkClick: this.onPivotChange,
|
||||
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import "@testing-library/jest-dom";
|
||||
|
||||
describe("ContainerPolicyComponent", () => {
|
||||
//CTODO: add tests
|
||||
it.skip("should render correctly", () => {});
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
import { DefaultButton, Pivot, PivotItem, Stack } from "@fluentui/react";
|
||||
import { FullTextPolicy, VectorEmbedding, VectorEmbeddingPolicy } from "Contracts/DataModels";
|
||||
import {
|
||||
FullTextPoliciesComponent,
|
||||
getFullTextLanguageOptions,
|
||||
} from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
||||
import { titleAndInputStackProps } from "Explorer/Controls/Settings/SettingsRenderUtils";
|
||||
import { ContainerPolicyTabTypes, isDirty } from "Explorer/Controls/Settings/SettingsUtils";
|
||||
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
||||
import React from "react";
|
||||
|
||||
export interface ContainerPolicyComponentProps {
|
||||
vectorEmbeddingPolicy: VectorEmbeddingPolicy;
|
||||
vectorEmbeddingPolicyBaseline: VectorEmbeddingPolicy;
|
||||
onVectorEmbeddingPolicyChange: (newVectorEmbeddingPolicy: VectorEmbeddingPolicy) => void;
|
||||
onVectorEmbeddingPolicyDirtyChange: (isVectorEmbeddingPolicyDirty: boolean) => void;
|
||||
isVectorSearchEnabled: boolean;
|
||||
fullTextPolicy: FullTextPolicy;
|
||||
fullTextPolicyBaseline: FullTextPolicy;
|
||||
onFullTextPolicyChange: (newFullTextPolicy: FullTextPolicy) => void;
|
||||
onFullTextPolicyDirtyChange: (isFullTextPolicyDirty: boolean) => void;
|
||||
isFullTextSearchEnabled: boolean;
|
||||
shouldDiscardContainerPolicies: boolean;
|
||||
resetShouldDiscardContainerPolicyChange: () => void;
|
||||
}
|
||||
|
||||
export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> = ({
|
||||
vectorEmbeddingPolicy,
|
||||
vectorEmbeddingPolicyBaseline,
|
||||
onVectorEmbeddingPolicyChange,
|
||||
onVectorEmbeddingPolicyDirtyChange,
|
||||
isVectorSearchEnabled,
|
||||
fullTextPolicy,
|
||||
fullTextPolicyBaseline,
|
||||
onFullTextPolicyChange,
|
||||
onFullTextPolicyDirtyChange,
|
||||
isFullTextSearchEnabled,
|
||||
shouldDiscardContainerPolicies,
|
||||
resetShouldDiscardContainerPolicyChange,
|
||||
}) => {
|
||||
const [selectedTab, setSelectedTab] = React.useState<ContainerPolicyTabTypes>(
|
||||
ContainerPolicyTabTypes.VectorPolicyTab,
|
||||
);
|
||||
const [vectorEmbeddings, setVectorEmbeddings] = React.useState<VectorEmbedding[]>();
|
||||
const [vectorEmbeddingsBaseline, setVectorEmbeddingsBaseline] = React.useState<VectorEmbedding[]>();
|
||||
const [discardVectorChanges, setDiscardVectorChanges] = React.useState<boolean>(false);
|
||||
const [fullTextSearchPolicy, setFullTextSearchPolicy] = React.useState<FullTextPolicy>();
|
||||
const [fullTextSearchPolicyBaseline, setFullTextSearchPolicyBaseline] = React.useState<FullTextPolicy>();
|
||||
const [discardFullTextChanges, setDiscardFullTextChanges] = React.useState<boolean>(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setVectorEmbeddings(vectorEmbeddingPolicy?.vectorEmbeddings);
|
||||
setVectorEmbeddingsBaseline(vectorEmbeddingPolicyBaseline?.vectorEmbeddings);
|
||||
}, [vectorEmbeddingPolicy]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setFullTextSearchPolicy(fullTextPolicy);
|
||||
setFullTextSearchPolicyBaseline(fullTextPolicyBaseline);
|
||||
}, [fullTextPolicy, fullTextPolicyBaseline]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (shouldDiscardContainerPolicies) {
|
||||
setVectorEmbeddings(vectorEmbeddingPolicyBaseline?.vectorEmbeddings);
|
||||
setDiscardVectorChanges(true);
|
||||
setFullTextSearchPolicy(fullTextPolicyBaseline);
|
||||
setDiscardFullTextChanges(true);
|
||||
resetShouldDiscardContainerPolicyChange();
|
||||
}
|
||||
});
|
||||
|
||||
const checkAndSendVectorEmbeddingPoliciesToSettings = (newVectorEmbeddings: VectorEmbedding[]): void => {
|
||||
if (isDirty(newVectorEmbeddings, vectorEmbeddingsBaseline)) {
|
||||
onVectorEmbeddingPolicyDirtyChange(true);
|
||||
onVectorEmbeddingPolicyChange({ vectorEmbeddings: newVectorEmbeddings });
|
||||
} else {
|
||||
resetShouldDiscardContainerPolicyChange();
|
||||
}
|
||||
};
|
||||
|
||||
const checkAndSendFullTextPolicyToSettings = (newFullTextPolicy: FullTextPolicy): void => {
|
||||
if (isDirty(newFullTextPolicy, fullTextSearchPolicyBaseline)) {
|
||||
onFullTextPolicyDirtyChange(true);
|
||||
onFullTextPolicyChange(newFullTextPolicy);
|
||||
} else {
|
||||
resetShouldDiscardContainerPolicyChange();
|
||||
}
|
||||
};
|
||||
|
||||
const onVectorChangesDiscarded = (): void => {
|
||||
setDiscardVectorChanges(false);
|
||||
};
|
||||
|
||||
const onFullTextChangesDiscarded = (): void => {
|
||||
setDiscardFullTextChanges(false);
|
||||
};
|
||||
|
||||
const onPivotChange = (item: PivotItem): void => {
|
||||
const selectedTab = ContainerPolicyTabTypes[item.props.itemKey as keyof typeof ContainerPolicyTabTypes];
|
||||
setSelectedTab(selectedTab);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Pivot onLinkClick={onPivotChange} selectedKey={ContainerPolicyTabTypes[selectedTab]}>
|
||||
{isVectorSearchEnabled && (
|
||||
<PivotItem
|
||||
itemKey={ContainerPolicyTabTypes[ContainerPolicyTabTypes.VectorPolicyTab]}
|
||||
style={{ marginTop: 20 }}
|
||||
headerText="Vector Policy"
|
||||
>
|
||||
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative", maxWidth: "400px" } }}>
|
||||
{vectorEmbeddings && (
|
||||
<VectorEmbeddingPoliciesComponent
|
||||
disabled={true}
|
||||
vectorEmbeddings={vectorEmbeddings}
|
||||
vectorIndexes={undefined}
|
||||
onVectorEmbeddingChange={(vectorEmbeddings: VectorEmbedding[]) =>
|
||||
checkAndSendVectorEmbeddingPoliciesToSettings(vectorEmbeddings)
|
||||
}
|
||||
discardChanges={discardVectorChanges}
|
||||
onChangesDiscarded={onVectorChangesDiscarded}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
)}
|
||||
{isFullTextSearchEnabled && (
|
||||
<PivotItem
|
||||
itemKey={ContainerPolicyTabTypes[ContainerPolicyTabTypes.FullTextPolicyTab]}
|
||||
style={{ marginTop: 20 }}
|
||||
headerText="Full Text Policy"
|
||||
>
|
||||
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative", maxWidth: "400px" } }}>
|
||||
{fullTextSearchPolicy ? (
|
||||
<FullTextPoliciesComponent
|
||||
fullTextPolicy={fullTextSearchPolicy}
|
||||
onFullTextPathChange={(newFullTextPolicy: FullTextPolicy) =>
|
||||
checkAndSendFullTextPolicyToSettings(newFullTextPolicy)
|
||||
}
|
||||
discardChanges={discardFullTextChanges}
|
||||
onChangesDiscarded={onFullTextChangesDiscarded}
|
||||
/>
|
||||
) : (
|
||||
<DefaultButton
|
||||
id={"create-full-text-policy"}
|
||||
styles={{ root: { fontSize: 12 } }}
|
||||
onClick={() => {
|
||||
checkAndSendFullTextPolicyToSettings({
|
||||
defaultLanguage: getFullTextLanguageOptions()[0].key as never,
|
||||
fullTextPaths: [],
|
||||
});
|
||||
}}
|
||||
>
|
||||
Create new full text search policy
|
||||
</DefaultButton>
|
||||
)}
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
)}
|
||||
</Pivot>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Stack } from "@fluentui/react";
|
||||
import { VectorEmbeddingPolicy } from "Contracts/DataModels";
|
||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||
import { titleAndInputStackProps } from "Explorer/Controls/Settings/SettingsRenderUtils";
|
||||
import React from "react";
|
||||
|
||||
export interface ContainerVectorPolicyComponentProps {
|
||||
vectorEmbeddingPolicy: VectorEmbeddingPolicy;
|
||||
}
|
||||
|
||||
export const ContainerVectorPolicyComponent: React.FC<ContainerVectorPolicyComponentProps> = ({
|
||||
vectorEmbeddingPolicy,
|
||||
}) => {
|
||||
return (
|
||||
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative" } }}>
|
||||
<EditorReact
|
||||
language={"json"}
|
||||
content={JSON.stringify(vectorEmbeddingPolicy || {}, null, 4)}
|
||||
isReadOnly={true}
|
||||
wordWrap={"on"}
|
||||
ariaLabel={"Container vector policy"}
|
||||
lineNumbers={"on"}
|
||||
scrollBeyondLastLine={false}
|
||||
className={"settingsV2Editor"}
|
||||
spinnerClassName={"settingsV2EditorSpinner"}
|
||||
fontSize={14}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -120,11 +120,6 @@ export class IndexingPolicyComponent extends React.Component<
|
||||
indexTransformationProgress={this.props.indexTransformationProgress}
|
||||
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
|
||||
/>
|
||||
{this.props.isVectorSearchEnabled && (
|
||||
<MessageBar messageBarType={MessageBarType.severeWarning}>
|
||||
Container vector policies and vector indexes are not modifiable after container creation
|
||||
</MessageBar>
|
||||
)}
|
||||
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
|
||||
<MessageBar messageBarType={MessageBarType.warning}>{unsavedEditorWarningMessage("indexPolicy")}</MessageBar>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { collection, container } from "../TestUtils";
|
||||
import { MaterializedViewComponent } from "./MaterializedViewComponent";
|
||||
import { MaterializedViewSourceComponent } from "./MaterializedViewSourceComponent";
|
||||
import { MaterializedViewTargetComponent } from "./MaterializedViewTargetComponent";
|
||||
|
||||
describe("MaterializedViewComponent", () => {
|
||||
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(<MaterializedViewComponent collection={testCollection} explorer={testExplorer} />);
|
||||
expect(wrapper.find(MaterializedViewSourceComponent).exists()).toBe(true);
|
||||
expect(wrapper.find(MaterializedViewTargetComponent).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(<MaterializedViewComponent collection={testCollection} explorer={testExplorer} />);
|
||||
expect(wrapper.find(MaterializedViewSourceComponent).exists()).toBe(false);
|
||||
expect(wrapper.find(MaterializedViewTargetComponent).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("renders neither component when both are missing", () => {
|
||||
testCollection.materializedViews(null);
|
||||
testCollection.materializedViewDefinition(null);
|
||||
const wrapper = shallow(<MaterializedViewComponent collection={testCollection} explorer={testExplorer} />);
|
||||
expect(wrapper.find(MaterializedViewSourceComponent).exists()).toBe(false);
|
||||
expect(wrapper.find(MaterializedViewTargetComponent).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { FontIcon, Link, Stack, Text } from "@fluentui/react";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import React from "react";
|
||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
import { MaterializedViewSourceComponent } from "./MaterializedViewSourceComponent";
|
||||
import { MaterializedViewTargetComponent } from "./MaterializedViewTargetComponent";
|
||||
|
||||
export interface MaterializedViewComponentProps {
|
||||
collection: ViewModels.Collection;
|
||||
explorer: Explorer;
|
||||
}
|
||||
|
||||
export const MaterializedViewComponent: React.FC<MaterializedViewComponentProps> = ({ collection, explorer }) => {
|
||||
const isTargetContainer = !!collection?.materializedViewDefinition();
|
||||
const isSourceContainer = !!collection?.materializedViews();
|
||||
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: 8 }} styles={{ root: { maxWidth: 600 } }}>
|
||||
<Stack horizontal verticalAlign="center" wrap tokens={{ childrenGap: 8 }}>
|
||||
<Text styles={{ root: { fontWeight: 600 } }}>This container has the following views defined for it.</Text>
|
||||
<Text>
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
|
||||
>
|
||||
Learn more
|
||||
<FontIcon iconName="NavigateExternalInline" style={{ marginLeft: "4px" }} />
|
||||
</Link>{" "}
|
||||
about how to define materialized views and how to use them.
|
||||
</Text>
|
||||
</Stack>
|
||||
{isSourceContainer && <MaterializedViewSourceComponent collection={collection} explorer={explorer} />}
|
||||
{isTargetContainer && <MaterializedViewTargetComponent collection={collection} />}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { PrimaryButton } from "@fluentui/react";
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { collection, container } from "../TestUtils";
|
||||
import { MaterializedViewSourceComponent } from "./MaterializedViewSourceComponent";
|
||||
|
||||
describe("MaterializedViewSourceComponent", () => {
|
||||
let testCollection: typeof collection;
|
||||
let testExplorer: typeof container;
|
||||
|
||||
beforeEach(() => {
|
||||
testCollection = { ...collection };
|
||||
});
|
||||
|
||||
it("renders without crashing", () => {
|
||||
const wrapper = shallow(<MaterializedViewSourceComponent collection={testCollection} explorer={testExplorer} />);
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("renders the PrimaryButton", () => {
|
||||
const wrapper = shallow(<MaterializedViewSourceComponent collection={testCollection} explorer={testExplorer} />);
|
||||
expect(wrapper.find(PrimaryButton).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("updates when new materialized views are provided", () => {
|
||||
const wrapper = shallow(<MaterializedViewSourceComponent collection={testCollection} explorer={testExplorer} />);
|
||||
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import { PrimaryButton } from "@fluentui/react";
|
||||
import { MaterializedViewsLabels } from "Common/Constants";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { loadMonaco } from "Explorer/LazyMonaco";
|
||||
import { AddMaterializedViewPanel } from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel";
|
||||
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 MaterializedViewSourceComponentProps {
|
||||
collection: ViewModels.Collection;
|
||||
explorer: Explorer;
|
||||
}
|
||||
|
||||
export const MaterializedViewSourceComponent: React.FC<MaterializedViewSourceComponentProps> = ({
|
||||
collection,
|
||||
explorer,
|
||||
}) => {
|
||||
const editorContainerRef = useRef<HTMLDivElement>(null);
|
||||
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>(null);
|
||||
|
||||
const materializedViews = collection?.materializedViews() ?? [];
|
||||
|
||||
// Helper function to fetch the definition and partition key of targetContainer by traversing through all collections and matching id from MaterializedViews[] 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 materializedViewDefinition = collection.materializedViewDefinition();
|
||||
materializedViewDefinition && (definition = materializedViewDefinition.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(
|
||||
materializedViews.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: "Materialized Views 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 (
|
||||
<div>
|
||||
<div
|
||||
ref={editorContainerRef}
|
||||
style={{
|
||||
height: 250,
|
||||
border: "1px solid #ccc",
|
||||
borderRadius: 4,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
/>
|
||||
<PrimaryButton
|
||||
text="Add view"
|
||||
styles={{ root: { width: "fit-content", marginTop: 12 } }}
|
||||
onClick={() =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
MaterializedViewsLabels.NewMaterializedView,
|
||||
<AddMaterializedViewPanel explorer={explorer} sourceContainer={collection} />,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 { MaterializedViewTargetComponent } from "./MaterializedViewTargetComponent";
|
||||
|
||||
describe("MaterializedViewTargetComponent", () => {
|
||||
let testCollection: Collection;
|
||||
|
||||
beforeEach(() => {
|
||||
testCollection = {
|
||||
...collection,
|
||||
materializedViewDefinition: collection.materializedViewDefinition,
|
||||
};
|
||||
});
|
||||
|
||||
it("renders without crashing", () => {
|
||||
const wrapper = shallow(<MaterializedViewTargetComponent collection={testCollection} />);
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("displays the source container ID", () => {
|
||||
const wrapper = shallow(<MaterializedViewTargetComponent collection={testCollection} />);
|
||||
expect(wrapper.find(Text).at(2).dive().text()).toBe("source1");
|
||||
});
|
||||
|
||||
it("displays the materialized view definition", () => {
|
||||
const wrapper = shallow(<MaterializedViewTargetComponent collection={testCollection} />);
|
||||
expect(wrapper.find(Text).at(4).dive().text()).toBe("SELECT * FROM c WHERE c.id = 1");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Stack, Text } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
|
||||
export interface MaterializedViewTargetComponentProps {
|
||||
collection: ViewModels.Collection;
|
||||
}
|
||||
|
||||
export const MaterializedViewTargetComponent: React.FC<MaterializedViewTargetComponentProps> = ({ collection }) => {
|
||||
const materializedViewDefinition = collection?.materializedViewDefinition();
|
||||
|
||||
const textHeadingStyle = {
|
||||
root: { fontWeight: "600", fontSize: 16 },
|
||||
};
|
||||
|
||||
const valueBoxStyle = {
|
||||
root: {
|
||||
backgroundColor: "#f3f3f3",
|
||||
padding: "5px 10px",
|
||||
borderRadius: "4px",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: 15 }} styles={{ root: { maxWidth: 600 } }}>
|
||||
<Text styles={textHeadingStyle}>Materialized View Settings</Text>
|
||||
|
||||
<Stack tokens={{ childrenGap: 5 }}>
|
||||
<Text styles={{ root: { fontWeight: "600" } }}>Source container</Text>
|
||||
<Stack styles={valueBoxStyle}>
|
||||
<Text>{materializedViewDefinition?.sourceCollectionId}</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Stack tokens={{ childrenGap: 5 }}>
|
||||
<Text styles={{ root: { fontWeight: "600" } }}>Materialized view definition</Text>
|
||||
<Stack styles={valueBoxStyle}>
|
||||
<Text>{materializedViewDefinition?.definition}</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -14,6 +14,7 @@ import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
|
||||
import { handleError } from "Common/ErrorHandlingUtils";
|
||||
import { cancelDataTransferJob, pollDataTransferJob } from "Common/dataAccess/dataTransfers";
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane";
|
||||
import {
|
||||
@@ -177,12 +178,14 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ da
|
||||
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.
|
||||
</Text>
|
||||
<PrimaryButton
|
||||
styles={{ root: { width: "fit-content" } }}
|
||||
text="Change"
|
||||
onClick={startPartitionkeyChangeWorkflow}
|
||||
disabled={isCurrentJobInProgress(portalDataTransferJob)}
|
||||
/>
|
||||
{configContext.platform !== Platform.Emulator && (
|
||||
<PrimaryButton
|
||||
styles={{ root: { width: "fit-content" } }}
|
||||
text="Change"
|
||||
onClick={startPartitionkeyChangeWorkflow}
|
||||
disabled={isCurrentJobInProgress(portalDataTransferJob)}
|
||||
/>
|
||||
)}
|
||||
{portalDataTransferJob && (
|
||||
<Stack>
|
||||
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { ThroughputBucketsComponent } from "./ThroughputBucketsComponent";
|
||||
|
||||
describe("ThroughputBucketsComponent", () => {
|
||||
const mockOnBucketsChange = jest.fn();
|
||||
const mockOnSaveableChange = jest.fn();
|
||||
|
||||
const defaultProps = {
|
||||
currentBuckets: [
|
||||
{ id: 1, maxThroughputPercentage: 50 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
],
|
||||
throughputBucketsBaseline: [
|
||||
{ id: 1, maxThroughputPercentage: 40 },
|
||||
{ id: 2, maxThroughputPercentage: 50 },
|
||||
],
|
||||
onBucketsChange: mockOnBucketsChange,
|
||||
onSaveableChange: mockOnSaveableChange,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders the correct number of buckets", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("renders buckets in the correct order even if input is unordered", () => {
|
||||
const unorderedBuckets = [
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
{ id: 1, maxThroughputPercentage: 50 },
|
||||
];
|
||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={unorderedBuckets} />);
|
||||
|
||||
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"]);
|
||||
});
|
||||
|
||||
it("renders all provided buckets even if they exceed the max default bucket count", () => {
|
||||
const oversizedBuckets = [
|
||||
{ id: 1, maxThroughputPercentage: 50 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
{ id: 3, maxThroughputPercentage: 70 },
|
||||
{ id: 4, maxThroughputPercentage: 80 },
|
||||
{ id: 5, maxThroughputPercentage: 90 },
|
||||
{ id: 6, maxThroughputPercentage: 100 },
|
||||
{ id: 7, maxThroughputPercentage: 40 },
|
||||
];
|
||||
|
||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={oversizedBuckets} />);
|
||||
|
||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(7);
|
||||
|
||||
expect(screen.getByDisplayValue("50")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("60")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("70")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("80")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("90")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("100")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("40")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onBucketsChange when a bucket value changes", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
const input = screen.getByDisplayValue("50");
|
||||
fireEvent.change(input, { target: { value: "70" } });
|
||||
|
||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||
{ id: 1, maxThroughputPercentage: 70 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
{ id: 3, maxThroughputPercentage: 100 },
|
||||
{ id: 4, maxThroughputPercentage: 100 },
|
||||
{ id: 5, maxThroughputPercentage: 100 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("triggers onSaveableChange when values change", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
const input = screen.getByDisplayValue("50");
|
||||
fireEvent.change(input, { target: { value: "80" } });
|
||||
|
||||
expect(mockOnSaveableChange).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("updates state consistently after multiple changes to different buckets", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
|
||||
const input1 = screen.getByDisplayValue("50");
|
||||
fireEvent.change(input1, { target: { value: "70" } });
|
||||
|
||||
const input2 = screen.getByDisplayValue("60");
|
||||
fireEvent.change(input2, { target: { value: "80" } });
|
||||
|
||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||
{ id: 1, maxThroughputPercentage: 70 },
|
||||
{ id: 2, maxThroughputPercentage: 80 },
|
||||
{ id: 3, maxThroughputPercentage: 100 },
|
||||
{ id: 4, maxThroughputPercentage: 100 },
|
||||
{ id: 5, maxThroughputPercentage: 100 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("resets to baseline when currentBuckets are reset", () => {
|
||||
const { rerender } = render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
const input1 = screen.getByDisplayValue("50");
|
||||
fireEvent.change(input1, { target: { value: "70" } });
|
||||
|
||||
rerender(<ThroughputBucketsComponent {...defaultProps} currentBuckets={defaultProps.throughputBucketsBaseline} />);
|
||||
|
||||
expect(screen.getByDisplayValue("40")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("50")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not call onBucketsChange when value remains unchanged", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
const input = screen.getByDisplayValue("50");
|
||||
fireEvent.change(input, { target: { value: "50" } });
|
||||
|
||||
expect(mockOnBucketsChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("disables input and slider when maxThroughputPercentage is 100", () => {
|
||||
render(
|
||||
<ThroughputBucketsComponent
|
||||
{...defaultProps}
|
||||
currentBuckets={[
|
||||
{ id: 1, maxThroughputPercentage: 100 },
|
||||
{ id: 2, maxThroughputPercentage: 50 },
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
const disabledInputs = screen.getAllByDisplayValue("100");
|
||||
expect(disabledInputs.length).toBeGreaterThan(0);
|
||||
expect(disabledInputs[0]).toBeDisabled();
|
||||
|
||||
const sliders = screen.getAllByRole("slider");
|
||||
expect(sliders.length).toBeGreaterThan(0);
|
||||
expect(sliders[0]).toHaveAttribute("aria-disabled", "true");
|
||||
expect(sliders[1]).toHaveAttribute("aria-disabled", "false");
|
||||
});
|
||||
|
||||
it("toggles bucket value between 50 and 100 with switch", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
const toggles = screen.getAllByRole("switch");
|
||||
|
||||
fireEvent.click(toggles[0]);
|
||||
|
||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||
{ id: 1, maxThroughputPercentage: 100 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
{ id: 3, maxThroughputPercentage: 100 },
|
||||
{ id: 4, maxThroughputPercentage: 100 },
|
||||
{ id: 5, maxThroughputPercentage: 100 },
|
||||
]);
|
||||
|
||||
fireEvent.click(toggles[0]);
|
||||
|
||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||
{ id: 1, maxThroughputPercentage: 50 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
{ id: 3, maxThroughputPercentage: 100 },
|
||||
{ id: 4, maxThroughputPercentage: 100 },
|
||||
{ id: 5, maxThroughputPercentage: 100 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("ensures default buckets are used when no buckets are provided", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={[]} />);
|
||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(5);
|
||||
expect(screen.getAllByDisplayValue("100")).toHaveLength(5);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Label, Slider, Stack, TextField, Toggle } from "@fluentui/react";
|
||||
import { ThroughputBucket } from "Contracts/DataModels";
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { isDirty } from "../../SettingsUtils";
|
||||
|
||||
const MAX_BUCKET_SIZES = 5;
|
||||
|
||||
const DEFAULT_BUCKETS = Array.from({ length: MAX_BUCKET_SIZES }, (_, i) => ({
|
||||
id: i + 1,
|
||||
maxThroughputPercentage: 100,
|
||||
}));
|
||||
|
||||
export interface ThroughputBucketsComponentProps {
|
||||
currentBuckets: ThroughputBucket[];
|
||||
throughputBucketsBaseline: ThroughputBucket[];
|
||||
onBucketsChange: (updatedBuckets: ThroughputBucket[]) => void;
|
||||
onSaveableChange: (isSaveable: boolean) => void;
|
||||
}
|
||||
|
||||
export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = ({
|
||||
currentBuckets,
|
||||
throughputBucketsBaseline,
|
||||
onBucketsChange,
|
||||
onSaveableChange,
|
||||
}) => {
|
||||
const getThroughputBuckets = (buckets: ThroughputBucket[]): ThroughputBucket[] => {
|
||||
if (!buckets || buckets.length === 0) {
|
||||
return DEFAULT_BUCKETS;
|
||||
}
|
||||
const maxBuckets = Math.max(DEFAULT_BUCKETS.length, buckets.length);
|
||||
const adjustedDefaultBuckets = Array.from({ length: maxBuckets }, (_, i) => ({
|
||||
id: i + 1,
|
||||
maxThroughputPercentage: 100,
|
||||
}));
|
||||
|
||||
return adjustedDefaultBuckets.map(
|
||||
(defaultBucket) => buckets?.find((bucket) => bucket.id === defaultBucket.id) || defaultBucket,
|
||||
);
|
||||
};
|
||||
|
||||
const [throughputBuckets, setThroughputBuckets] = useState<ThroughputBucket[]>(getThroughputBuckets(currentBuckets));
|
||||
|
||||
useEffect(() => {
|
||||
setThroughputBuckets(getThroughputBuckets(currentBuckets));
|
||||
onSaveableChange(false);
|
||||
}, [currentBuckets]);
|
||||
|
||||
useEffect(() => {
|
||||
const isChanged = isDirty(throughputBuckets, getThroughputBuckets(throughputBucketsBaseline));
|
||||
onSaveableChange(isChanged);
|
||||
}, [throughputBuckets]);
|
||||
|
||||
const handleBucketChange = (id: number, newValue: number) => {
|
||||
const updatedBuckets = throughputBuckets.map((bucket) =>
|
||||
bucket.id === id ? { ...bucket, maxThroughputPercentage: newValue } : bucket,
|
||||
);
|
||||
setThroughputBuckets(updatedBuckets);
|
||||
const settingsChanged = isDirty(updatedBuckets, throughputBuckets);
|
||||
settingsChanged && onBucketsChange(updatedBuckets);
|
||||
};
|
||||
|
||||
const onToggle = (id: number, checked: boolean) => {
|
||||
handleBucketChange(id, checked ? 50 : 100);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: "m" }} styles={{ root: { width: "70%", maxWidth: 700 } }}>
|
||||
<Label>Throughput Buckets</Label>
|
||||
<Stack>
|
||||
{throughputBuckets?.map((bucket) => (
|
||||
<Stack key={bucket.id} horizontal tokens={{ childrenGap: 8 }} verticalAlign="center">
|
||||
<Slider
|
||||
min={1}
|
||||
max={100}
|
||||
step={1}
|
||||
value={bucket.maxThroughputPercentage}
|
||||
onChange={(newValue) => handleBucketChange(bucket.id, newValue)}
|
||||
showValue={false}
|
||||
label={`Group ${bucket.id}${bucket.id === 1 ? " (Data Explorer Query Bucket)" : ""}`}
|
||||
styles={{ root: { flex: 2, maxWidth: 400 } }}
|
||||
disabled={bucket.maxThroughputPercentage === 100}
|
||||
/>
|
||||
<TextField
|
||||
value={bucket.maxThroughputPercentage.toString()}
|
||||
onChange={(event, newValue) => handleBucketChange(bucket.id, parseInt(newValue || "0", 10))}
|
||||
type="number"
|
||||
suffix="%"
|
||||
styles={{
|
||||
fieldGroup: { width: 80 },
|
||||
}}
|
||||
disabled={bucket.maxThroughputPercentage === 100}
|
||||
/>
|
||||
<Toggle
|
||||
onText="Active"
|
||||
offText="Inactive"
|
||||
checked={bucket.maxThroughputPercentage !== 100}
|
||||
onChange={(event, checked) => onToggle(bucket.id, checked)}
|
||||
styles={{ root: { marginBottom: 0 }, text: { fontSize: 12 } }}
|
||||
></Toggle>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -17,14 +17,13 @@ import {
|
||||
} from "@fluentui/react";
|
||||
import React from "react";
|
||||
import * as DataModels from "../../../../../Contracts/DataModels";
|
||||
import { SubscriptionType } from "../../../../../Contracts/SubscriptionType";
|
||||
import * as SharedConstants from "../../../../../Shared/Constants";
|
||||
import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../../../../UserContext";
|
||||
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
|
||||
import { autoPilotThroughput1K } from "../../../../../Utils/AutoPilotUtils";
|
||||
import { calculateEstimateNumber, usageInGB } from "../../../../../Utils/PricingUtils";
|
||||
import { calculateEstimateNumber } from "../../../../../Utils/PricingUtils";
|
||||
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||
import {
|
||||
PriceBreakdown,
|
||||
@@ -366,29 +365,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
});
|
||||
};
|
||||
|
||||
private minRUperGBSurvey = (): JSX.Element => {
|
||||
const href = `https://ncv.microsoft.com/vRBTO37jmO?ctx={"AzureSubscriptionId":"${userContext.subscriptionId}","CosmosDBAccountName":"${userContext.databaseAccount?.name}"}`;
|
||||
const oneTBinKB = 1000000000;
|
||||
const minRUperGB = 10;
|
||||
const featureFlagEnabled = userContext.features.showMinRUSurvey;
|
||||
const collectionIsEligible =
|
||||
userContext.subscriptionType !== SubscriptionType.Internal &&
|
||||
this.props.usageSizeInKB > oneTBinKB &&
|
||||
this.props.minimum >= usageInGB(this.props.usageSizeInKB) * minRUperGB;
|
||||
if (featureFlagEnabled || collectionIsEligible) {
|
||||
return (
|
||||
<Text>
|
||||
Need to scale below {this.props.minimum} RU/s? Reach out by filling{" "}
|
||||
<a target="_blank" rel="noreferrer" href={href}>
|
||||
this questionnaire
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
private renderThroughputModeChoices = (): JSX.Element => {
|
||||
const labelId = "settingsV2RadioButtonLabelId";
|
||||
return (
|
||||
@@ -661,7 +637,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
</Link>
|
||||
</Text>
|
||||
)}
|
||||
{this.minRUperGBSurvey()}
|
||||
{this.props.spendAckVisible && (
|
||||
<Checkbox
|
||||
id="spendAckCheckBox"
|
||||
|
||||
@@ -4,7 +4,15 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
||||
|
||||
const zeroValue = 0;
|
||||
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy | DataModels.ComputedProperties;
|
||||
export type isDirtyTypes =
|
||||
| boolean
|
||||
| string
|
||||
| number
|
||||
| DataModels.IndexingPolicy
|
||||
| DataModels.ComputedProperties
|
||||
| DataModels.VectorEmbedding[]
|
||||
| DataModels.FullTextPolicy
|
||||
| DataModels.ThroughputBucket[];
|
||||
export const TtlOff = "off";
|
||||
export const TtlOn = "on";
|
||||
export const TtlOnNoDefault = "on-nodefault";
|
||||
@@ -48,6 +56,13 @@ export enum SettingsV2TabTypes {
|
||||
PartitionKeyTab,
|
||||
ComputedPropertiesTab,
|
||||
ContainerVectorPolicyTab,
|
||||
ThroughputBucketsTab,
|
||||
MaterializedViewTab,
|
||||
}
|
||||
|
||||
export enum ContainerPolicyTabTypes {
|
||||
VectorPolicyTab,
|
||||
FullTextPolicyTab,
|
||||
}
|
||||
|
||||
export interface IsComponentDirtyResult {
|
||||
@@ -154,7 +169,11 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
|
||||
case SettingsV2TabTypes.ComputedPropertiesTab:
|
||||
return "Computed Properties";
|
||||
case SettingsV2TabTypes.ContainerVectorPolicyTab:
|
||||
return "Container Vector Policy (preview)";
|
||||
return "Container Policies";
|
||||
case SettingsV2TabTypes.ThroughputBucketsTab:
|
||||
return "Throughput Buckets";
|
||||
case SettingsV2TabTypes.MaterializedViewTab:
|
||||
return "Materialized Views (Preview)";
|
||||
default:
|
||||
throw new Error(`Unknown tab ${tab}`);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,17 @@ export const collection = {
|
||||
query: "query",
|
||||
},
|
||||
]),
|
||||
vectorEmbeddingPolicy: ko.observable<DataModels.VectorEmbeddingPolicy>({} as DataModels.VectorEmbeddingPolicy),
|
||||
fullTextPolicy: ko.observable<DataModels.FullTextPolicy>({} as DataModels.FullTextPolicy),
|
||||
materializedViews: ko.observable<DataModels.MaterializedView[]>([
|
||||
{ id: "view1", _rid: "rid1" },
|
||||
{ id: "view2", _rid: "rid2" },
|
||||
]),
|
||||
materializedViewDefinition: ko.observable<DataModels.MaterializedViewDefinition>({
|
||||
definition: "SELECT * FROM c WHERE c.id = 1",
|
||||
sourceCollectionId: "source1",
|
||||
sourceCollectionRid: "rid123",
|
||||
}),
|
||||
readSettings: () => {
|
||||
return;
|
||||
},
|
||||
|
||||
@@ -55,10 +55,13 @@ exports[`SettingsComponent renders 1`] = `
|
||||
},
|
||||
"databaseId": "test",
|
||||
"defaultTtl": [Function],
|
||||
"fullTextPolicy": [Function],
|
||||
"geospatialConfig": [Function],
|
||||
"getDatabase": [Function],
|
||||
"id": [Function],
|
||||
"indexingPolicy": [Function],
|
||||
"materializedViewDefinition": [Function],
|
||||
"materializedViews": [Function],
|
||||
"offer": [Function],
|
||||
"partitionKey": {
|
||||
"kind": "hash",
|
||||
@@ -71,6 +74,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"readSettings": [Function],
|
||||
"uniqueKeyPolicy": {},
|
||||
"usageSizeInKB": [Function],
|
||||
"vectorEmbeddingPolicy": [Function],
|
||||
}
|
||||
}
|
||||
isAutoPilotSelected={false}
|
||||
@@ -132,10 +136,13 @@ exports[`SettingsComponent renders 1`] = `
|
||||
},
|
||||
"databaseId": "test",
|
||||
"defaultTtl": [Function],
|
||||
"fullTextPolicy": [Function],
|
||||
"geospatialConfig": [Function],
|
||||
"getDatabase": [Function],
|
||||
"id": [Function],
|
||||
"indexingPolicy": [Function],
|
||||
"materializedViewDefinition": [Function],
|
||||
"materializedViews": [Function],
|
||||
"offer": [Function],
|
||||
"partitionKey": {
|
||||
"kind": "hash",
|
||||
@@ -148,6 +155,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"readSettings": [Function],
|
||||
"uniqueKeyPolicy": {},
|
||||
"usageSizeInKB": [Function],
|
||||
"vectorEmbeddingPolicy": [Function],
|
||||
}
|
||||
}
|
||||
displayedTtlSeconds="5"
|
||||
@@ -249,10 +257,13 @@ exports[`SettingsComponent renders 1`] = `
|
||||
},
|
||||
"databaseId": "test",
|
||||
"defaultTtl": [Function],
|
||||
"fullTextPolicy": [Function],
|
||||
"geospatialConfig": [Function],
|
||||
"getDatabase": [Function],
|
||||
"id": [Function],
|
||||
"indexingPolicy": [Function],
|
||||
"materializedViewDefinition": [Function],
|
||||
"materializedViews": [Function],
|
||||
"offer": [Function],
|
||||
"partitionKey": {
|
||||
"kind": "hash",
|
||||
@@ -265,6 +276,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"readSettings": [Function],
|
||||
"uniqueKeyPolicy": {},
|
||||
"usageSizeInKB": [Function],
|
||||
"vectorEmbeddingPolicy": [Function],
|
||||
}
|
||||
}
|
||||
explorer={
|
||||
@@ -330,6 +342,101 @@ exports[`SettingsComponent renders 1`] = `
|
||||
shouldDiscardComputedProperties={false}
|
||||
/>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="Materialized Views (Preview)"
|
||||
itemKey="MaterializedViewTab"
|
||||
key="MaterializedViewTab"
|
||||
style={
|
||||
{
|
||||
"marginTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<MaterializedViewComponent
|
||||
collection={
|
||||
{
|
||||
"analyticalStorageTtl": [Function],
|
||||
"changeFeedPolicy": [Function],
|
||||
"computedProperties": [Function],
|
||||
"conflictResolutionPolicy": [Function],
|
||||
"container": Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isTabsContentExpanded": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"phoenixClient": PhoenixClient {
|
||||
"armResourceId": undefined,
|
||||
"retryOptions": {
|
||||
"maxTimeout": 5000,
|
||||
"minTimeout": 5000,
|
||||
"retries": 3,
|
||||
},
|
||||
},
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
},
|
||||
"refreshNotebookList": [Function],
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"parameters": [Function],
|
||||
},
|
||||
},
|
||||
"databaseId": "test",
|
||||
"defaultTtl": [Function],
|
||||
"fullTextPolicy": [Function],
|
||||
"geospatialConfig": [Function],
|
||||
"getDatabase": [Function],
|
||||
"id": [Function],
|
||||
"indexingPolicy": [Function],
|
||||
"materializedViewDefinition": [Function],
|
||||
"materializedViews": [Function],
|
||||
"offer": [Function],
|
||||
"partitionKey": {
|
||||
"kind": "hash",
|
||||
"paths": [],
|
||||
"version": 2,
|
||||
},
|
||||
"partitionKeyProperties": [
|
||||
"partitionKey",
|
||||
],
|
||||
"readSettings": [Function],
|
||||
"uniqueKeyPolicy": {},
|
||||
"usageSizeInKB": [Function],
|
||||
"vectorEmbeddingPolicy": [Function],
|
||||
}
|
||||
}
|
||||
explorer={
|
||||
Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isTabsContentExpanded": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"phoenixClient": PhoenixClient {
|
||||
"armResourceId": undefined,
|
||||
"retryOptions": {
|
||||
"maxTimeout": 5000,
|
||||
"minTimeout": 5000,
|
||||
"retries": 3,
|
||||
},
|
||||
},
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
},
|
||||
"refreshNotebookList": [Function],
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"parameters": [Function],
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</PivotItem>
|
||||
</StyledPivot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
|
||||
import { getWorkloadType } from "Common/DatabaseAccountUtility";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
@@ -34,10 +35,23 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
setIsThroughputCapExceeded,
|
||||
onCostAcknowledgeChange,
|
||||
}: ThroughputInputProps) => {
|
||||
let defaultThroughput: number;
|
||||
const workloadType: Constants.WorkloadType = getWorkloadType();
|
||||
|
||||
if (
|
||||
isFreeTier ||
|
||||
isQuickstart ||
|
||||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType)
|
||||
) {
|
||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput1K;
|
||||
} else if (workloadType === Constants.WorkloadType.Production) {
|
||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput10K;
|
||||
} else {
|
||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput4K;
|
||||
}
|
||||
|
||||
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
|
||||
const [throughput, setThroughput] = useState<number>(
|
||||
isFreeTier || isQuickstart ? AutoPilotUtils.autoPilotThroughput1K : AutoPilotUtils.autoPilotThroughput4K,
|
||||
);
|
||||
const [throughput, setThroughput] = useState<number>(defaultThroughput);
|
||||
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
|
||||
const [throughputError, setThroughputError] = useState<string>("");
|
||||
const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0);
|
||||
@@ -47,7 +61,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
|
||||
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
|
||||
|
||||
useEffect(() => {
|
||||
// throughput cap check for the initial state
|
||||
let totalThroughput = 0;
|
||||
@@ -157,9 +170,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
|
||||
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
|
||||
if (mode === "Autoscale") {
|
||||
const defaultThroughput = isFreeTier
|
||||
? AutoPilotUtils.autoPilotThroughput1K
|
||||
: AutoPilotUtils.autoPilotThroughput4K;
|
||||
setThroughput(defaultThroughput);
|
||||
setIsAutoScaleSelected(true);
|
||||
setThroughputValue(defaultThroughput);
|
||||
|
||||
@@ -2,7 +2,7 @@ import "@testing-library/jest-dom";
|
||||
import { RenderResult, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||
import React from "react";
|
||||
import { AddVectorEmbeddingPolicyForm } from "./AddVectorEmbeddingPolicyForm";
|
||||
import { VectorEmbeddingPoliciesComponent } from "./VectorEmbeddingPoliciesComponent";
|
||||
|
||||
const mockVectorEmbedding: VectorEmbedding[] = [
|
||||
{ path: "/vector1", dataType: "float32", distanceFunction: "euclidean", dimensions: 0 },
|
||||
@@ -17,9 +17,9 @@ describe("AddVectorEmbeddingPolicyForm", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
component = render(
|
||||
<AddVectorEmbeddingPolicyForm
|
||||
vectorEmbedding={mockVectorEmbedding}
|
||||
vectorIndex={mockVectorIndex}
|
||||
<VectorEmbeddingPoliciesComponent
|
||||
vectorEmbeddings={mockVectorEmbedding}
|
||||
vectorIndexes={mockVectorIndex}
|
||||
onVectorEmbeddingChange={mockOnVectorEmbeddingChange}
|
||||
/>,
|
||||
);
|
||||
@@ -36,7 +36,7 @@ describe("AddVectorEmbeddingPolicyForm", () => {
|
||||
});
|
||||
|
||||
test("calls onDelete when delete button is clicked", async () => {
|
||||
const deleteButton = component.container.querySelector("#delete-vector-policy-1");
|
||||
const deleteButton = component.container.querySelector("#delete-Vector-embedding-1");
|
||||
fireEvent.click(deleteButton);
|
||||
expect(mockOnVectorEmbeddingChange).toHaveBeenCalled();
|
||||
expect(screen.queryByText("Vector embedding 1")).toBeNull();
|
||||
@@ -49,21 +49,19 @@ describe("AddVectorEmbeddingPolicyForm", () => {
|
||||
|
||||
test("validates input correctly", async () => {
|
||||
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "" } });
|
||||
await waitFor(() => expect(screen.getByText("Vector embedding path should not be empty")).toBeInTheDocument(), {
|
||||
await waitFor(() => expect(screen.getByText("Path should not be empty")).toBeInTheDocument(), {
|
||||
timeout: 1500,
|
||||
});
|
||||
await waitFor(
|
||||
() =>
|
||||
expect(
|
||||
screen.getByText("Vector embedding dimension must be greater than 0 and less than or equal 4096"),
|
||||
).toBeInTheDocument(),
|
||||
expect(screen.getByText("Dimension must be greater than 0 and less than or equal 4096")).toBeInTheDocument(),
|
||||
{
|
||||
timeout: 1500,
|
||||
},
|
||||
);
|
||||
fireEvent.change(component.container.querySelector("#vector-policy-dimension-1"), { target: { value: "4096" } });
|
||||
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "/vector1" } });
|
||||
await waitFor(() => expect(screen.queryByText("Vector embedding path should not be empty")).toBeNull(), {
|
||||
await waitFor(() => expect(screen.queryByText("Path should not be empty")).toBeNull(), {
|
||||
timeout: 1500,
|
||||
});
|
||||
await waitFor(
|
||||
@@ -0,0 +1,470 @@
|
||||
import {
|
||||
DefaultButton,
|
||||
Dropdown,
|
||||
IDropdownOption,
|
||||
IStyleFunctionOrObject,
|
||||
ITextFieldStyleProps,
|
||||
ITextFieldStyles,
|
||||
Label,
|
||||
Stack,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import {
|
||||
getDataTypeOptions,
|
||||
getDistanceFunctionOptions,
|
||||
getIndexTypeOptions,
|
||||
} from "Explorer/Controls/VectorSearch/VectorSearchUtils";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
|
||||
export interface IVectorEmbeddingPoliciesComponentProps {
|
||||
vectorEmbeddings: VectorEmbedding[];
|
||||
onVectorEmbeddingChange: (
|
||||
vectorEmbeddings: VectorEmbedding[],
|
||||
vectorIndexingPolicies: VectorIndex[],
|
||||
validationPassed: boolean,
|
||||
) => void;
|
||||
vectorIndexes?: VectorIndex[];
|
||||
discardChanges?: boolean;
|
||||
onChangesDiscarded?: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface VectorEmbeddingPolicyData {
|
||||
path: string;
|
||||
dataType: VectorEmbedding["dataType"];
|
||||
distanceFunction: VectorEmbedding["distanceFunction"];
|
||||
dimensions: number;
|
||||
indexType: VectorIndex["type"] | "none";
|
||||
pathError: string;
|
||||
dimensionsError: string;
|
||||
diskANNShardKey?: string;
|
||||
diskANNShardKeyError?: string;
|
||||
indexingSearchListSize?: number;
|
||||
indexingSearchListSizeError?: string;
|
||||
quantizationByteSize?: number;
|
||||
quantizationByteSizeError?: string;
|
||||
}
|
||||
|
||||
type VectorEmbeddingPolicyProperty = "dataType" | "distanceFunction" | "indexType";
|
||||
|
||||
const labelStyles = {
|
||||
root: {
|
||||
fontSize: 12,
|
||||
},
|
||||
};
|
||||
|
||||
const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldStyles> = {
|
||||
fieldGroup: {
|
||||
height: 27,
|
||||
},
|
||||
field: {
|
||||
fontSize: 12,
|
||||
padding: "0 8px",
|
||||
},
|
||||
};
|
||||
|
||||
const dropdownStyles = {
|
||||
title: {
|
||||
height: 27,
|
||||
lineHeight: "24px",
|
||||
fontSize: 12,
|
||||
},
|
||||
dropdown: {
|
||||
height: 27,
|
||||
lineHeight: "24px",
|
||||
},
|
||||
dropdownItem: {
|
||||
fontSize: 12,
|
||||
},
|
||||
};
|
||||
|
||||
export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddingPoliciesComponentProps> = ({
|
||||
vectorEmbeddings,
|
||||
vectorIndexes,
|
||||
onVectorEmbeddingChange,
|
||||
discardChanges,
|
||||
onChangesDiscarded,
|
||||
disabled,
|
||||
}): JSX.Element => {
|
||||
const onVectorEmbeddingPathError = (path: string, index?: number): string => {
|
||||
let error = "";
|
||||
if (!path) {
|
||||
error = "Path should not be empty";
|
||||
}
|
||||
if (
|
||||
index >= 0 &&
|
||||
vectorEmbeddingPolicyData?.find(
|
||||
(vectorEmbedding: VectorEmbeddingPolicyData, dataIndex: number) =>
|
||||
dataIndex !== index && vectorEmbedding.path === path,
|
||||
)
|
||||
) {
|
||||
error = "Path is already defined";
|
||||
}
|
||||
return error;
|
||||
};
|
||||
|
||||
const onVectorEmbeddingDimensionError = (dimension: number, indexType: VectorIndex["type"] | "none"): string => {
|
||||
let error = "";
|
||||
if (dimension <= 0 || dimension > 4096) {
|
||||
error = "Dimension must be greater than 0 and less than or equal 4096";
|
||||
}
|
||||
if (indexType === "flat" && dimension > 505) {
|
||||
error = "Maximum allowed dimension for flat index is 505";
|
||||
}
|
||||
return error;
|
||||
};
|
||||
|
||||
const onQuantizationByteSizeError = (size: number): string => {
|
||||
let error = "";
|
||||
if (size < 1 || size > 512) {
|
||||
error = "Quantization byte size must be greater than 0 and less than or equal to 512";
|
||||
}
|
||||
return error;
|
||||
};
|
||||
|
||||
const onIndexingSearchListSizeError = (size: number): string => {
|
||||
let error = "";
|
||||
if (size < 25 || size > 500) {
|
||||
error = "Indexing search list size must be greater than or equal to 25 and less than or equal to 500";
|
||||
}
|
||||
return error;
|
||||
};
|
||||
|
||||
//TODO: no restrictions yet due to this field being removed for now.
|
||||
// Uncomment and replace with validation code when field is reinstated
|
||||
// const onDiskANNShardKeyError = (shardKey: string): string => {
|
||||
// return "";
|
||||
// };
|
||||
|
||||
const initializeData = (vectorEmbeddings: VectorEmbedding[], vectorIndexes: VectorIndex[]) => {
|
||||
const mergedData: VectorEmbeddingPolicyData[] = [];
|
||||
vectorEmbeddings.forEach((embedding) => {
|
||||
const matchingIndex = displayIndexes ? vectorIndexes.find((index) => index.path === embedding.path) : undefined;
|
||||
mergedData.push({
|
||||
...embedding,
|
||||
indexType: matchingIndex?.type || "none",
|
||||
indexingSearchListSize: matchingIndex?.indexingSearchListSize || undefined,
|
||||
quantizationByteSize: matchingIndex?.quantizationByteSize || undefined,
|
||||
pathError: onVectorEmbeddingPathError(embedding.path),
|
||||
dimensionsError: onVectorEmbeddingDimensionError(embedding.dimensions, matchingIndex?.type || "none"),
|
||||
});
|
||||
});
|
||||
return mergedData;
|
||||
};
|
||||
|
||||
const [displayIndexes] = useState<boolean>(!!vectorIndexes);
|
||||
const [vectorEmbeddingPolicyData, setVectorEmbeddingPolicyData] = useState<VectorEmbeddingPolicyData[]>(
|
||||
initializeData(vectorEmbeddings, vectorIndexes),
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
propagateData();
|
||||
}, [vectorEmbeddingPolicyData]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (discardChanges) {
|
||||
setVectorEmbeddingPolicyData(initializeData(vectorEmbeddings, vectorIndexes));
|
||||
onChangesDiscarded();
|
||||
}
|
||||
}, [discardChanges]);
|
||||
|
||||
const propagateData = () => {
|
||||
const vectorEmbeddings: VectorEmbedding[] = vectorEmbeddingPolicyData.map((policy: VectorEmbeddingPolicyData) => ({
|
||||
path: policy.path,
|
||||
dataType: policy.dataType,
|
||||
dimensions: policy.dimensions,
|
||||
distanceFunction: policy.distanceFunction,
|
||||
}));
|
||||
const vectorIndexes: VectorIndex[] = vectorEmbeddingPolicyData
|
||||
.filter((policy: VectorEmbeddingPolicyData) => policy.indexType !== "none")
|
||||
.map(
|
||||
(policy) =>
|
||||
({
|
||||
path: policy.path,
|
||||
type: policy.indexType,
|
||||
indexingSearchListSize: policy.indexingSearchListSize,
|
||||
quantizationByteSize: policy.quantizationByteSize,
|
||||
}) as VectorIndex,
|
||||
);
|
||||
const validationPassed = vectorEmbeddingPolicyData.every(
|
||||
(policy: VectorEmbeddingPolicyData) => policy.pathError === "" && policy.dimensionsError === "",
|
||||
);
|
||||
onVectorEmbeddingChange(vectorEmbeddings, vectorIndexes, validationPassed);
|
||||
};
|
||||
|
||||
const onVectorEmbeddingPathChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value.trim();
|
||||
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||
if (!vectorEmbeddings[index]?.path && !value.startsWith("/")) {
|
||||
vectorEmbeddings[index].path = "/" + value;
|
||||
} else {
|
||||
vectorEmbeddings[index].path = value;
|
||||
}
|
||||
const error = onVectorEmbeddingPathError(value, index);
|
||||
vectorEmbeddings[index].pathError = error;
|
||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
};
|
||||
|
||||
const onVectorEmbeddingDimensionsChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseInt(event.target.value.trim()) || 0;
|
||||
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||
const vectorEmbedding = vectorEmbeddings[index];
|
||||
vectorEmbeddings[index].dimensions = value;
|
||||
const error = onVectorEmbeddingDimensionError(value, vectorEmbedding.indexType);
|
||||
vectorEmbeddings[index].dimensionsError = error;
|
||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
};
|
||||
|
||||
const onVectorEmbeddingIndexTypeChange = (index: number, option: IDropdownOption): void => {
|
||||
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||
const vectorEmbedding = vectorEmbeddings[index];
|
||||
vectorEmbeddings[index].indexType = option.key as never;
|
||||
const error = onVectorEmbeddingDimensionError(vectorEmbedding.dimensions, vectorEmbedding.indexType);
|
||||
vectorEmbeddings[index].dimensionsError = error;
|
||||
if (vectorEmbedding.indexType === "diskANN") {
|
||||
vectorEmbedding.indexingSearchListSize = 100;
|
||||
} else {
|
||||
vectorEmbedding.indexingSearchListSize = undefined;
|
||||
}
|
||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
};
|
||||
|
||||
const onQuantizationByteSizeChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseInt(event.target.value.trim()) || 0;
|
||||
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||
vectorEmbeddings[index].quantizationByteSize = value;
|
||||
vectorEmbeddings[index].quantizationByteSizeError = onQuantizationByteSizeError(value);
|
||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
};
|
||||
|
||||
const onIndexingSearchListSizeChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseInt(event.target.value.trim()) || 0;
|
||||
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||
vectorEmbeddings[index].indexingSearchListSize = value;
|
||||
vectorEmbeddings[index].indexingSearchListSizeError = onIndexingSearchListSizeError(value);
|
||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
};
|
||||
|
||||
// TODO: uncomment after Ignite
|
||||
// DiskANNShardKey was removed for Ignite due to backend problems. Leaving this here as it will be reinstated immediately after Ignite
|
||||
// const onDiskANNShardKeyChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// 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 onVectorEmbeddingPolicyChange = (
|
||||
index: number,
|
||||
option: IDropdownOption,
|
||||
property: VectorEmbeddingPolicyProperty,
|
||||
): void => {
|
||||
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||
vectorEmbeddings[index][property] = option.key as never;
|
||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
};
|
||||
|
||||
const onAdd = () => {
|
||||
setVectorEmbeddingPolicyData([
|
||||
...vectorEmbeddingPolicyData,
|
||||
{
|
||||
path: "",
|
||||
dataType: "float32",
|
||||
distanceFunction: "euclidean",
|
||||
dimensions: 0,
|
||||
indexType: "none",
|
||||
pathError: onVectorEmbeddingPathError(""),
|
||||
dimensionsError: onVectorEmbeddingDimensionError(0, "none"),
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const onDelete = (index: number) => {
|
||||
const vectorEmbeddings = vectorEmbeddingPolicyData.filter((_uniqueKey, j) => index !== j);
|
||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: 4 }}>
|
||||
{vectorEmbeddingPolicyData &&
|
||||
vectorEmbeddingPolicyData.length > 0 &&
|
||||
vectorEmbeddingPolicyData.map((vectorEmbeddingPolicy: VectorEmbeddingPolicyData, index: number) => (
|
||||
<CollapsibleSectionComponent
|
||||
disabled={disabled}
|
||||
key={index}
|
||||
isExpandedByDefault={true}
|
||||
title={`Vector embedding ${index + 1}`}
|
||||
showDelete={true}
|
||||
onDelete={() => onDelete(index)}
|
||||
>
|
||||
<Stack horizontal tokens={{ childrenGap: 4 }}>
|
||||
<Stack
|
||||
styles={{
|
||||
root: {
|
||||
margin: "0 0 6px 20px !important",
|
||||
paddingLeft: 20,
|
||||
width: "80%",
|
||||
borderLeft: "1px solid",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack>
|
||||
<Label disabled={disabled} styles={labelStyles}>
|
||||
Path
|
||||
</Label>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
id={`vector-policy-path-${index + 1}`}
|
||||
required={true}
|
||||
placeholder="/vector1"
|
||||
styles={textFieldStyles}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onVectorEmbeddingPathChange(index, event)}
|
||||
value={vectorEmbeddingPolicy.path || ""}
|
||||
errorMessage={vectorEmbeddingPolicy.pathError}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Label disabled={disabled} styles={labelStyles}>
|
||||
Data type
|
||||
</Label>
|
||||
<Dropdown
|
||||
disabled={disabled}
|
||||
required={true}
|
||||
styles={dropdownStyles}
|
||||
options={getDataTypeOptions()}
|
||||
selectedKey={vectorEmbeddingPolicy.dataType}
|
||||
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||
onVectorEmbeddingPolicyChange(index, option, "dataType")
|
||||
}
|
||||
></Dropdown>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Label disabled={disabled} styles={labelStyles}>
|
||||
Distance function
|
||||
</Label>
|
||||
<Dropdown
|
||||
disabled={disabled}
|
||||
required={true}
|
||||
styles={dropdownStyles}
|
||||
options={getDistanceFunctionOptions()}
|
||||
selectedKey={vectorEmbeddingPolicy.distanceFunction}
|
||||
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||
onVectorEmbeddingPolicyChange(index, option, "distanceFunction")
|
||||
}
|
||||
></Dropdown>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Label disabled={disabled} styles={labelStyles}>
|
||||
Dimensions
|
||||
</Label>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
id={`vector-policy-dimension-${index + 1}`}
|
||||
required={true}
|
||||
styles={textFieldStyles}
|
||||
value={String(vectorEmbeddingPolicy.dimensions || 0)}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onVectorEmbeddingDimensionsChange(index, event)
|
||||
}
|
||||
errorMessage={vectorEmbeddingPolicy.dimensionsError}
|
||||
/>
|
||||
</Stack>
|
||||
{displayIndexes && (
|
||||
<Stack>
|
||||
<Label disabled={disabled} styles={labelStyles}>
|
||||
Index type
|
||||
</Label>
|
||||
<Dropdown
|
||||
disabled={disabled}
|
||||
required={true}
|
||||
styles={dropdownStyles}
|
||||
options={getIndexTypeOptions()}
|
||||
selectedKey={vectorEmbeddingPolicy.indexType}
|
||||
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||
onVectorEmbeddingIndexTypeChange(index, option)
|
||||
}
|
||||
></Dropdown>
|
||||
<Stack style={{ marginLeft: "10px" }}>
|
||||
<Label
|
||||
disabled={
|
||||
disabled ||
|
||||
(vectorEmbeddingPolicy.indexType !== "quantizedFlat" &&
|
||||
vectorEmbeddingPolicy.indexType !== "diskANN")
|
||||
}
|
||||
styles={labelStyles}
|
||||
>
|
||||
Quantization byte size
|
||||
</Label>
|
||||
<TextField
|
||||
disabled={
|
||||
disabled ||
|
||||
(vectorEmbeddingPolicy.indexType !== "quantizedFlat" &&
|
||||
vectorEmbeddingPolicy.indexType !== "diskANN")
|
||||
}
|
||||
id={`vector-policy-quantizationByteSize-${index + 1}`}
|
||||
styles={textFieldStyles}
|
||||
value={String(vectorEmbeddingPolicy.quantizationByteSize || "")}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onQuantizationByteSizeChange(index, event)
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack style={{ marginLeft: "10px" }}>
|
||||
<Label disabled={disabled || vectorEmbeddingPolicy.indexType !== "diskANN"} styles={labelStyles}>
|
||||
Indexing search list size
|
||||
</Label>
|
||||
<TextField
|
||||
disabled={disabled || vectorEmbeddingPolicy.indexType !== "diskANN"}
|
||||
id={`vector-policy-indexingSearchListSize-${index + 1}`}
|
||||
styles={textFieldStyles}
|
||||
value={String(vectorEmbeddingPolicy.indexingSearchListSize || "")}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onIndexingSearchListSizeChange(index, event)
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
{/*TODO: uncomment after Ignite */}
|
||||
{/* DiskANNShardKey was removed for Ignite due to backend problems. Leaving this here as it will be reinstated immediately after Ignite
|
||||
<Stack
|
||||
style={{ marginLeft: "10px" }}
|
||||
>
|
||||
<Label
|
||||
disabled={disabled || vectorEmbeddingPolicy.indexType !== "diskANN"}
|
||||
styles={labelStyles}
|
||||
>DiskANN shard key</Label>
|
||||
<TextField
|
||||
disabled={disabled || vectorEmbeddingPolicy.indexType !== "diskANN"}
|
||||
id={`vector-policy-diskANNShardKey-${index + 1}`}
|
||||
styles={textFieldStyles}
|
||||
value={String(vectorEmbeddingPolicy.diskANNShardKey || "")}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onDiskANNShardKeyChange(index, event)
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
*/}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CollapsibleSectionComponent>
|
||||
))}
|
||||
<DefaultButton
|
||||
disabled={disabled}
|
||||
id={`add-vector-policy`}
|
||||
styles={{ root: { maxWidth: 170, fontSize: 12 } }}
|
||||
onClick={onAdd}
|
||||
>
|
||||
Add vector embedding
|
||||
</DefaultButton>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -8,7 +8,7 @@ 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 { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
||||
import { isFabricMirrored, isFabricMirroredKey, scheduleRefreshFabricToken } from "Platform/Fabric/FabricUtil";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
||||
@@ -35,7 +35,7 @@ import { PhoenixClient } from "../Phoenix/PhoenixClient";
|
||||
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import { isAccountNewerThanThresholdInMs, updateUserContext, userContext } from "../UserContext";
|
||||
import { updateUserContext, userContext } from "../UserContext";
|
||||
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
||||
import { stringToBlob } from "../Utils/BlobUtils";
|
||||
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
||||
@@ -43,7 +43,7 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { useTabs } from "../hooks/useTabs";
|
||||
import { ReactTabKind, useTabs } from "../hooks/useTabs";
|
||||
import "./ComponentRegisterer";
|
||||
import { DialogProps, useDialog } from "./Controls/Dialog";
|
||||
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
||||
@@ -55,7 +55,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";
|
||||
@@ -187,6 +187,10 @@ export default class Explorer {
|
||||
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
|
||||
}
|
||||
|
||||
if (isFabricMirrored()) {
|
||||
useTabs.getState().closeReactTab(ReactTabKind.Home);
|
||||
}
|
||||
|
||||
this.refreshExplorer();
|
||||
}
|
||||
|
||||
@@ -278,37 +282,6 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
public openNPSSurveyDialog(): void {
|
||||
if (!Platform.Portal || !["Postgres", "SQL", "Mongo"].includes(userContext.apiType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ONE_DAY_IN_MS = 86400000;
|
||||
const SEVEN_DAYS_IN_MS = 604800000;
|
||||
|
||||
// Try Cosmos DB subscription - survey shown to 100% of users at day 1 in Data Explorer.
|
||||
if (userContext.isTryCosmosDBSubscription) {
|
||||
if (isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", ONE_DAY_IN_MS)) {
|
||||
Logger.logInfo(
|
||||
`Sending message to Portal to check if NPS Survey can be displayed in Try Cosmos DB ${userContext.apiType}`,
|
||||
"Explorer/openNPSSurveyDialog",
|
||||
);
|
||||
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
||||
}
|
||||
} else {
|
||||
// Show survey when an existing account is older than 7 days
|
||||
if (
|
||||
!isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", SEVEN_DAYS_IN_MS)
|
||||
) {
|
||||
Logger.logInfo(
|
||||
`Sending message to Portal to check if NPS Survey can be displayed for existing ${userContext.apiType} account older than 7 days`,
|
||||
"Explorer/openNPSSurveyDialog",
|
||||
);
|
||||
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async openCESCVAFeedbackBlade(): Promise<void> {
|
||||
sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade });
|
||||
Logger.logInfo(
|
||||
@@ -378,8 +351,8 @@ export default class Explorer {
|
||||
};
|
||||
|
||||
public onRefreshResourcesClick = async (): Promise<void> => {
|
||||
if (configContext.platform === Platform.Fabric) {
|
||||
scheduleRefreshDatabaseResourceToken(true).then(() => this.refreshAllDatabases());
|
||||
if (isFabricMirroredKey()) {
|
||||
scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1134,7 +1107,7 @@ export default class Explorer {
|
||||
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
|
||||
userContext.authType === AuthType.ResourceToken
|
||||
? this.refreshDatabaseForResourceToken()
|
||||
: this.refreshAllDatabases();
|
||||
: await this.refreshAllDatabases(); // await: we rely on the databases to be loaded before restoring the tabs further in the flow
|
||||
}
|
||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||
|
||||
@@ -1158,7 +1131,7 @@ export default class Explorer {
|
||||
await this.initNotebooks(userContext.databaseAccount);
|
||||
}
|
||||
|
||||
await this.refreshSampleData();
|
||||
this.refreshSampleData();
|
||||
}
|
||||
|
||||
public async configureCopilot(): Promise<void> {
|
||||
@@ -1183,26 +1156,27 @@ export default class Explorer {
|
||||
.setCopilotSampleDBEnabled(copilotEnabled && copilotUserDBEnabled && copilotSampleDBEnabled);
|
||||
}
|
||||
|
||||
public async refreshSampleData(): Promise<void> {
|
||||
try {
|
||||
if (!userContext.sampleDataConnectionInfo) {
|
||||
return;
|
||||
}
|
||||
const collection: DataModels.Collection = await readSampleCollection();
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const databaseId = userContext.sampleDataConnectionInfo?.databaseId;
|
||||
if (!databaseId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
|
||||
useDatabases.setState({ sampleDataResourceTokenCollection });
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "Explorer");
|
||||
public refreshSampleData(): void {
|
||||
if (!userContext.sampleDataConnectionInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const databaseId = userContext.sampleDataConnectionInfo?.databaseId;
|
||||
if (!databaseId) {
|
||||
return;
|
||||
}
|
||||
|
||||
readSampleCollection()
|
||||
.then((collection: DataModels.Collection) => {
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
|
||||
useDatabases.setState({ sampleDataResourceTokenCollection });
|
||||
})
|
||||
.catch((error) => {
|
||||
Logger.logError(getErrorMessage(error), "Explorer/refreshSampleData");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,6 @@
|
||||
.flex-direction(@direction: row);
|
||||
padding: 4px 5px;
|
||||
|
||||
label {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.valueCol {
|
||||
flex-grow: 1;
|
||||
padding-right: 5px;
|
||||
@@ -63,6 +59,10 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.customTrashIcon {
|
||||
padding-top: 33px;
|
||||
}
|
||||
|
||||
.rightPaneTrashIconImg {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
@@ -142,10 +142,11 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
||||
<div className="labelCol">
|
||||
<TextField
|
||||
className="edgeInput"
|
||||
label={index === 0 && "Key"}
|
||||
type="text"
|
||||
id="propertyKeyNewVertexPane"
|
||||
componentRef={input}
|
||||
aria-required="true"
|
||||
required
|
||||
placeholder="Key"
|
||||
autoComplete="off"
|
||||
aria-label={`Enter value for propery ${index + 1}`}
|
||||
@@ -153,11 +154,11 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onKeyChange(event, index)}
|
||||
/>
|
||||
</div>
|
||||
<span className="mandatoryStar">* </span>
|
||||
|
||||
<div className="valueCol">
|
||||
<TextField
|
||||
className="edgeInput"
|
||||
label={index === 0 && "Value"}
|
||||
type="text"
|
||||
placeholder="Value"
|
||||
autoComplete="off"
|
||||
@@ -169,6 +170,8 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
||||
<div>
|
||||
<Dropdown
|
||||
role="combobox"
|
||||
label={index === 0 && "Type"}
|
||||
ariaLabel="Type"
|
||||
placeholder="Select an option"
|
||||
defaultSelectedKey={data.values[0].type}
|
||||
style={{ width: 100 }}
|
||||
@@ -181,7 +184,7 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
||||
</div>
|
||||
<div className="actionCol">
|
||||
<div
|
||||
className="rightPaneTrashIcon rightPaneBtns"
|
||||
className={`rightPaneTrashIcon rightPaneBtns ${index === 0 && "customTrashIcon"}`}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-label={`Delete ${data.key}`}
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||
import { userContext } from "UserContext";
|
||||
import * as React from "react";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
|
||||
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||
import { Platform, configContext } from "../../../ConfigContext";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
@@ -93,19 +93,18 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
const rootStyle =
|
||||
configContext.platform === Platform.Fabric
|
||||
? {
|
||||
root: {
|
||||
backgroundColor: "transparent",
|
||||
padding: "2px 8px 0px 8px",
|
||||
},
|
||||
}
|
||||
: {
|
||||
root: {
|
||||
backgroundColor: backgroundColor,
|
||||
},
|
||||
};
|
||||
const rootStyle = isFabric()
|
||||
? {
|
||||
root: {
|
||||
backgroundColor: "transparent",
|
||||
padding: "2px 8px 0px 8px",
|
||||
},
|
||||
}
|
||||
: {
|
||||
root: {
|
||||
backgroundColor: backgroundColor,
|
||||
},
|
||||
};
|
||||
|
||||
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
|
||||
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);
|
||||
|
||||
@@ -37,21 +37,25 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
||||
});
|
||||
|
||||
it("Button should not be visible for Tables API", () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableTable" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableAzureSynapseLinkBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||
);
|
||||
expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
||||
});
|
||||
// TODO: Now that Tables API supports dataplane RBAC, calling createStaticCommandBarButtons will enable the
|
||||
// Entra ID Login button, which causes this test to fail due to "Invalid hook call.". This seems to be
|
||||
// unsupported in jest and needs to be tested with react-hooks-testing-library.
|
||||
//
|
||||
// it("Button should not be visible for Tables API", () => {
|
||||
// updateUserContext({
|
||||
// databaseAccount: {
|
||||
// properties: {
|
||||
// capabilities: [{ name: "EnableTable" }],
|
||||
// },
|
||||
// } as DatabaseAccount,
|
||||
// });
|
||||
//
|
||||
// const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
// const enableAzureSynapseLinkBtn = buttons.find(
|
||||
// (button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||
// );
|
||||
// expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
||||
//});
|
||||
|
||||
it("Button should not be visible for Cassandra API", () => {
|
||||
updateUserContext({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
||||
import * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
|
||||
@@ -61,7 +62,7 @@ export function createStaticCommandBarButtons(
|
||||
}
|
||||
}
|
||||
|
||||
if (userContext.apiType === "SQL") {
|
||||
if (isDataplaneRbacSupported(userContext.apiType)) {
|
||||
const [loginButtonProps, setLoginButtonProps] = useState<CommandButtonComponentProps | undefined>(undefined);
|
||||
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
|
||||
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
||||
|
||||
@@ -36,6 +36,10 @@
|
||||
&:active {
|
||||
background-color:@NotificationHigh;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
.focusedBorder();
|
||||
}
|
||||
|
||||
.statusBar {
|
||||
.dataTypeIcons {
|
||||
|
||||
@@ -81,10 +81,6 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
}
|
||||
}
|
||||
|
||||
public setElememntRef = (element: HTMLElement): void => {
|
||||
this.consoleHeaderElement = element;
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const numInProgress = this.state.allConsoleData.filter(
|
||||
(data: ConsoleData) => data.type === ConsoleDataType.InProgress,
|
||||
@@ -101,7 +97,9 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
<div
|
||||
className="notificationConsoleHeader"
|
||||
id="notificationConsoleHeader"
|
||||
ref={this.setElememntRef}
|
||||
role="button"
|
||||
aria-label="Console"
|
||||
aria-expanded={this.props.isConsoleExpanded}
|
||||
onClick={() => this.expandCollapseConsole()}
|
||||
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
|
||||
tabIndex={0}
|
||||
@@ -109,15 +107,15 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
<div className="statusBar">
|
||||
<span className="dataTypeIcons">
|
||||
<span className="notificationConsoleHeaderIconWithData">
|
||||
<img src={LoadingIcon} alt="in progress items" />
|
||||
<img src={LoadingIcon} alt="In progress items" />
|
||||
<span className="numInProgress">{numInProgress}</span>
|
||||
</span>
|
||||
<span className="notificationConsoleHeaderIconWithData">
|
||||
<img src={ErrorBlackIcon} alt="error items" />
|
||||
<img src={ErrorBlackIcon} alt="Error items" />
|
||||
<span className="numErroredItems">{numErroredItems}</span>
|
||||
</span>
|
||||
<span className="notificationConsoleHeaderIconWithData">
|
||||
<img src={infoBubbleIcon} alt="info items" />
|
||||
<img src={infoBubbleIcon} alt="Info items" />
|
||||
<span className="numInfoItems">{numInfoItems}</span>
|
||||
</span>
|
||||
</span>
|
||||
@@ -129,17 +127,10 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="expandCollapseButton"
|
||||
data-test="NotificationConsole/ExpandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={"console button" + (this.props.isConsoleExpanded ? " expanded" : " collapsed")}
|
||||
aria-expanded={!this.props.isConsoleExpanded}
|
||||
>
|
||||
<div className="expandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton">
|
||||
<img
|
||||
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
|
||||
alt={this.props.isConsoleExpanded ? "ChevronDownIcon" : "ChevronUpIcon"}
|
||||
alt={this.props.isConsoleExpanded ? "Collapse icon" : "Expand icon"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -259,9 +250,6 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
}
|
||||
|
||||
private onConsoleWasExpanded = (): void => {
|
||||
if (this.props.isConsoleExpanded && this.consoleHeaderElement) {
|
||||
this.consoleHeaderElement.focus();
|
||||
}
|
||||
useNotificationConsole.getState().setConsoleAnimationFinished(true);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,10 +5,13 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
className="notificationConsoleContainer"
|
||||
>
|
||||
<div
|
||||
aria-expanded={false}
|
||||
aria-label="Console"
|
||||
className="notificationConsoleHeader"
|
||||
id="notificationConsoleHeader"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
@@ -21,7 +24,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="in progress items"
|
||||
alt="In progress items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@@ -34,7 +37,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="error items"
|
||||
alt="Error items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@@ -47,7 +50,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="info items"
|
||||
alt="Info items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@@ -71,15 +74,11 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-expanded={true}
|
||||
aria-label="console button collapsed"
|
||||
className="expandCollapseButton"
|
||||
data-test="NotificationConsole/ExpandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<img
|
||||
alt="ChevronUpIcon"
|
||||
alt="Expand icon"
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
@@ -176,10 +175,13 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
className="notificationConsoleContainer"
|
||||
>
|
||||
<div
|
||||
aria-expanded={false}
|
||||
aria-label="Console"
|
||||
className="notificationConsoleHeader"
|
||||
id="notificationConsoleHeader"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
@@ -192,7 +194,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="in progress items"
|
||||
alt="In progress items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@@ -205,7 +207,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="error items"
|
||||
alt="Error items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@@ -218,7 +220,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="info items"
|
||||
alt="Info items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@@ -244,15 +246,11 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-expanded={true}
|
||||
aria-label="console button collapsed"
|
||||
className="expandCollapseButton"
|
||||
data-test="NotificationConsole/ExpandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<img
|
||||
alt="ChevronUpIcon"
|
||||
alt="Expand icon"
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
||||
import React from "react";
|
||||
import { ActionContracts } from "../../Contracts/ExplorerContracts";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
@@ -56,6 +57,19 @@ function openCollectionTab(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
isFabricMirrored() &&
|
||||
!(
|
||||
// whitelist the tab kinds that are allowed to be opened in Fabric mirrored
|
||||
(
|
||||
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
|
||||
action.tabKind === ActionContracts.TabKind.SQLQuery
|
||||
)
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//expand database first if not expanded to load the collections
|
||||
if (!database.isDatabaseExpanded?.()) {
|
||||
database.expandDatabase?.();
|
||||
@@ -121,10 +135,28 @@ function openCollectionTab(
|
||||
action.tabKind === ActionContracts.TabKind.SQLQuery ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
|
||||
) {
|
||||
const openQueryTabAction = action as ActionContracts.OpenQueryTab;
|
||||
collection.onNewQueryClick(
|
||||
collection,
|
||||
undefined,
|
||||
generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperties),
|
||||
generateQueryText(openQueryTabAction, collection.partitionKeyProperties),
|
||||
openQueryTabAction.splitterDirection,
|
||||
openQueryTabAction.queryViewSizePercent,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.MongoQuery ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoQuery]
|
||||
) {
|
||||
const openQueryTabAction = action as ActionContracts.OpenQueryTab;
|
||||
collection.onNewMongoQueryClick(
|
||||
collection,
|
||||
undefined,
|
||||
generateQueryText(openQueryTabAction, collection.partitionKeyProperties),
|
||||
openQueryTabAction.splitterDirection,
|
||||
openQueryTabAction.queryViewSizePercent,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import Explorer from "../Explorer";
|
||||
import Explorer from "../../Explorer";
|
||||
import { AddCollectionPanel } from "./AddCollectionPanel";
|
||||
|
||||
const props = {
|
||||
@@ -21,26 +21,50 @@ import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
|
||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||
import { configContext, Platform } from "ConfigContext";
|
||||
import * as DataModels from "Contracts/DataModels";
|
||||
import { AddVectorEmbeddingPolicyForm } from "Explorer/Panes/VectorSearchPanel/AddVectorEmbeddingPolicyForm";
|
||||
import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
||||
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
||||
import {
|
||||
AllPropertiesIndexed,
|
||||
AnalyticalStorageContent,
|
||||
ContainerVectorPolicyTooltipContent,
|
||||
FullTextPolicyDefault,
|
||||
getPartitionKey,
|
||||
getPartitionKeyName,
|
||||
getPartitionKeyPlaceHolder,
|
||||
getPartitionKeyTooltipText,
|
||||
isFreeTierAccount,
|
||||
isSynapseLinkEnabled,
|
||||
parseUniqueKeys,
|
||||
scrollToSection,
|
||||
SharedDatabaseDefault,
|
||||
shouldShowAnalyticalStoreOptions,
|
||||
UniqueKeysHeader,
|
||||
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import React from "react";
|
||||
import { CollectionCreation } from "Shared/Constants";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "UserContext";
|
||||
import { getCollectionName } from "Utils/APITypeUtils";
|
||||
import { isCapabilityEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import {
|
||||
isCapabilityEnabled,
|
||||
isFullTextSearchEnabled,
|
||||
isServerlessAccount,
|
||||
isVectorSearchEnabled,
|
||||
} from "Utils/CapabilityUtils";
|
||||
import { getUpsellMessage } from "Utils/PricingUtils";
|
||||
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
|
||||
import "../Controls/ThroughputInput/ThroughputInput.less";
|
||||
import { ContainerSampleGenerator } from "../DataSamples/ContainerSampleGenerator";
|
||||
import Explorer from "../Explorer";
|
||||
import { useDatabases } from "../useDatabases";
|
||||
import { PanelFooterComponent } from "./PanelFooterComponent";
|
||||
import { PanelInfoErrorComponent } from "./PanelInfoErrorComponent";
|
||||
import { PanelLoadingScreen } from "./PanelLoadingScreen";
|
||||
import { CollapsibleSectionComponent } from "../../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
||||
import "../../Controls/ThroughputInput/ThroughputInput.less";
|
||||
import { ContainerSampleGenerator } from "../../DataSamples/ContainerSampleGenerator";
|
||||
import Explorer from "../../Explorer";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
import { PanelFooterComponent } from "../PanelFooterComponent";
|
||||
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
|
||||
import { PanelLoadingScreen } from "../PanelLoadingScreen";
|
||||
|
||||
export interface AddCollectionPanelProps {
|
||||
explorer: Explorer;
|
||||
@@ -48,40 +72,6 @@ export interface AddCollectionPanelProps {
|
||||
isQuickstart?: boolean;
|
||||
}
|
||||
|
||||
const SharedDatabaseDefault: DataModels.IndexingPolicy = {
|
||||
indexingMode: "consistent",
|
||||
automatic: true,
|
||||
includedPaths: [],
|
||||
excludedPaths: [
|
||||
{
|
||||
path: "/*",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
|
||||
indexingMode: "consistent",
|
||||
automatic: true,
|
||||
includedPaths: [
|
||||
{
|
||||
path: "/*",
|
||||
indexes: [
|
||||
{
|
||||
kind: "Range",
|
||||
dataType: "Number",
|
||||
precision: -1,
|
||||
},
|
||||
{
|
||||
kind: "Range",
|
||||
dataType: "String",
|
||||
precision: -1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
excludedPaths: [],
|
||||
};
|
||||
|
||||
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
|
||||
vectorEmbeddings: [],
|
||||
};
|
||||
@@ -109,6 +99,9 @@ export interface AddCollectionPanelState {
|
||||
vectorIndexingPolicy: DataModels.VectorIndex[];
|
||||
vectorEmbeddingPolicy: DataModels.VectorEmbedding[];
|
||||
vectorPolicyValidated: boolean;
|
||||
fullTextPolicy: DataModels.FullTextPolicy;
|
||||
fullTextIndexes: DataModels.FullTextIndex[];
|
||||
fullTextPolicyValidated: boolean;
|
||||
}
|
||||
|
||||
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
|
||||
@@ -131,7 +124,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "",
|
||||
enableIndexing: true,
|
||||
isSharded: userContext.apiType !== "Tables",
|
||||
partitionKey: this.getPartitionKey(),
|
||||
partitionKey: getPartitionKey(props.isQuickstart),
|
||||
subPartitionKeys: [],
|
||||
enableDedicatedThroughput: false,
|
||||
createMongoWildCardIndex:
|
||||
@@ -147,6 +140,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
vectorEmbeddingPolicy: [],
|
||||
vectorIndexingPolicy: [],
|
||||
vectorPolicyValidated: true,
|
||||
fullTextPolicy: FullTextPolicyDefault,
|
||||
fullTextIndexes: [],
|
||||
fullTextPolicyValidated: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -158,7 +154,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
componentDidUpdate(_prevProps: AddCollectionPanelProps, prevState: AddCollectionPanelState): void {
|
||||
if (this.state.errorMessage && this.state.errorMessage !== prevState.errorMessage) {
|
||||
this.scrollToSection("panelContainer");
|
||||
scrollToSection("panelContainer");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +171,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
/>
|
||||
)}
|
||||
|
||||
{!this.state.errorMessage && this.isFreeTierAccount() && (
|
||||
{!this.state.errorMessage && isFreeTierAccount() && (
|
||||
<PanelInfoErrorComponent
|
||||
message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
|
||||
messageType="info"
|
||||
@@ -269,150 +265,152 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
)}
|
||||
|
||||
<div className="panelMainContent">
|
||||
<Stack hidden={userContext.apiType === "Tables"}>
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Database {userContext.apiType === "Mongo" ? "name" : "id"}
|
||||
</Text>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||||
true,
|
||||
).toLocaleLowerCase()}.`}
|
||||
>
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||||
{!(isFabricNative() && this.props.databaseId !== undefined) && (
|
||||
<Stack hidden={userContext.apiType === "Tables"}>
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Database {userContext.apiType === "Mongo" ? "name" : "id"}
|
||||
</Text>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||||
true,
|
||||
).toLocaleLowerCase()}.`}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
{configContext.platform !== Platform.Fabric && (
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<div role="radiogroup">
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={this.state.createNewDatabase}
|
||||
aria-label="Create new database"
|
||||
aria-checked={this.state.createNewDatabase}
|
||||
name="databaseType"
|
||||
type="radio"
|
||||
role="radio"
|
||||
id="databaseCreateNew"
|
||||
>
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
|
||||
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||||
true,
|
||||
).toLocaleLowerCase()}.`}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Create new</span>
|
||||
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={!this.state.createNewDatabase}
|
||||
aria-label="Use existing database"
|
||||
aria-checked={!this.state.createNewDatabase}
|
||||
name="databaseType"
|
||||
type="radio"
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Use existing</span>
|
||||
</div>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{this.state.createNewDatabase && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<input
|
||||
name="newDatabaseId"
|
||||
id="newDatabaseId"
|
||||
aria-required
|
||||
required
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
placeholder="Type a new database id"
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
aria-label="New database id, Type a new database id"
|
||||
autoFocus
|
||||
tabIndex={0}
|
||||
value={this.state.newDatabaseId}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
this.setState({ newDatabaseId: event.target.value })
|
||||
}
|
||||
/>
|
||||
|
||||
{!isServerlessAccount() && (
|
||||
<Stack horizontal>
|
||||
<Checkbox
|
||||
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
|
||||
checked={this.state.isSharedThroughputChecked}
|
||||
styles={{
|
||||
text: { fontSize: 12 },
|
||||
checkbox: { width: 12, height: 12 },
|
||||
label: { padding: 0, alignItems: "center" },
|
||||
}}
|
||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
||||
this.setState({ isSharedThroughputChecked: isChecked })
|
||||
}
|
||||
{configContext.platform !== Platform.Fabric && (
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<div role="radiogroup">
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={this.state.createNewDatabase}
|
||||
aria-label="Create new database"
|
||||
aria-checked={this.state.createNewDatabase}
|
||||
name="databaseType"
|
||||
type="radio"
|
||||
role="radio"
|
||||
id="databaseCreateNew"
|
||||
tabIndex={0}
|
||||
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={`Throughput configured at the database level will be shared across all ${getCollectionName(
|
||||
true,
|
||||
).toLocaleLowerCase()} within the database.`}
|
||||
>
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName(
|
||||
<span className="panelRadioBtnLabel">Create new</span>
|
||||
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={!this.state.createNewDatabase}
|
||||
aria-label="Use existing database"
|
||||
aria-checked={!this.state.createNewDatabase}
|
||||
name="databaseType"
|
||||
type="radio"
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Use existing</span>
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{this.state.createNewDatabase && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<input
|
||||
name="newDatabaseId"
|
||||
id="newDatabaseId"
|
||||
aria-required
|
||||
required
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
placeholder="Type a new database id"
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
aria-label="New database id, Type a new database id"
|
||||
autoFocus
|
||||
tabIndex={0}
|
||||
value={this.state.newDatabaseId}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
this.setState({ newDatabaseId: event.target.value })
|
||||
}
|
||||
/>
|
||||
|
||||
{!isServerlessAccount() && (
|
||||
<Stack horizontal>
|
||||
<Checkbox
|
||||
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
|
||||
checked={this.state.isSharedThroughputChecked}
|
||||
styles={{
|
||||
text: { fontSize: 12 },
|
||||
checkbox: { width: 12, height: 12 },
|
||||
label: { padding: 0, alignItems: "center" },
|
||||
}}
|
||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
||||
this.setState({ isSharedThroughputChecked: isChecked })
|
||||
}
|
||||
/>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={`Throughput configured at the database level will be shared across all ${getCollectionName(
|
||||
true,
|
||||
).toLocaleLowerCase()} within the database.`}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName(
|
||||
true,
|
||||
).toLocaleLowerCase()} within the database.`}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{!isServerlessAccount() && this.state.isSharedThroughputChecked && (
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
|
||||
isDatabase={true}
|
||||
isSharded={this.state.isSharded}
|
||||
isFreeTier={this.isFreeTierAccount()}
|
||||
isQuickstart={this.props.isQuickstart}
|
||||
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
|
||||
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
|
||||
this.setState({ isThroughputCapExceeded })
|
||||
}
|
||||
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
{!this.state.createNewDatabase && (
|
||||
<Dropdown
|
||||
ariaLabel="Choose an existing database"
|
||||
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
|
||||
style={{ width: 300, fontSize: 12 }}
|
||||
placeholder="Choose an existing database"
|
||||
options={this.getDatabaseOptions()}
|
||||
onChange={(event: React.FormEvent<HTMLDivElement>, database: IDropdownOption) =>
|
||||
this.setState({ selectedDatabaseId: database.key as string })
|
||||
}
|
||||
defaultSelectedKey={this.props.databaseId}
|
||||
responsiveMode={999}
|
||||
/>
|
||||
)}
|
||||
<Separator className="panelSeparator" />
|
||||
</Stack>
|
||||
{!isServerlessAccount() && this.state.isSharedThroughputChecked && (
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
|
||||
isDatabase={true}
|
||||
isSharded={this.state.isSharded}
|
||||
isFreeTier={isFreeTierAccount()}
|
||||
isQuickstart={this.props.isQuickstart}
|
||||
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
|
||||
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
|
||||
this.setState({ isThroughputCapExceeded })
|
||||
}
|
||||
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
{!this.state.createNewDatabase && (
|
||||
<Dropdown
|
||||
ariaLabel="Choose an existing database"
|
||||
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
|
||||
style={{ width: 300, fontSize: 12 }}
|
||||
placeholder="Choose an existing database"
|
||||
options={this.getDatabaseOptions()}
|
||||
onChange={(event: React.FormEvent<HTMLDivElement>, database: IDropdownOption) =>
|
||||
this.setState({ selectedDatabaseId: database.key as string })
|
||||
}
|
||||
defaultSelectedKey={this.props.databaseId}
|
||||
responsiveMode={999}
|
||||
/>
|
||||
)}
|
||||
<Separator className="panelSeparator" />
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Stack>
|
||||
<Stack horizontal>
|
||||
@@ -561,17 +559,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
{this.getPartitionKeyName()}
|
||||
{getPartitionKeyName()}
|
||||
</Text>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={this.getPartitionKeyTooltipText()}
|
||||
>
|
||||
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={getPartitionKeyTooltipText()}>
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={this.getPartitionKeyTooltipText()}
|
||||
ariaLabel={getPartitionKeyTooltipText()}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
@@ -585,8 +580,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
required
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
placeholder={this.getPartitionKeyPlaceHolder()}
|
||||
aria-label={this.getPartitionKeyName()}
|
||||
placeholder={getPartitionKeyPlaceHolder()}
|
||||
aria-label={getPartitionKeyName()}
|
||||
pattern={userContext.apiType === "Gremlin" ? "^/[^/]*" : ".*"}
|
||||
title={userContext.apiType === "Gremlin" ? "May not use composite partition key" : ""}
|
||||
value={this.state.partitionKey}
|
||||
@@ -624,8 +619,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
tabIndex={index > 0 ? 1 : 0}
|
||||
className="panelTextField"
|
||||
autoComplete="off"
|
||||
placeholder={this.getPartitionKeyPlaceHolder(index)}
|
||||
aria-label={this.getPartitionKeyName()}
|
||||
placeholder={getPartitionKeyPlaceHolder(index)}
|
||||
aria-label={getPartitionKeyName()}
|
||||
pattern={".*"}
|
||||
title={""}
|
||||
value={subPartitionKey}
|
||||
@@ -651,7 +646,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
{userContext.apiType === "SQL" && (
|
||||
{!isFabricNative() && userContext.apiType === "SQL" && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<DefaultButton
|
||||
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
||||
@@ -716,10 +711,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
{this.shouldShowCollectionThroughputInput() && (
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
|
||||
isDatabase={false}
|
||||
isSharded={this.state.isSharded}
|
||||
isFreeTier={this.isFreeTierAccount()}
|
||||
isFreeTier={isFreeTierAccount()}
|
||||
isQuickstart={this.props.isQuickstart}
|
||||
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
|
||||
@@ -732,29 +727,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
/>
|
||||
)}
|
||||
|
||||
{userContext.apiType === "SQL" && (
|
||||
{!isFabricNative() && userContext.apiType === "SQL" && (
|
||||
<Stack>
|
||||
<Stack horizontal>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Unique keys
|
||||
</Text>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={
|
||||
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key."
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={
|
||||
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key."
|
||||
}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
{UniqueKeysHeader()}
|
||||
{this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => {
|
||||
return (
|
||||
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${i}`} horizontal>
|
||||
@@ -802,31 +777,18 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{this.shouldShowAnalyticalStoreOptions() && (
|
||||
{shouldShowAnalyticalStoreOptions() && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Stack horizontal>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Analytical store
|
||||
</Text>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={this.getAnalyticalStorageTooltipContent()}
|
||||
>
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
{AnalyticalStorageContent()}
|
||||
</Text>
|
||||
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<div role="radiogroup">
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={this.state.enableAnalyticalStore}
|
||||
disabled={!this.isSynapseLinkEnabled()}
|
||||
disabled={!isSynapseLinkEnabled()}
|
||||
aria-label="Enable analytical store"
|
||||
aria-checked={this.state.enableAnalyticalStore}
|
||||
name="analyticalStore"
|
||||
@@ -841,7 +803,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={!this.state.enableAnalyticalStore}
|
||||
disabled={!this.isSynapseLinkEnabled()}
|
||||
disabled={!isSynapseLinkEnabled()}
|
||||
aria-label="Disable analytical store"
|
||||
aria-checked={!this.state.enableAnalyticalStore}
|
||||
name="analyticalStore"
|
||||
@@ -855,7 +817,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
{!this.isSynapseLinkEnabled() && (
|
||||
{!isSynapseLinkEnabled() && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Text variant="small">
|
||||
Azure Synapse Link is required for creating an analytical store{" "}
|
||||
@@ -863,6 +825,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<Link
|
||||
href="https://aka.ms/cosmosdb-synapselink"
|
||||
target="_blank"
|
||||
aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink}
|
||||
className="capacitycalculator-link"
|
||||
>
|
||||
Learn more
|
||||
@@ -884,15 +847,15 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
title="Container Vector Policy"
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
this.scrollToSection("collapsibleVectorPolicySectionContent");
|
||||
scrollToSection("collapsibleVectorPolicySectionContent");
|
||||
}}
|
||||
tooltipContent={this.getContainerVectorPolicyTooltipContent()}
|
||||
tooltipContent={ContainerVectorPolicyTooltipContent()}
|
||||
>
|
||||
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
||||
<AddVectorEmbeddingPolicyForm
|
||||
vectorEmbedding={this.state.vectorEmbeddingPolicy}
|
||||
vectorIndex={this.state.vectorIndexingPolicy}
|
||||
<VectorEmbeddingPoliciesComponent
|
||||
vectorEmbeddings={this.state.vectorEmbeddingPolicy}
|
||||
vectorIndexes={this.state.vectorIndexingPolicy}
|
||||
onVectorEmbeddingChange={(
|
||||
vectorEmbeddingPolicy: DataModels.VectorEmbedding[],
|
||||
vectorIndexingPolicy: DataModels.VectorIndex[],
|
||||
@@ -906,13 +869,41 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</CollapsibleSectionComponent>
|
||||
</Stack>
|
||||
)}
|
||||
{userContext.apiType !== "Tables" && (
|
||||
{this.shouldShowFullTextSearchParameters() && (
|
||||
<Stack>
|
||||
<CollapsibleSectionComponent
|
||||
title="Container Full Text Search Policy"
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
scrollToSection("collapsibleFullTextPolicySectionContent");
|
||||
}}
|
||||
//TODO: uncomment when learn more text becomes available
|
||||
// tooltipContent={this.getContainerFullTextPolicyTooltipContent()}
|
||||
>
|
||||
<Stack id="collapsibleFullTextPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
||||
<FullTextPoliciesComponent
|
||||
fullTextPolicy={this.state.fullTextPolicy}
|
||||
onFullTextPathChange={(
|
||||
fullTextPolicy: DataModels.FullTextPolicy,
|
||||
fullTextIndexes: DataModels.FullTextIndex[],
|
||||
fullTextPolicyValidated: boolean,
|
||||
) => {
|
||||
this.setState({ fullTextPolicy, fullTextIndexes, fullTextPolicyValidated });
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CollapsibleSectionComponent>
|
||||
</Stack>
|
||||
)}
|
||||
{!isFabricNative() && userContext.apiType !== "Tables" && (
|
||||
<CollapsibleSectionComponent
|
||||
title="Advanced"
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
|
||||
this.scrollToSection("collapsibleAdvancedSectionContent");
|
||||
scrollToSection("collapsibleAdvancedSectionContent");
|
||||
}}
|
||||
>
|
||||
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
|
||||
@@ -1022,31 +1013,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}));
|
||||
}
|
||||
|
||||
private getPartitionKeyName(isLowerCase?: boolean): string {
|
||||
const partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
|
||||
|
||||
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
|
||||
}
|
||||
|
||||
private getPartitionKeyPlaceHolder(index?: number): string {
|
||||
switch (userContext.apiType) {
|
||||
case "Mongo":
|
||||
return "e.g., categoryId";
|
||||
case "Gremlin":
|
||||
return "e.g., /address";
|
||||
case "SQL":
|
||||
return `${
|
||||
index === undefined
|
||||
? "Required - first partition key e.g., /TenantId"
|
||||
: index === 0
|
||||
? "second partition key e.g., /UserId"
|
||||
: "third partition key e.g., /SessionId"
|
||||
}`;
|
||||
default:
|
||||
return "e.g., /address/zipCode";
|
||||
}
|
||||
}
|
||||
|
||||
private onCreateNewDatabaseRadioBtnChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||
if (event.target.checked && !this.state.createNewDatabase) {
|
||||
this.setState({
|
||||
@@ -1134,48 +1100,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return !!selectedDatabase?.offer();
|
||||
}
|
||||
|
||||
private isFreeTierAccount(): boolean {
|
||||
return userContext.databaseAccount?.properties?.enableFreeTier;
|
||||
}
|
||||
|
||||
private getFreeTierIndexingText(): string {
|
||||
return this.state.enableIndexing
|
||||
? "All properties in your documents will be indexed by default for flexible and efficient queries."
|
||||
: "Indexing will be turned off. Recommended if you don't need to run queries or only have key value operations.";
|
||||
}
|
||||
|
||||
private getPartitionKeyTooltipText(): string {
|
||||
if (userContext.apiType === "Mongo") {
|
||||
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. It’s critical to choose a field that will evenly distribute your data.";
|
||||
}
|
||||
|
||||
let tooltipText = `The ${this.getPartitionKeyName(
|
||||
true,
|
||||
)} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`;
|
||||
|
||||
if (userContext.apiType === "SQL") {
|
||||
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
|
||||
}
|
||||
|
||||
return tooltipText;
|
||||
}
|
||||
|
||||
private getPartitionKey(): string {
|
||||
if (userContext.apiType !== "SQL" && userContext.apiType !== "Mongo") {
|
||||
return "";
|
||||
}
|
||||
if (userContext.features.partitionKeyDefault) {
|
||||
return userContext.apiType === "SQL" ? "/id" : "_id";
|
||||
}
|
||||
if (userContext.features.partitionKeyDefault2) {
|
||||
return userContext.apiType === "SQL" ? "/pk" : "pk";
|
||||
}
|
||||
if (this.props.isQuickstart) {
|
||||
return userContext.apiType === "SQL" ? "/categoryId" : "categoryId";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private getPartitionKeySubtext(): string {
|
||||
if (
|
||||
userContext.features.partitionKeyDefault &&
|
||||
@@ -1187,32 +1117,21 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return "";
|
||||
}
|
||||
|
||||
private getAnalyticalStorageTooltipContent(): JSX.Element {
|
||||
return (
|
||||
<Text variant="small">
|
||||
Enable analytical store capability to perform near real-time analytics on your operational data, without
|
||||
impacting the performance of transactional workloads.{" "}
|
||||
<Link target="_blank" href="https://aka.ms/analytical-store-overview">
|
||||
Learn more
|
||||
</Link>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
private getContainerVectorPolicyTooltipContent(): JSX.Element {
|
||||
return (
|
||||
<Text variant="small">
|
||||
Describe any properties in your data that contain vectors, so that they can be made available for similarity
|
||||
queries.{" "}
|
||||
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
|
||||
Learn more
|
||||
</Link>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
//TODO: uncomment when learn more text becomes available
|
||||
// private getContainerFullTextPolicyTooltipContent(): JSX.Element {
|
||||
// return (
|
||||
// <Text variant="small">
|
||||
// Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
|
||||
// magna aliqua.{" "}
|
||||
// <Link target="_blank" href="https://aka.ms/CosmosFullTextSearch">
|
||||
// Learn more
|
||||
// </Link>
|
||||
// </Text>
|
||||
// );
|
||||
// }
|
||||
|
||||
private shouldShowCollectionThroughputInput(): boolean {
|
||||
if (isServerlessAccount()) {
|
||||
if (isFabricNative() || isServerlessAccount()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1228,7 +1147,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}
|
||||
|
||||
private shouldShowIndexingOptionsForFreeTierAccount(): boolean {
|
||||
if (!this.isFreeTierAccount()) {
|
||||
if (!isFreeTierAccount()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1237,43 +1156,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
: this.isSelectedDatabaseSharedThroughput();
|
||||
}
|
||||
|
||||
private shouldShowAnalyticalStoreOptions(): boolean {
|
||||
if (configContext.platform === Platform.Emulator) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (userContext.apiType) {
|
||||
case "SQL":
|
||||
case "Mongo":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private isSynapseLinkEnabled(): boolean {
|
||||
if (!userContext.databaseAccount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { properties } = userContext.databaseAccount;
|
||||
if (!properties) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (properties.enableAnalyticalStorage) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return properties.capabilities?.some(
|
||||
(capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics,
|
||||
);
|
||||
}
|
||||
|
||||
private shouldShowVectorSearchParameters() {
|
||||
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
||||
}
|
||||
|
||||
private shouldShowFullTextSearchParameters() {
|
||||
return isFullTextSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
||||
}
|
||||
|
||||
private parseUniqueKeys(): DataModels.UniqueKeyPolicy {
|
||||
if (this.state.uniqueKeys?.length === 0) {
|
||||
return undefined;
|
||||
@@ -1330,20 +1220,27 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.shouldShowVectorSearchParameters() && !this.state.vectorPolicyValidated) {
|
||||
this.setState({ errorMessage: "Please fix errors in container vector policy" });
|
||||
return false;
|
||||
if (this.shouldShowVectorSearchParameters()) {
|
||||
if (!this.state.vectorPolicyValidated) {
|
||||
this.setState({ errorMessage: "Please fix errors in container vector policy" });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.state.fullTextPolicyValidated) {
|
||||
this.setState({ errorMessage: "Please fix errors in container full text search polilcy" });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private getAnalyticalStorageTtl(): number {
|
||||
if (!this.isSynapseLinkEnabled()) {
|
||||
if (!isSynapseLinkEnabled()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!this.shouldShowAnalyticalStoreOptions()) {
|
||||
if (!shouldShowAnalyticalStoreOptions()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1357,10 +1254,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return Constants.AnalyticalStorageTtl.Disabled;
|
||||
}
|
||||
|
||||
private scrollToSection(id: string): void {
|
||||
document.getElementById(id)?.scrollIntoView();
|
||||
}
|
||||
|
||||
private getSampleDBName(): string {
|
||||
const existingSampleDBs = useDatabases
|
||||
.getState()
|
||||
@@ -1395,7 +1288,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
partitionKeyString = "/'$pk'";
|
||||
}
|
||||
|
||||
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = this.parseUniqueKeys();
|
||||
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = parseUniqueKeys(this.state.uniqueKeys);
|
||||
const partitionKeyVersion = this.state.useHashV1 ? undefined : 2;
|
||||
const partitionKey: DataModels.PartitionKey = partitionKeyString
|
||||
? {
|
||||
@@ -1423,6 +1316,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
};
|
||||
}
|
||||
|
||||
if (this.shouldShowFullTextSearchParameters()) {
|
||||
indexingPolicy.fullTextIndexes = this.state.fullTextIndexes;
|
||||
}
|
||||
|
||||
const telemetryData = {
|
||||
database: {
|
||||
id: databaseId,
|
||||
@@ -1482,6 +1379,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
uniqueKeyPolicy,
|
||||
createMongoWildcardIndex: this.state.createMongoWildCardIndex,
|
||||
vectorEmbeddingPolicy,
|
||||
fullTextPolicy: this.state.fullTextPolicy,
|
||||
};
|
||||
|
||||
this.setState({ isExecuting: true });
|
||||
@@ -0,0 +1,217 @@
|
||||
import { DirectionalHint, Icon, Link, Stack, Text, TooltipHost } from "@fluentui/react";
|
||||
import * as Constants from "Common/Constants";
|
||||
import { configContext, Platform } from "ConfigContext";
|
||||
import * as DataModels from "Contracts/DataModels";
|
||||
import { getFullTextLanguageOptions } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import React from "react";
|
||||
import { userContext } from "UserContext";
|
||||
|
||||
export function getPartitionKeyTooltipText(): string {
|
||||
if (userContext.apiType === "Mongo") {
|
||||
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. It’s critical to choose a field that will evenly distribute your data.";
|
||||
}
|
||||
|
||||
let tooltipText = `The ${getPartitionKeyName(
|
||||
true,
|
||||
)} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`;
|
||||
|
||||
if (userContext.apiType === "SQL") {
|
||||
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
|
||||
}
|
||||
|
||||
return tooltipText;
|
||||
}
|
||||
|
||||
export function getPartitionKeyName(isLowerCase?: boolean): string {
|
||||
const partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
|
||||
|
||||
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
|
||||
}
|
||||
|
||||
export function getPartitionKeyPlaceHolder(index?: number): string {
|
||||
switch (userContext.apiType) {
|
||||
case "Mongo":
|
||||
return "e.g., categoryId";
|
||||
case "Gremlin":
|
||||
return "e.g., /address";
|
||||
case "SQL":
|
||||
return `${
|
||||
index === undefined
|
||||
? "Required - first partition key e.g., /TenantId"
|
||||
: index === 0
|
||||
? "second partition key e.g., /UserId"
|
||||
: "third partition key e.g., /SessionId"
|
||||
}`;
|
||||
default:
|
||||
return "e.g., /address/zipCode";
|
||||
}
|
||||
}
|
||||
|
||||
export function getPartitionKey(isQuickstart?: boolean): string {
|
||||
if (userContext.apiType !== "SQL" && userContext.apiType !== "Mongo") {
|
||||
return "";
|
||||
}
|
||||
if (userContext.features.partitionKeyDefault) {
|
||||
return userContext.apiType === "SQL" ? "/id" : "_id";
|
||||
}
|
||||
if (userContext.features.partitionKeyDefault2) {
|
||||
return userContext.apiType === "SQL" ? "/pk" : "pk";
|
||||
}
|
||||
if (isQuickstart) {
|
||||
return userContext.apiType === "SQL" ? "/categoryId" : "categoryId";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function isFreeTierAccount(): boolean {
|
||||
return userContext.databaseAccount?.properties?.enableFreeTier;
|
||||
}
|
||||
|
||||
export function UniqueKeysHeader(): JSX.Element {
|
||||
const tooltipContent =
|
||||
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key.";
|
||||
|
||||
return (
|
||||
<Stack horizontal>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Unique keys
|
||||
</Text>
|
||||
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}>
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldShowAnalyticalStoreOptions(): boolean {
|
||||
if (isFabricNative() || configContext.platform === Platform.Emulator) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (userContext.apiType) {
|
||||
case "SQL":
|
||||
case "Mongo":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function AnalyticalStorageContent(): JSX.Element {
|
||||
return (
|
||||
<Text variant="small">
|
||||
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting
|
||||
the performance of transactional workloads.{" "}
|
||||
<Link
|
||||
aria-label={Constants.ariaLabelForLearnMoreLink.AnalyticalStore}
|
||||
target="_blank"
|
||||
href="https://aka.ms/analytical-store-overview"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
export function isSynapseLinkEnabled(): boolean {
|
||||
if (!userContext.databaseAccount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { properties } = userContext.databaseAccount;
|
||||
if (!properties) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (properties.enableAnalyticalStorage) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return properties.capabilities?.some(
|
||||
(capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics,
|
||||
);
|
||||
}
|
||||
|
||||
export function scrollToSection(id: string): void {
|
||||
document.getElementById(id)?.scrollIntoView();
|
||||
}
|
||||
|
||||
export function ContainerVectorPolicyTooltipContent(): JSX.Element {
|
||||
return (
|
||||
<Text variant="small">
|
||||
Describe any properties in your data that contain vectors, so that they can be made available for similarity
|
||||
queries.{" "}
|
||||
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
|
||||
Learn more
|
||||
</Link>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
export function parseUniqueKeys(uniqueKeys: string[]): DataModels.UniqueKeyPolicy {
|
||||
if (uniqueKeys?.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = { uniqueKeys: [] };
|
||||
uniqueKeys.forEach((uniqueKey: string) => {
|
||||
if (uniqueKey) {
|
||||
const validPaths: string[] = uniqueKey.split(",")?.filter((path) => path?.length > 0);
|
||||
const trimmedPaths: string[] = validPaths?.map((path) => path.trim());
|
||||
if (trimmedPaths?.length > 0) {
|
||||
if (userContext.apiType === "Mongo") {
|
||||
trimmedPaths.map((path) => {
|
||||
const transformedPath = path.split(".").join("/");
|
||||
if (transformedPath[0] !== "/") {
|
||||
return "/" + transformedPath;
|
||||
}
|
||||
return transformedPath;
|
||||
});
|
||||
}
|
||||
uniqueKeyPolicy.uniqueKeys.push({ paths: trimmedPaths });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return uniqueKeyPolicy;
|
||||
}
|
||||
|
||||
export const SharedDatabaseDefault: DataModels.IndexingPolicy = {
|
||||
indexingMode: "consistent",
|
||||
automatic: true,
|
||||
includedPaths: [],
|
||||
excludedPaths: [
|
||||
{
|
||||
path: "/*",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const FullTextPolicyDefault: DataModels.FullTextPolicy = {
|
||||
defaultLanguage: getFullTextLanguageOptions()[0].key as never,
|
||||
fullTextPaths: [],
|
||||
};
|
||||
|
||||
export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
|
||||
indexingMode: "consistent",
|
||||
automatic: true,
|
||||
includedPaths: [
|
||||
{
|
||||
path: "/*",
|
||||
indexes: [
|
||||
{
|
||||
kind: "Range",
|
||||
dataType: "Number",
|
||||
precision: -1,
|
||||
},
|
||||
{
|
||||
kind: "Range",
|
||||
dataType: "String",
|
||||
precision: -1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
excludedPaths: [],
|
||||
};
|
||||
@@ -309,40 +309,24 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
<Stack
|
||||
className="panelGroupSpacing"
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
<Text
|
||||
className="panelTextBold"
|
||||
variant="small"
|
||||
>
|
||||
<Text
|
||||
className="panelTextBold"
|
||||
variant="small"
|
||||
>
|
||||
Analytical store
|
||||
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.
|
||||
|
||||
<StyledLinkBase
|
||||
aria-label="Learn more about analytical store."
|
||||
href="https://aka.ms/analytical-store-overview"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
<StyledTooltipHostBase
|
||||
content={
|
||||
<Text
|
||||
variant="small"
|
||||
>
|
||||
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.
|
||||
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/analytical-store-overview"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
}
|
||||
directionalHint={4}
|
||||
>
|
||||
<Icon
|
||||
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
</Stack>
|
||||
</Text>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
@@ -400,6 +384,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
. Enable Synapse Link for this Cosmos DB account.
|
||||
|
||||
<StyledLinkBase
|
||||
aria-label="Learn more about Azure Synapse Link."
|
||||
className="capacitycalculator-link"
|
||||
href="https://aka.ms/cosmosdb-synapselink"
|
||||
target="_blank"
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Checkbox, Icon, Link, Stack, Text } from "@fluentui/react";
|
||||
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import { scrollToSection } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||
import React from "react";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||
|
||||
export interface AddMVAdvancedComponentProps {
|
||||
useHashV1: boolean;
|
||||
setUseHashV1: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setSubPartitionKeys: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
}
|
||||
export const AddMVAdvancedComponent = (props: AddMVAdvancedComponentProps): JSX.Element => {
|
||||
const { useHashV1, setUseHashV1, setSubPartitionKeys } = props;
|
||||
|
||||
const useHashV1CheckboxOnChange = (isChecked: boolean): void => {
|
||||
setUseHashV1(isChecked);
|
||||
setSubPartitionKeys([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<CollapsibleSectionComponent
|
||||
title="Advanced"
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
TelemetryProcessor.traceOpen(Action.ExpandAddMaterializedViewPaneAdvancedSection);
|
||||
scrollToSection("collapsibleAdvancedSectionContent");
|
||||
}}
|
||||
>
|
||||
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
|
||||
<Checkbox
|
||||
label="My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)"
|
||||
checked={useHashV1}
|
||||
styles={{
|
||||
text: { fontSize: 12 },
|
||||
checkbox: { width: 12, height: 12 },
|
||||
label: { padding: 0, alignItems: "center", wordWrap: "break-word", whiteSpace: "break-spaces" },
|
||||
}}
|
||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => {
|
||||
useHashV1CheckboxOnChange(isChecked);
|
||||
}}
|
||||
/>
|
||||
<Text variant="small">
|
||||
<Icon iconName="InfoSolid" className="removeIcon" /> To ensure compatibility with older SDKs, the created
|
||||
container will use a legacy partitioning scheme that supports partition key values of size only up to 101
|
||||
bytes. If this is enabled, you will not be able to use hierarchical partition keys.{" "}
|
||||
<Link href="https://aka.ms/cosmos-large-pk" target="_blank">
|
||||
Learn more
|
||||
</Link>
|
||||
</Text>
|
||||
</Stack>
|
||||
</CollapsibleSectionComponent>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
import { DefaultButton, Link, Stack, Text } from "@fluentui/react";
|
||||
import * as Constants from "Common/Constants";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import {
|
||||
AnalyticalStorageContent,
|
||||
isSynapseLinkEnabled,
|
||||
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||
import React from "react";
|
||||
import { getCollectionName } from "Utils/APITypeUtils";
|
||||
|
||||
export interface AddMVAnalyticalStoreComponentProps {
|
||||
explorer: Explorer;
|
||||
enableAnalyticalStore: boolean;
|
||||
setEnableAnalyticalStore: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
export const AddMVAnalyticalStoreComponent = (props: AddMVAnalyticalStoreComponentProps): JSX.Element => {
|
||||
const { explorer, enableAnalyticalStore, setEnableAnalyticalStore } = props;
|
||||
|
||||
const onEnableAnalyticalStoreRadioButtonChange = (checked: boolean): void => {
|
||||
if (checked && !enableAnalyticalStore) {
|
||||
setEnableAnalyticalStore(true);
|
||||
}
|
||||
};
|
||||
|
||||
const onDisableAnalyticalStoreRadioButtonnChange = (checked: boolean): void => {
|
||||
if (checked && enableAnalyticalStore) {
|
||||
setEnableAnalyticalStore(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Text className="panelTextBold" variant="small">
|
||||
{AnalyticalStorageContent()}
|
||||
</Text>
|
||||
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<div role="radiogroup">
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={enableAnalyticalStore}
|
||||
disabled={!isSynapseLinkEnabled()}
|
||||
aria-label="Enable analytical store"
|
||||
aria-checked={enableAnalyticalStore}
|
||||
name="analyticalStore"
|
||||
type="radio"
|
||||
role="radio"
|
||||
id="enableAnalyticalStoreBtn"
|
||||
tabIndex={0}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onEnableAnalyticalStoreRadioButtonChange(event.target.checked);
|
||||
}}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">On</span>
|
||||
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={!enableAnalyticalStore}
|
||||
disabled={!isSynapseLinkEnabled()}
|
||||
aria-label="Disable analytical store"
|
||||
aria-checked={!enableAnalyticalStore}
|
||||
name="analyticalStore"
|
||||
type="radio"
|
||||
role="radio"
|
||||
id="disableAnalyticalStoreBtn"
|
||||
tabIndex={0}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onDisableAnalyticalStoreRadioButtonnChange(event.target.checked);
|
||||
}}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Off</span>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
{!isSynapseLinkEnabled() && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Text variant="small">
|
||||
Azure Synapse Link is required for creating an analytical store {getCollectionName().toLocaleLowerCase()}.
|
||||
Enable Synapse Link for this Cosmos DB account.{" "}
|
||||
<Link
|
||||
href="https://aka.ms/cosmosdb-synapselink"
|
||||
target="_blank"
|
||||
aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink}
|
||||
className="capacitycalculator-link"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</Text>
|
||||
<DefaultButton
|
||||
text="Enable"
|
||||
onClick={() => explorer.openEnableSynapseLinkDialog()}
|
||||
style={{ height: 27, width: 80 }}
|
||||
styles={{ label: { fontSize: 12 } }}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Stack } from "@fluentui/react";
|
||||
import { FullTextIndex, FullTextPolicy } from "Contracts/DataModels";
|
||||
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
||||
import { scrollToSection } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||
import React from "react";
|
||||
|
||||
export interface AddMVFullTextSearchComponentProps {
|
||||
fullTextPolicy: FullTextPolicy;
|
||||
setFullTextPolicy: React.Dispatch<React.SetStateAction<FullTextPolicy>>;
|
||||
setFullTextIndexes: React.Dispatch<React.SetStateAction<FullTextIndex[]>>;
|
||||
setFullTextPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
export const AddMVFullTextSearchComponent = (props: AddMVFullTextSearchComponentProps): JSX.Element => {
|
||||
const { fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated } = props;
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<CollapsibleSectionComponent
|
||||
title="Container Full Text Search Policy"
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
scrollToSection("collapsibleFullTextPolicySectionContent");
|
||||
}}
|
||||
>
|
||||
<Stack id="collapsibleFullTextPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
||||
<FullTextPoliciesComponent
|
||||
fullTextPolicy={fullTextPolicy}
|
||||
onFullTextPathChange={(
|
||||
fullTextPolicy: FullTextPolicy,
|
||||
fullTextIndexes: FullTextIndex[],
|
||||
fullTextPolicyValidated: boolean,
|
||||
) => {
|
||||
setFullTextPolicy(fullTextPolicy);
|
||||
setFullTextIndexes(fullTextIndexes);
|
||||
setFullTextPolicyValidated(fullTextPolicyValidated);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CollapsibleSectionComponent>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
import { DefaultButton, DirectionalHint, Icon, IconButton, Link, Stack, Text, TooltipHost } from "@fluentui/react";
|
||||
import * as Constants from "Common/Constants";
|
||||
import {
|
||||
getPartitionKeyName,
|
||||
getPartitionKeyPlaceHolder,
|
||||
getPartitionKeyTooltipText,
|
||||
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||
import React from "react";
|
||||
|
||||
export interface AddMVPartitionKeyComponentProps {
|
||||
partitionKey?: string;
|
||||
setPartitionKey: React.Dispatch<React.SetStateAction<string>>;
|
||||
subPartitionKeys: string[];
|
||||
setSubPartitionKeys: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
useHashV1: boolean;
|
||||
}
|
||||
|
||||
export const AddMVPartitionKeyComponent = (props: AddMVPartitionKeyComponentProps): JSX.Element => {
|
||||
const { partitionKey, setPartitionKey, subPartitionKeys, setSubPartitionKeys, useHashV1 } = props;
|
||||
|
||||
const partitionKeyValueOnChange = (value: string): void => {
|
||||
if (!partitionKey && !value.startsWith("/")) {
|
||||
setPartitionKey("/" + value);
|
||||
} else {
|
||||
setPartitionKey(value);
|
||||
}
|
||||
};
|
||||
|
||||
const subPartitionKeysValueOnChange = (value: string, index: number): void => {
|
||||
const updatedSubPartitionKeys: string[] = [...subPartitionKeys];
|
||||
if (!updatedSubPartitionKeys[index] && !value.startsWith("/")) {
|
||||
updatedSubPartitionKeys[index] = "/" + value.trim();
|
||||
} else {
|
||||
updatedSubPartitionKeys[index] = value.trim();
|
||||
}
|
||||
setSubPartitionKeys(updatedSubPartitionKeys);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Partition key
|
||||
</Text>
|
||||
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={getPartitionKeyTooltipText()}>
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
id="addmaterializedView-partitionKeyValue"
|
||||
aria-required
|
||||
required
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
placeholder={getPartitionKeyPlaceHolder()}
|
||||
aria-label={getPartitionKeyName()}
|
||||
pattern=".*"
|
||||
value={partitionKey}
|
||||
style={{ marginBottom: 8 }}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
partitionKeyValueOnChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
{subPartitionKeys.map((subPartitionKey: string, subPartitionKeyIndex: number) => {
|
||||
return (
|
||||
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${subPartitionKeyIndex}`} horizontal>
|
||||
<div
|
||||
style={{
|
||||
width: "20px",
|
||||
border: "solid",
|
||||
borderWidth: "0px 0px 1px 1px",
|
||||
marginRight: "5px",
|
||||
}}
|
||||
></div>
|
||||
<input
|
||||
type="text"
|
||||
id="addMaterializedView-partitionKeyValue"
|
||||
key={`addMaterializedView-partitionKeyValue_${subPartitionKeyIndex}`}
|
||||
aria-required
|
||||
required
|
||||
size={40}
|
||||
tabIndex={subPartitionKeyIndex > 0 ? 1 : 0}
|
||||
className="panelTextField"
|
||||
autoComplete="off"
|
||||
placeholder={getPartitionKeyPlaceHolder(subPartitionKeyIndex)}
|
||||
aria-label={getPartitionKeyName()}
|
||||
pattern={".*"}
|
||||
title={""}
|
||||
value={subPartitionKey}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
subPartitionKeysValueOnChange(event.target.value, subPartitionKeyIndex);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Delete" }}
|
||||
style={{ height: 27 }}
|
||||
onClick={() => {
|
||||
const updatedSubPartitionKeys = subPartitionKeys.filter(
|
||||
(_, subPartitionKeyIndexToRemove) => subPartitionKeyIndex !== subPartitionKeyIndexToRemove,
|
||||
);
|
||||
setSubPartitionKeys(updatedSubPartitionKeys);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
<Stack className="panelGroupSpacing">
|
||||
<DefaultButton
|
||||
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
||||
hidden={useHashV1}
|
||||
disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
|
||||
onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])}
|
||||
>
|
||||
Add hierarchical partition key
|
||||
</DefaultButton>
|
||||
{subPartitionKeys.length > 0 && (
|
||||
<Text variant="small">
|
||||
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to partition your
|
||||
data with up to three levels of keys for better data distribution. Requires .NET V3, Java V4 SDK, or preview
|
||||
JavaScript V3 SDK.{" "}
|
||||
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
|
||||
Learn more
|
||||
</Link>
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Checkbox, Stack } from "@fluentui/react";
|
||||
import { ThroughputInput } from "Explorer/Controls/ThroughputInput/ThroughputInput";
|
||||
import { isFreeTierAccount } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import React from "react";
|
||||
import { getCollectionName } from "Utils/APITypeUtils";
|
||||
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
||||
|
||||
export interface AddMVThroughputComponentProps {
|
||||
enableDedicatedThroughput: boolean;
|
||||
setEnabledDedicatedThroughput: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isSelectedSourceContainerSharedThroughput: () => boolean;
|
||||
showCollectionThroughputInput: () => boolean;
|
||||
materializedViewThroughputOnChange: (materializedViewThroughputValue: number) => void;
|
||||
isMaterializedViewAutoscaleOnChange: (isMaterializedViewAutoscaleValue: boolean) => void;
|
||||
setIsThroughputCapExceeded: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isCostAknowledgedOnChange: (isCostAknowledgedValue: boolean) => void;
|
||||
}
|
||||
|
||||
export const AddMVThroughputComponent = (props: AddMVThroughputComponentProps): JSX.Element => {
|
||||
const {
|
||||
enableDedicatedThroughput,
|
||||
setEnabledDedicatedThroughput,
|
||||
isSelectedSourceContainerSharedThroughput,
|
||||
showCollectionThroughputInput,
|
||||
materializedViewThroughputOnChange,
|
||||
isMaterializedViewAutoscaleOnChange,
|
||||
setIsThroughputCapExceeded,
|
||||
isCostAknowledgedOnChange,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
{!isServerlessAccount() && isSelectedSourceContainerSharedThroughput() && (
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<Checkbox
|
||||
label={`Provision dedicated throughput for this ${getCollectionName().toLocaleLowerCase()}`}
|
||||
checked={enableDedicatedThroughput}
|
||||
styles={{
|
||||
text: { fontSize: 12 },
|
||||
checkbox: { width: 12, height: 12 },
|
||||
label: { padding: 0, alignItems: "center" },
|
||||
}}
|
||||
onChange={(_, isChecked: boolean) => setEnabledDedicatedThroughput(isChecked)}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{showCollectionThroughputInput() && (
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !useDatabases.getState().isFirstResourceCreated()}
|
||||
isDatabase={false}
|
||||
isSharded={false}
|
||||
isFreeTier={isFreeTierAccount()}
|
||||
isQuickstart={false}
|
||||
setThroughputValue={(throughput: number) => {
|
||||
materializedViewThroughputOnChange(throughput);
|
||||
}}
|
||||
setIsAutoscale={(isAutoscale: boolean) => {
|
||||
isMaterializedViewAutoscaleOnChange(isAutoscale);
|
||||
}}
|
||||
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) => {
|
||||
setIsThroughputCapExceeded(isThroughputCapExceeded);
|
||||
}}
|
||||
onCostAcknowledgeChange={(isAcknowledged: boolean) => {
|
||||
isCostAknowledgedOnChange(isAcknowledged);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
import { ActionButton, IconButton, Stack } from "@fluentui/react";
|
||||
import { UniqueKeysHeader } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||
import React from "react";
|
||||
import { userContext } from "UserContext";
|
||||
|
||||
export interface AddMVUniqueKeysComponentProps {
|
||||
uniqueKeys: string[];
|
||||
setUniqueKeys: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
}
|
||||
|
||||
export const AddMVUniqueKeysComponent = (props: AddMVUniqueKeysComponentProps): JSX.Element => {
|
||||
const { uniqueKeys, setUniqueKeys } = props;
|
||||
|
||||
const updateUniqueKeysOnChange = (value: string, uniqueKeyToReplaceIndex: number): void => {
|
||||
const updatedUniqueKeys = uniqueKeys.map((uniqueKey: string, uniqueKeyIndex: number) => {
|
||||
if (uniqueKeyToReplaceIndex === uniqueKeyIndex) {
|
||||
return value;
|
||||
}
|
||||
return uniqueKey;
|
||||
});
|
||||
setUniqueKeys(updatedUniqueKeys);
|
||||
};
|
||||
|
||||
const deleteUniqueKeyOnClick = (uniqueKeyToDeleteIndex: number): void => {
|
||||
const updatedUniqueKeys = uniqueKeys.filter((_, uniqueKeyIndex) => uniqueKeyToDeleteIndex !== uniqueKeyIndex);
|
||||
setUniqueKeys(updatedUniqueKeys);
|
||||
};
|
||||
|
||||
const addUniqueKeyOnClick = (): void => {
|
||||
setUniqueKeys([...uniqueKeys, ""]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
{UniqueKeysHeader()}
|
||||
|
||||
{uniqueKeys.map((uniqueKey: string, uniqueKeyIndex: number): JSX.Element => {
|
||||
return (
|
||||
<Stack style={{ marginBottom: 8 }} key={`uniqueKey-${uniqueKeyIndex}`} horizontal>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder={
|
||||
userContext.apiType === "Mongo"
|
||||
? "Comma separated paths e.g. firstName,address.zipCode"
|
||||
: "Comma separated paths e.g. /firstName,/address/zipCode"
|
||||
}
|
||||
className="panelTextField"
|
||||
autoFocus
|
||||
value={uniqueKey}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateUniqueKeysOnChange(event.target.value, uniqueKeyIndex);
|
||||
}}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Delete" }}
|
||||
style={{ height: 27 }}
|
||||
onClick={() => {
|
||||
deleteUniqueKeyOnClick(uniqueKeyIndex);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
|
||||
<ActionButton
|
||||
iconProps={{ iconName: "Add" }}
|
||||
styles={{ root: { padding: 0 }, label: { fontSize: 12 } }}
|
||||
onClick={() => {
|
||||
addUniqueKeyOnClick();
|
||||
}}
|
||||
>
|
||||
Add unique key
|
||||
</ActionButton>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Stack } from "@fluentui/react";
|
||||
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
||||
import {
|
||||
ContainerVectorPolicyTooltipContent,
|
||||
scrollToSection,
|
||||
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||
import React from "react";
|
||||
|
||||
export interface AddMVVectorSearchComponentProps {
|
||||
vectorEmbeddingPolicy: VectorEmbedding[];
|
||||
setVectorEmbeddingPolicy: React.Dispatch<React.SetStateAction<VectorEmbedding[]>>;
|
||||
vectorIndexingPolicy: VectorIndex[];
|
||||
setVectorIndexingPolicy: React.Dispatch<React.SetStateAction<VectorIndex[]>>;
|
||||
setVectorPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const AddMVVectorSearchComponent = (props: AddMVVectorSearchComponentProps): JSX.Element => {
|
||||
const {
|
||||
vectorEmbeddingPolicy,
|
||||
setVectorEmbeddingPolicy,
|
||||
vectorIndexingPolicy,
|
||||
setVectorIndexingPolicy,
|
||||
setVectorPolicyValidated,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<CollapsibleSectionComponent
|
||||
title="Container Vector Policy"
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
scrollToSection("collapsibleVectorPolicySectionContent");
|
||||
}}
|
||||
tooltipContent={ContainerVectorPolicyTooltipContent()}
|
||||
>
|
||||
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
||||
<VectorEmbeddingPoliciesComponent
|
||||
vectorEmbeddings={vectorEmbeddingPolicy}
|
||||
vectorIndexes={vectorIndexingPolicy}
|
||||
onVectorEmbeddingChange={(
|
||||
vectorEmbeddingPolicy: VectorEmbedding[],
|
||||
vectorIndexingPolicy: VectorIndex[],
|
||||
vectorPolicyValidated: boolean,
|
||||
) => {
|
||||
setVectorEmbeddingPolicy(vectorEmbeddingPolicy);
|
||||
setVectorIndexingPolicy(vectorIndexingPolicy);
|
||||
setVectorPolicyValidated(vectorPolicyValidated);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CollapsibleSectionComponent>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { shallow, ShallowWrapper } from "enzyme";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import {
|
||||
AddMaterializedViewPanel,
|
||||
AddMaterializedViewPanelProps,
|
||||
} from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel";
|
||||
import React, { Component } from "react";
|
||||
|
||||
const props: AddMaterializedViewPanelProps = {
|
||||
explorer: new Explorer(),
|
||||
};
|
||||
|
||||
describe("AddMaterializedViewPanel", () => {
|
||||
it("render default panel", () => {
|
||||
const wrapper: ShallowWrapper<AddMaterializedViewPanelProps, object, Component> = shallow(
|
||||
<AddMaterializedViewPanel {...props} />,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render form", () => {
|
||||
const wrapper: ShallowWrapper<AddMaterializedViewPanelProps, object, Component> = shallow(
|
||||
<AddMaterializedViewPanel {...props} />,
|
||||
);
|
||||
const form = wrapper.find("form").first();
|
||||
expect(form).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,430 @@
|
||||
import {
|
||||
DirectionalHint,
|
||||
Dropdown,
|
||||
DropdownMenuItemType,
|
||||
Icon,
|
||||
IDropdownOption,
|
||||
Link,
|
||||
Separator,
|
||||
Stack,
|
||||
Text,
|
||||
TooltipHost,
|
||||
} from "@fluentui/react";
|
||||
import * as Constants from "Common/Constants";
|
||||
import { createMaterializedView } from "Common/dataAccess/createMaterializedView";
|
||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||
import * as DataModels from "Contracts/DataModels";
|
||||
import { FullTextIndex, FullTextPolicy, VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||
import { Collection, Database } from "Contracts/ViewModels";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import {
|
||||
AllPropertiesIndexed,
|
||||
FullTextPolicyDefault,
|
||||
getPartitionKey,
|
||||
isSynapseLinkEnabled,
|
||||
parseUniqueKeys,
|
||||
scrollToSection,
|
||||
shouldShowAnalyticalStoreOptions,
|
||||
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||
import {
|
||||
chooseSourceContainerStyle,
|
||||
chooseSourceContainerStyles,
|
||||
} from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanelStyles";
|
||||
import { AddMVAdvancedComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVAdvancedComponent";
|
||||
import { AddMVAnalyticalStoreComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVAnalyticalStoreComponent";
|
||||
import { AddMVFullTextSearchComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVFullTextSearchComponent";
|
||||
import { AddMVPartitionKeyComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVPartitionKeyComponent";
|
||||
import { AddMVThroughputComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVThroughputComponent";
|
||||
import { AddMVUniqueKeysComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVUniqueKeysComponent";
|
||||
import { AddMVVectorSearchComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVVectorSearchComponent";
|
||||
import { PanelFooterComponent } from "Explorer/Panes/PanelFooterComponent";
|
||||
import { PanelInfoErrorComponent } from "Explorer/Panes/PanelInfoErrorComponent";
|
||||
import { PanelLoadingScreen } from "Explorer/Panes/PanelLoadingScreen";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { CollectionCreation } from "Shared/Constants";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "UserContext";
|
||||
import { isFullTextSearchEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
|
||||
export interface AddMaterializedViewPanelProps {
|
||||
explorer: Explorer;
|
||||
sourceContainer?: Collection;
|
||||
}
|
||||
export const AddMaterializedViewPanel = (props: AddMaterializedViewPanelProps): JSX.Element => {
|
||||
const { explorer, sourceContainer } = props;
|
||||
|
||||
const [sourceContainerOptions, setSourceContainerOptions] = useState<IDropdownOption[]>();
|
||||
const [selectedSourceContainer, setSelectedSourceContainer] = useState<Collection>(sourceContainer);
|
||||
const [materializedViewId, setMaterializedViewId] = useState<string>();
|
||||
const [definition, setDefinition] = useState<string>();
|
||||
const [partitionKey, setPartitionKey] = useState<string>(getPartitionKey());
|
||||
const [subPartitionKeys, setSubPartitionKeys] = useState<string[]>([]);
|
||||
const [useHashV1, setUseHashV1] = useState<boolean>();
|
||||
const [enableDedicatedThroughput, setEnabledDedicatedThroughput] = useState<boolean>();
|
||||
const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState<boolean>();
|
||||
const [uniqueKeys, setUniqueKeys] = useState<string[]>([]);
|
||||
const [enableAnalyticalStore, setEnableAnalyticalStore] = useState<boolean>();
|
||||
const [vectorEmbeddingPolicy, setVectorEmbeddingPolicy] = useState<VectorEmbedding[]>();
|
||||
const [vectorIndexingPolicy, setVectorIndexingPolicy] = useState<VectorIndex[]>();
|
||||
const [vectorPolicyValidated, setVectorPolicyValidated] = useState<boolean>();
|
||||
const [fullTextPolicy, setFullTextPolicy] = useState<FullTextPolicy>(FullTextPolicyDefault);
|
||||
const [fullTextIndexes, setFullTextIndexes] = useState<FullTextIndex[]>();
|
||||
const [fullTextPolicyValidated, setFullTextPolicyValidated] = useState<boolean>();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
const [showErrorDetails, setShowErrorDetails] = useState<boolean>();
|
||||
const [isExecuting, setIsExecuting] = useState<boolean>();
|
||||
|
||||
useEffect(() => {
|
||||
const sourceContainerOptions: IDropdownOption[] = [];
|
||||
useDatabases.getState().databases.forEach((database: Database) => {
|
||||
sourceContainerOptions.push({
|
||||
key: database.rid,
|
||||
text: database.id(),
|
||||
itemType: DropdownMenuItemType.Header,
|
||||
});
|
||||
|
||||
database.collections().forEach((collection: Collection) => {
|
||||
const isMaterializedView: boolean = !!collection.materializedViewDefinition();
|
||||
sourceContainerOptions.push({
|
||||
key: collection.rid,
|
||||
text: collection.id(),
|
||||
disabled: isMaterializedView,
|
||||
...(isMaterializedView && {
|
||||
title: "This is a materialized view.",
|
||||
}),
|
||||
data: collection,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
setSourceContainerOptions(sourceContainerOptions);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToSection("panelContainer");
|
||||
}, [errorMessage]);
|
||||
|
||||
let materializedViewThroughput: number;
|
||||
let isMaterializedViewAutoscale: boolean;
|
||||
let isCostAcknowledged: boolean;
|
||||
|
||||
const materializedViewThroughputOnChange = (materializedViewThroughputValue: number): void => {
|
||||
materializedViewThroughput = materializedViewThroughputValue;
|
||||
};
|
||||
|
||||
const isMaterializedViewAutoscaleOnChange = (isMaterializedViewAutoscaleValue: boolean): void => {
|
||||
isMaterializedViewAutoscale = isMaterializedViewAutoscaleValue;
|
||||
};
|
||||
|
||||
const isCostAknowledgedOnChange = (isCostAcknowledgedValue: boolean): void => {
|
||||
isCostAcknowledged = isCostAcknowledgedValue;
|
||||
};
|
||||
|
||||
const isSelectedSourceContainerSharedThroughput = (): boolean => {
|
||||
if (!selectedSourceContainer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!selectedSourceContainer.getDatabase().offer();
|
||||
};
|
||||
|
||||
const showCollectionThroughputInput = (): boolean => {
|
||||
if (isServerlessAccount()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (enableDedicatedThroughput) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !!selectedSourceContainer && !isSelectedSourceContainerSharedThroughput();
|
||||
};
|
||||
|
||||
const showVectorSearchParameters = (): boolean => {
|
||||
return isVectorSearchEnabled() && (isServerlessAccount() || showCollectionThroughputInput());
|
||||
};
|
||||
|
||||
const showFullTextSearchParameters = (): boolean => {
|
||||
return isFullTextSearchEnabled() && (isServerlessAccount() || showCollectionThroughputInput());
|
||||
};
|
||||
|
||||
const getAnalyticalStorageTtl = (): number => {
|
||||
if (!isSynapseLinkEnabled()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!shouldShowAnalyticalStoreOptions()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (enableAnalyticalStore) {
|
||||
// TODO: always default to 90 days once the backend hotfix is deployed
|
||||
return userContext.features.ttl90Days
|
||||
? Constants.AnalyticalStorageTtl.Days90
|
||||
: Constants.AnalyticalStorageTtl.Infinite;
|
||||
}
|
||||
|
||||
return Constants.AnalyticalStorageTtl.Disabled;
|
||||
};
|
||||
|
||||
const validateInputs = (): boolean => {
|
||||
if (!selectedSourceContainer) {
|
||||
setErrorMessage("Please select a source container");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (materializedViewThroughput > CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
|
||||
const errorMessage = isMaterializedViewAutoscale
|
||||
? "Please acknowledge the estimated monthly spend."
|
||||
: "Please acknowledge the estimated daily spend.";
|
||||
setErrorMessage(errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (materializedViewThroughput > CollectionCreation.MaxRUPerPartition) {
|
||||
setErrorMessage("Unsharded collections support up to 10,000 RUs");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (showVectorSearchParameters()) {
|
||||
if (!vectorPolicyValidated) {
|
||||
setErrorMessage("Please fix errors in container vector policy");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!fullTextPolicyValidated) {
|
||||
setErrorMessage("Please fix errors in container full text search policy");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const submit = async (event?: React.FormEvent<HTMLFormElement>): Promise<void> => {
|
||||
event?.preventDefault();
|
||||
|
||||
if (!validateInputs()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const materializedViewIdTrimmed: string = materializedViewId.trim();
|
||||
|
||||
const materializedViewDefinition: DataModels.MaterializedViewDefinition = {
|
||||
sourceCollectionId: sourceContainer.id(),
|
||||
definition: definition,
|
||||
};
|
||||
|
||||
const partitionKeyTrimmed: string = partitionKey.trim();
|
||||
|
||||
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = parseUniqueKeys(uniqueKeys);
|
||||
const partitionKeyVersion = useHashV1 ? undefined : 2;
|
||||
const partitionKeyPaths: DataModels.PartitionKey = partitionKeyTrimmed
|
||||
? {
|
||||
paths: [
|
||||
partitionKeyTrimmed,
|
||||
...(userContext.apiType === "SQL" && subPartitionKeys.length > 0 ? subPartitionKeys : []),
|
||||
],
|
||||
kind: userContext.apiType === "SQL" && subPartitionKeys.length > 0 ? "MultiHash" : "Hash",
|
||||
version: partitionKeyVersion,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const indexingPolicy: DataModels.IndexingPolicy = AllPropertiesIndexed;
|
||||
let vectorEmbeddingPolicyFinal: DataModels.VectorEmbeddingPolicy;
|
||||
|
||||
if (showVectorSearchParameters()) {
|
||||
indexingPolicy.vectorIndexes = vectorIndexingPolicy;
|
||||
vectorEmbeddingPolicyFinal = {
|
||||
vectorEmbeddings: vectorEmbeddingPolicy,
|
||||
};
|
||||
}
|
||||
|
||||
if (showFullTextSearchParameters()) {
|
||||
indexingPolicy.fullTextIndexes = fullTextIndexes;
|
||||
}
|
||||
|
||||
const telemetryData: TelemetryProcessor.TelemetryData = {
|
||||
database: {
|
||||
id: selectedSourceContainer.databaseId,
|
||||
shared: isSelectedSourceContainerSharedThroughput(),
|
||||
},
|
||||
collection: {
|
||||
id: materializedViewIdTrimmed,
|
||||
throughput: materializedViewThroughput,
|
||||
isAutoscale: isMaterializedViewAutoscale,
|
||||
partitionKeyPaths,
|
||||
uniqueKeyPolicy,
|
||||
collectionWithDedicatedThroughput: enableDedicatedThroughput,
|
||||
},
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
};
|
||||
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, telemetryData);
|
||||
const databaseLevelThroughput: boolean = isSelectedSourceContainerSharedThroughput() && !enableDedicatedThroughput;
|
||||
|
||||
let offerThroughput: number;
|
||||
let autoPilotMaxThroughput: number;
|
||||
|
||||
if (!databaseLevelThroughput) {
|
||||
if (isMaterializedViewAutoscale) {
|
||||
autoPilotMaxThroughput = materializedViewThroughput;
|
||||
} else {
|
||||
offerThroughput = materializedViewThroughput;
|
||||
}
|
||||
}
|
||||
|
||||
const createMaterializedViewParams: DataModels.CreateMaterializedViewsParams = {
|
||||
materializedViewId: materializedViewIdTrimmed,
|
||||
materializedViewDefinition: materializedViewDefinition,
|
||||
databaseId: selectedSourceContainer.databaseId,
|
||||
databaseLevelThroughput: databaseLevelThroughput,
|
||||
offerThroughput: offerThroughput,
|
||||
autoPilotMaxThroughput: autoPilotMaxThroughput,
|
||||
analyticalStorageTtl: getAnalyticalStorageTtl(),
|
||||
indexingPolicy: indexingPolicy,
|
||||
partitionKey: partitionKeyPaths,
|
||||
uniqueKeyPolicy: uniqueKeyPolicy,
|
||||
vectorEmbeddingPolicy: vectorEmbeddingPolicyFinal,
|
||||
fullTextPolicy: fullTextPolicy,
|
||||
};
|
||||
|
||||
setIsExecuting(true);
|
||||
|
||||
try {
|
||||
await createMaterializedView(createMaterializedViewParams);
|
||||
await explorer.refreshAllDatabases();
|
||||
TelemetryProcessor.traceSuccess(Action.CreateMaterializedView, telemetryData, startKey);
|
||||
useSidePanel.getState().closeSidePanel();
|
||||
} catch (error) {
|
||||
const errorMessage: string = getErrorMessage(error);
|
||||
setErrorMessage(errorMessage);
|
||||
setShowErrorDetails(true);
|
||||
const failureTelemetryData = { ...telemetryData, error: errorMessage, errorStack: getErrorStack(error) };
|
||||
TelemetryProcessor.traceFailure(Action.CreateMaterializedView, failureTelemetryData, startKey);
|
||||
} finally {
|
||||
setIsExecuting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="panelFormWrapper" id="panelMaterializedView" onSubmit={submit}>
|
||||
{errorMessage && (
|
||||
<PanelInfoErrorComponent message={errorMessage} messageType="error" showErrorDetails={showErrorDetails} />
|
||||
)}
|
||||
<div className="panelMainContent">
|
||||
<Stack>
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Source container id
|
||||
</Text>
|
||||
</Stack>
|
||||
<Dropdown
|
||||
placeholder="Choose source container"
|
||||
options={sourceContainerOptions}
|
||||
defaultSelectedKey={sourceContainer?.rid}
|
||||
styles={chooseSourceContainerStyles()}
|
||||
style={chooseSourceContainerStyle()}
|
||||
onChange={(_, options: IDropdownOption) => setSelectedSourceContainer(options.data as Collection)}
|
||||
/>
|
||||
<Separator className="panelSeparator" />
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
View container id
|
||||
</Text>
|
||||
</Stack>
|
||||
<input
|
||||
id="materializedViewId"
|
||||
type="text"
|
||||
aria-required
|
||||
required
|
||||
autoComplete="off"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
placeholder={`e.g., viewByEmailId`}
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
value={materializedViewId}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setMaterializedViewId(event.target.value)}
|
||||
/>
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Materialized View Definition
|
||||
</Text>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={
|
||||
<Link
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
|
||||
target="blank"
|
||||
>
|
||||
Learn more about defining materialized views.
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
<Icon role="button" iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
<input
|
||||
id="materializedViewDefinition"
|
||||
type="text"
|
||||
aria-required
|
||||
required
|
||||
autoComplete="off"
|
||||
placeholder={"SELECT c.email, c.accountId FROM c"}
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
value={definition || ""}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setDefinition(event.target.value)}
|
||||
/>
|
||||
<AddMVPartitionKeyComponent
|
||||
{...{ partitionKey, setPartitionKey, subPartitionKeys, setSubPartitionKeys, useHashV1 }}
|
||||
/>
|
||||
<AddMVThroughputComponent
|
||||
{...{
|
||||
enableDedicatedThroughput,
|
||||
setEnabledDedicatedThroughput,
|
||||
isSelectedSourceContainerSharedThroughput,
|
||||
showCollectionThroughputInput,
|
||||
materializedViewThroughputOnChange,
|
||||
isMaterializedViewAutoscaleOnChange,
|
||||
setIsThroughputCapExceeded,
|
||||
isCostAknowledgedOnChange,
|
||||
}}
|
||||
/>
|
||||
<AddMVUniqueKeysComponent {...{ uniqueKeys, setUniqueKeys }} />
|
||||
{shouldShowAnalyticalStoreOptions() && (
|
||||
<AddMVAnalyticalStoreComponent {...{ explorer, enableAnalyticalStore, setEnableAnalyticalStore }} />
|
||||
)}
|
||||
{showVectorSearchParameters() && (
|
||||
<AddMVVectorSearchComponent
|
||||
{...{
|
||||
vectorEmbeddingPolicy,
|
||||
setVectorEmbeddingPolicy,
|
||||
vectorIndexingPolicy,
|
||||
setVectorIndexingPolicy,
|
||||
vectorPolicyValidated,
|
||||
setVectorPolicyValidated,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showFullTextSearchParameters() && (
|
||||
<AddMVFullTextSearchComponent
|
||||
{...{ fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated }}
|
||||
/>
|
||||
)}
|
||||
<AddMVAdvancedComponent {...{ useHashV1, setUseHashV1, setSubPartitionKeys }} />
|
||||
</Stack>
|
||||
</div>
|
||||
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={isThroughputCapExceeded} />
|
||||
{isExecuting && <PanelLoadingScreen />}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { IDropdownStyleProps, IDropdownStyles, IStyleFunctionOrObject } from "@fluentui/react";
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export function chooseSourceContainerStyles(): IStyleFunctionOrObject<IDropdownStyleProps, IDropdownStyles> {
|
||||
return {
|
||||
title: { height: 27, lineHeight: 27 },
|
||||
dropdownItem: { fontSize: 12 },
|
||||
dropdownItemDisabled: { fontSize: 12 },
|
||||
dropdownItemSelected: { fontSize: 12 },
|
||||
};
|
||||
}
|
||||
|
||||
export function chooseSourceContainerStyle(): CSSProperties {
|
||||
return { width: 300, fontSize: 12 };
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AddMaterializedViewPanel render default panel 1`] = `
|
||||
<form
|
||||
className="panelFormWrapper"
|
||||
id="panelMaterializedView"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="panelMainContent"
|
||||
>
|
||||
<Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<span
|
||||
className="mandatoryStar"
|
||||
>
|
||||
*
|
||||
</span>
|
||||
<Text
|
||||
className="panelTextBold"
|
||||
variant="small"
|
||||
>
|
||||
Source container id
|
||||
</Text>
|
||||
</Stack>
|
||||
<Dropdown
|
||||
onChange={[Function]}
|
||||
placeholder="Choose source container"
|
||||
style={
|
||||
{
|
||||
"fontSize": 12,
|
||||
"width": 300,
|
||||
}
|
||||
}
|
||||
styles={
|
||||
{
|
||||
"dropdownItem": {
|
||||
"fontSize": 12,
|
||||
},
|
||||
"dropdownItemDisabled": {
|
||||
"fontSize": 12,
|
||||
},
|
||||
"dropdownItemSelected": {
|
||||
"fontSize": 12,
|
||||
},
|
||||
"title": {
|
||||
"height": 27,
|
||||
"lineHeight": 27,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Separator
|
||||
className="panelSeparator"
|
||||
/>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<span
|
||||
className="mandatoryStar"
|
||||
>
|
||||
*
|
||||
</span>
|
||||
<Text
|
||||
className="panelTextBold"
|
||||
variant="small"
|
||||
>
|
||||
View container id
|
||||
</Text>
|
||||
</Stack>
|
||||
<input
|
||||
aria-required={true}
|
||||
autoComplete="off"
|
||||
className="panelTextField"
|
||||
id="materializedViewId"
|
||||
onChange={[Function]}
|
||||
pattern="[^/?#\\\\]*[^/?# \\\\]"
|
||||
placeholder="e.g., viewByEmailId"
|
||||
required={true}
|
||||
size={40}
|
||||
title="May not end with space nor contain characters '\\' '/' '#' '?'"
|
||||
type="text"
|
||||
/>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<span
|
||||
className="mandatoryStar"
|
||||
>
|
||||
*
|
||||
</span>
|
||||
<Text
|
||||
className="panelTextBold"
|
||||
variant="small"
|
||||
>
|
||||
Materialized View Definition
|
||||
</Text>
|
||||
<StyledTooltipHostBase
|
||||
content={
|
||||
<StyledLinkBase
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
|
||||
target="blank"
|
||||
>
|
||||
Learn more about defining materialized views.
|
||||
</StyledLinkBase>
|
||||
}
|
||||
directionalHint={4}
|
||||
>
|
||||
<Icon
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
</Stack>
|
||||
<input
|
||||
aria-required={true}
|
||||
autoComplete="off"
|
||||
className="panelTextField"
|
||||
id="materializedViewDefinition"
|
||||
onChange={[Function]}
|
||||
placeholder="SELECT c.email, c.accountId FROM c"
|
||||
required={true}
|
||||
size={40}
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<AddMVPartitionKeyComponent
|
||||
partitionKey=""
|
||||
setPartitionKey={[Function]}
|
||||
setSubPartitionKeys={[Function]}
|
||||
subPartitionKeys={[]}
|
||||
/>
|
||||
<AddMVThroughputComponent
|
||||
isCostAknowledgedOnChange={[Function]}
|
||||
isMaterializedViewAutoscaleOnChange={[Function]}
|
||||
isSelectedSourceContainerSharedThroughput={[Function]}
|
||||
materializedViewThroughputOnChange={[Function]}
|
||||
setEnabledDedicatedThroughput={[Function]}
|
||||
setIsThroughputCapExceeded={[Function]}
|
||||
showCollectionThroughputInput={[Function]}
|
||||
/>
|
||||
<AddMVUniqueKeysComponent
|
||||
setUniqueKeys={[Function]}
|
||||
uniqueKeys={[]}
|
||||
/>
|
||||
<AddMVAnalyticalStoreComponent
|
||||
explorer={
|
||||
Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isTabsContentExpanded": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"phoenixClient": PhoenixClient {
|
||||
"armResourceId": undefined,
|
||||
"retryOptions": {
|
||||
"maxTimeout": 5000,
|
||||
"minTimeout": 5000,
|
||||
"retries": 3,
|
||||
},
|
||||
},
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
},
|
||||
"refreshNotebookList": [Function],
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"parameters": [Function],
|
||||
},
|
||||
}
|
||||
}
|
||||
setEnableAnalyticalStore={[Function]}
|
||||
/>
|
||||
<AddMVAdvancedComponent
|
||||
setSubPartitionKeys={[Function]}
|
||||
setUseHashV1={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
<PanelFooterComponent
|
||||
buttonLabel="OK"
|
||||
/>
|
||||
</form>
|
||||
`;
|
||||
@@ -94,6 +94,7 @@
|
||||
padding-left: @MediumSpace;
|
||||
|
||||
.paneErrorLink {
|
||||
color: @LinkColor;
|
||||
cursor: pointer;
|
||||
font-size: @mediumFontSize;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
} from "Shared/StorageUtility";
|
||||
import * as StringUtility from "Shared/StringUtility";
|
||||
import { updateUserContext, userContext } from "UserContext";
|
||||
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
||||
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
||||
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
||||
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
||||
@@ -174,15 +175,26 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
const styles = useStyles();
|
||||
|
||||
const explorerVersion = configContext.gitSha;
|
||||
const isEmulator = configContext.platform === Platform.Emulator;
|
||||
const shouldShowQueryPageOptions = userContext.apiType === "SQL";
|
||||
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin";
|
||||
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
|
||||
const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
|
||||
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled();
|
||||
const showRetrySettings =
|
||||
(userContext.apiType === "SQL" || userContext.apiType === "Tables" || userContext.apiType === "Gremlin") &&
|
||||
!isEmulator;
|
||||
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin" && !isEmulator;
|
||||
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin" && !isEmulator;
|
||||
const shouldShowParallelismOption = userContext.apiType !== "Gremlin" && !isEmulator;
|
||||
const showEnableEntraIdRbac =
|
||||
isDataplaneRbacSupported(userContext.apiType) &&
|
||||
userContext.authType === AuthType.AAD &&
|
||||
configContext.platform !== Platform.Fabric &&
|
||||
!isEmulator;
|
||||
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled() && !isEmulator;
|
||||
const shouldShowCopilotSampleDBOption =
|
||||
userContext.apiType === "SQL" &&
|
||||
useQueryCopilot.getState().copilotEnabled &&
|
||||
useDatabases.getState().sampleDataResourceTokenCollection;
|
||||
useDatabases.getState().sampleDataResourceTokenCollection &&
|
||||
!isEmulator;
|
||||
|
||||
const handlerOnSubmit = async () => {
|
||||
setIsExecuting(true);
|
||||
|
||||
@@ -193,6 +205,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
|
||||
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
|
||||
|
||||
if (
|
||||
enableDataPlaneRBACOption !== LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled) ||
|
||||
retryAttempts !== LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts) ||
|
||||
retryInterval !== LocalStorageUtility.getEntryNumber(StorageKey.RetryInterval) ||
|
||||
MaxWaitTimeInSeconds !== LocalStorageUtility.getEntryNumber(StorageKey.MaxWaitTimeInSeconds)
|
||||
) {
|
||||
updateUserContext({
|
||||
refreshCosmosClient: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (configContext.platform !== Platform.Fabric) {
|
||||
LocalStorageUtility.setEntryString(StorageKey.DataPlaneRbacEnabled, enableDataPlaneRBACOption);
|
||||
if (
|
||||
@@ -202,7 +225,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
) {
|
||||
updateUserContext({
|
||||
dataPlaneRbacEnabled: true,
|
||||
hasDataPlaneRbacSettingChanged: true,
|
||||
});
|
||||
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
|
||||
try {
|
||||
@@ -226,7 +248,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
} else {
|
||||
updateUserContext({
|
||||
dataPlaneRbacEnabled: false,
|
||||
hasDataPlaneRbacSettingChanged: true,
|
||||
});
|
||||
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
|
||||
if (!userContext.features.enableAadDataPlane && !userContext.masterKey) {
|
||||
@@ -482,7 +503,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
return (
|
||||
<RightPaneForm {...genericPaneProps}>
|
||||
<div className={`paneMainContent ${styles.container}`}>
|
||||
<Accordion className={styles.firstItem}>
|
||||
<Accordion className={`customAccordion ${styles.firstItem}`}>
|
||||
{shouldShowQueryPageOptions && (
|
||||
<AccordionItem value="1">
|
||||
<AccordionHeader>
|
||||
@@ -532,40 +553,37 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
{userContext.apiType === "SQL" &&
|
||||
userContext.authType === AuthType.AAD &&
|
||||
configContext.platform !== Platform.Fabric && (
|
||||
<AccordionItem value="2">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enable Entra ID RBAC</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra
|
||||
ID RBAC.
|
||||
<a
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{" "}
|
||||
Learn more{" "}
|
||||
</a>
|
||||
</div>
|
||||
<ChoiceGroup
|
||||
ariaLabelledBy="enableDataPlaneRBACOptions"
|
||||
options={dataPlaneRBACOptionsList}
|
||||
styles={choiceButtonStyles}
|
||||
selectedKey={enableDataPlaneRBACOption}
|
||||
onChange={handleOnDataPlaneRBACOptionChange}
|
||||
/>
|
||||
{showEnableEntraIdRbac && (
|
||||
<AccordionItem value="2">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enable Entra ID RBAC</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra ID
|
||||
RBAC.
|
||||
<a
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{" "}
|
||||
Learn more{" "}
|
||||
</a>
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{userContext.apiType === "SQL" && (
|
||||
<ChoiceGroup
|
||||
ariaLabelledBy="enableDataPlaneRBACOptions"
|
||||
options={dataPlaneRBACOptionsList}
|
||||
styles={choiceButtonStyles}
|
||||
selectedKey={enableDataPlaneRBACOption}
|
||||
onChange={handleOnDataPlaneRBACOptionChange}
|
||||
/>
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
{userContext.apiType === "SQL" && !isEmulator && (
|
||||
<>
|
||||
<AccordionItem value="3">
|
||||
<AccordionHeader>
|
||||
@@ -663,102 +681,103 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
<AccordionItem value="6">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Retry Settings</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Retry policy associated with throttled requests during CosmosDB queries.
|
||||
{showRetrySettings && (
|
||||
<AccordionItem value="6">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Retry Settings</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Retry policy associated with throttled requests during CosmosDB queries.
|
||||
</div>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Max retry attempts</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Max number of retries to be performed for a request. Default value 9.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<SpinButton
|
||||
labelPosition={Position.top}
|
||||
min={1}
|
||||
step={1}
|
||||
value={"" + retryAttempts}
|
||||
onChange={handleOnQueryRetryAttemptsSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1"
|
||||
decrementButtonAriaLabel="Decrease value by 1"
|
||||
onIncrement={(newValue) => setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)}
|
||||
onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)}
|
||||
onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Fixed retry interval (ms)</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned
|
||||
as part of the response. Default value is 0 milliseconds.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<SpinButton
|
||||
labelPosition={Position.top}
|
||||
min={1000}
|
||||
step={1000}
|
||||
value={"" + retryInterval}
|
||||
onChange={handleOnRetryIntervalSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1000"
|
||||
decrementButtonAriaLabel="Decrease value by 1000"
|
||||
onIncrement={(newValue) => setRetryInterval(parseInt(newValue) + 1000 || retryInterval)}
|
||||
onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)}
|
||||
onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Max wait time (s)</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Max wait time in seconds to wait for a request while the retries are happening. Default value 30
|
||||
seconds.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<SpinButton
|
||||
labelPosition={Position.top}
|
||||
min={1}
|
||||
step={1}
|
||||
value={"" + MaxWaitTimeInSeconds}
|
||||
onChange={handleOnMaxWaitTimeSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1"
|
||||
decrementButtonAriaLabel="Decrease value by 1"
|
||||
onIncrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)}
|
||||
onDecrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds)}
|
||||
onValidate={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) || MaxWaitTimeInSeconds)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Max retry attempts</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Max number of retries to be performed for a request. Default value 9.
|
||||
</InfoTooltip>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
{!isEmulator && (
|
||||
<AccordionItem value="7">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enable container pagination</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
||||
</div>
|
||||
<Checkbox
|
||||
styles={{
|
||||
label: { padding: 0 },
|
||||
}}
|
||||
className="padding"
|
||||
ariaLabel="Enable container pagination"
|
||||
checked={containerPaginationEnabled}
|
||||
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
|
||||
label="Enable container pagination"
|
||||
/>
|
||||
</div>
|
||||
<SpinButton
|
||||
labelPosition={Position.top}
|
||||
min={1}
|
||||
step={1}
|
||||
value={"" + retryAttempts}
|
||||
onChange={handleOnQueryRetryAttemptsSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1"
|
||||
decrementButtonAriaLabel="Decrease value by 1"
|
||||
onIncrement={(newValue) => setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)}
|
||||
onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)}
|
||||
onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Fixed retry interval (ms)</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned as
|
||||
part of the response. Default value is 0 milliseconds.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<SpinButton
|
||||
labelPosition={Position.top}
|
||||
min={1000}
|
||||
step={1000}
|
||||
value={"" + retryInterval}
|
||||
onChange={handleOnRetryIntervalSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1000"
|
||||
decrementButtonAriaLabel="Decrease value by 1000"
|
||||
onIncrement={(newValue) => setRetryInterval(parseInt(newValue) + 1000 || retryInterval)}
|
||||
onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)}
|
||||
onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Max wait time (s)</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Max wait time in seconds to wait for a request while the retries are happening. Default value 30
|
||||
seconds.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<SpinButton
|
||||
labelPosition={Position.top}
|
||||
min={1}
|
||||
step={1}
|
||||
value={"" + MaxWaitTimeInSeconds}
|
||||
onChange={handleOnMaxWaitTimeSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1"
|
||||
decrementButtonAriaLabel="Decrease value by 1"
|
||||
onIncrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)}
|
||||
onDecrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds)}
|
||||
onValidate={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) || MaxWaitTimeInSeconds)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="7">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enable container pagination</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
||||
</div>
|
||||
<Checkbox
|
||||
styles={{
|
||||
label: { padding: 0 },
|
||||
}}
|
||||
className="padding"
|
||||
ariaLabel="Enable container pagination"
|
||||
checked={containerPaginationEnabled}
|
||||
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
|
||||
label="Enable container pagination"
|
||||
/>
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
{shouldShowCrossPartitionOption && (
|
||||
<AccordionItem value="8">
|
||||
<AccordionHeader>
|
||||
@@ -784,7 +803,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{shouldShowParallelismOption && (
|
||||
<AccordionItem value="9">
|
||||
<AccordionHeader>
|
||||
@@ -818,7 +836,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{shouldShowPriorityLevelOption && (
|
||||
<AccordionItem value="10">
|
||||
<AccordionHeader>
|
||||
@@ -842,7 +859,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{shouldShowGraphAutoVizOption && (
|
||||
<AccordionItem value="11">
|
||||
<AccordionHeader>
|
||||
@@ -864,7 +880,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
|
||||
{shouldShowCopilotSampleDBOption && (
|
||||
<AccordionItem value="12">
|
||||
<AccordionHeader>
|
||||
|
||||
@@ -11,7 +11,7 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
className="paneMainContent ___133e6fg_0000000 f22iagw f1vx9l62 f1l02sjl"
|
||||
>
|
||||
<Accordion
|
||||
className="___1uf6361_0000000 fz7g6wx"
|
||||
className="customAccordion ___1uf6361_0000000 fz7g6wx"
|
||||
>
|
||||
<AccordionItem
|
||||
value="1"
|
||||
@@ -572,7 +572,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
||||
className="paneMainContent ___133e6fg_0000000 f22iagw f1vx9l62 f1l02sjl"
|
||||
>
|
||||
<Accordion
|
||||
className="___1uf6361_0000000 fz7g6wx"
|
||||
className="customAccordion ___1uf6361_0000000 fz7g6wx"
|
||||
>
|
||||
<AccordionItem
|
||||
value="6"
|
||||
|
||||
@@ -1,300 +0,0 @@
|
||||
import {
|
||||
DefaultButton,
|
||||
Dropdown,
|
||||
IDropdownOption,
|
||||
IStyleFunctionOrObject,
|
||||
ITextFieldStyleProps,
|
||||
ITextFieldStyles,
|
||||
IconButton,
|
||||
Label,
|
||||
Stack,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import {
|
||||
getDataTypeOptions,
|
||||
getDistanceFunctionOptions,
|
||||
getIndexTypeOptions,
|
||||
} from "Explorer/Panes/VectorSearchPanel/VectorSearchUtils";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
|
||||
export interface IAddVectorEmbeddingPolicyFormProps {
|
||||
vectorEmbedding: VectorEmbedding[];
|
||||
vectorIndex: VectorIndex[];
|
||||
onVectorEmbeddingChange: (
|
||||
vectorEmbeddings: VectorEmbedding[],
|
||||
vectorIndexingPolicies: VectorIndex[],
|
||||
validationPassed: boolean,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface VectorEmbeddingPolicyData {
|
||||
path: string;
|
||||
dataType: VectorEmbedding["dataType"];
|
||||
distanceFunction: VectorEmbedding["distanceFunction"];
|
||||
dimensions: number;
|
||||
indexType: VectorIndex["type"] | "none";
|
||||
pathError: string;
|
||||
dimensionsError: string;
|
||||
}
|
||||
|
||||
type VectorEmbeddingPolicyProperty = "dataType" | "distanceFunction" | "indexType";
|
||||
|
||||
const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldStyles> = {
|
||||
fieldGroup: {
|
||||
height: 27,
|
||||
},
|
||||
field: {
|
||||
fontSize: 12,
|
||||
padding: "0 8px",
|
||||
},
|
||||
};
|
||||
|
||||
const dropdownStyles = {
|
||||
title: {
|
||||
height: 27,
|
||||
lineHeight: "24px",
|
||||
fontSize: 12,
|
||||
},
|
||||
dropdown: {
|
||||
height: 27,
|
||||
lineHeight: "24px",
|
||||
},
|
||||
dropdownItem: {
|
||||
fontSize: 12,
|
||||
},
|
||||
};
|
||||
|
||||
export const AddVectorEmbeddingPolicyForm: FunctionComponent<IAddVectorEmbeddingPolicyFormProps> = ({
|
||||
vectorEmbedding,
|
||||
vectorIndex,
|
||||
onVectorEmbeddingChange,
|
||||
}): JSX.Element => {
|
||||
const onVectorEmbeddingPathError = (path: string, index?: number): string => {
|
||||
let error = "";
|
||||
if (!path) {
|
||||
error = "Vector embedding path should not be empty";
|
||||
}
|
||||
if (
|
||||
index >= 0 &&
|
||||
vectorEmbeddingPolicyData?.find(
|
||||
(vectorEmbedding: VectorEmbeddingPolicyData, dataIndex: number) =>
|
||||
dataIndex !== index && vectorEmbedding.path === path,
|
||||
)
|
||||
) {
|
||||
error = "Vector embedding path is already defined";
|
||||
}
|
||||
return error;
|
||||
};
|
||||
|
||||
const onVectorEmbeddingDimensionError = (dimension: number, indexType: VectorIndex["type"] | "none"): string => {
|
||||
let error = "";
|
||||
if (dimension <= 0 || dimension > 4096) {
|
||||
error = "Vector embedding dimension must be greater than 0 and less than or equal 4096";
|
||||
}
|
||||
if (indexType === "flat" && dimension > 505) {
|
||||
error = "Maximum allowed dimension for flat index is 505";
|
||||
}
|
||||
return error;
|
||||
};
|
||||
|
||||
const initializeData = (vectorEmbedding: VectorEmbedding[], vectorIndex: VectorIndex[]) => {
|
||||
const mergedData: VectorEmbeddingPolicyData[] = [];
|
||||
vectorEmbedding.forEach((embedding) => {
|
||||
const matchingIndex = vectorIndex.find((index) => index.path === embedding.path);
|
||||
mergedData.push({
|
||||
...embedding,
|
||||
indexType: matchingIndex?.type || "none",
|
||||
pathError: onVectorEmbeddingPathError(embedding.path),
|
||||
dimensionsError: onVectorEmbeddingDimensionError(embedding.dimensions, matchingIndex?.type || "none"),
|
||||
});
|
||||
});
|
||||
return mergedData;
|
||||
};
|
||||
|
||||
const [vectorEmbeddingPolicyData, setVectorEmbeddingPolicyData] = useState<VectorEmbeddingPolicyData[]>(
|
||||
initializeData(vectorEmbedding, vectorIndex),
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
propagateData();
|
||||
}, [vectorEmbeddingPolicyData]);
|
||||
|
||||
const propagateData = () => {
|
||||
const vectorEmbeddings: VectorEmbedding[] = vectorEmbeddingPolicyData.map((policy: VectorEmbeddingPolicyData) => ({
|
||||
dataType: policy.dataType,
|
||||
dimensions: policy.dimensions,
|
||||
distanceFunction: policy.distanceFunction,
|
||||
path: policy.path,
|
||||
}));
|
||||
const vectorIndexingPolicies: VectorIndex[] = vectorEmbeddingPolicyData
|
||||
.filter((policy: VectorEmbeddingPolicyData) => policy.indexType !== "none")
|
||||
.map(
|
||||
(policy) =>
|
||||
({
|
||||
path: policy.path,
|
||||
type: policy.indexType,
|
||||
}) as VectorIndex,
|
||||
);
|
||||
const validationPassed = vectorEmbeddingPolicyData.every(
|
||||
(policy: VectorEmbeddingPolicyData) => policy.pathError === "" && policy.dimensionsError === "",
|
||||
);
|
||||
onVectorEmbeddingChange(vectorEmbeddings, vectorIndexingPolicies, validationPassed);
|
||||
};
|
||||
|
||||
const onVectorEmbeddingPathChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value.trim();
|
||||
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||
if (!vectorEmbeddings[index]?.path && !value.startsWith("/")) {
|
||||
vectorEmbeddings[index].path = "/" + value;
|
||||
} else {
|
||||
vectorEmbeddings[index].path = value;
|
||||
}
|
||||
const error = onVectorEmbeddingPathError(value, index);
|
||||
vectorEmbeddings[index].pathError = error;
|
||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
};
|
||||
|
||||
const onVectorEmbeddingDimensionsChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseInt(event.target.value.trim()) || 0;
|
||||
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||
const vectorEmbedding = vectorEmbeddings[index];
|
||||
vectorEmbeddings[index].dimensions = value;
|
||||
const error = onVectorEmbeddingDimensionError(value, vectorEmbedding.indexType);
|
||||
vectorEmbeddings[index].dimensionsError = error;
|
||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
};
|
||||
|
||||
const onVectorEmbeddingIndexTypeChange = (index: number, option: IDropdownOption): void => {
|
||||
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||
const vectorEmbedding = vectorEmbeddings[index];
|
||||
vectorEmbeddings[index].indexType = option.key as never;
|
||||
const error = onVectorEmbeddingDimensionError(vectorEmbedding.dimensions, vectorEmbedding.indexType);
|
||||
vectorEmbeddings[index].dimensionsError = error;
|
||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
};
|
||||
|
||||
const onVectorEmbeddingPolicyChange = (
|
||||
index: number,
|
||||
option: IDropdownOption,
|
||||
property: VectorEmbeddingPolicyProperty,
|
||||
): void => {
|
||||
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||
vectorEmbeddings[index][property] = option.key as never;
|
||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
};
|
||||
|
||||
const onAdd = () => {
|
||||
setVectorEmbeddingPolicyData([
|
||||
...vectorEmbeddingPolicyData,
|
||||
{
|
||||
path: "",
|
||||
dataType: "float32",
|
||||
distanceFunction: "euclidean",
|
||||
dimensions: 0,
|
||||
indexType: "none",
|
||||
pathError: onVectorEmbeddingPathError(""),
|
||||
dimensionsError: onVectorEmbeddingDimensionError(0, "none"),
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const onDelete = (index: number) => {
|
||||
const vectorEmbeddings = vectorEmbeddingPolicyData.filter((_uniqueKey, j) => index !== j);
|
||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: 4 }}>
|
||||
{vectorEmbeddingPolicyData.length > 0 &&
|
||||
vectorEmbeddingPolicyData.map((vectorEmbeddingPolicy: VectorEmbeddingPolicyData, index: number) => (
|
||||
<CollapsibleSectionComponent key={index} isExpandedByDefault={true} title={`Vector embedding ${index + 1}`}>
|
||||
<Stack horizontal tokens={{ childrenGap: 4 }}>
|
||||
<Stack
|
||||
styles={{
|
||||
root: {
|
||||
margin: "0 0 6px 20px !important",
|
||||
paddingLeft: 20,
|
||||
width: "80%",
|
||||
borderLeft: "1px solid",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack>
|
||||
<Label styles={{ root: { fontSize: 12 } }}>Path</Label>
|
||||
<TextField
|
||||
id={`vector-policy-path-${index + 1}`}
|
||||
required={true}
|
||||
placeholder="/vector1"
|
||||
styles={textFieldStyles}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onVectorEmbeddingPathChange(index, event)}
|
||||
value={vectorEmbeddingPolicy.path || ""}
|
||||
errorMessage={vectorEmbeddingPolicy.pathError}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Label styles={{ root: { fontSize: 12 } }}>Data type</Label>
|
||||
<Dropdown
|
||||
required={true}
|
||||
styles={dropdownStyles}
|
||||
options={getDataTypeOptions()}
|
||||
selectedKey={vectorEmbeddingPolicy.dataType}
|
||||
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||
onVectorEmbeddingPolicyChange(index, option, "dataType")
|
||||
}
|
||||
></Dropdown>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Label styles={{ root: { fontSize: 12 } }}>Distance function</Label>
|
||||
<Dropdown
|
||||
required={true}
|
||||
styles={dropdownStyles}
|
||||
options={getDistanceFunctionOptions()}
|
||||
selectedKey={vectorEmbeddingPolicy.distanceFunction}
|
||||
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||
onVectorEmbeddingPolicyChange(index, option, "distanceFunction")
|
||||
}
|
||||
></Dropdown>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Label styles={{ root: { fontSize: 12 } }}>Dimensions</Label>
|
||||
<TextField
|
||||
id={`vector-policy-dimension-${index + 1}`}
|
||||
required={true}
|
||||
styles={textFieldStyles}
|
||||
value={String(vectorEmbeddingPolicy.dimensions || 0)}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onVectorEmbeddingDimensionsChange(index, event)
|
||||
}
|
||||
errorMessage={vectorEmbeddingPolicy.dimensionsError}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Label styles={{ root: { fontSize: 12 } }}>Index type</Label>
|
||||
<Dropdown
|
||||
required={true}
|
||||
styles={dropdownStyles}
|
||||
options={getIndexTypeOptions()}
|
||||
selectedKey={vectorEmbeddingPolicy.indexType}
|
||||
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||
onVectorEmbeddingIndexTypeChange(index, option)
|
||||
}
|
||||
></Dropdown>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<IconButton
|
||||
id={`delete-vector-policy-${index + 1}`}
|
||||
iconProps={{ iconName: "Delete" }}
|
||||
style={{ height: 27, margin: "auto" }}
|
||||
onClick={() => onDelete(index)}
|
||||
/>
|
||||
</Stack>
|
||||
</CollapsibleSectionComponent>
|
||||
))}
|
||||
<DefaultButton id={`add-vector-policy`} styles={{ root: { maxWidth: 170, fontSize: 12 } }} onClick={onAdd}>
|
||||
Add vector embedding
|
||||
</DefaultButton>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -18,7 +18,7 @@ import { createCollection } from "Common/dataAccess/createCollection";
|
||||
import * as DataModels from "Contracts/DataModels";
|
||||
import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel";
|
||||
import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||
import { PromptCard } from "Explorer/QueryCopilot/PromptCard";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { useCarousel } from "hooks/useCarousel";
|
||||
|
||||
@@ -79,9 +79,13 @@ export const QueryCopilotFeedbackModal = ({
|
||||
readOnly
|
||||
/>
|
||||
<Text style={{ fontSize: 12, marginBottom: 14 }}>
|
||||
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the{" "}
|
||||
Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to
|
||||
improve your and your organization’s experience with this product. If you have any questions about the use
|
||||
of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the
|
||||
Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the
|
||||
feedback you submit is considered Personal Data under that addendum. Please see the{" "}
|
||||
{
|
||||
<Link href="https://privacy.microsoft.com/privacystatement" target="_blank">
|
||||
<Link href="https://go.microsoft.com/fwlink/?LinkId=521839" target="_blank">
|
||||
Privacy statement
|
||||
</Link>
|
||||
}{" "}
|
||||
|
||||
@@ -99,10 +99,10 @@ exports[`Query Copilot Feedback Modal snapshot test shoud render and match snaps
|
||||
}
|
||||
}
|
||||
>
|
||||
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
|
||||
Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the
|
||||
|
||||
<StyledLinkBase
|
||||
href="https://privacy.microsoft.com/privacystatement"
|
||||
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
||||
target="_blank"
|
||||
>
|
||||
Privacy statement
|
||||
@@ -236,10 +236,10 @@ exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`]
|
||||
}
|
||||
}
|
||||
>
|
||||
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
|
||||
Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the
|
||||
|
||||
<StyledLinkBase
|
||||
href="https://privacy.microsoft.com/privacystatement"
|
||||
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
||||
target="_blank"
|
||||
>
|
||||
Privacy statement
|
||||
@@ -373,10 +373,10 @@ exports[`Query Copilot Feedback Modal snapshot test should close on cancel click
|
||||
}
|
||||
}
|
||||
>
|
||||
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
|
||||
Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the
|
||||
|
||||
<StyledLinkBase
|
||||
href="https://privacy.microsoft.com/privacystatement"
|
||||
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
||||
target="_blank"
|
||||
>
|
||||
Privacy statement
|
||||
@@ -510,10 +510,10 @@ exports[`Query Copilot Feedback Modal snapshot test should get user unput 1`] =
|
||||
}
|
||||
}
|
||||
>
|
||||
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
|
||||
Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the
|
||||
|
||||
<StyledLinkBase
|
||||
href="https://privacy.microsoft.com/privacystatement"
|
||||
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
||||
target="_blank"
|
||||
>
|
||||
Privacy statement
|
||||
@@ -647,10 +647,10 @@ exports[`Query Copilot Feedback Modal snapshot test should not render dont show
|
||||
}
|
||||
}
|
||||
>
|
||||
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
|
||||
Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the
|
||||
|
||||
<StyledLinkBase
|
||||
href="https://privacy.microsoft.com/privacystatement"
|
||||
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
||||
target="_blank"
|
||||
>
|
||||
Privacy statement
|
||||
@@ -784,10 +784,10 @@ exports[`Query Copilot Feedback Modal snapshot test should render dont show agai
|
||||
}
|
||||
}
|
||||
>
|
||||
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
|
||||
Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the
|
||||
|
||||
<StyledLinkBase
|
||||
href="https://privacy.microsoft.com/privacystatement"
|
||||
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
||||
target="_blank"
|
||||
>
|
||||
Privacy statement
|
||||
@@ -936,10 +936,10 @@ exports[`Query Copilot Feedback Modal snapshot test should submit submission 1`]
|
||||
}
|
||||
}
|
||||
>
|
||||
By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
|
||||
Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. Please see the
|
||||
|
||||
<StyledLinkBase
|
||||
href="https://privacy.microsoft.com/privacystatement"
|
||||
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
||||
target="_blank"
|
||||
>
|
||||
Privacy statement
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user