Compare commits

..

3 Commits

Author SHA1 Message Date
Sung-Hyun Kang
0b7cbfbf23 move nps survey open dialog call to after explorer initialization 2024-08-13 13:06:01 -05:00
Sung-Hyun Kang
76913d42f1 Merge branch 'master' into NPS_Dialog 2024-08-13 12:44:32 -05:00
Sung-Hyun Kang
7ab2bd38f4 move nps survey open dialog call to after explorer initialization 2024-08-13 12:34:46 -05:00
183 changed files with 7481 additions and 11726 deletions

View File

@@ -1,16 +0,0 @@
FROM mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm
# Install pre-reqs for gyp, and 'canvas' npm module
RUN apt-get update && \
apt-get install -y \
make \
gcc \
g++ \
python3-minimal \
libcairo2-dev \
libpango1.0-dev \
&& \
rm -rf /var/lib/apt/lists/*
# Install node-gyp to build native modules
RUN npm install -g node-gyp

View File

@@ -1,32 +0,0 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
{
"name": "Azure Cosmos DB Explorer",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"build": {
"dockerfile": "Dockerfile"
},
"onCreateCommand": ".devcontainer/oncreate",
"features": {
"ghcr.io/devcontainers/features/azure-cli:1": {
"version": "latest"
},
"ghcr.io/devcontainers/features/github-cli:1": {
"installDirectlyFromGitHubRelease": true,
"version": "latest"
},
"ghcr.io/devcontainers/features/sshd:1": {
"version": "latest"
}
}
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "yarn install",
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env bash
# Install packages once, to prime the node_modules directory.
npm ci

View File

@@ -1 +1 @@
[Preview this branch](https://dataexplorer-preview.azurewebsites.net/pull/EDIT_THIS_NUMBER_IN_THE_PR_DESCRIPTION?feature.someFeatureFlagYouMightNeed=true) [Preview this branch](https://cosmos-explorer-preview.azurewebsites.net/pull/EDIT_THIS_NUMBER_IN_THE_PR_DESCRIPTION?feature.someFeatureFlagYouMightNeed=true)

View File

@@ -83,7 +83,7 @@ jobs:
- run: npm ci - run: npm ci
- run: npm run build:contracts - run: npm run build:contracts
- name: Restore Build Cache - name: Restore Build Cache
uses: actions/cache@v4 uses: actions/cache@v2
with: with:
path: .cache path: .cache
key: ${{ runner.os }}-build-cache key: ${{ runner.os }}-build-cache
@@ -92,20 +92,18 @@ jobs:
NODE_OPTIONS: "--max-old-space-size=4096" NODE_OPTIONS: "--max-old-space-size=4096"
- run: cp -r ./Contracts ./dist/contracts - run: cp -r ./Contracts ./dist/contracts
- run: cp -r ./configs ./dist/configs - run: cp -r ./configs ./dist/configs
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v3
with: with:
name: dist name: dist
path: 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 - name: Upload build to preview blob storage
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 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 }}
- name: Upload preview config to blob storage - name: Upload preview config to blob storage
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 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 }}
nuget: nuget:
name: Publish Nuget name: Publish Nuget
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
@@ -115,21 +113,21 @@ jobs:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }} AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
steps: steps:
- uses: nuget/setup-nuget@v1
with:
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
- name: Download Dist Folder - name: Download Dist Folder
uses: actions/download-artifact@v4 uses: actions/download-artifact@v3
with: with:
name: dist name: dist
- run: cp ./configs/prod.json config.json - run: cp ./configs/prod.json config.json
- run: dotnet nuget add source "$NUGET_SOURCE" --name "ADO" --username "jawelton@microsoft.com" --password "$AZURE_DEVOPS_PAT" --store-password-in-clear-text - run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
- run: dotnet pack DataExplorer.proj /p:PackageVersion="2.0.0-github-${GITHUB_SHA}" - run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
- run: dotnet nuget push "bin/Release/*.nupkg" --skip-duplicate --api-key Az --source="$NUGET_SOURCE" - run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
- run: dotnet nuget remove source "ADO" - uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4 name: packages
name: Upload package to Artifacts
with: with:
name: prod-package path: "*.nupkg"
path: "bin/Release/*.nupkg"
nugetmpac: nugetmpac:
name: Publish Nuget MPAC name: Publish Nuget MPAC
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
@@ -139,21 +137,22 @@ jobs:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }} AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
steps: steps:
- uses: nuget/setup-nuget@v1
with:
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
- name: Download Dist Folder - name: Download Dist Folder
uses: actions/download-artifact@v4 uses: actions/download-artifact@v3
with: with:
name: dist name: dist
- run: cp ./configs/mpac.json config.json - 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: sed -i 's/Azure.Cosmos.DB.Data.Explorer/Azure.Cosmos.DB.Data.Explorer.MPAC/g' DataExplorer.nuspec
- run: dotnet nuget add source "$NUGET_SOURCE" --name "ADO" --username "jawelton@microsoft.com" --password "$AZURE_DEVOPS_PAT" --store-password-in-clear-text - run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
- run: dotnet pack DataExplorer.proj /p:PackageVersion="2.0.0-github-${GITHUB_SHA}" - run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
- run: dotnet nuget push "bin/Release/*.nupkg" --skip-duplicate --api-key Az --source="$NUGET_SOURCE" - run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
- run: dotnet nuget remove source "ADO" - uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4 name: packages
name: Upload package to Artifacts
with: with:
name: mpac-package path: "*.nupkg"
path: "bin/Release/*.nupkg"
playwright-tests: playwright-tests:
name: "Run Playwright Tests (Shard ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }})" name: "Run Playwright Tests (Shard ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }})"

View File

@@ -1,9 +0,0 @@
<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>

View File

@@ -2,7 +2,6 @@
UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), https://cosmos.azure.com/, and the [Cosmos DB Emulator](https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator) UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), https://cosmos.azure.com/, and the [Cosmos DB Emulator](https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator)
![](https://sdkctlstore.blob.core.windows.net/exe/dataexplorer.gif) ![](https://sdkctlstore.blob.core.windows.net/exe/dataexplorer.gif)
## Getting Started ## Getting Started
@@ -19,7 +18,7 @@ Run `npm start` to start the development server and automatically rebuild on cha
### Hosted Development (https://cosmos.azure.com) ### Hosted Development (https://cosmos.azure.com)
- Visit: `https://localhost:1234/hostedExplorer.html` - Visit: `https://localhost:1234/hostedExplorer.html`
- The default webpack dev server configuration will proxy requests to the production portal backend: `https://cdb-ms-mpac-pbe.cosmos.azure.com`. This will allow you to use production connection strings on your local machine. - The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine.
### Emulator Development ### Emulator Development

View File

@@ -82,7 +82,7 @@
</a> </a>
<ul> <ul>
<li>Visit: <code>https://localhost:1234/hostedExplorer.html</code></li> <li>Visit: <code>https://localhost:1234/hostedExplorer.html</code></li>
<li>The default webpack dev server configuration will proxy requests to the production portal backend: <code>https://cdb-ms-mpac-pbe.cosmos.azure.com</code>. This will allow you to use production connection strings on your local machine.</li> <li>The default webpack dev server configuration will proxy requests to the production portal backend: <code>https://main.documentdb.ext.azure.com</code>. This will allow you to use production connection strings on your local machine.</li>
</ul> </ul>
<a href="#emulator-development" id="emulator-development" style="color: inherit; text-decoration: none;"> <a href="#emulator-development" id="emulator-development" style="color: inherit; text-decoration: none;">
<h3>Emulator Development</h3> <h3>Emulator Development</h3>

View File

@@ -174,11 +174,7 @@ module.exports = {
}, },
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
transformIgnorePatterns: [ transformIgnorePatterns: ["/node_modules/", "/externals/"],
"/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 // 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, // unmockedModulePathPatterns: undefined,

View File

@@ -1830,14 +1830,6 @@ input::-webkit-calendar-picker-indicator::after {
transform: rotate(90deg); transform: rotate(90deg);
} }
.customAccordion button:focus {
.focus();
}
.customAccordion {
margin-top: 1px;
}
.datalist-arrow:after:hover { .datalist-arrow:after:hover {
content: "\276F"; content: "\276F";
position: absolute; position: absolute;
@@ -1914,14 +1906,8 @@ input::-webkit-calendar-picker-indicator::after {
} }
.nav-tabs-margin { .nav-tabs-margin {
height: 32px; padding-top: 5px;
background-color: #f2f2f2; background-color: #f2f2f2;
.nav-tabs {
display: flex;
align-items: flex-end;
height: 100%;
}
} }
.navTabHeight { .navTabHeight {
@@ -2366,8 +2352,8 @@ a:link {
.tabsManagerContainer { .tabsManagerContainer {
height: 100%; height: 100%;
display: flex; display: grid;
flex-direction: column; grid-template-rows: 36px 36px 1fr;
min-width: 0; // This prevents it to grow past the parent's width if its content is too wide min-width: 0; // This prevents it to grow past the parent's width if its content is too wide
} }
@@ -2624,8 +2610,9 @@ a:link {
} }
.tabPanesContainer { .tabPanesContainer {
grid-row: span 2; // Fill the remaining space
display: flex; display: flex;
flex-grow: 1; height: 100%;
overflow: hidden; overflow: hidden;
} }
@@ -3125,7 +3112,3 @@ a:link {
background: white; background: white;
height: 100%; height: 100%;
} }
.sidebarContainer {
height: 100%;
}

View File

@@ -20,10 +20,6 @@ a:focus {
text-decoration: underline; text-decoration: underline;
} }
.splashLoaderContainer {
background-color: #f5f5f5;
}
#divExplorer { #divExplorer {
background-color: #f5f5f5; background-color: #f5f5f5;
} }
@@ -31,24 +27,26 @@ a:focus {
.resourceTreeAndTabs { .resourceTreeAndTabs {
border-radius: 0px; border-radius: 0px;
box-shadow: @FabricBoxBorderShadow; box-shadow: @FabricBoxBorderShadow;
margin: @FabricBoxMargin;
margin-top: 0px; margin-top: 0px;
margin-bottom: 0px; margin-bottom: 0px;
background-color: #ffffff; background-color: #ffffff;
} }
.tabsManagerContainer { .tabsManagerContainer {
background-color: #ffffff; background-color: #ffffff
} }
.nav-tabs-margin { .nav-tabs-margin {
padding-top: 5px; padding-top: 5px;
background-color: #ffffff; background-color: #ffffff
} }
.commandBarContainer { .commandBarContainer {
background-color: #ffffff; background-color: #ffffff;
border-radius: @FabricBoxBorderRadius @FabricBoxBorderRadius 0px 0px; border-radius: @FabricBoxBorderRadius @FabricBoxBorderRadius 0px 0px;
box-shadow: @FabricBoxBorderShadow; box-shadow: @FabricBoxBorderShadow;
margin: @FabricBoxMargin;
margin-top: 0px; margin-top: 0px;
margin-bottom: 0px; margin-bottom: 0px;
padding-top: 2px; padding-top: 2px;
@@ -67,6 +65,7 @@ a:focus {
} }
} }
.nav-tabs>li>.tabNavContentContainer>.tab_Content:hover { .nav-tabs>li>.tabNavContentContainer>.tab_Content:hover {
border-bottom: 2px solid #e0e0e0; border-bottom: 2px solid #e0e0e0;
} }
@@ -120,6 +119,7 @@ a:focus {
} }
} }
.resourceTree { .resourceTree {
padding: 12px; padding: 12px;
} }
@@ -163,14 +163,18 @@ a:focus {
} }
} }
.dataExplorerErrorConsoleContainer { .dataExplorerErrorConsoleContainer {
border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius; border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius;
box-shadow: @FabricBoxBorderShadow; box-shadow: @FabricBoxBorderShadow;
margin: @FabricBoxMargin;
margin-top: 0px; margin-top: 0px;
width: auto; width: auto;
align-self: auto; align-self: auto;
} }
.filterbtnstyle { .filterbtnstyle {
background: #fff; background: #fff;
color: #000; color: #000;
@@ -196,10 +200,12 @@ a:focus {
border: solid 1px #d1d1d1; border: solid 1px #d1d1d1;
} }
.gridRowSelected .tabdocumentsGridElement:hover { .gridRowSelected .tabdocumentsGridElement:hover {
background-color: @FabricAccentLight !important; background-color: @FabricAccentLight !important;
} }
.refreshcol { .refreshcol {
filter: brightness(0) saturate(100%); filter: brightness(0) saturate(100%);
} }

1617
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,9 +5,10 @@
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@azure/arm-cosmosdb": "9.1.0", "@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "4.2.0-beta.1", "@azure/cosmos": "4.0.1-beta.3",
"@azure/cosmos-language-service": "0.0.5", "@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "4.5.0", "@azure/identity": "1.5.2",
"@azure/ms-rest-nodeauth": "3.1.1",
"@azure/msal-browser": "2.14.2", "@azure/msal-browser": "2.14.2",
"@babel/plugin-proposal-class-properties": "7.12.1", "@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-decorators": "7.12.12", "@babel/plugin-proposal-decorators": "7.12.12",
@@ -116,7 +117,7 @@
"@babel/preset-env": "7.24.7", "@babel/preset-env": "7.24.7",
"@babel/preset-react": "7.24.7", "@babel/preset-react": "7.24.7",
"@babel/preset-typescript": "7.24.7", "@babel/preset-typescript": "7.24.7",
"@playwright/test": "1.49.1", "@playwright/test": "1.44.0",
"@testing-library/react": "11.2.3", "@testing-library/react": "11.2.3",
"@types/applicationinsights-js": "1.0.7", "@types/applicationinsights-js": "1.0.7",
"@types/codemirror": "0.0.56", "@types/codemirror": "0.0.56",
@@ -169,10 +170,10 @@
"jest": "29.7.0", "jest": "29.7.0",
"jest-canvas-mock": "2.5.2", "jest-canvas-mock": "2.5.2",
"jest-circus": "29.7.0", "jest-circus": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jest-html-loader": "1.0.0", "jest-html-loader": "1.0.0",
"jest-react-hooks-shallow": "1.5.1", "jest-react-hooks-shallow": "1.5.1",
"jest-trx-results-processor": "3.0.2", "jest-trx-results-processor": "3.0.2",
"jest-environment-jsdom": "29.7.0",
"less": "3.8.1", "less": "3.8.1",
"less-loader": "11.1.3", "less-loader": "11.1.3",
"less-vars-loader": "1.1.0", "less-vars-loader": "1.1.0",

View File

@@ -12,6 +12,7 @@ export default defineConfig({
reporter: process.env.CI ? "blob" : "html", reporter: process.env.CI ? "blob" : "html",
timeout: 10 * 60 * 1000, timeout: 10 * 60 * 1000,
use: { use: {
actionTimeout: 5 * 60 * 1000,
trace: "off", trace: "off",
video: "off", video: "off",
screenshot: "on", screenshot: "on",
@@ -22,8 +23,7 @@ export default defineConfig({
}, },
expect: { expect: {
// Many of our expectations take a little longer than the default 5 seconds. timeout: 5 * 60 * 1000,
timeout: 15 * 1000,
}, },
projects: [ projects: [

View File

@@ -1,7 +1,7 @@
[defaults] [defaults]
group = dataexplorer-preview group = stfaul
sku = P1V2 sku = P1v2
appserviceplan = dataexplorer-preview appserviceplan = stfaul_asp_Linux_centralus_0
location = westus2 location = centralus
web = dataexplorer-preview web = cosmos-explorer-preview

View File

@@ -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: Initial support is for Hosted (Connection string only) or the Azure Portal. Examples:
Connection string URLs: https://dataexplorer-preview.azurewebsites.net/commit/COMMIT_SHA/hostedExplorer.html Connection string URLs: https://cosmos-explorer-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 Portal URLs: https://ms.portal.azure.com/?dataExplorerSource=https://cosmos-explorer-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. In both cases replace `COMMIT_SHA` with the commit you want to view. It must have already completed its build on GitHub Actions.

View File

@@ -1,4 +1,4 @@
{ {
"PROXY_PATH": "/proxy", "PROXY_PATH": "/proxy",
"msalRedirectURI": "https://dataexplorer-preview.azurewebsites.net/" "msalRedirectURI": "https://cosmos-explorer-preview.azurewebsites.net/"
} }

View File

@@ -3,15 +3,8 @@ const { createProxyMiddleware } = require("http-proxy-middleware");
const port = process.env.PORT || 3000; const port = process.env.PORT || 3000;
const fetch = require("node-fetch"); const fetch = require("node-fetch");
const backendEndpoint = "https://cdb-ms-mpac-pbe.cosmos.azure.com"; const api = createProxyMiddleware("/api", {
const previewSiteEndpoint = "https://dataexplorer-preview.azurewebsites.net"; target: "https://main.documentdb.ext.azure.com",
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, changeOrigin: true,
logLevel: "debug", logLevel: "debug",
bypass: (req, res) => { bypass: (req, res) => {
@@ -22,8 +15,8 @@ const api = createProxyMiddleware({
}, },
}); });
const proxy = createProxyMiddleware({ const proxy = createProxyMiddleware("/proxy", {
target: backendEndpoint, target: "https://main.documentdb.ext.azure.com",
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
logLevel: "debug", logLevel: "debug",
@@ -34,38 +27,35 @@ const proxy = createProxyMiddleware({
}, },
}); });
const commit = createProxyMiddleware({ const commit = createProxyMiddleware("/commit", {
target: previewStorageWebsiteEndpoint, target: "https://cosmosexplorerpreview.blob.core.windows.net",
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
logLevel: "debug", logLevel: "debug",
pathRewrite: { "^/commit": "/" }, pathRewrite: { "^/commit": "$web/" },
}); });
const app = express(); const app = express();
app.use("/api", api); app.use(api);
app.use("/proxy", proxy); app.use(proxy);
app.use("/commit", commit); app.use(commit);
app.get("/pull/:pr(\\d+)", (req, res) => { app.get("/pull/:pr(\\d+)", (req, res) => {
const pr = req.params.pr; const pr = req.params.pr;
if (!/^\d+$/.test(pr)) {
return res.status(400).send("Invalid pull request number");
}
const [, query] = req.originalUrl.split("?"); const [, query] = req.originalUrl.split("?");
const search = new URLSearchParams(query); const search = new URLSearchParams(query);
fetch(`${githubApiUrl}/pulls/${pr}`) fetch("https://api.github.com/repos/Azure/cosmos-explorer/pulls/" + pr)
.then((response) => response.json()) .then((response) => response.json())
.then(({ head: { ref, sha } }) => { .then(({ head: { ref, sha } }) => {
const prUrl = new URL(`${githubPullRequestUrl}/${pr}`); const prUrl = new URL("https://github.com/Azure/cosmos-explorer/pull/" + pr);
prUrl.hash = ref; prUrl.hash = ref;
search.set("feature.pr", prUrl.href); search.set("feature.pr", prUrl.href);
const explorer = new URL(`${previewSiteEndpoint}/commit/${sha}/explorer.html`); const explorer = new URL("https://cosmos-explorer-preview.azurewebsites.net/commit/" + sha + "/explorer.html");
explorer.search = search.toString(); explorer.search = search.toString();
const portal = new URL(azurePortalMpacEndpoint); const portal = new URL("https://ms.portal.azure.com/");
portal.searchParams.set("dataExplorerSource", explorer.href); portal.searchParams.set("dataExplorerSource", explorer.href);
return res.redirect(portal.href); return res.redirect(portal.href);
@@ -73,10 +63,12 @@ app.get("/pull/:pr(\\d+)", (req, res) => {
.catch(() => res.sendStatus(500)); .catch(() => res.sendStatus(500));
}); });
app.get("/", (req, res) => { app.get("/", (req, res) => {
fetch(`${githubApiUrl}/branches/master`) fetch("https://api.github.com/repos/Azure/cosmos-explorer/branches/master")
.then((response) => response.json()) .then((response) => response.json())
.then(({ commit: { sha } }) => { .then(({ commit: { sha } }) => {
const explorer = new URL(`${previewSiteEndpoint}/commit/${sha}/hostedExplorer.html`); const explorer = new URL(
"https://cosmos-explorer-preview.azurewebsites.net/commit/" + sha + "/hostedExplorer.html"
);
return res.redirect(explorer.href); return res.redirect(explorer.href);
}) })
.catch(() => res.sendStatus(500)); .catch(() => res.sendStatus(500));

1358
preview/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"deploy": "az webapp up --name \"dataexplorer-preview\" --subscription \"cosmosdb-portalteam-runners\" --resource-group \"dataexplorer-preview\" --runtime \"NODE:18-lts\" --sku P1V2", "deploy": "az webapp up --name \"cosmos-explorer-preview\" --subscription \"cosmosdb-portalteam-generaltest-msft\" --resource-group \"stfaul\"",
"start": "node index.js", "start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
@@ -12,8 +12,7 @@
"author": "Microsoft Corporation", "author": "Microsoft Corporation",
"dependencies": { "dependencies": {
"express": "^4.17.1", "express": "^4.17.1",
"http-proxy-middleware": "^3.0.3", "http-proxy-middleware": "^1.1.0",
"node": "^18.20.6",
"node-fetch": "^2.6.1" "node-fetch": "^2.6.1"
} }
} }

View File

@@ -89,7 +89,6 @@ export class CapabilityNames {
public static readonly EnableMongo: string = "EnableMongo"; public static readonly EnableMongo: string = "EnableMongo";
public static readonly EnableServerless: string = "EnableServerless"; public static readonly EnableServerless: string = "EnableServerless";
public static readonly EnableNoSQLVectorSearch: string = "EnableNoSQLVectorSearch"; public static readonly EnableNoSQLVectorSearch: string = "EnableNoSQLVectorSearch";
public static readonly EnableNoSQLFullTextSearch: string = "EnableNoSQLFullTextSearch";
} }
export enum CapacityMode { export enum CapacityMode {
@@ -97,12 +96,6 @@ export enum CapacityMode {
Serverless = "Serverless", Serverless = "Serverless",
} }
export enum WorkloadType {
Learning = "Learning",
DevelopmentTesting = "Development/Testing",
Production = "Production",
None = "None",
}
// flight names returned from the portal are always lowercase // flight names returned from the portal are always lowercase
export class Flights { export class Flights {
public static readonly SettingsV2 = "settingsv2"; public static readonly SettingsV2 = "settingsv2";
@@ -125,7 +118,6 @@ export class AfecFeatures {
export class TagNames { export class TagNames {
public static defaultExperience: string = "defaultExperience"; public static defaultExperience: string = "defaultExperience";
public static WorkloadType: string = "hidden-workload-type";
} }
export class MongoDBAccounts { export class MongoDBAccounts {
@@ -142,9 +134,6 @@ export class BackendApi {
public static readonly GenerateToken: string = "GenerateToken"; public static readonly GenerateToken: string = "GenerateToken";
public static readonly PortalSettings: string = "PortalSettings"; public static readonly PortalSettings: string = "PortalSettings";
public static readonly AccountRestrictions: string = "AccountRestrictions"; 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 { export class PortalBackendEndpoints {
@@ -156,25 +145,13 @@ export class PortalBackendEndpoints {
} }
export class MongoProxyEndpoints { export class MongoProxyEndpoints {
public static readonly Development: string = "https://localhost:7238"; public static readonly Local: string = "https://localhost:7238";
public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com"; public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com"; public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us"; public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn"; 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 { export class CassandraProxyEndpoints {
public static readonly Development: string = "https://localhost:7240"; public static readonly Development: string = "https://localhost:7240";
public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com"; public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com";
@@ -206,12 +183,6 @@ export class CassandraProxyAPIs {
public static readonly connectionStringSchemaApi: string = "api/connectionstring/cassandra/schema"; public static readonly connectionStringSchemaApi: string = "api/connectionstring/cassandra/schema";
} }
export class AadEndpoints {
public static readonly Prod: string = "https://login.microsoftonline.com/";
public static readonly Fairfax: string = "https://login.microsoftonline.us/";
public static readonly Mooncake: string = "https://login.partner.microsoftonline.cn/";
}
export class Queries { export class Queries {
public static CustomPageOption: string = "custom"; public static CustomPageOption: string = "custom";
public static UnlimitedPageOption: string = "unlimited"; public static UnlimitedPageOption: string = "unlimited";
@@ -313,7 +284,6 @@ export class HttpStatusCodes {
public static readonly Accepted: number = 202; public static readonly Accepted: number = 202;
public static readonly NoContent: number = 204; public static readonly NoContent: number = 204;
public static readonly NotModified: number = 304; public static readonly NotModified: number = 304;
public static readonly BadRequest: number = 400;
public static readonly Unauthorized: number = 401; public static readonly Unauthorized: number = 401;
public static readonly Forbidden: number = 403; public static readonly Forbidden: number = 403;
public static readonly NotFound: number = 404; public static readonly NotFound: number = 404;
@@ -525,12 +495,7 @@ export class PriorityLevel {
public static readonly Default = "low"; public static readonly Default = "low";
} }
export class ariaLabelForLearnMoreLink { export const QueryCopilotSampleDatabaseId = "CopilotSampleDb";
public static readonly AnalyticalStore = "Learn more about analytical store.";
public static readonly AzureSynapseLink = "Learn more about Azure Synapse Link.";
}
export const QueryCopilotSampleDatabaseId = "CopilotSampleDB";
export const QueryCopilotSampleContainerId = "SampleContainer"; export const QueryCopilotSampleContainerId = "SampleContainer";
export const QueryCopilotSampleContainerSchema = { export const QueryCopilotSampleContainerSchema = {

View File

@@ -1,5 +1,4 @@
import { PortalBackendEndpoints } from "Common/Constants"; import { Platform, resetConfigContext, updateConfigContext } from "../ConfigContext";
import { configContext, Platform, resetConfigContext, updateConfigContext } from "../ConfigContext";
import { updateUserContext } from "../UserContext"; import { updateUserContext } from "../UserContext";
import { endpoint, getTokenFromAuthService, requestPlugin } from "./CosmosClient"; import { endpoint, getTokenFromAuthService, requestPlugin } from "./CosmosClient";
@@ -21,22 +20,22 @@ describe("getTokenFromAuthService", () => {
it("builds the correct URL in production", () => { it("builds the correct URL in production", () => {
updateConfigContext({ updateConfigContext({
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod, BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
}); });
getTokenFromAuthService("GET", "dbs", "foo"); getTokenFromAuthService("GET", "dbs", "foo");
expect(window.fetch).toHaveBeenCalledWith( expect(window.fetch).toHaveBeenCalledWith(
`${configContext.PORTAL_BACKEND_ENDPOINT}/api/connectionstring/runtimeproxy/authorizationtokens`, "https://main.documentdb.ext.azure.com/api/guest/runtimeproxy/authorizationTokens",
expect.any(Object), expect.any(Object),
); );
}); });
it("builds the correct URL in dev", () => { it("builds the correct URL in dev", () => {
updateConfigContext({ updateConfigContext({
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Development, BACKEND_ENDPOINT: "https://localhost:1234",
}); });
getTokenFromAuthService("GET", "dbs", "foo"); getTokenFromAuthService("GET", "dbs", "foo");
expect(window.fetch).toHaveBeenCalledWith( expect(window.fetch).toHaveBeenCalledWith(
`${configContext.PORTAL_BACKEND_ENDPOINT}/api/connectionstring/runtimeproxy/authorizationtokens`, "https://localhost:1234/api/guest/runtimeproxy/authorizationTokens",
expect.any(Object), expect.any(Object),
); );
}); });
@@ -79,7 +78,7 @@ describe("requestPlugin", () => {
const next = jest.fn(); const next = jest.fn();
updateConfigContext({ updateConfigContext({
platform: Platform.Hosted, platform: Platform.Hosted,
PORTAL_BACKEND_ENDPOINT: "https://localhost:1234", BACKEND_ENDPOINT: "https://localhost:1234",
PROXY_PATH: "/proxy", PROXY_PATH: "/proxy",
}); });
const headers = {}; const headers = {};

View File

@@ -3,16 +3,15 @@ import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizatio
import { AuthorizationToken } from "Contracts/FabricMessageTypes"; import { AuthorizationToken } from "Contracts/FabricMessageTypes";
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil"; import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { BackendApi, PriorityLevel } from "../Common/Constants"; import { PriorityLevel } from "../Common/Constants";
import * as Logger from "../Common/Logger";
import { Platform, configContext } from "../ConfigContext"; import { Platform, configContext } from "../ConfigContext";
import { updateUserContext, userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils"; import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
import { EmulatorMasterKey, HttpHeaders } from "./Constants"; import { EmulatorMasterKey, HttpHeaders } from "./Constants";
import { getErrorMessage } from "./ErrorHandlingUtils"; import { getErrorMessage } from "./ErrorHandlingUtils";
import * as Logger from "../Common/Logger";
const _global = typeof self === "undefined" ? window : self; const _global = typeof self === "undefined" ? window : self;
@@ -27,7 +26,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
); );
if (!userContext.aadToken) { if (!userContext.aadToken) {
logConsoleError( logConsoleError(
`AAD token does not exist. Please use the "Login for Entra ID" button in the Toolbar prior to performing Entra ID RBAC operations`, `AAD token does not exist. Please click on "Login for Entra ID" button prior to performing Entra ID RBAC operations`,
); );
return null; return null;
} }
@@ -124,37 +123,6 @@ export async function getTokenFromAuthService(
verb: string, verb: string,
resourceType: string, resourceType: string,
resourceId?: string, resourceId?: string,
): Promise<AuthorizationToken> {
if (!useNewPortalBackendEndpoint(BackendApi.RuntimeProxy)) {
return getTokenFromAuthService_ToBeDeprecated(verb, resourceType, resourceId);
}
try {
const host: string = configContext.PORTAL_BACKEND_ENDPOINT;
const response: Response = await _global.fetch(host + "/api/connectionstring/runtimeproxy/authorizationtokens", {
method: "POST",
headers: {
"content-type": "application/json",
"x-ms-encrypted-auth-token": userContext.accessToken,
},
body: JSON.stringify({
verb,
resourceType,
resourceId,
}),
});
const result: AuthorizationToken = await response.json();
return result;
} catch (error) {
logConsoleError(`Failed to get authorization headers for ${resourceType}: ${getErrorMessage(error)}`);
return Promise.reject(error);
}
}
export async function getTokenFromAuthService_ToBeDeprecated(
verb: string,
resourceType: string,
resourceId?: string,
): Promise<AuthorizationToken> { ): Promise<AuthorizationToken> {
try { try {
const host = configContext.BACKEND_ENDPOINT; const host = configContext.BACKEND_ENDPOINT;
@@ -189,19 +157,10 @@ let _client: Cosmos.CosmosClient;
export function client(): Cosmos.CosmosClient { export function client(): Cosmos.CosmosClient {
if (_client) { if (_client) {
if (!userContext.refreshCosmosClient) { if (!userContext.hasDataPlaneRbacSettingChanged) {
return _client; return _client;
} }
_client.dispose();
_client = null;
} }
if (userContext.refreshCosmosClient) {
updateUserContext({
refreshCosmosClient: false,
});
}
let _defaultHeaders: Cosmos.CosmosHeaders = {}; let _defaultHeaders: Cosmos.CosmosHeaders = {};
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] = _defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge; SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;

View File

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

View File

@@ -1,5 +1,3 @@
import { TagNames, WorkloadType } from "Common/Constants";
import { Tags } from "Contracts/DataModels";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
function isVirtualNetworkFilterEnabled() { function isVirtualNetworkFilterEnabled() {
@@ -17,12 +15,3 @@ function isPrivateEndpointConnectionsEnabled() {
export function isPublicInternetAccessAllowed(): boolean { export function isPublicInternetAccessAllowed(): boolean {
return !isVirtualNetworkFilterEnabled() && !isIpRulesEnabled() && !isPrivateEndpointConnectionsEnabled(); 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;
}

View File

@@ -1,3 +0,0 @@
export function getNewDatabaseSharedThroughputDefault(): boolean {
return false;
}

View File

@@ -10,7 +10,6 @@ export interface TableEntityProps {
isEntityValueDisable?: boolean; isEntityValueDisable?: boolean;
entityTimeValue: string; entityTimeValue: string;
entityValueType: string; entityValueType: string;
entityProperty: string;
onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void; onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
onSelectDate: (date: Date | null | undefined) => void; onSelectDate: (date: Date | null | undefined) => void;
onEntityTimeValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void; onEntityTimeValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
@@ -27,7 +26,6 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
onSelectDate, onSelectDate,
isEntityValueDisable, isEntityValueDisable,
onEntityTimeValueChange, onEntityTimeValueChange,
entityProperty,
}: TableEntityProps): JSX.Element => { }: TableEntityProps): JSX.Element => {
if (isEntityTypeDate) { if (isEntityTypeDate) {
return ( return (
@@ -53,10 +51,6 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
} }
return ( return (
<>
<span id={entityProperty} className="screenReaderOnly">
Edit Property {entityProperty} {attributeValueLabel}
</span>
<TextField <TextField
label={entityValueLabel && entityValueLabel} label={entityValueLabel && entityValueLabel}
className="addEntityTextField" className="addEntityTextField"
@@ -65,8 +59,7 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
placeholder={entityValuePlaceholder} placeholder={entityValuePlaceholder}
value={typeof entityValue === "string" ? entityValue : ""} value={typeof entityValue === "string" ? entityValue : ""}
onChange={onEntityValueChange} onChange={onEntityValueChange}
aria-labelledby={entityProperty} ariaLabel={attributeValueLabel}
/> />
</>
); );
}; };

View File

@@ -1,5 +1,3 @@
import { PortalBackendEndpoints } from "Common/Constants";
import { updateConfigContext } from "ConfigContext";
import * as EnvironmentUtility from "./EnvironmentUtility"; import * as EnvironmentUtility from "./EnvironmentUtility";
describe("Environment Utility Test", () => { describe("Environment Utility Test", () => {
@@ -13,18 +11,4 @@ describe("Environment Utility Test", () => {
const expectedResult = "test/"; const expectedResult = "test/";
expect(EnvironmentUtility.normalizeArmEndpoint(uri)).toEqual(expectedResult); expect(EnvironmentUtility.normalizeArmEndpoint(uri)).toEqual(expectedResult);
}); });
it("Detect environment is Mpac", () => {
updateConfigContext({
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac,
});
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Mpac);
});
it("Detect environment is Development", () => {
updateConfigContext({
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Development,
});
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Development);
});
}); });

View File

@@ -1,29 +1,6 @@
import { PortalBackendEndpoints } from "Common/Constants";
import { configContext } from "ConfigContext";
export function normalizeArmEndpoint(uri: string): string { export function normalizeArmEndpoint(uri: string): string {
if (uri && uri.slice(-1) !== "/") { if (uri && uri.slice(-1) !== "/") {
return `${uri}/`; return `${uri}/`;
} }
return uri; return uri;
} }
export enum Environment {
Development = "Development",
Mpac = "MPAC",
Prod = "Prod",
Fairfax = "Fairfax",
Mooncake = "Mooncake",
}
export const getEnvironment = (): Environment => {
const environmentMap: { [key: string]: Environment } = {
[PortalBackendEndpoints.Development]: Environment.Development,
[PortalBackendEndpoints.Mpac]: Environment.Mpac,
[PortalBackendEndpoints.Prod]: Environment.Prod,
[PortalBackendEndpoints.Fairfax]: Environment.Fairfax,
[PortalBackendEndpoints.Mooncake]: Environment.Mooncake,
};
return environmentMap[configContext.PORTAL_BACKEND_ENDPOINT];
};

View File

@@ -53,8 +53,7 @@ const replaceKnownError = (errorMessage: string): string => {
return "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character."; return "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character.";
} else if ( } else if (
errorMessage?.indexOf("The user aborted a request") >= 0 || errorMessage?.indexOf("The user aborted a request") >= 0 ||
errorMessage?.indexOf("The operation was aborted") >= 0 || errorMessage?.indexOf("The operation was aborted") >= 0
errorMessage === "signal is aborted without reason"
) { ) {
return "User aborted query."; return "User aborted query.";
} }

View File

@@ -70,6 +70,7 @@ export function sendMessage(data: any): void {
} }
export function sendReadyMessage(): void { export function sendReadyMessage(): void {
console.log("SENDING READY MESSAGE");
_sendMessage({ _sendMessage({
signature: "pcIframe", signature: "pcIframe",
kind: "ready", kind: "ready",

View File

@@ -1,6 +1,5 @@
import { MongoProxyEndpoints } from "Common/Constants";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { configContext, resetConfigContext, updateConfigContext } from "../ConfigContext"; import { resetConfigContext, updateConfigContext } from "../ConfigContext";
import { DatabaseAccount } from "../Contracts/DataModels"; import { DatabaseAccount } from "../Contracts/DataModels";
import { Collection } from "../Contracts/ViewModels"; import { Collection } from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
@@ -72,8 +71,7 @@ describe("MongoProxyClient", () => {
databaseAccount, databaseAccount,
}); });
updateConfigContext({ updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
globallyEnabledMongoAPIs: [],
}); });
window.fetch = jest.fn().mockImplementation(fetchMock); window.fetch = jest.fn().mockImplementation(fetchMock);
}); });
@@ -84,19 +82,16 @@ describe("MongoProxyClient", () => {
it("builds the correct URL", () => { it("builds the correct URL", () => {
queryDocuments(databaseId, collection, true, "{}"); queryDocuments(databaseId, collection, true, "{}");
expect(window.fetch).toHaveBeenCalledWith( expect(window.fetch).toHaveBeenCalledWith(
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/resourcelist`, "https://main.documentdb.ext.azure.com/api/mongo/explorer/resourcelist?db=testDB&coll=testCollection&resourceUrl=bardbs%2FtestDB%2Fcolls%2FtestCollection%2Fdocs%2F&rid=testCollectionrid&rtype=docs&sid=&rg=&dba=foo&pk=pk",
expect.any(Object), expect.any(Object),
); );
}); });
it("builds the correct proxy URL in development", () => { it("builds the correct proxy URL in development", () => {
updateConfigContext({ updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
globallyEnabledMongoAPIs: [],
});
queryDocuments(databaseId, collection, true, "{}"); queryDocuments(databaseId, collection, true, "{}");
expect(window.fetch).toHaveBeenCalledWith( expect(window.fetch).toHaveBeenCalledWith(
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/resourcelist`, "https://localhost:1234/api/mongo/explorer/resourcelist?db=testDB&coll=testCollection&resourceUrl=bardbs%2FtestDB%2Fcolls%2FtestCollection%2Fdocs%2F&rid=testCollectionrid&rtype=docs&sid=&rg=&dba=foo&pk=pk",
expect.any(Object), expect.any(Object),
); );
}); });
@@ -108,8 +103,7 @@ describe("MongoProxyClient", () => {
databaseAccount, databaseAccount,
}); });
updateConfigContext({ updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
globallyEnabledMongoAPIs: [],
}); });
window.fetch = jest.fn().mockImplementation(fetchMock); window.fetch = jest.fn().mockImplementation(fetchMock);
}); });
@@ -120,19 +114,16 @@ describe("MongoProxyClient", () => {
it("builds the correct URL", () => { it("builds the correct URL", () => {
readDocument(databaseId, collection, documentId); readDocument(databaseId, collection, documentId);
expect(window.fetch).toHaveBeenCalledWith( expect(window.fetch).toHaveBeenCalledWith(
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`, "https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
expect.any(Object), expect.any(Object),
); );
}); });
it("builds the correct proxy URL in development", () => { it("builds the correct proxy URL in development", () => {
updateConfigContext({ updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
globallyEnabledMongoAPIs: [],
});
readDocument(databaseId, collection, documentId); readDocument(databaseId, collection, documentId);
expect(window.fetch).toHaveBeenCalledWith( expect(window.fetch).toHaveBeenCalledWith(
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`, "https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
expect.any(Object), expect.any(Object),
); );
}); });
@@ -144,8 +135,7 @@ describe("MongoProxyClient", () => {
databaseAccount, databaseAccount,
}); });
updateConfigContext({ updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
globallyEnabledMongoAPIs: [],
}); });
window.fetch = jest.fn().mockImplementation(fetchMock); window.fetch = jest.fn().mockImplementation(fetchMock);
}); });
@@ -156,19 +146,16 @@ describe("MongoProxyClient", () => {
it("builds the correct URL", () => { it("builds the correct URL", () => {
readDocument(databaseId, collection, documentId); readDocument(databaseId, collection, documentId);
expect(window.fetch).toHaveBeenCalledWith( expect(window.fetch).toHaveBeenCalledWith(
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`, "https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
expect.any(Object), expect.any(Object),
); );
}); });
it("builds the correct proxy URL in development", () => { it("builds the correct proxy URL in development", () => {
updateConfigContext({ updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
globallyEnabledMongoAPIs: [],
});
readDocument(databaseId, collection, documentId); readDocument(databaseId, collection, documentId);
expect(window.fetch).toHaveBeenCalledWith( expect(window.fetch).toHaveBeenCalledWith(
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`, "https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
expect.any(Object), expect.any(Object),
); );
}); });
@@ -180,8 +167,7 @@ describe("MongoProxyClient", () => {
databaseAccount, databaseAccount,
}); });
updateConfigContext({ updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
globallyEnabledMongoAPIs: [],
}); });
window.fetch = jest.fn().mockImplementation(fetchMock); window.fetch = jest.fn().mockImplementation(fetchMock);
}); });
@@ -192,19 +178,16 @@ describe("MongoProxyClient", () => {
it("builds the correct URL", () => { it("builds the correct URL", () => {
updateDocument(databaseId, collection, documentId, "{}"); updateDocument(databaseId, collection, documentId, "{}");
expect(window.fetch).toHaveBeenCalledWith( expect(window.fetch).toHaveBeenCalledWith(
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`, "https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
expect.any(Object), expect.any(Object),
); );
}); });
it("builds the correct proxy URL in development", () => { it("builds the correct proxy URL in development", () => {
updateConfigContext({ updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
MONGO_BACKEND_ENDPOINT: "https://localhost:1234",
globallyEnabledMongoAPIs: [],
});
updateDocument(databaseId, collection, documentId, "{}"); updateDocument(databaseId, collection, documentId, "{}");
expect(window.fetch).toHaveBeenCalledWith( expect(window.fetch).toHaveBeenCalledWith(
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`, "https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
expect.any(Object), expect.any(Object),
); );
}); });
@@ -216,8 +199,7 @@ describe("MongoProxyClient", () => {
databaseAccount, databaseAccount,
}); });
updateConfigContext({ updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
globallyEnabledMongoAPIs: [],
}); });
window.fetch = jest.fn().mockImplementation(fetchMock); window.fetch = jest.fn().mockImplementation(fetchMock);
}); });
@@ -228,19 +210,16 @@ describe("MongoProxyClient", () => {
it("builds the correct URL", () => { it("builds the correct URL", () => {
deleteDocument(databaseId, collection, documentId); deleteDocument(databaseId, collection, documentId);
expect(window.fetch).toHaveBeenCalledWith( expect(window.fetch).toHaveBeenCalledWith(
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`, "https://main.documentdb.ext.azure.com/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
expect.any(Object), expect.any(Object),
); );
}); });
it("builds the correct proxy URL in development", () => { it("builds the correct proxy URL in development", () => {
updateConfigContext({ updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
globallyEnabledMongoAPIs: [],
});
deleteDocument(databaseId, collection, documentId); deleteDocument(databaseId, collection, documentId);
expect(window.fetch).toHaveBeenCalledWith( expect(window.fetch).toHaveBeenCalledWith(
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`, "https://localhost:1234/api/mongo/explorer?db=testDB&coll=testCollection&resourceUrl=bardb%2FtestDB%2Fdb%2FtestCollection%2Fdocs%2FtestId&rid=testId&rtype=docs&sid=&rg=&dba=foo&pk=pk",
expect.any(Object), expect.any(Object),
); );
}); });
@@ -252,14 +231,13 @@ describe("MongoProxyClient", () => {
databaseAccount, databaseAccount,
}); });
updateConfigContext({ updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
globallyEnabledMongoAPIs: [],
}); });
}); });
it("returns a production endpoint", () => { it("returns a production endpoint", () => {
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT); const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`); expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
}); });
it("returns a development endpoint", () => { it("returns a development endpoint", () => {
@@ -271,20 +249,18 @@ describe("MongoProxyClient", () => {
updateUserContext({ updateUserContext({
authType: AuthType.EncryptedToken, authType: AuthType.EncryptedToken,
}); });
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT); const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/connectionstring/mongo/explorer`); expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
}); });
}); });
describe("getFeatureEndpointOrDefault", () => { describe("getFeatureEndpointOrDefault", () => {
beforeEach(() => { beforeEach(() => {
resetConfigContext(); resetConfigContext();
updateConfigContext({ updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
globallyEnabledMongoAPIs: [],
}); });
const params = new URLSearchParams({ const params = new URLSearchParams({
"feature.mongoProxyEndpoint": MongoProxyEndpoints.Prod, "feature.mongoProxyEndpoint": "https://localhost:12901",
"feature.mongoProxyAPIs": "readDocument|createDocument", "feature.mongoProxyAPIs": "readDocument|createDocument",
}); });
const features = extractFeatures(params); const features = extractFeatures(params);
@@ -296,12 +272,12 @@ describe("MongoProxyClient", () => {
it("returns a local endpoint", () => { it("returns a local endpoint", () => {
const endpoint = getFeatureEndpointOrDefault("readDocument"); const endpoint = getFeatureEndpointOrDefault("readDocument");
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`); expect(endpoint).toEqual("https://localhost:12901/api/mongo/explorer");
}); });
it("returns a production endpoint", () => { it("returns a production endpoint", () => {
const endpoint = getFeatureEndpointOrDefault("DeleteDocument"); const endpoint = getFeatureEndpointOrDefault("deleteDocument");
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`); expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
}); });
}); });
}); });

View File

@@ -1,7 +1,7 @@
import { Constants as CosmosSDKConstants } from "@azure/cosmos"; import { Constants as CosmosSDKConstants } from "@azure/cosmos";
import { import {
allowedMongoProxyEndpoints,
allowedMongoProxyEndpoints_ToBeDeprecated, allowedMongoProxyEndpoints_ToBeDeprecated,
defaultAllowedMongoProxyEndpoints,
validateEndpoint, validateEndpoint,
} from "Utils/EndpointUtils"; } from "Utils/EndpointUtils";
import queryString from "querystring"; import queryString from "querystring";
@@ -14,7 +14,7 @@ import DocumentId from "../Explorer/Tree/DocumentId";
import { hasFlag } from "../Platform/Hosted/extractFeatures"; import { hasFlag } from "../Platform/Hosted/extractFeatures";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyApi, MongoProxyEndpoints } from "./Constants"; import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyEndpoints } from "./Constants";
import { MinimalQueryIterator } from "./IteratorUtilities"; import { MinimalQueryIterator } from "./IteratorUtilities";
import { sendMessage } from "./MessageHandler"; import { sendMessage } from "./MessageHandler";
@@ -67,7 +67,7 @@ export function queryDocuments(
query: string, query: string,
continuationToken?: string, continuationToken?: string,
): Promise<QueryResponse> { ): Promise<QueryResponse> {
if (!useMongoProxyEndpoint(MongoProxyApi.ResourceList) || !useMongoProxyEndpoint(MongoProxyApi.QueryDocuments)) { if (!useMongoProxyEndpoint("resourcelist") || !useMongoProxyEndpoint("queryDocuments")) {
return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken); return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken);
} }
@@ -89,7 +89,7 @@ export function queryDocuments(
query, query,
}; };
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ResourceList) || ""; const endpoint = getFeatureEndpointOrDefault("resourcelist") || "";
const headers = { const headers = {
...defaultHeaders, ...defaultHeaders,
@@ -194,7 +194,7 @@ export function readDocument(
collection: Collection, collection: Collection,
documentId: DocumentId, documentId: DocumentId,
): Promise<DataModels.DocumentId> { ): Promise<DataModels.DocumentId> {
if (!useMongoProxyEndpoint(MongoProxyApi.ReadDocument)) { if (!useMongoProxyEndpoint("readDocument")) {
return readDocument_ToBeDeprecated(databaseId, collection, documentId); return readDocument_ToBeDeprecated(databaseId, collection, documentId);
} }
const { databaseAccount } = userContext; const { databaseAccount } = userContext;
@@ -217,7 +217,7 @@ export function readDocument(
: "", : "",
}; };
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ReadDocument); const endpoint = getFeatureEndpointOrDefault("readDocument");
return window return window
.fetch(endpoint, { .fetch(endpoint, {
@@ -289,7 +289,7 @@ export function createDocument(
partitionKeyProperty: string, partitionKeyProperty: string,
documentContent: unknown, documentContent: unknown,
): Promise<DataModels.DocumentId> { ): Promise<DataModels.DocumentId> {
if (!useMongoProxyEndpoint(MongoProxyApi.CreateDocument)) { if (!useMongoProxyEndpoint("createDocument")) {
return createDocument_ToBeDeprecated(databaseId, collection, partitionKeyProperty, documentContent); return createDocument_ToBeDeprecated(databaseId, collection, partitionKeyProperty, documentContent);
} }
const { databaseAccount } = userContext; const { databaseAccount } = userContext;
@@ -308,7 +308,7 @@ export function createDocument(
documentContent: JSON.stringify(documentContent), documentContent: JSON.stringify(documentContent),
}; };
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateDocument); const endpoint = getFeatureEndpointOrDefault("createDocument");
return window return window
.fetch(`${endpoint}/createDocument`, { .fetch(`${endpoint}/createDocument`, {
@@ -373,7 +373,7 @@ export function updateDocument(
documentId: DocumentId, documentId: DocumentId,
documentContent: string, documentContent: string,
): Promise<DataModels.DocumentId> { ): Promise<DataModels.DocumentId> {
if (!useMongoProxyEndpoint(MongoProxyApi.UpdateDocument)) { if (!useMongoProxyEndpoint("updateDocument")) {
return updateDocument_ToBeDeprecated(databaseId, collection, documentId, documentContent); return updateDocument_ToBeDeprecated(databaseId, collection, documentId, documentContent);
} }
const { databaseAccount } = userContext; const { databaseAccount } = userContext;
@@ -396,7 +396,7 @@ export function updateDocument(
: "", : "",
documentContent, documentContent,
}; };
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.UpdateDocument); const endpoint = getFeatureEndpointOrDefault("updateDocument");
return window return window
.fetch(endpoint, { .fetch(endpoint, {
@@ -464,7 +464,7 @@ export function updateDocument_ToBeDeprecated(
} }
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> { export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
if (!useMongoProxyEndpoint(MongoProxyApi.DeleteDocument)) { if (!useMongoProxyEndpoint("deleteDocument")) {
return deleteDocument_ToBeDeprecated(databaseId, collection, documentId); return deleteDocument_ToBeDeprecated(databaseId, collection, documentId);
} }
const { databaseAccount } = userContext; const { databaseAccount } = userContext;
@@ -486,7 +486,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
? documentId.partitionKeyProperties?.[0] ? documentId.partitionKeyProperties?.[0]
: "", : "",
}; };
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.DeleteDocument); const endpoint = getFeatureEndpointOrDefault("deleteDocument");
return window return window
.fetch(endpoint, { .fetch(endpoint, {
@@ -550,56 +550,10 @@ export function deleteDocument_ToBeDeprecated(
}); });
} }
export function deleteDocuments(
databaseId: string,
collection: Collection,
documentIds: DocumentId[],
): Promise<{
deletedCount: number;
isAcknowledged: boolean;
}> {
const { databaseAccount } = userContext;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const rids: string[] = documentIds.map((documentId) => {
const idComponents = documentId.self.split("/");
return idComponents[5];
});
const params = {
databaseID: databaseId,
collectionID: collection.id(),
resourceUrl: `${resourceEndpoint}`,
resourceIDs: rids,
subscriptionID: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
databaseAccountName: databaseAccount.name,
};
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.BulkDelete);
return window
.fetch(`${endpoint}/bulkdelete`, {
method: "DELETE",
body: JSON.stringify(params),
headers: {
...defaultHeaders,
...authHeaders(),
[HttpHeaders.contentType]: ContentType.applicationJson,
},
})
.then(async (response) => {
if (response.ok) {
const result = await response.json();
return result;
}
return await errorHandling(response, "deleting documents", params);
});
}
export function createMongoCollectionWithProxy( export function createMongoCollectionWithProxy(
params: DataModels.CreateCollectionParams, params: DataModels.CreateCollectionParams,
): Promise<DataModels.Collection> { ): Promise<DataModels.Collection> {
if (!useMongoProxyEndpoint(MongoProxyApi.CreateCollectionWithProxy)) { if (!useMongoProxyEndpoint("createCollectionWithProxy")) {
return createMongoCollectionWithProxy_ToBeDeprecated(params); return createMongoCollectionWithProxy_ToBeDeprecated(params);
} }
const { databaseAccount } = userContext; const { databaseAccount } = userContext;
@@ -622,7 +576,7 @@ export function createMongoCollectionWithProxy(
isSharded: !!shardKey, isSharded: !!shardKey,
}; };
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateCollectionWithProxy); const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy");
return window return window
.fetch(`${endpoint}/createCollection`, { .fetch(`${endpoint}/createCollection`, {
@@ -692,13 +646,12 @@ export function getFeatureEndpointOrDefault(feature: string): string {
if (useMongoProxyEndpoint(feature)) { if (useMongoProxyEndpoint(feature)) {
endpoint = configContext.MONGO_PROXY_ENDPOINT; endpoint = configContext.MONGO_PROXY_ENDPOINT;
} else { } else {
const allowedMongoProxyEndpoints = configContext.allowedMongoProxyEndpoints || [
...defaultAllowedMongoProxyEndpoints,
...allowedMongoProxyEndpoints_ToBeDeprecated,
];
endpoint = endpoint =
hasFlag(userContext.features.mongoProxyAPIs, feature) && hasFlag(userContext.features.mongoProxyAPIs, feature) &&
validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints) validateEndpoint(userContext.features.mongoProxyEndpoint, [
...allowedMongoProxyEndpoints,
...allowedMongoProxyEndpoints_ToBeDeprecated,
])
? userContext.features.mongoProxyEndpoint ? userContext.features.mongoProxyEndpoint
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT; : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
} }
@@ -719,88 +672,26 @@ export function getEndpoint(endpoint: string): string {
return url; return url;
} }
export function useMongoProxyEndpoint(mongoProxyApi: string): boolean { export function useMongoProxyEndpoint(api: string): boolean {
const mongoProxyEnvironmentMap: { [key: string]: string[] } = { const activeMongoProxyEndpoints: string[] = [
[MongoProxyApi.ResourceList]: [ MongoProxyEndpoints.Local,
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod, MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax, MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake, ];
], let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
[MongoProxyApi.QueryDocuments]: [ if (
MongoProxyEndpoints.Development, configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local &&
MongoProxyEndpoints.Mpac, userContext.databaseAccount.properties.ipRules?.length > 0
MongoProxyEndpoints.Prod, ) {
MongoProxyEndpoints.Fairfax, canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED;
MongoProxyEndpoints.Mooncake,
],
[MongoProxyApi.CreateDocument]: [
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
[MongoProxyApi.ReadDocument]: [
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
[MongoProxyApi.UpdateDocument]: [
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
[MongoProxyApi.DeleteDocument]: [
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
[MongoProxyApi.CreateCollectionWithProxy]: [
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
[MongoProxyApi.LegacyMongoShell]: [
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
[MongoProxyApi.BulkDelete]: [
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
],
};
if (!mongoProxyEnvironmentMap[mongoProxyApi] || !configContext.MONGO_PROXY_ENDPOINT) {
return false;
} }
if (configContext.globallyEnabledMongoAPIs.includes(mongoProxyApi)) { return (
return true; canAccessMongoProxy &&
} configContext.NEW_MONGO_APIS?.includes(api) &&
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
return mongoProxyEnvironmentMap[mongoProxyApi].includes(configContext.MONGO_PROXY_ENDPOINT); );
}
export class ThrottlingError extends Error {
constructor(message: string) {
super(message);
}
} }
// TODO: This function throws most of the time except on Forbidden which is a bit strange // TODO: This function throws most of the time except on Forbidden which is a bit strange
@@ -812,14 +703,6 @@ async function errorHandling(response: Response, action: string, params: unknown
if (response.status === HttpStatusCodes.Forbidden) { if (response.status === HttpStatusCodes.Forbidden) {
sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage }); sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage });
return; return;
} else if (
response.status === HttpStatusCodes.BadRequest &&
errorMessage.includes("Error=16500") &&
errorMessage.includes("RetryAfterMs=")
) {
// If throttling is happening, Cosmos DB will return a 400 with a body of:
// A write operation resulted in an error. Error=16500, RetryAfterMs=4, Details='Batch write error.
throw new ThrottlingError(errorMessage);
} }
throw new Error(errorMessage); throw new Error(errorMessage);
} }

View File

@@ -0,0 +1,39 @@
import { configContext, Platform } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import { userContext } from "../UserContext";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
const notificationsPath = () => {
switch (configContext.platform) {
case Platform.Hosted:
return "/api/guest/notifications";
case Platform.Portal:
return "/api/notifications";
default:
throw new Error(`Unknown platform: ${configContext.platform}`);
}
};
export const fetchPortalNotifications = async (): Promise<DataModels.Notification[]> => {
if (configContext.platform === Platform.Emulator || configContext.platform === Platform.Hosted) {
return [];
}
const { databaseAccount, resourceGroup, subscriptionId } = userContext;
const url = `${configContext.BACKEND_ENDPOINT}${notificationsPath()}?accountName=${
databaseAccount.name
}&subscriptionId=${subscriptionId}&resourceGroup=${resourceGroup}`;
const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers = { [authorizationHeader.header]: authorizationHeader.token };
const response = await window.fetch(url, {
headers,
});
if (!response.ok) {
throw new Error(await response.text());
}
return (await response.json()) as DataModels.Notification[];
};

View File

@@ -1,113 +0,0 @@
import QueryError, { QueryErrorLocation, QueryErrorSeverity } from "Common/QueryError";
describe("QueryError.tryParse", () => {
const testErrorLocationResolver = ({ start, end }: { start: number; end: number }) =>
new QueryErrorLocation(
{ offset: start, lineNumber: start, column: start },
{ offset: end, lineNumber: end, column: end },
);
it("handles a string error", () => {
const error = "error";
const result = QueryError.tryParse(error, testErrorLocationResolver);
expect(result).toEqual([new QueryError("error", QueryErrorSeverity.Error)]);
});
it("handles an error object", () => {
const error = {
message: "error",
severity: "Warning",
location: { start: 0, end: 1 },
code: "code",
};
const result = QueryError.tryParse(error, testErrorLocationResolver);
expect(result).toEqual([
new QueryError(
"error",
QueryErrorSeverity.Warning,
"code",
new QueryErrorLocation({ offset: 0, lineNumber: 0, column: 0 }, { offset: 1, lineNumber: 1, column: 1 }),
),
]);
});
it("handles a JSON message without syntax errors", () => {
const innerError = {
code: "BadRequest",
message: "Your query is bad, and you should feel bad",
};
const message = `Message: ${JSON.stringify(innerError)}\r\nActivity ID: 42`;
const outerError = {
code: "BadRequest",
message,
};
const result = QueryError.tryParse(outerError, testErrorLocationResolver);
expect(result).toEqual([
new QueryError("Your query is bad, and you should feel bad", QueryErrorSeverity.Error, "BadRequest"),
]);
});
// 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 = [
{
message: "error1",
severity: "Warning",
location: { start: 0, end: 1 },
code: "code1",
},
{
message: "error2",
severity: "Error",
location: { start: 2, end: 3 },
code: "code2",
},
];
const innerError = {
code: "BadRequest",
message: "Your query is bad, and you should feel bad",
errors,
};
const message = `Message: ${JSON.stringify(innerError)}\r\nActivity ID: 42`;
const outerError = {
code: "BadRequest",
message,
};
const result = QueryError.tryParse(outerError, testErrorLocationResolver);
expect(result).toEqual([
new QueryError(
"error1",
QueryErrorSeverity.Warning,
"code1",
new QueryErrorLocation({ offset: 0, lineNumber: 0, column: 0 }, { offset: 1, lineNumber: 1, column: 1 }),
),
new QueryError(
"error2",
QueryErrorSeverity.Error,
"code2",
new QueryErrorLocation({ offset: 2, lineNumber: 2, column: 2 }, { offset: 3, lineNumber: 3, column: 3 }),
),
]);
});
// 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 }),
),
]);
});
});

View File

@@ -1,259 +0,0 @@
import { monaco } from "Explorer/LazyMonaco";
import { getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
export enum QueryErrorSeverity {
Error = "Error",
Warning = "Warning",
}
export class QueryErrorLocation {
constructor(
public start: ErrorPosition,
public end: ErrorPosition,
) {}
}
export class ErrorPosition {
constructor(
public offset: number,
public lineNumber?: number,
public column?: number,
) {}
}
// Maps severities to numbers for sorting.
const severityMap: Record<QueryErrorSeverity, number> = {
Error: 1,
Warning: 0,
};
export function compareSeverity(left: QueryErrorSeverity, right: QueryErrorSeverity): number {
return severityMap[left] - severityMap[right];
}
export function createMonacoErrorLocationResolver(
editor: monaco.editor.IStandaloneCodeEditor,
selection?: monaco.Selection,
): (location: { start: number; end: number }) => QueryErrorLocation {
return ({ start, end }) => {
// Start and end are absolute offsets (character index) in the document.
// But we need line numbers and columns for the monaco editor.
// To get those, we use the editor's model to convert the offsets to positions.
const model = editor.getModel();
if (!model) {
return new QueryErrorLocation(new ErrorPosition(start), new ErrorPosition(end));
}
// If the error was found in a selection, adjust the start and end positions to be relative to the document.
if (selection) {
// Get the character index of the start of the selection.
const selectionStartOffset = model.getOffsetAt(selection.getStartPosition());
// Adjust the start and end positions to be relative to the document.
start = selectionStartOffset + start;
end = selectionStartOffset + end;
// Now, when we resolve the positions, they will be relative to the document and appear in the correct location.
}
const startPos = model.getPositionAt(start);
const endPos = model.getPositionAt(end);
return new QueryErrorLocation(
new ErrorPosition(start, startPos.lineNumber, startPos.column),
new ErrorPosition(end, endPos.lineNumber, endPos.column),
);
};
}
export const createMonacoMarkersForQueryErrors = (errors: QueryError[]) => {
if (!errors) {
return [];
}
return errors
.map((error): monaco.editor.IMarkerData => {
// Validate that we have what we need to make a marker
if (
error.location === undefined ||
error.location.start === undefined ||
error.location.end === undefined ||
error.location.start.lineNumber === undefined ||
error.location.end.lineNumber === undefined ||
error.location.start.column === undefined ||
error.location.end.column === undefined
) {
return null;
}
return {
message: error.message,
severity: error.getMonacoSeverity(),
startLineNumber: error.location.start.lineNumber,
startColumn: error.location.start.column,
endLineNumber: error.location.end.lineNumber,
endColumn: error.location.end.column,
};
})
.filter((marker) => !!marker);
};
export interface ErrorEnrichment {
title?: string;
message: string;
learnMoreUrl?: string;
}
const REPLACEMENT_MESSAGES: Record<string, (original: string) => string> = {
OPERATION_RU_LIMIT_EXCEEDED: (original) => {
if (ruThresholdEnabled()) {
const threshold = getRUThreshold();
return `Query exceeded the Request Unit (RU) limit of ${threshold} RUs. You can change this limit in Data Explorer settings.`;
}
return original;
},
};
const HELP_LINKS: Record<string, string> = {
OPERATION_RU_LIMIT_EXCEEDED:
"https://learn.microsoft.com/en-us/azure/cosmos-db/data-explorer#configure-request-unit-threshold",
};
export default class QueryError {
message: string;
helpLink?: string;
constructor(
message: string,
public severity: QueryErrorSeverity,
public code?: string,
public location?: QueryErrorLocation,
helpLink?: string,
) {
// Automatically replace the message with a more Data Explorer-specific message if we have for this error code.
this.message = REPLACEMENT_MESSAGES[code] ? REPLACEMENT_MESSAGES[code](message) : message;
// Automatically set the help link if we have one for this error code.
this.helpLink = helpLink ?? HELP_LINKS[code];
}
getMonacoSeverity(): monaco.MarkerSeverity {
// It's very difficult to use the monaco.MarkerSeverity enum from here, so we'll just use the numbers directly.
// See: https://microsoft.github.io/monaco-editor/typedoc/enums/MarkerSeverity.html
switch (this.severity) {
case QueryErrorSeverity.Error:
return 8;
case QueryErrorSeverity.Warning:
return 4;
default:
return 2; // Info
}
}
/** Attempts to parse a query error from a string or object.
*
* @param error The error to parse.
* @returns An array of query errors if the error could be parsed, or null otherwise.
*/
static tryParse(
error: unknown,
locationResolver?: (location: { start: number; end: number }) => QueryErrorLocation,
): QueryError[] {
locationResolver =
locationResolver ||
(({ start, end }) => new QueryErrorLocation(new ErrorPosition(start), new ErrorPosition(end)));
const errors = QueryError.tryParseObject(error, locationResolver);
if (errors !== null) {
return errors;
}
const errorMessage = error as string;
// Map some well known messages to richer errors
const knownError = knownErrors[errorMessage];
if (knownError) {
return [knownError];
} else {
return [new QueryError(errorMessage, QueryErrorSeverity.Error)];
}
}
static read(
error: unknown,
locationResolver: (location: { start: number; end: number }) => QueryErrorLocation,
): QueryError | null {
if (typeof error !== "object" || error === null) {
return null;
}
const message = "message" in error && typeof error.message === "string" ? error.message : undefined;
if (!message) {
return null; // Invalid error (no message).
}
const severity =
"severity" in error && typeof error.severity === "string"
? (error.severity as QueryErrorSeverity)
: QueryErrorSeverity.Error;
const location =
"location" in error && typeof error.location === "object"
? locationResolver(error.location as { start: number; end: number })
: undefined;
const code = "code" in error && typeof error.code === "string" ? error.code : undefined;
return new QueryError(message, severity, code, location);
}
private static tryParseObject(
error: unknown,
locationResolver: (location: { start: number; end: number }) => QueryErrorLocation,
): QueryError[] | null {
let message: string | undefined;
if (typeof error === "object" && "message" in error && typeof error.message === "string") {
message = error.message;
} else {
// Unsupported error format.
return null;
}
// 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);
} catch (e) {
// The message doesn't contain a nested error.
return [QueryError.read(error, locationResolver)];
}
if (typeof parsed === "object") {
if ("errors" in parsed && Array.isArray(parsed.errors)) {
return parsed.errors.map((e) => QueryError.read(e, locationResolver)).filter((e) => e !== null);
}
return [QueryError.read(parsed, locationResolver)];
}
return null;
}
}
const knownErrors: Record<string, QueryError> = {
"User aborted query.": new QueryError("User aborted query.", QueryErrorSeverity.Warning),
};

View File

@@ -135,7 +135,6 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
onEntityValueChange={onEntityValueChange} onEntityValueChange={onEntityValueChange}
onSelectDate={onSelectDate} onSelectDate={onSelectDate}
onEntityTimeValueChange={onEntityTimeValueChange} onEntityTimeValueChange={onEntityTimeValueChange}
entityProperty={entityProperty}
/> />
{!isEntityValueDisable && ( {!isEntityValueDisable && (
<TooltipHost content="Edit property" id="editTooltip"> <TooltipHost content="Edit property" id="editTooltip">

View File

@@ -3,12 +3,11 @@ import * as React from "react";
export interface TooltipProps { export interface TooltipProps {
children: string; children: string;
className?: string;
} }
export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children, className }: TooltipProps) => { export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children }: TooltipProps) => {
return ( return (
<span className={className}> <span>
<TooltipHost content={children}> <TooltipHost content={children}>
<Icon iconName="Info" ariaLabel={children} className="panelInfoIcon" tabIndex={0} /> <Icon iconName="Info" ariaLabel={children} className="panelInfoIcon" tabIndex={0} />
</TooltipHost> </TooltipHost>

View File

@@ -99,9 +99,6 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
if (params.vectorEmbeddingPolicy) { if (params.vectorEmbeddingPolicy) {
resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy; resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy;
} }
if (params.fullTextPolicy) {
resource.fullTextPolicy = params.fullTextPolicy;
}
const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = { const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = {
properties: { properties: {
@@ -273,7 +270,6 @@ const createCollectionWithSDK = async (params: DataModels.CreateCollectionParams
uniqueKeyPolicy: params.uniqueKeyPolicy || undefined, uniqueKeyPolicy: params.uniqueKeyPolicy || undefined,
analyticalStorageTtl: params.analyticalStorageTtl, analyticalStorageTtl: params.analyticalStorageTtl,
vectorEmbeddingPolicy: params.vectorEmbeddingPolicy, vectorEmbeddingPolicy: params.vectorEmbeddingPolicy,
fullTextPolicy: params.fullTextPolicy,
} as ContainerRequest; // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed } as ContainerRequest; // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed
const collectionOptions: RequestOptions = {}; const collectionOptions: RequestOptions = {};
const createDatabaseBody: DatabaseRequest = { id: params.databaseId }; const createDatabaseBody: DatabaseRequest = { id: params.databaseId };

View File

@@ -26,23 +26,14 @@ export const deleteDocument = async (collection: CollectionBase, documentId: Doc
} }
}; };
export interface IBulkDeleteResult {
documentId: DocumentId;
requestCharge: number;
statusCode: number;
retryAfterMilliseconds?: number;
}
/** /**
* Bulk delete documents * Bulk delete documents
* @param collection * @param collection
* @param documentId * @param documentId
* @returns array of results and status codes * @returns array of ids that were successfully deleted
*/ */
export const deleteDocuments = async ( export const deleteDocuments = async (collection: CollectionBase, documentIds: DocumentId[]): Promise<DocumentId[]> => {
collection: CollectionBase, const nbDocuments = documentIds.length;
documentIds: DocumentId[],
): Promise<IBulkDeleteResult[]> => {
const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`); const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`);
try { try {
const v2Container = await client().database(collection.databaseId).container(collection.id()); const v2Container = await client().database(collection.databaseId).container(collection.id());
@@ -65,17 +56,18 @@ export const deleteDocuments = async (
operationType: BulkOperationType.Delete, operationType: BulkOperationType.Delete,
})); }));
const promise = v2Container.items.bulk(operations).then((bulkResults) => { const promise = v2Container.items.bulk(operations).then((bulkResult) => {
return bulkResults.map((bulkResult, index) => { return documentIdsChunk.filter((_, index) => bulkResult[index].statusCode === 204);
const documentId = documentIdsChunk[index];
return { ...bulkResult, documentId };
});
}); });
promiseArray.push(promise); promiseArray.push(promise);
} }
const allResult = await Promise.all(promiseArray); const allResult = await Promise.all(promiseArray);
const flatAllResult = Array.prototype.concat.apply([], allResult); const flatAllResult = Array.prototype.concat.apply([], allResult);
logConsoleInfo(
`Successfully deleted ${getEntityName(flatAllResult.length > 1)}: ${flatAllResult.length} out of ${nbDocuments}`,
);
// TODO: handle case result.length != nbDocuments
return flatAllResult; return flatAllResult;
} catch (error) { } catch (error) {
handleError( handleError(

View File

@@ -8,16 +8,16 @@ import {
import { import {
allowedAadEndpoints, allowedAadEndpoints,
allowedArcadiaEndpoints, allowedArcadiaEndpoints,
allowedCassandraProxyEndpoints,
allowedEmulatorEndpoints, allowedEmulatorEndpoints,
allowedGraphEndpoints, allowedGraphEndpoints,
allowedHostedExplorerEndpoints, allowedHostedExplorerEndpoints,
allowedJunoOrigins, allowedJunoOrigins,
allowedMongoBackendEndpoints, allowedMongoBackendEndpoints,
allowedMongoProxyEndpoints,
allowedMsalRedirectEndpoints, allowedMsalRedirectEndpoints,
defaultAllowedArmEndpoints, defaultAllowedArmEndpoints,
defaultAllowedBackendEndpoints, defaultAllowedBackendEndpoints,
defaultAllowedCassandraProxyEndpoints,
defaultAllowedMongoProxyEndpoints,
validateEndpoint, validateEndpoint,
} from "Utils/EndpointUtils"; } from "Utils/EndpointUtils";
@@ -32,8 +32,6 @@ export interface ConfigContext {
platform: Platform; platform: Platform;
allowedArmEndpoints: ReadonlyArray<string>; allowedArmEndpoints: ReadonlyArray<string>;
allowedBackendEndpoints: ReadonlyArray<string>; allowedBackendEndpoints: ReadonlyArray<string>;
allowedCassandraProxyEndpoints: ReadonlyArray<string>;
allowedMongoProxyEndpoints: ReadonlyArray<string>;
allowedParentFrameOrigins: ReadonlyArray<string>; allowedParentFrameOrigins: ReadonlyArray<string>;
gitSha?: string; gitSha?: string;
proxyPath?: string; proxyPath?: string;
@@ -51,11 +49,14 @@ export interface ConfigContext {
ARCADIA_ENDPOINT: string; ARCADIA_ENDPOINT: string;
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string; ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
BACKEND_ENDPOINT?: string; BACKEND_ENDPOINT?: string;
PORTAL_BACKEND_ENDPOINT: string; PORTAL_BACKEND_ENDPOINT?: string;
NEW_BACKEND_APIS?: BackendApi[]; NEW_BACKEND_APIS?: BackendApi[];
MONGO_BACKEND_ENDPOINT?: string; MONGO_BACKEND_ENDPOINT?: string;
MONGO_PROXY_ENDPOINT: string; MONGO_PROXY_ENDPOINT?: string;
CASSANDRA_PROXY_ENDPOINT: string; MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED?: boolean;
NEW_MONGO_APIS?: string[];
CASSANDRA_PROXY_ENDPOINT?: string;
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: boolean;
NEW_CASSANDRA_APIS?: string[]; NEW_CASSANDRA_APIS?: string[];
PROXY_PATH?: string; PROXY_PATH?: string;
JUNO_ENDPOINT: string; JUNO_ENDPOINT: string;
@@ -67,8 +68,6 @@ export interface ConfigContext {
hostedExplorerURL: string; hostedExplorerURL: string;
armAPIVersion?: string; armAPIVersion?: string;
msalRedirectURI?: string; msalRedirectURI?: string;
globallyEnabledCassandraAPIs?: string[];
globallyEnabledMongoAPIs?: string[];
} }
// Default configuration // Default configuration
@@ -76,12 +75,9 @@ let configContext: Readonly<ConfigContext> = {
platform: Platform.Portal, platform: Platform.Portal,
allowedArmEndpoints: defaultAllowedArmEndpoints, allowedArmEndpoints: defaultAllowedArmEndpoints,
allowedBackendEndpoints: defaultAllowedBackendEndpoints, allowedBackendEndpoints: defaultAllowedBackendEndpoints,
allowedCassandraProxyEndpoints: defaultAllowedCassandraProxyEndpoints,
allowedMongoProxyEndpoints: defaultAllowedMongoProxyEndpoints,
allowedParentFrameOrigins: [ allowedParentFrameOrigins: [
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`, `^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`, `^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
`^https:\\/\\/cdb-(ms|ff|mc)-prod-pbe\\.cosmos\\.azure\\.(com|us|cn)$`,
`^https:\\/\\/[\\.\\w]*portal\\.microsoftazure\\.de$`, `^https:\\/\\/[\\.\\w]*portal\\.microsoftazure\\.de$`,
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`, `^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`, `^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
@@ -91,7 +87,7 @@ let configContext: Readonly<ConfigContext> = {
`^https:\\/\\/.*\\.analysis-df\\.net$`, `^https:\\/\\/.*\\.analysis-df\\.net$`,
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`, `^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
`^https:\\/\\/.*\\.azure-test\\.net$`, `^https:\\/\\/.*\\.azure-test\\.net$`,
`^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`, `^https:\\/\\/cosmos-explorer-preview\\.azurewebsites\\.net`,
], // Webpack injects this at build time ], // Webpack injects this at build time
gitSha: process.env.GIT_SHA, gitSha: process.env.GIT_SHA,
hostedExplorerURL: "https://cosmos.azure.com/", hostedExplorerURL: "https://cosmos.azure.com/",
@@ -112,12 +108,22 @@ let configContext: Readonly<ConfigContext> = {
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod, PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
NEW_MONGO_APIS: [
// "resourcelist",
// "queryDocuments",
// "createDocument",
// "readDocument",
// "updateDocument",
// "deleteDocument",
// "createCollectionWithProxy",
// "legacyMongoShell",
],
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod, CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"], NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
isTerminalEnabled: false, isTerminalEnabled: false,
isPhoenixEnabled: false, isPhoenixEnabled: false,
globallyEnabledCassandraAPIs: [],
globallyEnabledMongoAPIs: [],
}; };
export function resetConfigContext(): void { export function resetConfigContext(): void {
@@ -161,12 +167,7 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
delete newContext.BACKEND_ENDPOINT; delete newContext.BACKEND_ENDPOINT;
} }
if ( if (!validateEndpoint(newContext.MONGO_PROXY_ENDPOINT, allowedMongoProxyEndpoints)) {
!validateEndpoint(
newContext.MONGO_PROXY_ENDPOINT,
configContext.allowedMongoProxyEndpoints || defaultAllowedMongoProxyEndpoints,
)
) {
delete newContext.MONGO_PROXY_ENDPOINT; delete newContext.MONGO_PROXY_ENDPOINT;
} }
@@ -174,12 +175,7 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
delete newContext.MONGO_BACKEND_ENDPOINT; delete newContext.MONGO_BACKEND_ENDPOINT;
} }
if ( if (!validateEndpoint(newContext.CASSANDRA_PROXY_ENDPOINT, allowedCassandraProxyEndpoints)) {
!validateEndpoint(
newContext.CASSANDRA_PROXY_ENDPOINT,
configContext.allowedCassandraProxyEndpoints || defaultAllowedCassandraProxyEndpoints,
)
) {
delete newContext.CASSANDRA_PROXY_ENDPOINT; delete newContext.CASSANDRA_PROXY_ENDPOINT;
} }

View File

@@ -9,7 +9,6 @@ export enum TabKind {
Graph, Graph,
SQLQuery, SQLQuery,
ScaleSettings, ScaleSettings,
MongoQuery,
} }
/** /**
@@ -52,8 +51,6 @@ export interface OpenCollectionTab extends OpenTab {
*/ */
export interface OpenQueryTab extends OpenCollectionTab { export interface OpenQueryTab extends OpenCollectionTab {
query: QueryInfo; query: QueryInfo;
splitterDirection?: "vertical" | "horizontal";
queryViewSizePercent?: number;
} }
/** /**

View File

@@ -6,7 +6,6 @@ export interface ArmEntity {
location: string; location: string;
type: string; type: string;
kind: string; kind: string;
tags?: Tags;
} }
export interface DatabaseAccount extends ArmEntity { export interface DatabaseAccount extends ArmEntity {
@@ -160,7 +159,6 @@ export interface Collection extends Resource {
analyticalStorageTtl?: number; analyticalStorageTtl?: number;
geospatialConfig?: GeospatialConfig; geospatialConfig?: GeospatialConfig;
vectorEmbeddingPolicy?: VectorEmbeddingPolicy; vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
fullTextPolicy?: FullTextPolicy;
schema?: ISchema; schema?: ISchema;
requestSchema?: () => void; requestSchema?: () => void;
computedProperties?: ComputedProperties; computedProperties?: ComputedProperties;
@@ -201,19 +199,11 @@ export interface IndexingPolicy {
compositeIndexes?: any[]; compositeIndexes?: any[];
spatialIndexes?: any[]; spatialIndexes?: any[];
vectorIndexes?: VectorIndex[]; vectorIndexes?: VectorIndex[];
fullTextIndexes?: FullTextIndex[];
} }
export interface VectorIndex { export interface VectorIndex {
path: string; path: string;
type: "flat" | "diskANN" | "quantizedFlat"; type: "flat" | "diskANN" | "quantizedFlat";
diskANNShardKey?: string;
indexingSearchListSize?: number;
quantizationByteSize?: number;
}
export interface FullTextIndex {
path: string;
} }
export interface ComputedProperty { export interface ComputedProperty {
@@ -352,7 +342,6 @@ export interface CreateCollectionParams {
uniqueKeyPolicy?: UniqueKeyPolicy; uniqueKeyPolicy?: UniqueKeyPolicy;
createMongoWildcardIndex?: boolean; createMongoWildcardIndex?: boolean;
vectorEmbeddingPolicy?: VectorEmbeddingPolicy; vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
fullTextPolicy?: FullTextPolicy;
} }
export interface VectorEmbeddingPolicy { export interface VectorEmbeddingPolicy {
@@ -366,16 +355,6 @@ export interface VectorEmbedding {
path: string; path: string;
} }
export interface FullTextPolicy {
defaultLanguage: string;
fullTextPaths: FullTextPath[];
}
export interface FullTextPath {
path: string;
language: string;
}
export interface ReadDatabaseOfferParams { export interface ReadDatabaseOfferParams {
databaseId: string; databaseId: string;
databaseResourceId?: string; databaseResourceId?: string;
@@ -664,5 +643,3 @@ export interface FeatureRegistration {
state: string; state: string;
}; };
} }
export type Tags = { [key: string]: string };

View File

@@ -41,7 +41,7 @@ export enum MessageTypes {
OpenPostgreSQLPasswordReset, OpenPostgreSQLPasswordReset,
OpenPostgresNetworkingBlade, OpenPostgresNetworkingBlade,
OpenCosmosDBNetworkingBlade, OpenCosmosDBNetworkingBlade,
DisplayNPSSurvey, // unused DisplayNPSSurvey,
OpenVCoreMongoNetworkingBlade, OpenVCoreMongoNetworkingBlade,
OpenVCoreMongoConnectionStringsBlade, OpenVCoreMongoConnectionStringsBlade,
GetAuthorizationToken, // unused. Can be removed if the portal uses the same list of enums. GetAuthorizationToken, // unused. Can be removed if the portal uses the same list of enums.

View File

@@ -98,6 +98,7 @@ export interface Database extends TreeNode {
openAddCollection(database: Database, event: MouseEvent): void; openAddCollection(database: Database, event: MouseEvent): void;
onSettingsClick: () => void; onSettingsClick: () => void;
loadOffer(): Promise<void>; loadOffer(): Promise<void>;
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
} }
export interface CollectionBase extends TreeNode { export interface CollectionBase extends TreeNode {
@@ -115,13 +116,7 @@ export interface CollectionBase extends TreeNode {
isSampleCollection?: boolean; isSampleCollection?: boolean;
onDocumentDBDocumentsClick(): void; onDocumentDBDocumentsClick(): void;
onNewQueryClick( onNewQueryClick(source: any, event?: MouseEvent, queryText?: string): void;
source: any,
event?: MouseEvent,
queryText?: string,
splitterDirection?: "horizontal" | "vertical",
queryViewSizePercent?: number,
): void;
expandCollection(): void; expandCollection(): void;
collapseCollection(): void; collapseCollection(): void;
getDatabase(): Database; getDatabase(): Database;
@@ -132,8 +127,6 @@ export interface Collection extends CollectionBase {
analyticalStorageTtl: ko.Observable<number>; analyticalStorageTtl: ko.Observable<number>;
schema?: DataModels.ISchema; schema?: DataModels.ISchema;
requestSchema?: () => void; requestSchema?: () => void;
vectorEmbeddingPolicy: ko.Observable<DataModels.VectorEmbeddingPolicy>;
fullTextPolicy: ko.Observable<DataModels.FullTextPolicy>;
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>; indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
uniqueKeyPolicy: DataModels.UniqueKeyPolicy; uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
usageSizeInKB: ko.Observable<number>; usageSizeInKB: ko.Observable<number>;
@@ -157,13 +150,7 @@ export interface Collection extends CollectionBase {
onSettingsClick: () => Promise<void>; onSettingsClick: () => Promise<void>;
onNewGraphClick(): void; onNewGraphClick(): void;
onNewMongoQueryClick( onNewMongoQueryClick(source: any, event?: MouseEvent, queryText?: string): void;
source: any,
event?: MouseEvent,
queryText?: string,
splitterDirection?: "horizontal" | "vertical",
queryViewSizePercent?: number,
): void;
onNewMongoShellClick(): void; onNewMongoShellClick(): void;
onNewStoredProcedureClick(source: Collection, event?: MouseEvent): void; onNewStoredProcedureClick(source: Collection, event?: MouseEvent): void;
onNewUserDefinedFunctionClick(source: Collection, event?: MouseEvent): void; onNewUserDefinedFunctionClick(source: Collection, event?: MouseEvent): void;
@@ -204,6 +191,8 @@ export interface Collection extends CollectionBase {
onDragOver(source: Collection, event: { originalEvent: DragEvent }): void; onDragOver(source: Collection, event: { originalEvent: DragEvent }): void;
onDrop(source: Collection, event: { originalEvent: DragEvent }): void; onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>; uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>;
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
} }
/** /**
@@ -323,8 +312,6 @@ export interface QueryTabOptions extends TabOptions {
partitionKey?: DataModels.PartitionKey; partitionKey?: DataModels.PartitionKey;
queryText?: string; queryText?: string;
resourceTokenPartitionKey?: string; resourceTokenPartitionKey?: string;
splitterDirection?: "horizontal" | "vertical";
queryViewSizePercent?: number;
} }
export interface ScriptTabOption extends TabOptions { export interface ScriptTabOption extends TabOptions {
@@ -398,8 +385,6 @@ export interface DataExplorerInputsFrame {
databaseAccount: any; databaseAccount: any;
subscriptionId?: string; subscriptionId?: string;
resourceGroup?: string; resourceGroup?: string;
tenantId?: string;
userName?: string;
masterKey?: string; masterKey?: string;
hasWriteAccess?: boolean; hasWriteAccess?: boolean;
authorizationToken?: string; authorizationToken?: string;

View File

@@ -41,10 +41,6 @@ export interface DatabaseContextMenuButtonParams {
* New resource tree (in ReactJS) * New resource tree (in ReactJS)
*/ */
export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => { export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => {
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) {
return undefined;
}
const items: TreeNodeMenuItem[] = [ const items: TreeNodeMenuItem[] = [
{ {
iconSrc: AddCollectionIcon, iconSrc: AddCollectionIcon,
@@ -56,15 +52,13 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
if (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations) { if (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations) {
items.push({ items.push({
iconSrc: DeleteDatabaseIcon, iconSrc: DeleteDatabaseIcon,
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => { onClick: () =>
(useSidePanel.getState().getRef = lastFocusedElement),
useSidePanel useSidePanel
.getState() .getState()
.openSidePanel( .openSidePanel(
"Delete " + getDatabaseName(), "Delete " + getDatabaseName(),
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />, <DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />,
); ),
},
label: `Delete ${getDatabaseName()}`, label: `Delete ${getDatabaseName()}`,
styleClass: "deleteDatabaseMenuItem", styleClass: "deleteDatabaseMenuItem",
}); });
@@ -148,9 +142,8 @@ export const createCollectionContextMenuButton = (
if (configContext.platform !== Platform.Fabric) { if (configContext.platform !== Platform.Fabric) {
items.push({ items.push({
iconSrc: DeleteCollectionIcon, iconSrc: DeleteCollectionIcon,
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => { onClick: () => {
useSelectedNode.getState().setSelectedNode(selectedCollection); useSelectedNode.getState().setSelectedNode(selectedCollection);
(useSidePanel.getState().getRef = lastFocusedElement),
useSidePanel useSidePanel
.getState() .getState()
.openSidePanel( .openSidePanel(

View File

@@ -1,4 +1,4 @@
import { DirectionalHint, Icon, IconButton, Label, Stack, TooltipHost } from "@fluentui/react"; import { DirectionalHint, Icon, Label, Stack, TooltipHost } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import { NormalizedEventKey } from "../../../Common/Constants"; import { NormalizedEventKey } from "../../../Common/Constants";
import { accordionStackTokens } from "../Settings/SettingsRenderUtils"; import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
@@ -9,9 +9,6 @@ export interface CollapsibleSectionProps {
onExpand?: () => void; onExpand?: () => void;
children: JSX.Element; children: JSX.Element;
tooltipContent?: string | JSX.Element | JSX.Element[]; tooltipContent?: string | JSX.Element | JSX.Element[];
showDelete?: boolean;
onDelete?: () => void;
disabled?: boolean;
} }
export interface CollapsibleSectionState { export interface CollapsibleSectionState {
@@ -72,20 +69,6 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} /> <Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
</TooltipHost> </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> </Stack>
{this.state.isExpanded && this.props.children} {this.state.isExpanded && this.props.children}
</> </>

View File

@@ -35,7 +35,7 @@ export interface DialogState {
textFieldProps?: TextFieldProps, textFieldProps?: TextFieldProps,
primaryButtonDisabled?: boolean, primaryButtonDisabled?: boolean,
) => void; ) => void;
showOkModalDialog: (title: string, subText: string, linkProps?: LinkProps) => void; showOkModalDialog: (title: string, subText: string) => void;
} }
export const useDialog: UseStore<DialogState> = create((set, get) => ({ export const useDialog: UseStore<DialogState> = create((set, get) => ({
@@ -83,7 +83,7 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
textFieldProps, textFieldProps,
primaryButtonDisabled, primaryButtonDisabled,
}), }),
showOkModalDialog: (title: string, subText: string, linkProps?: LinkProps): void => showOkModalDialog: (title: string, subText: string): void =>
get().openDialog({ get().openDialog({
isModal: true, isModal: true,
title, title,
@@ -94,7 +94,6 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
get().closeDialog(); get().closeDialog();
}, },
onSecondaryButtonClick: undefined, onSecondaryButtonClick: undefined,
linkProps,
}), }),
})); }));

View File

@@ -3,37 +3,6 @@ import * as React from "react";
import { loadMonaco, monaco } from "../../LazyMonaco"; import { loadMonaco, monaco } from "../../LazyMonaco";
// import "./EditorReact.less"; // import "./EditorReact.less";
// In development, add a function to window to allow us to get the editor instance for a given element
if (process.env.NODE_ENV === "development") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const win = window as any;
win._monaco_getEditorForElement =
win._monaco_getEditorForElement ||
((element: HTMLElement) => {
const editorId = element.dataset["monacoEditorId"];
if (!editorId || !win.__monaco_editors || typeof win.__monaco_editors !== "object") {
return null;
}
return win.__monaco_editors[editorId];
});
win._monaco_getEditorContentForElement =
win._monaco_getEditorContentForElement ||
((element: HTMLElement) => {
const editor = win._monaco_getEditorForElement(element);
return editor ? editor.getValue() : null;
});
win._monaco_setEditorContentForElement =
win._monaco_setEditorContentForElement ||
((element: HTMLElement, text: string) => {
const editor = win._monaco_getEditorForElement(element);
if (editor) {
editor.setValue(text);
}
});
}
interface EditorReactStates { interface EditorReactStates {
showEditor: boolean; showEditor: boolean;
} }
@@ -42,7 +11,7 @@ export interface EditorReactProps {
content: string; content: string;
isReadOnly: boolean; isReadOnly: boolean;
ariaLabel: string; // Sets what will be read to the user to define the control ariaLabel: string; // Sets what will be read to the user to define the control
onContentSelected?: (selectedContent: string, selection: monaco.Selection) => void; // Called when text is selected onContentSelected?: (selectedContent: string) => void; // Called when text is selected
onContentChanged?: (newContent: string) => void; // Called when text is changed onContentChanged?: (newContent: string) => void; // Called when text is changed
theme?: string; // Monaco editor theme theme?: string; // Monaco editor theme
wordWrap?: monaco.editor.IEditorOptions["wordWrap"]; wordWrap?: monaco.editor.IEditorOptions["wordWrap"];
@@ -56,7 +25,6 @@ export interface EditorReactProps {
className?: string; className?: string;
spinnerClassName?: string; spinnerClassName?: string;
modelMarkers?: monaco.editor.IMarkerData[];
enableWordWrapContextMenuItem?: boolean; // Enable/Disable "Word Wrap" context menu item enableWordWrapContextMenuItem?: boolean; // Enable/Disable "Word Wrap" context menu item
onWordWrapChanged?: (wordWrap: "on" | "off") => void; // Called when word wrap is changed onWordWrapChanged?: (wordWrap: "on" | "off") => void; // Called when word wrap is changed
} }
@@ -64,25 +32,10 @@ export interface EditorReactProps {
export class EditorReact extends React.Component<EditorReactProps, EditorReactStates> { export class EditorReact extends React.Component<EditorReactProps, EditorReactStates> {
private static readonly VIEWING_OPTIONS_GROUP_ID = "viewingoptions"; // Group ID for the context menu group private static readonly VIEWING_OPTIONS_GROUP_ID = "viewingoptions"; // Group ID for the context menu group
private rootNode: HTMLElement; private rootNode: HTMLElement;
public editor: monaco.editor.IStandaloneCodeEditor; private editor: monaco.editor.IStandaloneCodeEditor;
private selectionListener: monaco.IDisposable; private selectionListener: monaco.IDisposable;
monacoApi: {
default: typeof monaco; private monacoEditorOptionsWordWrap: monaco.editor.EditorOption;
Emitter: typeof monaco.Emitter;
MarkerTag: typeof monaco.MarkerTag;
MarkerSeverity: typeof monaco.MarkerSeverity;
CancellationTokenSource: typeof monaco.CancellationTokenSource;
Uri: typeof monaco.Uri;
KeyCode: typeof monaco.KeyCode;
KeyMod: typeof monaco.KeyMod;
Position: typeof monaco.Position;
Range: typeof monaco.Range;
Selection: typeof monaco.Selection;
SelectionDirection: typeof monaco.SelectionDirection;
Token: typeof monaco.Token;
editor: typeof monaco.editor;
languages: typeof monaco.languages;
};
public constructor(props: EditorReactProps) { public constructor(props: EditorReactProps) {
super(props); super(props);
@@ -111,7 +64,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
if (this.props.content !== existingContent) { if (this.props.content !== existingContent) {
if (this.props.isReadOnly) { if (this.props.isReadOnly) {
this.editor.setValue(this.props.content || ""); // Monaco throws an error if you set the value to undefined. this.editor.setValue(this.props.content);
} else { } else {
this.editor.pushUndoStop(); this.editor.pushUndoStop();
this.editor.executeEdits("", [ this.editor.executeEdits("", [
@@ -122,8 +75,6 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
]); ]);
} }
} }
this.monacoApi.editor.setModelMarkers(this.editor.getModel(), "owner", this.props.modelMarkers || []);
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {
@@ -137,7 +88,6 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
<Spinner size={SpinnerSize.large} className={this.props.spinnerClassName || "spinner"} /> <Spinner size={SpinnerSize.large} className={this.props.spinnerClassName || "spinner"} />
)} )}
<div <div
data-test="EditorReact/Host/Unloaded"
className={this.props.className || "jsonEditor"} className={this.props.className || "jsonEditor"}
style={this.props.monacoContainerStyles} style={this.props.monacoContainerStyles}
ref={(elt: HTMLElement) => this.setRef(elt)} ref={(elt: HTMLElement) => this.setRef(elt)}
@@ -148,18 +98,6 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) { protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
this.editor = editor; this.editor = editor;
this.rootNode.dataset["test"] = "EditorReact/Host/Loaded";
// In development, we want to be able to access the editor instance from the console
if (process.env.NODE_ENV === "development") {
this.rootNode.dataset["monacoEditorId"] = this.editor.getId();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const win = window as any;
win["__monaco_editors"] = win["__monaco_editors"] || {};
win["__monaco_editors"][this.editor.getId()] = this.editor;
}
if (!this.props.isReadOnly && this.props.onContentChanged) { if (!this.props.isReadOnly && this.props.onContentChanged) {
// Hooking the model's onDidChangeContent event because of some event ordering issues. // Hooking the model's onDidChangeContent event because of some event ordering issues.
// If a single user input causes BOTH the editor content to change AND the cursor selection to change (which is likely), // If a single user input causes BOTH the editor content to change AND the cursor selection to change (which is likely),
@@ -177,7 +115,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
this.selectionListener = this.editor.onDidChangeCursorSelection( this.selectionListener = this.editor.onDidChangeCursorSelection(
(event: monaco.editor.ICursorSelectionChangedEvent) => { (event: monaco.editor.ICursorSelectionChangedEvent) => {
const selectedContent: string = this.editor.getModel().getValueInRange(event.selection); const selectedContent: string = this.editor.getModel().getValueInRange(event.selection);
this.props.onContentSelected(selectedContent, event.selection); this.props.onContentSelected(selectedContent);
}, },
); );
} }
@@ -192,7 +130,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
// Method that will be executed when the action is triggered. // Method that will be executed when the action is triggered.
// @param editor The editor instance is passed in as a convenience // @param editor The editor instance is passed in as a convenience
run: (ed) => { run: (ed) => {
const newOption = ed.getOption(this.monacoApi.editor.EditorOption.wordWrap) === "on" ? "off" : "on"; const newOption = ed.getOption(this.monacoEditorOptionsWordWrap) === "on" ? "off" : "on";
ed.updateOptions({ wordWrap: newOption }); ed.updateOptions({ wordWrap: newOption });
this.props.onWordWrapChanged(newOption); this.props.onWordWrapChanged(newOption);
}, },
@@ -218,14 +156,16 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
lineDecorationsWidth: this.props.lineDecorationsWidth, lineDecorationsWidth: this.props.lineDecorationsWidth,
minimap: this.props.minimap, minimap: this.props.minimap,
scrollBeyondLastLine: this.props.scrollBeyondLastLine, scrollBeyondLastLine: this.props.scrollBeyondLastLine,
fixedOverflowWidgets: true,
}; };
this.rootNode.innerHTML = ""; this.rootNode.innerHTML = "";
this.monacoApi = await loadMonaco(); const lazymonaco = await loadMonaco();
// We can only get this constant after loading monaco lazily
this.monacoEditorOptionsWordWrap = lazymonaco.editor.EditorOption.wordWrap;
try { try {
createCallback(this.monacoApi.editor.create(this.rootNode, options)); createCallback(lazymonaco?.editor?.create(this.rootNode, options));
} catch (error) { } catch (error) {
// This could happen if the parent node suddenly disappears during create() // This could happen if the parent node suddenly disappears during create()
console.error("Unable to create EditorReact", error); console.error("Unable to create EditorReact", error);

View File

@@ -1,6 +0,0 @@
import "@testing-library/jest-dom";
describe("AddFullTextPolicyForm", () => {
//CTODO: add tests
it.skip("should render correctly", () => {});
});

View File

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

View File

@@ -1,37 +0,0 @@
import { ProgressBar, makeStyles } from "@fluentui/react-components";
import React from "react";
const useStyles = makeStyles({
indeterminateProgressBarRoot: {
"@media screen and (prefers-reduced-motion: reduce)": {
animationIterationCount: "infinite",
animationDuration: "3s",
animationName: {
"0%": {
opacity: ".2", // matches indeterminate bar width
},
"50%": {
opacity: "1",
},
"100%": {
opacity: ".2",
},
},
},
},
indeterminateProgressBarBar: {
"@media screen and (prefers-reduced-motion: reduce)": {
maxWidth: "100%",
},
},
});
export const IndeterminateProgressBar: React.FC = () => {
const styles = useStyles();
return (
<ProgressBar
bar={{ className: styles.indeterminateProgressBarBar }}
className={styles.indeterminateProgressBarRoot}
/>
);
};

View File

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

View File

@@ -1,68 +0,0 @@
import { Button, MessageBar, MessageBarActions, MessageBarBody } from "@fluentui/react-components";
import { DismissRegular } from "@fluentui/react-icons";
import React, { useState } from "react";
export enum MessageBannerState {
/** The banner should be visible if the triggering conditions are met. */
Allowed = "allowed",
/** The banner has been dismissed by the user and will not be shown until the component is recreated, even if the visibility condition is true. */
Dismissed = "dismissed",
/** The banner has been supressed by the user and will not be shown at all, even if the visibility condition is true. */
Suppressed = "suppressed",
}
export type MessageBannerProps = {
/** A CSS class for the root MessageBar component */
className: string;
/** A unique ID for the message that will be used to store it's dismiss/suppress state across sessions. */
messageId: string;
/** The current visibility state for the banner IGNORING the user's dimiss/suppress preference
*
* If this value is true but the user has dismissed the banner, the banner will NOT be shown.
*/
visible: boolean;
};
/** A component that shows a message banner which can be dismissed by the user.
*
* In the future, this can also support persisting the dismissed state in local storage without requiring changes to all the components that use it.
*
* A message banner can be in three "states":
* - Allowed: The banner should be visible if the triggering conditions are met.
* - Dismissed: The banner has been dismissed by the user and will not be shown until the component is recreated, even if the visibility condition is true.
* - Suppressed: The banner has been supressed by the user and will not be shown at all, even if the visibility condition is true.
*
* The "Dismissed" state represents the user clicking the "x" in the banner to dismiss it.
* The "Suppressed" state represents the user clicking "Don't show this again".
*/
export const MessageBanner: React.FC<MessageBannerProps> = ({ visible, className, children }) => {
const [state, setState] = useState<MessageBannerState>(MessageBannerState.Allowed);
if (state !== MessageBannerState.Allowed) {
return null;
}
if (!visible) {
return null;
}
return (
<MessageBar className={className}>
<MessageBarBody>{children}</MessageBarBody>
<MessageBarActions
containerAction={
<Button
aria-label="dismiss"
appearance="transparent"
icon={<DismissRegular />}
onClick={() => setState(MessageBannerState.Dismissed)}
/>
}
></MessageBarActions>
</MessageBar>
);
};

View File

@@ -1,79 +0,0 @@
import {
Button,
Dialog,
DialogActions,
DialogBody,
DialogContent,
DialogSurface,
DialogTitle,
DialogTrigger,
Field,
ProgressBar,
} from "@fluentui/react-components";
import * as React from "react";
interface ProgressModalDialogProps {
isOpen: boolean;
title: string;
message: string;
maxValue: number;
value: number;
dismissText: string;
onDismiss: () => void;
onCancel?: () => void;
/* mode drives the state of the action buttons
* inProgress: Show cancel button
* completed: Show close button
* aborting: Show cancel button, but disabled
* aborted: Show close button
*/
mode?: "inProgress" | "completed" | "aborting" | "aborted";
}
/**
* React component that renders a modal dialog with a progress bar.
*/
export const ProgressModalDialog: React.FC<ProgressModalDialogProps> = ({
isOpen,
title,
message,
maxValue,
value,
dismissText,
onCancel,
onDismiss,
children,
mode = "completed",
}) => (
<Dialog
open={isOpen}
onOpenChange={(event, data) => {
if (!data.open) {
onDismiss();
}
}}
>
<DialogSurface>
<DialogBody>
<DialogTitle>{title}</DialogTitle>
<DialogContent>
<Field validationMessage={message} validationState="none">
<ProgressBar max={maxValue} value={value} />
</Field>
{children}
</DialogContent>
<DialogActions>
{mode === "inProgress" || mode === "aborting" ? (
<Button appearance="secondary" onClick={onCancel} disabled={mode === "aborting"}>
{dismissText}
</Button>
) : (
<DialogTrigger disableButtonEnhancement>
<Button appearance="primary">Close</Button>
</DialogTrigger>
)}
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
);

View File

@@ -134,6 +134,7 @@ describe("SettingsComponent", () => {
readSettings: undefined, readSettings: undefined,
onSettingsClick: undefined, onSettingsClick: undefined,
loadOffer: undefined, loadOffer: undefined,
getPendingThroughputSplitNotification: undefined,
} as ViewModels.Database; } as ViewModels.Database;
newCollection.getDatabase = () => newDatabase; newCollection.getDatabase = () => newDatabase;
newCollection.offer = ko.observable(undefined); newCollection.offer = ko.observable(undefined);

View File

@@ -4,11 +4,11 @@ import {
ComputedPropertiesComponentProps, ComputedPropertiesComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent"; } from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent";
import { import {
ContainerPolicyComponent, ContainerVectorPolicyComponent,
ContainerPolicyComponentProps, ContainerVectorPolicyComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent"; } from "Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils"; import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isRunningOnPublicCloud } from "Utils/CloudUtils"; import { isRunningOnPublicCloud } from "Utils/CloudUtils";
import * as React from "react"; import * as React from "react";
import DiscardIcon from "../../../../images/discard.svg"; import DiscardIcon from "../../../../images/discard.svg";
@@ -105,13 +105,6 @@ export interface SettingsComponentState {
isSubSettingsSaveable: boolean; isSubSettingsSaveable: boolean;
isSubSettingsDiscardable: boolean; isSubSettingsDiscardable: boolean;
vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
vectorEmbeddingPolicyBaseline: DataModels.VectorEmbeddingPolicy;
fullTextPolicy: DataModels.FullTextPolicy;
fullTextPolicyBaseline: DataModels.FullTextPolicy;
shouldDiscardContainerPolicies: boolean;
isContainerPolicyDirty: boolean;
indexingPolicyContent: DataModels.IndexingPolicy; indexingPolicyContent: DataModels.IndexingPolicy;
indexingPolicyContentBaseline: DataModels.IndexingPolicy; indexingPolicyContentBaseline: DataModels.IndexingPolicy;
shouldDiscardIndexingPolicy: boolean; shouldDiscardIndexingPolicy: boolean;
@@ -137,6 +130,7 @@ export interface SettingsComponentState {
conflictResolutionPolicyProcedureBaseline: string; conflictResolutionPolicyProcedureBaseline: string;
isConflictResolutionDirty: boolean; isConflictResolutionDirty: boolean;
initialNotification: DataModels.Notification;
selectedTab: SettingsV2TabTypes; selectedTab: SettingsV2TabTypes;
} }
@@ -156,7 +150,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private shouldShowIndexingPolicyEditor: boolean; private shouldShowIndexingPolicyEditor: boolean;
private shouldShowPartitionKeyEditor: boolean; private shouldShowPartitionKeyEditor: boolean;
private isVectorSearchEnabled: boolean; private isVectorSearchEnabled: boolean;
private isFullTextSearchEnabled: boolean;
private totalThroughputUsed: number; private totalThroughputUsed: number;
public mongoDBCollectionResource: MongoDBCollectionResource; public mongoDBCollectionResource: MongoDBCollectionResource;
@@ -172,7 +165,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo"; this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud(); this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection); this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy; this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
@@ -212,13 +204,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
isSubSettingsSaveable: false, isSubSettingsSaveable: false,
isSubSettingsDiscardable: false, isSubSettingsDiscardable: false,
vectorEmbeddingPolicy: undefined,
vectorEmbeddingPolicyBaseline: undefined,
fullTextPolicy: undefined,
fullTextPolicyBaseline: undefined,
shouldDiscardContainerPolicies: false,
isContainerPolicyDirty: false,
indexingPolicyContent: undefined, indexingPolicyContent: undefined,
indexingPolicyContentBaseline: undefined, indexingPolicyContentBaseline: undefined,
shouldDiscardIndexingPolicy: false, shouldDiscardIndexingPolicy: false,
@@ -244,6 +229,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
conflictResolutionPolicyProcedureBaseline: undefined, conflictResolutionPolicyProcedureBaseline: undefined,
isConflictResolutionDirty: false, isConflictResolutionDirty: false,
initialNotification: undefined,
selectedTab: SettingsV2TabTypes.ScaleTab, selectedTab: SettingsV2TabTypes.ScaleTab,
}; };
@@ -323,7 +309,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
return ( return (
this.state.isScaleSaveable || this.state.isScaleSaveable ||
this.state.isSubSettingsSaveable || this.state.isSubSettingsSaveable ||
this.state.isContainerPolicyDirty ||
this.state.isIndexingPolicyDirty || this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty || this.state.isConflictResolutionDirty ||
this.state.isComputedPropertiesDirty || this.state.isComputedPropertiesDirty ||
@@ -335,7 +320,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
return ( return (
this.state.isScaleDiscardable || this.state.isScaleDiscardable ||
this.state.isSubSettingsDiscardable || this.state.isSubSettingsDiscardable ||
this.state.isContainerPolicyDirty ||
this.state.isIndexingPolicyDirty || this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty || this.state.isConflictResolutionDirty ||
this.state.isComputedPropertiesDirty || this.state.isComputedPropertiesDirty ||
@@ -423,8 +407,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
timeToLiveSeconds: this.state.timeToLiveSecondsBaseline, timeToLiveSeconds: this.state.timeToLiveSecondsBaseline,
displayedTtlSeconds: this.state.displayedTtlSecondsBaseline, displayedTtlSeconds: this.state.displayedTtlSecondsBaseline,
geospatialConfigType: this.state.geospatialConfigTypeBaseline, geospatialConfigType: this.state.geospatialConfigTypeBaseline,
vectorEmbeddingPolicy: this.state.vectorEmbeddingPolicyBaseline,
fullTextPolicy: this.state.fullTextPolicyBaseline,
indexingPolicyContent: this.state.indexingPolicyContentBaseline, indexingPolicyContent: this.state.indexingPolicyContentBaseline,
indexesToAdd: [], indexesToAdd: [],
indexesToDrop: [], indexesToDrop: [],
@@ -436,13 +418,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
changeFeedPolicy: this.state.changeFeedPolicyBaseline, changeFeedPolicy: this.state.changeFeedPolicyBaseline,
autoPilotThroughput: this.state.autoPilotThroughputBaseline, autoPilotThroughput: this.state.autoPilotThroughputBaseline,
isAutoPilotSelected: this.state.wasAutopilotOriginallySet, isAutoPilotSelected: this.state.wasAutopilotOriginallySet,
shouldDiscardContainerPolicies: true,
shouldDiscardIndexingPolicy: true, shouldDiscardIndexingPolicy: true,
isScaleSaveable: false, isScaleSaveable: false,
isScaleDiscardable: false, isScaleDiscardable: false,
isSubSettingsSaveable: false, isSubSettingsSaveable: false,
isSubSettingsDiscardable: false, isSubSettingsDiscardable: false,
isContainerPolicyDirty: false,
isIndexingPolicyDirty: false, isIndexingPolicyDirty: false,
isMongoIndexingPolicySaveable: false, isMongoIndexingPolicySaveable: false,
isMongoIndexingPolicyDiscardable: false, isMongoIndexingPolicyDiscardable: false,
@@ -470,17 +450,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private onScaleDiscardableChange = (isScaleDiscardable: boolean): void => private onScaleDiscardableChange = (isScaleDiscardable: boolean): void =>
this.setState({ isScaleDiscardable: isScaleDiscardable }); 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 => private onIndexingPolicyContentChange = (newIndexingPolicy: DataModels.IndexingPolicy): void =>
this.setState({ indexingPolicyContent: newIndexingPolicy }); this.setState({ indexingPolicyContent: newIndexingPolicy });
private resetShouldDiscardContainerPolicies = (): void => this.setState({ shouldDiscardContainerPolicies: false });
private resetShouldDiscardIndexingPolicy = (): void => this.setState({ shouldDiscardIndexingPolicy: false }); private resetShouldDiscardIndexingPolicy = (): void => this.setState({ shouldDiscardIndexingPolicy: false });
private logIndexingPolicySuccessMessage = (): void => { private logIndexingPolicySuccessMessage = (): void => {
@@ -568,12 +540,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private onSubSettingsDiscardableChange = (isSubSettingsDiscardable: boolean): void => private onSubSettingsDiscardableChange = (isSubSettingsDiscardable: boolean): void =>
this.setState({ isSubSettingsDiscardable: isSubSettingsDiscardable }); 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 => private onIndexingPolicyDirtyChange = (isIndexingPolicyDirty: boolean): void =>
this.setState({ isIndexingPolicyDirty: isIndexingPolicyDirty }); this.setState({ isIndexingPolicyDirty: isIndexingPolicyDirty });
@@ -727,10 +693,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
? ChangeFeedPolicyState.On ? ChangeFeedPolicyState.On
: ChangeFeedPolicyState.Off; : 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 indexingPolicyContent = this.collection.indexingPolicy();
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy = const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy(); this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
@@ -764,10 +726,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
analyticalStorageTtlSelectionBaseline: analyticalStorageTtlSelection, analyticalStorageTtlSelectionBaseline: analyticalStorageTtlSelection,
analyticalStorageTtlSeconds: analyticalStorageTtlSeconds, analyticalStorageTtlSeconds: analyticalStorageTtlSeconds,
analyticalStorageTtlSecondsBaseline: analyticalStorageTtlSeconds, analyticalStorageTtlSecondsBaseline: analyticalStorageTtlSeconds,
vectorEmbeddingPolicy: vectorEmbeddingPolicy,
vectorEmbeddingPolicyBaseline: vectorEmbeddingPolicy,
fullTextPolicy: fullTextPolicy,
fullTextPolicyBaseline: fullTextPolicy,
indexingPolicyContent: indexingPolicyContent, indexingPolicyContent: indexingPolicyContent,
indexingPolicyContentBaseline: indexingPolicyContent, indexingPolicyContentBaseline: indexingPolicyContent,
conflictResolutionPolicyMode: conflictResolutionPolicyMode, conflictResolutionPolicyMode: conflictResolutionPolicyMode,
@@ -898,7 +856,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
if ( if (
this.state.isSubSettingsSaveable || this.state.isSubSettingsSaveable ||
this.state.isContainerPolicyDirty ||
this.state.isIndexingPolicyDirty || this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty || this.state.isConflictResolutionDirty ||
this.state.isComputedPropertiesDirty this.state.isComputedPropertiesDirty
@@ -920,10 +877,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const wasIndexingPolicyModified = this.state.isIndexingPolicyDirty; const wasIndexingPolicyModified = this.state.isIndexingPolicyDirty;
newCollection.defaultTtl = defaultTtl; newCollection.defaultTtl = defaultTtl;
newCollection.vectorEmbeddingPolicy = this.state.vectorEmbeddingPolicy;
newCollection.fullTextPolicy = this.state.fullTextPolicy;
newCollection.indexingPolicy = this.state.indexingPolicyContent; newCollection.indexingPolicy = this.state.indexingPolicyContent;
newCollection.changeFeedPolicy = newCollection.changeFeedPolicy =
@@ -962,8 +915,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy); this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
this.collection.geospatialConfig(updatedCollection.geospatialConfig); this.collection.geospatialConfig(updatedCollection.geospatialConfig);
this.collection.computedProperties(updatedCollection.computedProperties); this.collection.computedProperties(updatedCollection.computedProperties);
this.collection.vectorEmbeddingPolicy(updatedCollection.vectorEmbeddingPolicy);
this.collection.fullTextPolicy(updatedCollection.fullTextPolicy);
if (wasIndexingPolicyModified) { if (wasIndexingPolicyModified) {
await this.refreshIndexTransformationProgress(); await this.refreshIndexTransformationProgress();
@@ -972,7 +923,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.setState({ this.setState({
isSubSettingsSaveable: false, isSubSettingsSaveable: false,
isSubSettingsDiscardable: false, isSubSettingsDiscardable: false,
isContainerPolicyDirty: false,
isIndexingPolicyDirty: false, isIndexingPolicyDirty: false,
isConflictResolutionDirty: false, isConflictResolutionDirty: false,
isComputedPropertiesDirty: false, isComputedPropertiesDirty: false,
@@ -1102,6 +1052,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
onMaxAutoPilotThroughputChange: this.onMaxAutoPilotThroughputChange, onMaxAutoPilotThroughputChange: this.onMaxAutoPilotThroughputChange,
onScaleSaveableChange: this.onScaleSaveableChange, onScaleSaveableChange: this.onScaleSaveableChange,
onScaleDiscardableChange: this.onScaleDiscardableChange, onScaleDiscardableChange: this.onScaleDiscardableChange,
initialNotification: this.props.settingsTab.pendingNotification(),
throughputError: this.state.throughputError, throughputError: this.state.throughputError,
}; };
@@ -1143,21 +1094,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
onSubSettingsDiscardableChange: this.onSubSettingsDiscardableChange, 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 = { const indexingPolicyComponentProps: IndexingPolicyComponentProps = {
shouldDiscardIndexingPolicy: this.state.shouldDiscardIndexingPolicy, shouldDiscardIndexingPolicy: this.state.shouldDiscardIndexingPolicy,
resetShouldDiscardIndexingPolicy: this.resetShouldDiscardIndexingPolicy, resetShouldDiscardIndexingPolicy: this.resetShouldDiscardIndexingPolicy,
@@ -1215,6 +1151,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
explorer: this.props.settingsTab.getContainer(), explorer: this.props.settingsTab.getContainer(),
}; };
const containerVectorPolicyProps: ContainerVectorPolicyComponentProps = {
vectorEmbeddingPolicy: this.collection.rawDataModel?.vectorEmbeddingPolicy,
};
const tabs: SettingsV2TabInfo[] = []; const tabs: SettingsV2TabInfo[] = [];
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) { if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
tabs.push({ tabs.push({
@@ -1228,10 +1168,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
content: <SubSettingsComponent {...subSettingsComponentProps} />, content: <SubSettingsComponent {...subSettingsComponentProps} />,
}); });
if (this.isVectorSearchEnabled || this.isFullTextSearchEnabled) { if (this.isVectorSearchEnabled) {
tabs.push({ tabs.push({
tab: SettingsV2TabTypes.ContainerVectorPolicyTab, tab: SettingsV2TabTypes.ContainerVectorPolicyTab,
content: <ContainerPolicyComponent {...containerPolicyComponentProps} />, content: <ContainerVectorPolicyComponent {...containerVectorPolicyProps} />,
}); });
} }

View File

@@ -1,6 +0,0 @@
import "@testing-library/jest-dom";
describe("ContainerPolicyComponent", () => {
//CTODO: add tests
it.skip("should render correctly", () => {});
});

View File

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

View File

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

View File

@@ -120,6 +120,11 @@ export class IndexingPolicyComponent extends React.Component<
indexTransformationProgress={this.props.indexTransformationProgress} indexTransformationProgress={this.props.indexTransformationProgress}
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress} 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) && ( {isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
<MessageBar messageBarType={MessageBarType.warning}>{unsavedEditorWarningMessage("indexPolicy")}</MessageBar> <MessageBar messageBarType={MessageBarType.warning}>{unsavedEditorWarningMessage("indexPolicy")}</MessageBar>
)} )}

View File

@@ -14,7 +14,6 @@ import * as ViewModels from "../../../../Contracts/ViewModels";
import { handleError } from "Common/ErrorHandlingUtils"; import { handleError } from "Common/ErrorHandlingUtils";
import { cancelDataTransferJob, pollDataTransferJob } from "Common/dataAccess/dataTransfers"; import { cancelDataTransferJob, pollDataTransferJob } from "Common/dataAccess/dataTransfers";
import { Platform, configContext } from "ConfigContext";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane"; import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane";
import { import {
@@ -178,14 +177,12 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ da
To change the partition key, a new destination container must be created or an existing destination container 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. selected. Data will then be copied to the destination container.
</Text> </Text>
{configContext.platform !== Platform.Emulator && (
<PrimaryButton <PrimaryButton
styles={{ root: { width: "fit-content" } }} styles={{ root: { width: "fit-content" } }}
text="Change" text="Change"
onClick={startPartitionkeyChangeWorkflow} onClick={startPartitionkeyChangeWorkflow}
disabled={isCurrentJobInProgress(portalDataTransferJob)} disabled={isCurrentJobInProgress(portalDataTransferJob)}
/> />
)}
{portalDataTransferJob && ( {portalDataTransferJob && (
<Stack> <Stack>
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text> <Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>

View File

@@ -1,10 +1,18 @@
import { shallow } from "enzyme";
import ko from "knockout";
import React from "react";
import * as Constants from "../../../../Common/Constants"; import * as Constants from "../../../../Common/Constants";
import * as DataModels from "../../../../Contracts/DataModels";
import { updateUserContext } from "../../../../UserContext"; import { updateUserContext } from "../../../../UserContext";
import Explorer from "../../../Explorer"; import Explorer from "../../../Explorer";
import { throughputUnit } from "../SettingsRenderUtils";
import { collection } from "../TestUtils"; import { collection } from "../TestUtils";
import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent"; import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
describe("ScaleComponent", () => { describe("ScaleComponent", () => {
const targetThroughput = 6000;
const baseProps: ScaleComponentProps = { const baseProps: ScaleComponentProps = {
collection: collection, collection: collection,
database: undefined, database: undefined,
@@ -28,8 +36,39 @@ describe("ScaleComponent", () => {
onScaleDiscardableChange: () => { onScaleDiscardableChange: () => {
return; return;
}, },
initialNotification: {
description: `Throughput update for ${targetThroughput} ${throughputUnit}`,
} as DataModels.Notification,
}; };
it("renders with correct initial notification", () => {
let wrapper = shallow(<ScaleComponent {...baseProps} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(true);
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(false);
expect(wrapper.find("#throughputApplyLongDelayMessage").html()).toContain(`${targetThroughput}`);
const newCollection = { ...collection };
const maxThroughput = 5000;
newCollection.offer = ko.observable({
manualThroughput: undefined,
autoscaleMaxThroughput: maxThroughput,
minimumThroughput: 400,
id: "offer",
offerReplacePending: true,
});
const newProps = {
...baseProps,
initialNotification: undefined as DataModels.Notification,
collection: newCollection,
};
wrapper = shallow(<ScaleComponent {...newProps} />);
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false);
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(`${maxThroughput}`);
});
it("autoScale disabled", () => { it("autoScale disabled", () => {
const scaleComponent = new ScaleComponent(baseProps); const scaleComponent = new ScaleComponent(baseProps);
expect(scaleComponent.isAutoScaleEnabled()).toEqual(false); expect(scaleComponent.isAutoScaleEnabled()).toEqual(false);

View File

@@ -10,6 +10,7 @@ import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
import { isRunningOnNationalCloud } from "../../../../Utils/CloudUtils"; import { isRunningOnNationalCloud } from "../../../../Utils/CloudUtils";
import { import {
getTextFieldStyles, getTextFieldStyles,
getThroughputApplyLongDelayMessage,
getThroughputApplyShortDelayMessage, getThroughputApplyShortDelayMessage,
subComponentStackProps, subComponentStackProps,
throughputUnit, throughputUnit,
@@ -33,6 +34,7 @@ export interface ScaleComponentProps {
onMaxAutoPilotThroughputChange: (newThroughput: number) => void; onMaxAutoPilotThroughputChange: (newThroughput: number) => void;
onScaleSaveableChange: (isScaleSaveable: boolean) => void; onScaleSaveableChange: (isScaleSaveable: boolean) => void;
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void; onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
initialNotification: DataModels.Notification;
throughputError?: string; throughputError?: string;
} }
@@ -100,6 +102,10 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
}; };
public getInitialNotificationElement = (): JSX.Element => { public getInitialNotificationElement = (): JSX.Element => {
if (this.props.initialNotification) {
return this.getLongDelayMessage();
}
if (this.offer?.offerReplacePending) { if (this.offer?.offerReplacePending) {
const throughput = this.offer.manualThroughput || this.offer.autoscaleMaxThroughput; const throughput = this.offer.manualThroughput || this.offer.autoscaleMaxThroughput;
return getThroughputApplyShortDelayMessage( return getThroughputApplyShortDelayMessage(
@@ -114,6 +120,26 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
return undefined; return undefined;
}; };
public getLongDelayMessage = (): JSX.Element => {
const matches: string[] = this.props.initialNotification?.description.match(
`Throughput update for (.*) ${throughputUnit}`,
);
const throughput = this.props.throughputBaseline;
const targetThroughput: number = matches.length > 1 && Number(matches[1]);
if (targetThroughput) {
return getThroughputApplyLongDelayMessage(
this.props.wasAutopilotOriginallySet,
throughput,
throughputUnit,
this.databaseId,
this.collectionId,
targetThroughput,
);
}
return <></>;
};
private getThroughputInputComponent = (): JSX.Element => ( private getThroughputInputComponent = (): JSX.Element => (
<ThroughputInputAutoPilotV3Component <ThroughputInputAutoPilotV3Component
databaseAccount={userContext?.databaseAccount} databaseAccount={userContext?.databaseAccount}

View File

@@ -17,13 +17,14 @@ import {
} from "@fluentui/react"; } from "@fluentui/react";
import React from "react"; import React from "react";
import * as DataModels from "../../../../../Contracts/DataModels"; import * as DataModels from "../../../../../Contracts/DataModels";
import { SubscriptionType } from "../../../../../Contracts/SubscriptionType";
import * as SharedConstants from "../../../../../Shared/Constants"; import * as SharedConstants from "../../../../../Shared/Constants";
import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../../../UserContext"; import { userContext } from "../../../../../UserContext";
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils"; import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
import { autoPilotThroughput1K } from "../../../../../Utils/AutoPilotUtils"; import { autoPilotThroughput1K } from "../../../../../Utils/AutoPilotUtils";
import { calculateEstimateNumber } from "../../../../../Utils/PricingUtils"; import { calculateEstimateNumber, usageInGB } from "../../../../../Utils/PricingUtils";
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon"; import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import { import {
PriceBreakdown, PriceBreakdown,
@@ -365,6 +366,29 @@ 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 => { private renderThroughputModeChoices = (): JSX.Element => {
const labelId = "settingsV2RadioButtonLabelId"; const labelId = "settingsV2RadioButtonLabelId";
return ( return (
@@ -637,6 +661,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
</Link> </Link>
</Text> </Text>
)} )}
{this.minRUperGBSurvey()}
{this.props.spendAckVisible && ( {this.props.spendAckVisible && (
<Checkbox <Checkbox
id="spendAckCheckBox" id="spendAckCheckBox"

View File

@@ -0,0 +1,64 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ScaleComponent renders with correct initial notification 1`] = `
<Stack
tokens={
{
"childrenGap": 20,
}
}
>
<StyledMessageBar
messageBarType={5}
>
<Text
id="throughputApplyLongDelayMessage"
styles={
{
"root": {
"color": "windowtext",
"fontSize": 14,
},
}
}
>
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to complete. View the latest status in Notifications.
<br />
Database: test, Container: test
, Current autoscale throughput: 100 - 1000 RU/s, Target autoscale throughput: 600 - 6000 RU/s
</Text>
</StyledMessageBar>
<Stack
tokens={
{
"childrenGap": 20,
}
}
>
<ThroughputInputAutoPilotV3Component
canExceedMaximumValue={true}
collectionName="test"
databaseName="test"
isAutoPilotSelected={false}
isEmulator={false}
isEnabled={true}
isFixed={false}
label="Throughput (6,000 - unlimited RU/s)"
maxAutoPilotThroughput={4000}
maxAutoPilotThroughputBaseline={4000}
maximum={1000000}
minimum={6000}
onAutoPilotSelected={[Function]}
onMaxAutoPilotThroughputChange={[Function]}
onScaleDiscardableChange={[Function]}
onScaleSaveableChange={[Function]}
onThroughputChange={[Function]}
spendAckChecked={false}
throughput={1000}
throughputBaseline={1000}
usageSizeInKB={100}
wasAutopilotOriginallySet={true}
/>
</Stack>
</Stack>
`;

View File

@@ -44,6 +44,7 @@ describe("SettingsUtils", () => {
readSettings: undefined, readSettings: undefined,
onSettingsClick: undefined, onSettingsClick: undefined,
loadOffer: undefined, loadOffer: undefined,
getPendingThroughputSplitNotification: undefined,
} as ViewModels.Database; } as ViewModels.Database;
}; };
newCollection.offer(undefined); newCollection.offer(undefined);

View File

@@ -4,14 +4,7 @@ import * as ViewModels from "../../../Contracts/ViewModels";
import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types"; import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
const zeroValue = 0; const zeroValue = 0;
export type isDirtyTypes = export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy | DataModels.ComputedProperties;
| boolean
| string
| number
| DataModels.IndexingPolicy
| DataModels.ComputedProperties
| DataModels.VectorEmbedding[]
| DataModels.FullTextPolicy;
export const TtlOff = "off"; export const TtlOff = "off";
export const TtlOn = "on"; export const TtlOn = "on";
export const TtlOnNoDefault = "on-nodefault"; export const TtlOnNoDefault = "on-nodefault";
@@ -57,11 +50,6 @@ export enum SettingsV2TabTypes {
ContainerVectorPolicyTab, ContainerVectorPolicyTab,
} }
export enum ContainerPolicyTabTypes {
VectorPolicyTab,
FullTextPolicyTab,
}
export interface IsComponentDirtyResult { export interface IsComponentDirtyResult {
isSaveable: boolean; isSaveable: boolean;
isDiscardable: boolean; isDiscardable: boolean;
@@ -166,7 +154,7 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
case SettingsV2TabTypes.ComputedPropertiesTab: case SettingsV2TabTypes.ComputedPropertiesTab:
return "Computed Properties"; return "Computed Properties";
case SettingsV2TabTypes.ContainerVectorPolicyTab: case SettingsV2TabTypes.ContainerVectorPolicyTab:
return "Container Policies"; return "Container Vector Policy (preview)";
default: default:
throw new Error(`Unknown tab ${tab}`); throw new Error(`Unknown tab ${tab}`);
} }

View File

@@ -46,8 +46,6 @@ export const collection = {
query: "query", query: "query",
}, },
]), ]),
vectorEmbeddingPolicy: ko.observable<DataModels.VectorEmbeddingPolicy>({} as DataModels.VectorEmbeddingPolicy),
fullTextPolicy: ko.observable<DataModels.FullTextPolicy>({} as DataModels.FullTextPolicy),
readSettings: () => { readSettings: () => {
return; return;
}, },

View File

@@ -55,7 +55,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
"databaseId": "test", "databaseId": "test",
"defaultTtl": [Function], "defaultTtl": [Function],
"fullTextPolicy": [Function],
"geospatialConfig": [Function], "geospatialConfig": [Function],
"getDatabase": [Function], "getDatabase": [Function],
"id": [Function], "id": [Function],
@@ -72,7 +71,6 @@ exports[`SettingsComponent renders 1`] = `
"readSettings": [Function], "readSettings": [Function],
"uniqueKeyPolicy": {}, "uniqueKeyPolicy": {},
"usageSizeInKB": [Function], "usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function],
} }
} }
isAutoPilotSelected={false} isAutoPilotSelected={false}
@@ -134,7 +132,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
"databaseId": "test", "databaseId": "test",
"defaultTtl": [Function], "defaultTtl": [Function],
"fullTextPolicy": [Function],
"geospatialConfig": [Function], "geospatialConfig": [Function],
"getDatabase": [Function], "getDatabase": [Function],
"id": [Function], "id": [Function],
@@ -151,7 +148,6 @@ exports[`SettingsComponent renders 1`] = `
"readSettings": [Function], "readSettings": [Function],
"uniqueKeyPolicy": {}, "uniqueKeyPolicy": {},
"usageSizeInKB": [Function], "usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function],
} }
} }
displayedTtlSeconds="5" displayedTtlSeconds="5"
@@ -253,7 +249,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
"databaseId": "test", "databaseId": "test",
"defaultTtl": [Function], "defaultTtl": [Function],
"fullTextPolicy": [Function],
"geospatialConfig": [Function], "geospatialConfig": [Function],
"getDatabase": [Function], "getDatabase": [Function],
"id": [Function], "id": [Function],
@@ -270,7 +265,6 @@ exports[`SettingsComponent renders 1`] = `
"readSettings": [Function], "readSettings": [Function],
"uniqueKeyPolicy": {}, "uniqueKeyPolicy": {},
"usageSizeInKB": [Function], "usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function],
} }
} }
explorer={ explorer={

View File

@@ -1,5 +1,4 @@
import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react"; import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
import { getWorkloadType } from "Common/DatabaseAccountUtility";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import React, { FunctionComponent, useEffect, useState } from "react"; import React, { FunctionComponent, useEffect, useState } from "react";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
@@ -35,15 +34,10 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
setIsThroughputCapExceeded, setIsThroughputCapExceeded,
onCostAcknowledgeChange, onCostAcknowledgeChange,
}: ThroughputInputProps) => { }: ThroughputInputProps) => {
const defaultThroughput: number =
isFreeTier ||
isQuickstart ||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(getWorkloadType())
? AutoPilotUtils.autoPilotThroughput1K
: AutoPilotUtils.autoPilotThroughput4K;
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true); const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
const [throughput, setThroughput] = useState<number>(defaultThroughput); const [throughput, setThroughput] = useState<number>(
isFreeTier || isQuickstart ? AutoPilotUtils.autoPilotThroughput1K : AutoPilotUtils.autoPilotThroughput4K,
);
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false); const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
const [throughputError, setThroughputError] = useState<string>(""); const [throughputError, setThroughputError] = useState<string>("");
const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0); const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0);
@@ -53,6 +47,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit; const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1; const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
useEffect(() => { useEffect(() => {
// throughput cap check for the initial state // throughput cap check for the initial state
let totalThroughput = 0; let totalThroughput = 0;
@@ -162,6 +157,9 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => { const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
if (mode === "Autoscale") { if (mode === "Autoscale") {
const defaultThroughput = isFreeTier
? AutoPilotUtils.autoPilotThroughput1K
: AutoPilotUtils.autoPilotThroughput4K;
setThroughput(defaultThroughput); setThroughput(defaultThroughput);
setIsAutoScaleSelected(true); setIsAutoScaleSelected(true);
setThroughputValue(defaultThroughput); setThroughputValue(defaultThroughput);

View File

@@ -17,7 +17,7 @@ export const useTreeStyles = makeStyles({
minWidth: "100%", minWidth: "100%",
rowGap: "0px", rowGap: "0px",
paddingTop: "0px", paddingTop: "0px",
[treeIconWidth]: "16px", [treeIconWidth]: "20px",
[leafNodeSpacing]: "24px", [leafNodeSpacing]: "24px",
}, },
nodeIcon: { nodeIcon: {
@@ -25,13 +25,12 @@ export const useTreeStyles = makeStyles({
height: `var(${treeIconWidth})`, height: `var(${treeIconWidth})`,
}, },
treeItem: {}, treeItem: {},
nodeLabel: { nodeLabel: {},
whiteSpace: "nowrap", // Don't wrap text, there will be a scrollbar.
},
treeItemLayout: { treeItemLayout: {
fontSize: tokens.fontSizeBase300, fontSize: tokens.fontSizeBase300,
height: tokens.layoutRowHeight, height: tokens.layoutRowHeight,
...cosmosShorthands.borderBottom(), ...cosmosShorthands.borderBottom(),
paddingLeft: `calc(var(${treeItemLevelToken}, 1) * ${tokens.spacingHorizontalXXL})`,
// Some sneaky CSS variables stuff to change the background color of the action button on hover. // Some sneaky CSS variables stuff to change the background color of the action button on hover.
[actionButtonBackground]: tokens.colorNeutralBackground1, [actionButtonBackground]: tokens.colorNeutralBackground1,

View File

@@ -23,7 +23,7 @@ import { useCallback } from "react";
export interface TreeNodeMenuItem { export interface TreeNodeMenuItem {
label: string; label: string;
onClick: (value?: React.RefObject<HTMLElement>) => void; onClick: () => void;
iconSrc?: string; iconSrc?: string;
isDisabled?: boolean; isDisabled?: boolean;
styleClass?: string; styleClass?: string;
@@ -74,7 +74,6 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
openItems, openItems,
}: TreeNodeComponentProps): JSX.Element => { }: TreeNodeComponentProps): JSX.Element => {
const [isLoading, setIsLoading] = React.useState<boolean>(false); const [isLoading, setIsLoading] = React.useState<boolean>(false);
const contextMenuRef = React.useRef<HTMLButtonElement>(null);
const treeStyles = useTreeStyles(); const treeStyles = useTreeStyles();
const getSortedChildren = (treeNode: TreeNode): TreeNode[] => { const getSortedChildren = (treeNode: TreeNode): TreeNode[] => {
@@ -142,7 +141,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
data-test={`TreeNode/ContextMenuItem:${menuItem.label}`} data-test={`TreeNode/ContextMenuItem:${menuItem.label}`}
disabled={menuItem.isDisabled} disabled={menuItem.isDisabled}
key={menuItem.label} key={menuItem.label}
onClick={() => menuItem.onClick(contextMenuRef)} onClick={menuItem.onClick}
> >
{menuItem.label} {menuItem.label}
</MenuItem> </MenuItem>
@@ -150,19 +149,18 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
// We use the expandIcon slot to hold the node icon too. // We use the expandIcon slot to hold the node icon too.
// We only show a node icon for leaf nodes, even if a branch node has an iconSrc. // We only show a node icon for leaf nodes, even if a branch node has an iconSrc.
const treeIcon = const expandIcon = isLoading ? (
node.iconSrc === undefined ? undefined : typeof node.iconSrc === "string" ? ( <Spinner size="extra-tiny" />
) : !isBranch ? (
typeof node.iconSrc === "string" ? (
<img src={node.iconSrc} className={treeStyles.nodeIcon} alt="" /> <img src={node.iconSrc} className={treeStyles.nodeIcon} alt="" />
) : ( ) : (
node.iconSrc node.iconSrc
); )
) : openItems.includes(treeNodeId) ? (
const expandIcon = isLoading ? ( <ChevronDown20Regular />
<Spinner size="extra-tiny" />
) : !isBranch ? undefined : openItems.includes(treeNodeId) ? (
<ChevronDown20Regular data-test="TreeNode/CollapseIcon" />
) : ( ) : (
<ChevronRight20Regular data-text="TreeNode/ExpandIcon" /> <ChevronRight20Regular />
); );
const treeItem = ( const treeItem = (
@@ -176,6 +174,7 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
<TreeItemLayout <TreeItemLayout
className={mergeClasses( className={mergeClasses(
treeStyles.treeItemLayout, treeStyles.treeItemLayout,
expandIcon ? undefined : treeStyles.treeItemLayoutNoIcon,
shouldShowAsSelected && treeStyles.selectedItem, shouldShowAsSelected && treeStyles.selectedItem,
node.className && treeStyles[node.className], node.className && treeStyles[node.className],
)} )}
@@ -191,7 +190,6 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
className={mergeClasses(treeStyles.actionsButton, shouldShowAsSelected && treeStyles.selectedItem)} className={mergeClasses(treeStyles.actionsButton, shouldShowAsSelected && treeStyles.selectedItem)}
data-test="TreeNode/ContextMenuTrigger" data-test="TreeNode/ContextMenuTrigger"
appearance="subtle" appearance="subtle"
ref={contextMenuRef}
icon={<MoreHorizontal20Regular />} icon={<MoreHorizontal20Regular />}
/> />
</MenuTrigger> </MenuTrigger>
@@ -202,13 +200,12 @@ export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
), ),
} }
} }
iconBefore={treeIcon}
expandIcon={expandIcon} expandIcon={expandIcon}
> >
<span className={treeStyles.nodeLabel}>{node.label}</span> <span className={treeStyles.nodeLabel}>{node.label}</span>
</TreeItemLayout> </TreeItemLayout>
{!node.isLoading && node.children?.length > 0 && ( {!node.isLoading && node.children?.length > 0 && (
<Tree data-test={`Tree:${treeNodeId}`} className={treeStyles.tree}> <Tree className={treeStyles.tree}>
{getSortedChildren(node).map((childNode: TreeNode) => ( {getSortedChildren(node).map((childNode: TreeNode) => (
<TreeNodeComponent <TreeNodeComponent
openItems={openItems} openItems={openItems}

View File

@@ -10,23 +10,12 @@ exports[`TreeNodeComponent does not render children if the node is loading 1`] =
> >
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-test="TreeNode:root"
expandIcon={ expandIcon={<ChevronRight20Regular />}
<ChevronRight20Regular
data-text="TreeNode/ExpandIcon"
/>
}
iconBefore={
<img
alt=""
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
src="rootIcon"
/>
}
> >
<span <span
className="___1h29e9h_0000000 fz5stix" className=""
> >
rootLabel rootLabel
</span> </span>
@@ -144,7 +133,6 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -163,7 +151,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
"itemType": "branch", "itemType": "branch",
"layoutRef": { "layoutRef": {
"current": <div "current": <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-test="TreeNode:root"
> >
<div <div
@@ -173,7 +161,6 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -186,21 +173,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
/> />
</svg> </svg>
</div> </div>
<div
aria-hidden="true"
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
>
<img
alt=""
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
src="rootIcon"
/>
</div>
<div <div
class="fui-TreeItemLayout__main rklbe47" class="fui-TreeItemLayout__main rklbe47"
> >
<span <span
class="___1h29e9h_0000000 fz5stix" class=""
> >
rootLabel rootLabel
</span> </span>
@@ -225,7 +202,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-test="TreeNode:root"
> >
<div <div
@@ -235,7 +212,6 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -248,29 +224,18 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
/> />
</svg> </svg>
</div> </div>
<div
aria-hidden="true"
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
>
<img
alt=""
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
src="rootIcon"
/>
</div>
<div <div
class="fui-TreeItemLayout__main rklbe47" class="fui-TreeItemLayout__main rklbe47"
> >
<span <span
class="___1h29e9h_0000000 fz5stix" class=""
> >
rootLabel rootLabel
</span> </span>
</div> </div>
</div> </div>
<div <div
class="fui-Tree rnv2ez3 ___17a32do_7zrvj80 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n" class="fui-Tree rnv2ez3 ___jy13a00_lpffjy0 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
data-test="Tree:root"
role="tree" role="tree"
> >
<div <div
@@ -283,7 +248,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
tabindex="0" tabindex="0"
> >
<div <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child1Label" data-test="TreeNode:root/child1Label"
> >
<div <div
@@ -293,7 +258,6 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -306,21 +270,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
/> />
</svg> </svg>
</div> </div>
<div
aria-hidden="true"
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
>
<img
alt=""
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
src="child1Icon"
/>
</div>
<div <div
class="fui-TreeItemLayout__main rklbe47" class="fui-TreeItemLayout__main rklbe47"
> >
<span <span
class="___1h29e9h_0000000 fz5stix" class=""
> >
child1Label child1Label
</span> </span>
@@ -337,7 +291,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child2LoadingLabel" data-test="TreeNode:root/child2LoadingLabel"
> >
<div <div
@@ -347,7 +301,6 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -360,21 +313,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
/> />
</svg> </svg>
</div> </div>
<div
aria-hidden="true"
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
>
<img
alt=""
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
src="child2LoadingIcon"
/>
</div>
<div <div
class="fui-TreeItemLayout__main rklbe47" class="fui-TreeItemLayout__main rklbe47"
> >
<span <span
class="___1h29e9h_0000000 fz5stix" class=""
> >
child2LoadingLabel child2LoadingLabel
</span> </span>
@@ -390,7 +333,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vrg09j fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child3ExpandingLabel" data-test="TreeNode:root/child3ExpandingLabel"
> >
<div <div
@@ -410,21 +353,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
</span> </span>
</div> </div>
</div> </div>
<div
aria-hidden="true"
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
>
<img
alt=""
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
src="child3ExpandingIcon"
/>
</div>
<div <div
class="fui-TreeItemLayout__main rklbe47" class="fui-TreeItemLayout__main rklbe47"
> >
<span <span
class="___1h29e9h_0000000 fz5stix" class=""
> >
child3ExpandingLabel child3ExpandingLabel
</span> </span>
@@ -440,36 +373,22 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
> >
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-test="TreeNode:root"
expandIcon={ expandIcon={<ChevronRight20Regular />}
<ChevronRight20Regular
data-text="TreeNode/ExpandIcon"
/>
}
iconBefore={
<img
alt=""
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
src="rootIcon"
/>
}
> >
<div <div
className="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-test="TreeNode:root"
> >
<div <div
aria-hidden={true} aria-hidden={true}
className="fui-TreeItemLayout__expandIcon rh4pu5o" className="fui-TreeItemLayout__expandIcon rh4pu5o"
> >
<ChevronRight20Regular <ChevronRight20Regular>
data-text="TreeNode/ExpandIcon"
>
<svg <svg
aria-hidden={true} aria-hidden={true}
className="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" className="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -483,21 +402,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
</svg> </svg>
</ChevronRight20Regular> </ChevronRight20Regular>
</div> </div>
<div
aria-hidden={true}
className="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
>
<img
alt=""
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
src="rootIcon"
/>
</div>
<div <div
className="fui-TreeItemLayout__main rklbe47" className="fui-TreeItemLayout__main rklbe47"
> >
<span <span
className="___1h29e9h_0000000 fz5stix" className=""
> >
rootLabel rootLabel
</span> </span>
@@ -505,8 +414,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
</div> </div>
</TreeItemLayout> </TreeItemLayout>
<Tree <Tree
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n" className="___jy13a00_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
data-test="Tree:root"
> >
<TreeProvider <TreeProvider
value={ value={
@@ -573,8 +481,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
} }
> >
<div <div
className="fui-Tree rnv2ez3 ___17a32do_7zrvj80 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n" className="fui-Tree rnv2ez3 ___jy13a00_lpffjy0 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
data-test="Tree:root"
role="tree" role="tree"
> >
<TreeNodeComponent <TreeNodeComponent
@@ -642,7 +549,6 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -661,7 +567,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
"itemType": "branch", "itemType": "branch",
"layoutRef": { "layoutRef": {
"current": <div "current": <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child1Label" data-test="TreeNode:root/child1Label"
> >
<div <div
@@ -671,7 +577,6 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -684,21 +589,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
/> />
</svg> </svg>
</div> </div>
<div
aria-hidden="true"
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
>
<img
alt=""
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
src="child1Icon"
/>
</div>
<div <div
class="fui-TreeItemLayout__main rklbe47" class="fui-TreeItemLayout__main rklbe47"
> >
<span <span
class="___1h29e9h_0000000 fz5stix" class=""
> >
child1Label child1Label
</span> </span>
@@ -723,7 +618,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
tabindex="0" tabindex="0"
> >
<div <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child1Label" data-test="TreeNode:root/child1Label"
> >
<div <div
@@ -733,7 +628,6 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -746,21 +640,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
/> />
</svg> </svg>
</div> </div>
<div
aria-hidden="true"
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
>
<img
alt=""
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
src="child1Icon"
/>
</div>
<div <div
class="fui-TreeItemLayout__main rklbe47" class="fui-TreeItemLayout__main rklbe47"
> >
<span <span
class="___1h29e9h_0000000 fz5stix" class=""
> >
child1Label child1Label
</span> </span>
@@ -774,36 +658,22 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
> >
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child1Label" data-test="TreeNode:root/child1Label"
expandIcon={ expandIcon={<ChevronRight20Regular />}
<ChevronRight20Regular
data-text="TreeNode/ExpandIcon"
/>
}
iconBefore={
<img
alt=""
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
src="child1Icon"
/>
}
> >
<div <div
className="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child1Label" data-test="TreeNode:root/child1Label"
> >
<div <div
aria-hidden={true} aria-hidden={true}
className="fui-TreeItemLayout__expandIcon rh4pu5o" className="fui-TreeItemLayout__expandIcon rh4pu5o"
> >
<ChevronRight20Regular <ChevronRight20Regular>
data-text="TreeNode/ExpandIcon"
>
<svg <svg
aria-hidden={true} aria-hidden={true}
className="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" className="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -817,21 +687,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
</svg> </svg>
</ChevronRight20Regular> </ChevronRight20Regular>
</div> </div>
<div
aria-hidden={true}
className="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
>
<img
alt=""
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
src="child1Icon"
/>
</div>
<div <div
className="fui-TreeItemLayout__main rklbe47" className="fui-TreeItemLayout__main rklbe47"
> >
<span <span
className="___1h29e9h_0000000 fz5stix" className=""
> >
child1Label child1Label
</span> </span>
@@ -839,8 +699,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
</div> </div>
</TreeItemLayout> </TreeItemLayout>
<Tree <Tree
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n" className="___jy13a00_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
data-test="Tree:root/child1Label"
> >
<TreeProvider <TreeProvider
value={ value={
@@ -913,7 +772,6 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -932,7 +790,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
"itemType": "branch", "itemType": "branch",
"layoutRef": { "layoutRef": {
"current": <div "current": <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child2LoadingLabel" data-test="TreeNode:root/child2LoadingLabel"
> >
<div <div
@@ -942,7 +800,6 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -955,21 +812,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
/> />
</svg> </svg>
</div> </div>
<div
aria-hidden="true"
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
>
<img
alt=""
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
src="child2LoadingIcon"
/>
</div>
<div <div
class="fui-TreeItemLayout__main rklbe47" class="fui-TreeItemLayout__main rklbe47"
> >
<span <span
class="___1h29e9h_0000000 fz5stix" class=""
> >
child2LoadingLabel child2LoadingLabel
</span> </span>
@@ -994,7 +841,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child2LoadingLabel" data-test="TreeNode:root/child2LoadingLabel"
> >
<div <div
@@ -1004,7 +851,6 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" class="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -1017,21 +863,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
/> />
</svg> </svg>
</div> </div>
<div
aria-hidden="true"
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
>
<img
alt=""
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
src="child2LoadingIcon"
/>
</div>
<div <div
class="fui-TreeItemLayout__main rklbe47" class="fui-TreeItemLayout__main rklbe47"
> >
<span <span
class="___1h29e9h_0000000 fz5stix" class=""
> >
child2LoadingLabel child2LoadingLabel
</span> </span>
@@ -1045,36 +881,22 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
> >
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child2LoadingLabel" data-test="TreeNode:root/child2LoadingLabel"
expandIcon={ expandIcon={<ChevronRight20Regular />}
<ChevronRight20Regular
data-text="TreeNode/ExpandIcon"
/>
}
iconBefore={
<img
alt=""
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
src="child2LoadingIcon"
/>
}
> >
<div <div
className="fui-TreeItemLayout r1bx0xiv ___9uolwu0_9b0r4g0 fk6fouc fkhj508 figsok6 f1i3iumi fo100m9 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vtp8mg fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child2LoadingLabel" data-test="TreeNode:root/child2LoadingLabel"
> >
<div <div
aria-hidden={true} aria-hidden={true}
className="fui-TreeItemLayout__expandIcon rh4pu5o" className="fui-TreeItemLayout__expandIcon rh4pu5o"
> >
<ChevronRight20Regular <ChevronRight20Regular>
data-text="TreeNode/ExpandIcon"
>
<svg <svg
aria-hidden={true} aria-hidden={true}
className="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0" className="___12fm75w_v8ls9a0 f1w7gpdv fez10in fg4l7m0"
data-text="TreeNode/ExpandIcon"
fill="currentColor" fill="currentColor"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -1088,21 +910,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
</svg> </svg>
</ChevronRight20Regular> </ChevronRight20Regular>
</div> </div>
<div
aria-hidden={true}
className="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
>
<img
alt=""
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
src="child2LoadingIcon"
/>
</div>
<div <div
className="fui-TreeItemLayout__main rklbe47" className="fui-TreeItemLayout__main rklbe47"
> >
<span <span
className="___1h29e9h_0000000 fz5stix" className=""
> >
child2LoadingLabel child2LoadingLabel
</span> </span>
@@ -1187,7 +999,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
"itemType": "leaf", "itemType": "leaf",
"layoutRef": { "layoutRef": {
"current": <div "current": <div
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vrg09j fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child3ExpandingLabel" data-test="TreeNode:root/child3ExpandingLabel"
> >
<div <div
@@ -1207,21 +1019,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
</span> </span>
</div> </div>
</div> </div>
<div
aria-hidden="true"
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
>
<img
alt=""
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
src="child3ExpandingIcon"
/>
</div>
<div <div
class="fui-TreeItemLayout__main rklbe47" class="fui-TreeItemLayout__main rklbe47"
> >
<span <span
class="___1h29e9h_0000000 fz5stix" class=""
> >
child3ExpandingLabel child3ExpandingLabel
</span> </span>
@@ -1245,7 +1047,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
tabindex="-1" tabindex="-1"
> >
<div <div
class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" class="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vrg09j fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child3ExpandingLabel" data-test="TreeNode:root/child3ExpandingLabel"
> >
<div <div
@@ -1265,21 +1067,11 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
</span> </span>
</div> </div>
</div> </div>
<div
aria-hidden="true"
class="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl"
>
<img
alt=""
class="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
src="child3ExpandingIcon"
/>
</div>
<div <div
class="fui-TreeItemLayout__main rklbe47" class="fui-TreeItemLayout__main rklbe47"
> >
<span <span
class="___1h29e9h_0000000 fz5stix" class=""
> >
child3ExpandingLabel child3ExpandingLabel
</span> </span>
@@ -1293,9 +1085,9 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
> >
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child3ExpandingLabel" data-test="TreeNode:root/child3ExpandingLabel"
iconBefore={ expandIcon={
<img <img
alt="" alt=""
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b" className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
@@ -1304,12 +1096,12 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
} }
> >
<div <div
className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_vz3p260 fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="fui-TreeItemLayout r1bx0xiv ___dxcrnh0_1vrg09j fk6fouc fkhj508 figsok6 f1i3iumi f1k1erfc fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root/child3ExpandingLabel" data-test="TreeNode:root/child3ExpandingLabel"
> >
<div <div
aria-hidden={true} aria-hidden={true}
className="fui-TreeItemLayout__iconBefore rphzgg1 ___1lqnc2u_1hdcey2 f7x41pl" className="fui-TreeItemLayout__expandIcon rh4pu5o"
> >
<img <img
alt="" alt=""
@@ -1321,7 +1113,7 @@ exports[`TreeNodeComponent fully renders a tree 1`] = `
className="fui-TreeItemLayout__main rklbe47" className="fui-TreeItemLayout__main rklbe47"
> >
<span <span
className="___1h29e9h_0000000 fz5stix" className=""
> >
child3ExpandingLabel child3ExpandingLabel
</span> </span>
@@ -1352,9 +1144,9 @@ exports[`TreeNodeComponent renders a loading spinner if the node is loading: loa
> >
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-test="TreeNode:root"
iconBefore={ expandIcon={
<img <img
alt="" alt=""
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b" className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
@@ -1363,7 +1155,7 @@ exports[`TreeNodeComponent renders a loading spinner if the node is loading: loa
} }
> >
<span <span
className="___1h29e9h_0000000 fz5stix" className=""
> >
rootLabel rootLabel
</span> </span>
@@ -1381,23 +1173,16 @@ exports[`TreeNodeComponent renders a loading spinner if the node is loading: loa
> >
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-test="TreeNode:root"
expandIcon={ expandIcon={
<Spinner <Spinner
size="extra-tiny" size="extra-tiny"
/> />
} }
iconBefore={
<img
alt=""
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
src="rootIcon"
/>
}
> >
<span <span
className="___1h29e9h_0000000 fz5stix" className=""
> >
rootLabel rootLabel
</span> </span>
@@ -1415,23 +1200,12 @@ exports[`TreeNodeComponent renders a node as expandable if it has empty, but def
> >
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-test="TreeNode:root"
expandIcon={ expandIcon={<ChevronRight20Regular />}
<ChevronRight20Regular
data-text="TreeNode/ExpandIcon"
/>
}
iconBefore={
<img
alt=""
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
src="rootIcon"
/>
}
> >
<span <span
className="___1h29e9h_0000000 fz5stix" className=""
> >
rootLabel rootLabel
</span> </span>
@@ -1478,14 +1252,14 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
<MenuList> <MenuList>
<MenuItem <MenuItem
data-test="TreeNode/ContextMenuItem:enabledItem" data-test="TreeNode/ContextMenuItem:enabledItem"
onClick={[Function]} onClick={[MockFunction enabledItemClick]}
> >
enabledItem enabledItem
</MenuItem> </MenuItem>
<MenuItem <MenuItem
data-test="TreeNode/ContextMenuItem:disabledItem" data-test="TreeNode/ContextMenuItem:disabledItem"
disabled={true} disabled={true}
onClick={[Function]} onClick={[MockFunction disabledItemClick]}
> >
disabledItem disabledItem
</MenuItem> </MenuItem>
@@ -1495,9 +1269,9 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
"className": "___1r8p62d_0000000 f1xg1ack f1e31b4d", "className": "___1r8p62d_0000000 f1xg1ack f1e31b4d",
} }
} }
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-test="TreeNode:root"
iconBefore={ expandIcon={
<img <img
alt="" alt=""
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b" className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
@@ -1506,7 +1280,7 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
} }
> >
<span <span
className="___1h29e9h_0000000 fz5stix" className=""
> >
rootLabel rootLabel
</span> </span>
@@ -1518,7 +1292,7 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
<MenuItem <MenuItem
data-test="TreeNode/ContextMenuItem:enabledItem" data-test="TreeNode/ContextMenuItem:enabledItem"
key="enabledItem" key="enabledItem"
onClick={[Function]} onClick={[MockFunction enabledItemClick]}
> >
enabledItem enabledItem
</MenuItem> </MenuItem>
@@ -1526,7 +1300,7 @@ exports[`TreeNodeComponent renders a node with a menu 1`] = `
data-test="TreeNode/ContextMenuItem:disabledItem" data-test="TreeNode/ContextMenuItem:disabledItem"
disabled={true} disabled={true}
key="disabledItem" key="disabledItem"
onClick={[Function]} onClick={[MockFunction disabledItemClick]}
> >
disabledItem disabledItem
</MenuItem> </MenuItem>
@@ -1545,9 +1319,9 @@ exports[`TreeNodeComponent renders a single node 1`] = `
> >
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-test="TreeNode:root"
iconBefore={ expandIcon={
<img <img
alt="" alt=""
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b" className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
@@ -1556,7 +1330,7 @@ exports[`TreeNodeComponent renders a single node 1`] = `
} }
> >
<span <span
className="___1h29e9h_0000000 fz5stix" className=""
> >
rootLabel rootLabel
</span> </span>
@@ -1574,9 +1348,9 @@ exports[`TreeNodeComponent renders an icon if the node has one 1`] = `
> >
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-test="TreeNode:root"
iconBefore={ expandIcon={
<img <img
alt="" alt=""
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b" className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
@@ -1585,7 +1359,7 @@ exports[`TreeNodeComponent renders an icon if the node has one 1`] = `
} }
> >
<span <span
className="___1h29e9h_0000000 fz5stix" className=""
> >
rootLabel rootLabel
</span> </span>
@@ -1603,30 +1377,18 @@ exports[`TreeNodeComponent renders selected parent node as selected if no descen
> >
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___rq9vxg0_1ykn2d2 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1nfm20t f1do9gdl" className="___kqkdor0_ihxn0o0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1nfm20t f1do9gdl"
data-test="TreeNode:root" data-test="TreeNode:root"
expandIcon={ expandIcon={<ChevronRight20Regular />}
<ChevronRight20Regular
data-text="TreeNode/ExpandIcon"
/>
}
iconBefore={
<img
alt=""
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
src="rootIcon"
/>
}
> >
<span <span
className="___1h29e9h_0000000 fz5stix" className=""
> >
rootLabel rootLabel
</span> </span>
</TreeItemLayout> </TreeItemLayout>
<Tree <Tree
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n" className="___jy13a00_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
data-test="Tree:root"
> >
<TreeNodeComponent <TreeNodeComponent
key="child1Label" key="child1Label"
@@ -1686,30 +1448,18 @@ exports[`TreeNodeComponent renders selected parent node as unselected if any des
> >
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___z7owk70_14ep1pe fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1do9gdl" className="___1kqyw53_iy2icj0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1do9gdl"
data-test="TreeNode:root" data-test="TreeNode:root"
expandIcon={ expandIcon={<ChevronRight20Regular />}
<ChevronRight20Regular
data-text="TreeNode/ExpandIcon"
/>
}
iconBefore={
<img
alt=""
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
src="rootIcon"
/>
}
> >
<span <span
className="___1h29e9h_0000000 fz5stix" className=""
> >
rootLabel rootLabel
</span> </span>
</TreeItemLayout> </TreeItemLayout>
<Tree <Tree
className="___17a32do_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp f19d5ny4 fzz4f4n" className="___jy13a00_0000000 f1acs6jw f11qra4b fepn2xe f1nbblvp fhxm7u5 fzz4f4n"
data-test="Tree:root"
> >
<TreeNodeComponent <TreeNodeComponent
key="child1Label" key="child1Label"
@@ -1770,9 +1520,9 @@ exports[`TreeNodeComponent renders single selected leaf node as selected 1`] = `
> >
<TreeItemLayout <TreeItemLayout
actions={false} actions={false}
className="___rq9vxg0_1ykn2d2 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1n8cmsf f1ktbui8 f1nfm20t f1do9gdl" className="___kqkdor0_ihxn0o0 fkhj508 fbv8p0b f1f09k3d fg706s2 frpde29 f1k1erfc f1n8cmsf f1ktbui8 f1nfm20t f1do9gdl"
data-test="TreeNode:root" data-test="TreeNode:root"
iconBefore={ expandIcon={
<img <img
alt="" alt=""
className="___i3nbrx0_0000000 f1do9gdl fbv8p0b" className="___i3nbrx0_0000000 f1do9gdl fbv8p0b"
@@ -1781,7 +1531,7 @@ exports[`TreeNodeComponent renders single selected leaf node as selected 1`] = `
} }
> >
<span <span
className="___1h29e9h_0000000 fz5stix" className=""
> >
rootLabel rootLabel
</span> </span>

View File

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

View File

@@ -1,7 +1,6 @@
import * as msal from "@azure/msal-browser"; import * as msal from "@azure/msal-browser";
import { Link } from "@fluentui/react/lib/Link"; import { Link } from "@fluentui/react/lib/Link";
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility"; import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { Environment, getEnvironment } from "Common/EnvironmentUtility";
import { sendMessage } from "Common/MessageHandler"; import { sendMessage } from "Common/MessageHandler";
import { Platform, configContext } from "ConfigContext"; import { Platform, configContext } from "ConfigContext";
import { MessageTypes } from "Contracts/ExplorerContracts"; import { MessageTypes } from "Contracts/ExplorerContracts";
@@ -10,7 +9,7 @@ import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCop
import { IGalleryItem } from "Juno/JunoClient"; import { IGalleryItem } from "Juno/JunoClient";
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil"; import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils"; import { acquireTokenWithMsal, getMsalInstance } from "Utils/AuthorizationUtils";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils"; import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts"; import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
@@ -35,7 +34,7 @@ import { PhoenixClient } from "../Phoenix/PhoenixClient";
import * as ExplorerSettings from "../Shared/ExplorerSettings"; import * as ExplorerSettings from "../Shared/ExplorerSettings";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { updateUserContext, userContext } from "../UserContext"; import { isAccountNewerThanThresholdInMs, updateUserContext, userContext } from "../UserContext";
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils"; import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
import { stringToBlob } from "../Utils/BlobUtils"; import { stringToBlob } from "../Utils/BlobUtils";
import { isCapabilityEnabled } from "../Utils/CapabilityUtils"; import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
@@ -259,8 +258,25 @@ export default class Explorer {
public async openLoginForEntraIDPopUp(): Promise<void> { public async openLoginForEntraIDPopUp(): Promise<void> {
if (userContext.databaseAccount.properties?.documentEndpoint) { if (userContext.databaseAccount.properties?.documentEndpoint) {
const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(
/\/$/,
"/.default",
);
const msalInstance = await getMsalInstance();
try { try {
const aadToken = await acquireMsalTokenForAccount(userContext.databaseAccount, false); const response = await msalInstance.loginPopup({
redirectUri: configContext.msalRedirectURI,
scopes: [],
});
localStorage.setItem("cachedTenantId", response.tenantId);
const cachedAccount = msalInstance.getAllAccounts()?.[0];
msalInstance.setActiveAccount(cachedAccount);
const aadToken = await acquireTokenWithMsal(msalInstance, {
forceRefresh: true,
scopes: [hrefEndpoint],
authority: `${configContext.AAD_ENDPOINT}${localStorage.getItem("cachedTenantId")}`,
});
updateUserContext({ aadToken: aadToken }); updateUserContext({ aadToken: aadToken });
useDataPlaneRbac.setState({ aadTokenUpdated: true }); useDataPlaneRbac.setState({ aadTokenUpdated: true });
} catch (error) { } catch (error) {
@@ -278,6 +294,37 @@ 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> { public async openCESCVAFeedbackBlade(): Promise<void> {
sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade }); sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade });
Logger.logInfo( Logger.logInfo(
@@ -1072,7 +1119,7 @@ export default class Explorer {
} }
} }
public openUploadItemsPane(): void { public openUploadItemsPanePane(): void {
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane />); useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane />);
} }
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void { public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
@@ -1103,7 +1150,7 @@ export default class Explorer {
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") { if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
userContext.authType === AuthType.ResourceToken userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken() ? this.refreshDatabaseForResourceToken()
: await this.refreshAllDatabases(); // await: we rely on the databases to be loaded before restoring the tabs further in the flow : this.refreshAllDatabases();
} }
await useNotebook.getState().refreshNotebooksEnabledStateForAccount(); await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
@@ -1131,11 +1178,7 @@ export default class Explorer {
} }
public async configureCopilot(): Promise<void> { public async configureCopilot(): Promise<void> {
if ( if (userContext.apiType !== "SQL" || !userContext.subscriptionId) {
userContext.apiType !== "SQL" ||
!userContext.subscriptionId ||
![Environment.Development, Environment.Mpac, Environment.Prod].includes(getEnvironment())
) {
return; return;
} }
const copilotEnabledPromise = getCopilotEnabled(); const copilotEnabledPromise = getCopilotEnabled();

View File

@@ -167,11 +167,15 @@ export function createContextCommandBarButtons(
} }
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] { export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [ const buttons: CommandButtonComponentProps[] =
configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly
? []
: [
{ {
iconSrc: SettingsIcon, iconSrc: SettingsIcon,
iconAlt: "Settings", iconAlt: "Settings",
onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />), onCommandClick: () =>
useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
commandButtonLabel: undefined, commandButtonLabel: undefined,
ariaLabel: "Settings", ariaLabel: "Settings",
tooltipText: "Settings", tooltipText: "Settings",

View File

@@ -131,7 +131,6 @@ export class NotificationConsoleComponent extends React.Component<
</div> </div>
<div <div
className="expandCollapseButton" className="expandCollapseButton"
data-test="NotificationConsole/ExpandCollapseButton"
role="button" role="button"
tabIndex={0} tabIndex={0}
aria-label={"console button" + (this.props.isConsoleExpanded ? " expanded" : " collapsed")} aria-label={"console button" + (this.props.isConsoleExpanded ? " expanded" : " collapsed")}
@@ -148,7 +147,7 @@ export class NotificationConsoleComponent extends React.Component<
height={this.props.isConsoleExpanded ? "auto" : 0} height={this.props.isConsoleExpanded ? "auto" : 0}
onAnimationEnd={this.onConsoleWasExpanded} onAnimationEnd={this.onConsoleWasExpanded}
> >
<div data-test="NotificationConsole/Contents" className="notificationConsoleContents"> <div className="notificationConsoleContents">
<div className="notificationConsoleControls"> <div className="notificationConsoleControls">
<Dropdown <Dropdown
label="Filter:" label="Filter:"

View File

@@ -74,7 +74,6 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
aria-expanded={true} aria-expanded={true}
aria-label="console button collapsed" aria-label="console button collapsed"
className="expandCollapseButton" className="expandCollapseButton"
data-test="NotificationConsole/ExpandCollapseButton"
role="button" role="button"
tabIndex={0} tabIndex={0}
> >
@@ -110,7 +109,6 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
> >
<div <div
className="notificationConsoleContents" className="notificationConsoleContents"
data-test="NotificationConsole/Contents"
> >
<div <div
className="notificationConsoleControls" className="notificationConsoleControls"
@@ -247,7 +245,6 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
aria-expanded={true} aria-expanded={true}
aria-label="console button collapsed" aria-label="console button collapsed"
className="expandCollapseButton" className="expandCollapseButton"
data-test="NotificationConsole/ExpandCollapseButton"
role="button" role="button"
tabIndex={0} tabIndex={0}
> >
@@ -283,7 +280,6 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
> >
<div <div
className="notificationConsoleContents" className="notificationConsoleContents"
data-test="NotificationConsole/Contents"
> >
<div <div
className="notificationConsoleControls" className="notificationConsoleControls"

View File

@@ -1,13 +1,13 @@
import { clear, collectionWasOpened, getItems, Type } from "Explorer/MostRecentActivity/MostRecentActivity";
import { observable } from "knockout"; import { observable } from "knockout";
import { mostRecentActivity } from "./MostRecentActivity";
describe("MostRecentActivity", () => { describe("MostRecentActivity", () => {
const accountName = "some account"; const accountId = "some account";
beforeEach(() => clear(accountName)); beforeEach(() => mostRecentActivity.clear(accountId));
it("Has no items at first", () => { it("Has no items at first", () => {
expect(getItems(accountName)).toStrictEqual([]); expect(mostRecentActivity.getItems(accountId)).toStrictEqual([]);
}); });
it("Can record collections being opened", () => { it("Can record collections being opened", () => {
@@ -18,9 +18,9 @@ describe("MostRecentActivity", () => {
databaseId, databaseId,
}; };
collectionWasOpened(accountName, collection); mostRecentActivity.collectionWasOpened(accountId, collection);
const activity = getItems(accountName); const activity = mostRecentActivity.getItems(accountId);
expect(activity).toEqual([ expect(activity).toEqual([
expect.objectContaining({ expect.objectContaining({
collectionId, collectionId,
@@ -29,24 +29,58 @@ describe("MostRecentActivity", () => {
]); ]);
}); });
it("Does not store duplicate entries", () => { it("Can record notebooks being opened", () => {
const collectionId = "some collection"; const name = "some notebook";
const databaseId = "some database"; const path = "some path";
const collection = { const notebook = { name, path };
id: observable(collectionId),
databaseId,
};
collectionWasOpened(accountName, collection); mostRecentActivity.notebookWasItemOpened(accountId, notebook);
collectionWasOpened(accountName, collection);
const activity = getItems(accountName); const activity = mostRecentActivity.getItems(accountId);
expect(activity).toEqual([ expect(activity).toEqual([expect.objectContaining(notebook)]);
expect.objectContaining({ });
type: Type.OpenCollection,
collectionId, it("Filters out duplicates", () => {
databaseId, const name = "some notebook";
}), const path = "some path";
]); const notebook = { name, path };
const sameNotebook = { name, path };
mostRecentActivity.notebookWasItemOpened(accountId, notebook);
mostRecentActivity.notebookWasItemOpened(accountId, sameNotebook);
const activity = mostRecentActivity.getItems(accountId);
expect(activity.length).toEqual(1);
expect(activity).toEqual([expect.objectContaining(notebook)]);
});
it("Allows for multiple accounts", () => {
const name = "some notebook";
const path = "some path";
const notebook = { name, path };
const anotherNotebook = { name: "Another " + name, path };
const anotherAccountId = "Another " + accountId;
mostRecentActivity.notebookWasItemOpened(accountId, notebook);
mostRecentActivity.notebookWasItemOpened(anotherAccountId, anotherNotebook);
expect(mostRecentActivity.getItems(accountId)).toEqual([expect.objectContaining(notebook)]);
expect(mostRecentActivity.getItems(anotherAccountId)).toEqual([expect.objectContaining(anotherNotebook)]);
});
it("Can store multiple distinct elements, in FIFO order", () => {
const name = "some notebook";
const path = "some path";
const first = { name, path };
const second = { name: "Another " + name, path };
const third = { name, path: "Another " + path };
mostRecentActivity.notebookWasItemOpened(accountId, first);
mostRecentActivity.notebookWasItemOpened(accountId, second);
mostRecentActivity.notebookWasItemOpened(accountId, third);
const activity = mostRecentActivity.getItems(accountId);
expect(activity).toEqual([third, second, first].map(expect.objectContaining));
}); });
}); });

View File

@@ -1,10 +1,10 @@
import { AppStateComponentNames, deleteState, loadState, saveState } from "Shared/AppStatePersistenceUtility";
import { CollectionBase } from "../../Contracts/ViewModels"; import { CollectionBase } from "../../Contracts/ViewModels";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { StorageKey, LocalStorageUtility } from "../../Shared/StorageUtility";
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
export enum Type { export enum Type {
OpenCollection = "OpenCollection", OpenCollection,
OpenNotebook = "OpenNotebook", OpenNotebook,
} }
export interface OpenNotebookItem { export interface OpenNotebookItem {
@@ -21,174 +21,158 @@ export interface OpenCollectionItem {
type Item = OpenNotebookItem | OpenCollectionItem; type Item = OpenNotebookItem | OpenCollectionItem;
const itemsMaxNumber: number = 5; // Update schemaVersion if you are going to change this interface
interface StoredData {
schemaVersion: string;
itemsMap: { [accountId: string]: Item[] }; // FIFO
}
/** /**
* Migrate old data to new AppState * Stores most recent activity
*/ */
const migrateOldData = () => { class MostRecentActivity {
private static readonly schemaVersion: string = "2";
private static itemsMaxNumber: number = 5;
private storedData: StoredData;
constructor() {
// Retrieve from local storage
if (LocalStorageUtility.hasItem(StorageKey.MostRecentActivity)) { if (LocalStorageUtility.hasItem(StorageKey.MostRecentActivity)) {
const oldDataSchemaVersion: string = "2";
const rawData = LocalStorageUtility.getEntryString(StorageKey.MostRecentActivity); const rawData = LocalStorageUtility.getEntryString(StorageKey.MostRecentActivity);
if (rawData) {
const oldData = JSON.parse(rawData); if (!rawData) {
if (oldData.schemaVersion === oldDataSchemaVersion) { this.storedData = MostRecentActivity.createEmptyData();
const itemsMap: Record<string, Item[]> = oldData.itemsMap; } else {
Object.keys(itemsMap).forEach((accountId: string) => { try {
const accountName = accountId.split("/").pop(); this.storedData = JSON.parse(rawData);
if (accountName) { } catch (e) {
saveState( console.error("Unable to parse stored most recent activity. Use empty data:", rawData);
{ this.storedData = MostRecentActivity.createEmptyData();
componentName: AppStateComponentNames.MostRecentActivity,
globalAccountName: accountName,
},
itemsMap[accountId].map((item) => {
if ((item.type as unknown as number) === 0) {
item.type = Type.OpenCollection;
} else if ((item.type as unknown as number) === 1) {
item.type = Type.OpenNotebook;
}
return item;
}),
);
}
});
}
} }
// Remove old data // If version doesn't match or schema broke, nuke it!
if (
!this.storedData.hasOwnProperty("schemaVersion") ||
this.storedData["schemaVersion"] !== MostRecentActivity.schemaVersion
) {
LocalStorageUtility.removeEntry(StorageKey.MostRecentActivity);
this.storedData = MostRecentActivity.createEmptyData();
}
}
} else {
this.storedData = MostRecentActivity.createEmptyData();
}
for (let p in this.storedData.itemsMap) {
this.cleanupItems(p);
}
this.saveToLocalStorage();
}
private static createEmptyData(): StoredData {
return {
schemaVersion: MostRecentActivity.schemaVersion,
itemsMap: {},
};
}
private static isEmpty(object: any) {
return Object.keys(object).length === 0 && object.constructor === Object;
}
private saveToLocalStorage() {
if (MostRecentActivity.isEmpty(this.storedData.itemsMap)) {
if (LocalStorageUtility.hasItem(StorageKey.MostRecentActivity)) {
LocalStorageUtility.removeEntry(StorageKey.MostRecentActivity); LocalStorageUtility.removeEntry(StorageKey.MostRecentActivity);
} }
}; // Don't save if empty
return;
}
const addItem = (accountName: string, newItem: Item): void => { LocalStorageUtility.setEntryString(StorageKey.MostRecentActivity, JSON.stringify(this.storedData));
}
private addItem(accountId: string, newItem: Item): void {
// When debugging, accountId is "undefined": most recent activity cannot be saved by account. Uncomment to disable. // When debugging, accountId is "undefined": most recent activity cannot be saved by account. Uncomment to disable.
// if (!accountId) { // if (!accountId) {
// return; // return;
// } // }
let items =
(loadState({
componentName: AppStateComponentNames.MostRecentActivity,
globalAccountName: accountName,
}) as Item[]) || [];
// Remove duplicate // Remove duplicate
items = removeDuplicate(newItem, items); MostRecentActivity.removeDuplicate(newItem, this.storedData.itemsMap[accountId]);
items.unshift(newItem); this.storedData.itemsMap[accountId] = this.storedData.itemsMap[accountId] || [];
items = cleanupItems(items, accountName); this.storedData.itemsMap[accountId].unshift(newItem);
saveState( this.cleanupItems(accountId);
{ this.saveToLocalStorage();
componentName: AppStateComponentNames.MostRecentActivity,
globalAccountName: accountName,
},
items,
);
};
export const getItems = (accountName: string): Item[] => {
if (!accountName) {
return [];
} }
return ( public getItems(accountId: string): Item[] {
(loadState({ return this.storedData.itemsMap[accountId] || [];
componentName: AppStateComponentNames.MostRecentActivity,
globalAccountName: accountName,
}) as Item[]) || []
);
};
export const collectionWasOpened = (
accountName: string,
{ id, databaseId }: Pick<CollectionBase, "id" | "databaseId">,
) => {
if (accountName === undefined) {
return;
} }
public collectionWasOpened(accountId: string, { id, databaseId }: Pick<CollectionBase, "id" | "databaseId">) {
const collectionId = id(); const collectionId = id();
addItem(accountName, { this.addItem(accountId, {
type: Type.OpenCollection, type: Type.OpenCollection,
databaseId, databaseId,
collectionId, collectionId,
}); });
}; }
export const clear = (accountName: string): void => { public notebookWasItemOpened(accountId: string, { name, path }: Pick<NotebookContentItem, "name" | "path">) {
if (!accountName) { this.addItem(accountId, {
type: Type.OpenNotebook,
name,
path,
});
}
public clear(accountId: string): void {
delete this.storedData.itemsMap[accountId];
this.saveToLocalStorage();
}
/**
* Find items by doing strict comparison and remove from array if duplicate is found
* @param item
*/
private static removeDuplicate(item: Item, itemsArray: Item[]): void {
if (!itemsArray) {
return; return;
} }
deleteState({
componentName: AppStateComponentNames.MostRecentActivity,
globalAccountName: accountName,
});
};
// Sort object by key
const sortObjectKeys = (unordered: Record<string, unknown>): Record<string, unknown> => {
return Object.keys(unordered)
.sort()
.reduce((obj: Record<string, unknown>, key: string) => {
obj[key] = unordered[key];
return obj;
}, {});
};
/**
* Find items by doing strict comparison and remove from array if duplicate is found.
* Modifies the array.
* @param item
* @param itemsArray
* @returns new array
*/
const removeDuplicate = (item: Item, itemsArray: Item[]): Item[] => {
if (!itemsArray) {
return itemsArray;
}
const result: Item[] = [...itemsArray];
let index = -1; let index = -1;
for (let i = 0; i < result.length; i++) { for (let i = 0; i < itemsArray.length; i++) {
const currentItem = result[i]; const currentItem = itemsArray[i];
if (JSON.stringify(currentItem) === JSON.stringify(item)) {
if (
JSON.stringify(sortObjectKeys(currentItem as unknown as Record<string, unknown>)) ===
JSON.stringify(sortObjectKeys(item as unknown as Record<string, unknown>))
) {
index = i; index = i;
break; break;
} }
} }
if (index !== -1) { if (index !== -1) {
result.splice(index, 1); itemsArray.splice(index, 1);
}
} }
return result;
};
/** /**
* Remove unknown types * Remove unknown types
* Limit items to max number * Limit items to max number
* Modifies the array.
*/ */
const cleanupItems = (items: Item[], accountName: string): Item[] => { private cleanupItems(accountId: string): void {
if (accountName === undefined) { if (!this.storedData.itemsMap.hasOwnProperty(accountId)) {
return []; return;
} }
const itemsArray = items.filter((item) => item.type in Type).slice(0, itemsMaxNumber); const itemsArray = this.storedData.itemsMap[accountId]
.filter((item) => item.type in Type)
.slice(0, MostRecentActivity.itemsMaxNumber);
if (itemsArray.length === 0) { if (itemsArray.length === 0) {
deleteState({ delete this.storedData.itemsMap[accountId];
componentName: AppStateComponentNames.MostRecentActivity, } else {
globalAccountName: accountName, this.storedData.itemsMap[accountId] = itemsArray;
}); }
}
} }
return itemsArray;
};
migrateOldData(); export const mostRecentActivity = new MostRecentActivity();

View File

@@ -1,6 +1,5 @@
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility"; import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { PhoenixClient } from "Phoenix/PhoenixClient"; import { PhoenixClient } from "Phoenix/PhoenixClient";
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
@@ -128,9 +127,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo" userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
? databaseAccount?.location ? databaseAccount?.location
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase(); : databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
const disallowedLocationsUri: string = useNewPortalBackendEndpoint(Constants.BackendApi.DisallowedLocations) const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
? `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`
: `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
const authorizationHeader = getAuthorizationHeader(); const authorizationHeader = getAuthorizationHeader();
try { try {
const response = await fetch(disallowedLocationsUri, { const response = await fetch(disallowedLocationsUri, {

View File

@@ -1,5 +1,4 @@
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled. // TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
import { configContext, Platform } from "ConfigContext";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import React from "react"; import React from "react";
import { ActionContracts } from "../../Contracts/ExplorerContracts"; import { ActionContracts } from "../../Contracts/ExplorerContracts";
@@ -57,19 +56,6 @@ function openCollectionTab(
continue; continue;
} }
if (
configContext.platform === Platform.Fabric &&
!(
// whitelist the tab kinds that are allowed to be opened in Fabric
(
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
action.tabKind === ActionContracts.TabKind.SQLQuery
)
)
) {
continue;
}
//expand database first if not expanded to load the collections //expand database first if not expanded to load the collections
if (!database.isDatabaseExpanded?.()) { if (!database.isDatabaseExpanded?.()) {
database.expandDatabase?.(); database.expandDatabase?.();
@@ -135,28 +121,10 @@ function openCollectionTab(
action.tabKind === ActionContracts.TabKind.SQLQuery || action.tabKind === ActionContracts.TabKind.SQLQuery ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery] action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
) { ) {
const openQueryTabAction = action as ActionContracts.OpenQueryTab;
collection.onNewQueryClick( collection.onNewQueryClick(
collection, collection,
undefined, undefined,
generateQueryText(openQueryTabAction, collection.partitionKeyProperties), generateQueryText(action as ActionContracts.OpenQueryTab, 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; break;
} }

View File

@@ -17,15 +17,11 @@ import {
} from "@fluentui/react"; } from "@fluentui/react";
import * as Constants from "Common/Constants"; import * as Constants from "Common/Constants";
import { createCollection } from "Common/dataAccess/createCollection"; import { createCollection } from "Common/dataAccess/createCollection";
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { configContext, Platform } from "ConfigContext"; import { configContext, Platform } from "ConfigContext";
import * as DataModels from "Contracts/DataModels"; import * as DataModels from "Contracts/DataModels";
import { import { SubscriptionType } from "Contracts/SubscriptionType";
FullTextPoliciesComponent, import { AddVectorEmbeddingPolicyForm } from "Explorer/Panes/VectorSearchPanel/AddVectorEmbeddingPolicyForm";
getFullTextLanguageOptions,
} from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import { useTeachingBubble } from "hooks/useTeachingBubble"; import { useTeachingBubble } from "hooks/useTeachingBubble";
import React from "react"; import React from "react";
@@ -34,12 +30,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { getCollectionName } from "Utils/APITypeUtils"; import { getCollectionName } from "Utils/APITypeUtils";
import { import { isCapabilityEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
isCapabilityEnabled,
isFullTextSearchEnabled,
isServerlessAccount,
isVectorSearchEnabled,
} from "Utils/CapabilityUtils";
import { getUpsellMessage } from "Utils/PricingUtils"; import { getUpsellMessage } from "Utils/PricingUtils";
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent"; import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput"; import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
@@ -118,9 +109,6 @@ export interface AddCollectionPanelState {
vectorIndexingPolicy: DataModels.VectorIndex[]; vectorIndexingPolicy: DataModels.VectorIndex[];
vectorEmbeddingPolicy: DataModels.VectorEmbedding[]; vectorEmbeddingPolicy: DataModels.VectorEmbedding[];
vectorPolicyValidated: boolean; vectorPolicyValidated: boolean;
fullTextPolicy: DataModels.FullTextPolicy;
fullTextIndexes: DataModels.FullTextIndex[];
fullTextPolicyValidated: boolean;
} }
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> { export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
@@ -137,7 +125,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
createNewDatabase: createNewDatabase:
userContext.apiType !== "Tables" && configContext.platform !== Platform.Fabric && !this.props.databaseId, userContext.apiType !== "Tables" && configContext.platform !== Platform.Fabric && !this.props.databaseId,
newDatabaseId: props.isQuickstart ? this.getSampleDBName() : "", newDatabaseId: props.isQuickstart ? this.getSampleDBName() : "",
isSharedThroughputChecked: getNewDatabaseSharedThroughputDefault(), isSharedThroughputChecked: this.getSharedThroughputDefault(),
selectedDatabaseId: selectedDatabaseId:
userContext.apiType === "Tables" ? CollectionCreation.TablesAPIDefaultDatabase : this.props.databaseId, userContext.apiType === "Tables" ? CollectionCreation.TablesAPIDefaultDatabase : this.props.databaseId,
collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "", collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "",
@@ -159,9 +147,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
vectorEmbeddingPolicy: [], vectorEmbeddingPolicy: [],
vectorIndexingPolicy: [], vectorIndexingPolicy: [],
vectorPolicyValidated: true, vectorPolicyValidated: true,
fullTextPolicy: { defaultLanguage: getFullTextLanguageOptions()[0].key as never, fullTextPaths: [] },
fullTextIndexes: [],
fullTextPolicyValidated: true,
}; };
} }
@@ -819,9 +804,22 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.shouldShowAnalyticalStoreOptions() && ( {this.shouldShowAnalyticalStoreOptions() && (
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<Stack horizontal>
<Text className="panelTextBold" variant="small"> <Text className="panelTextBold" variant="small">
{this.getAnalyticalStorageContent()} Analytical store
</Text> </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>
<Stack horizontal verticalAlign="center"> <Stack horizontal verticalAlign="center">
<div role="radiogroup"> <div role="radiogroup">
@@ -865,7 +863,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Link <Link
href="https://aka.ms/cosmosdb-synapselink" href="https://aka.ms/cosmosdb-synapselink"
target="_blank" target="_blank"
aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink}
className="capacitycalculator-link" className="capacitycalculator-link"
> >
Learn more Learn more
@@ -893,9 +890,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
> >
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}> <Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Stack styles={{ root: { paddingLeft: 40 } }}> <Stack styles={{ root: { paddingLeft: 40 } }}>
<VectorEmbeddingPoliciesComponent <AddVectorEmbeddingPolicyForm
vectorEmbeddings={this.state.vectorEmbeddingPolicy} vectorEmbedding={this.state.vectorEmbeddingPolicy}
vectorIndexes={this.state.vectorIndexingPolicy} vectorIndex={this.state.vectorIndexingPolicy}
onVectorEmbeddingChange={( onVectorEmbeddingChange={(
vectorEmbeddingPolicy: DataModels.VectorEmbedding[], vectorEmbeddingPolicy: DataModels.VectorEmbedding[],
vectorIndexingPolicy: DataModels.VectorIndex[], vectorIndexingPolicy: DataModels.VectorIndex[],
@@ -909,34 +906,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</CollapsibleSectionComponent> </CollapsibleSectionComponent>
</Stack> </Stack>
)} )}
{this.shouldShowFullTextSearchParameters() && (
<Stack>
<CollapsibleSectionComponent
title="Container Full Text Search Policy"
isExpandedByDefault={false}
onExpand={() => {
this.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>
)}
{userContext.apiType !== "Tables" && ( {userContext.apiType !== "Tables" && (
<CollapsibleSectionComponent <CollapsibleSectionComponent
title="Advanced" title="Advanced"
@@ -1169,6 +1138,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return userContext.databaseAccount?.properties?.enableFreeTier; return userContext.databaseAccount?.properties?.enableFreeTier;
} }
private getSharedThroughputDefault(): boolean {
return userContext.subscriptionType !== SubscriptionType.EA && !isServerlessAccount();
}
private getFreeTierIndexingText(): string { private getFreeTierIndexingText(): string {
return this.state.enableIndexing return this.state.enableIndexing
? "All properties in your documents will be indexed by default for flexible and efficient queries." ? "All properties in your documents will be indexed by default for flexible and efficient queries."
@@ -1218,16 +1191,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return ""; return "";
} }
private getAnalyticalStorageContent(): JSX.Element { private getAnalyticalStorageTooltipContent(): JSX.Element {
return ( return (
<Text variant="small"> <Text variant="small">
Enable analytical store capability to perform near real-time analytics on your operational data, without Enable analytical store capability to perform near real-time analytics on your operational data, without
impacting the performance of transactional workloads.{" "} impacting the performance of transactional workloads.{" "}
<Link <Link target="_blank" href="https://aka.ms/analytical-store-overview">
aria-label={Constants.ariaLabelForLearnMoreLink.AnalyticalStore}
target="_blank"
href="https://aka.ms/analytical-store-overview"
>
Learn more Learn more
</Link> </Link>
</Text> </Text>
@@ -1246,19 +1215,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
); );
} }
//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 { private shouldShowCollectionThroughputInput(): boolean {
if (isServerlessAccount()) { if (isServerlessAccount()) {
return false; return false;
@@ -1322,10 +1278,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput()); return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
} }
private shouldShowFullTextSearchParameters() {
return isFullTextSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
}
private parseUniqueKeys(): DataModels.UniqueKeyPolicy { private parseUniqueKeys(): DataModels.UniqueKeyPolicy {
if (this.state.uniqueKeys?.length === 0) { if (this.state.uniqueKeys?.length === 0) {
return undefined; return undefined;
@@ -1382,18 +1334,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return false; return false;
} }
if (this.shouldShowVectorSearchParameters()) { if (this.shouldShowVectorSearchParameters() && !this.state.vectorPolicyValidated) {
if (!this.state.vectorPolicyValidated) {
this.setState({ errorMessage: "Please fix errors in container vector policy" }); this.setState({ errorMessage: "Please fix errors in container vector policy" });
return false; return false;
} }
if (!this.state.fullTextPolicyValidated) {
this.setState({ errorMessage: "Please fix errors in container full text search polilcy" });
return false;
}
}
return true; return true;
} }
@@ -1482,10 +1427,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}; };
} }
if (this.shouldShowFullTextSearchParameters()) {
indexingPolicy.fullTextIndexes = this.state.fullTextIndexes;
}
const telemetryData = { const telemetryData = {
database: { database: {
id: databaseId, id: databaseId,
@@ -1545,7 +1486,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
uniqueKeyPolicy, uniqueKeyPolicy,
createMongoWildcardIndex: this.state.createMongoWildCardIndex, createMongoWildcardIndex: this.state.createMongoWildCardIndex,
vectorEmbeddingPolicy, vectorEmbeddingPolicy,
fullTextPolicy: this.state.fullTextPolicy,
}; };
this.setState({ isExecuting: true }); this.setState({ isExecuting: true });

View File

@@ -1,5 +1,4 @@
import { Checkbox, Stack, Text, TextField } from "@fluentui/react"; import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
import React, { FunctionComponent, useEffect, useState } from "react"; import React, { FunctionComponent, useEffect, useState } from "react";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
@@ -49,7 +48,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
const databaseLevelThroughputTooltipText = `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`; const databaseLevelThroughputTooltipText = `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`;
const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState<boolean>( const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState<boolean>(
getNewDatabaseSharedThroughputDefault(), subscriptionType !== SubscriptionType.EA && !isServerlessAccount(),
); );
const [formErrors, setFormErrors] = useState<string>(""); const [formErrors, setFormErrors] = useState<string>("");
const [isExecuting, setIsExecuting] = useState<boolean>(false); const [isExecuting, setIsExecuting] = useState<boolean>(false);

View File

@@ -65,7 +65,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
horizontal={true} horizontal={true}
> >
<StyledCheckboxBase <StyledCheckboxBase
checked={false} checked={true}
label="Provision throughput" label="Provision throughput"
onChange={[Function]} onChange={[Function]}
styles={ styles={
@@ -90,6 +90,14 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
</InfoTooltip> </InfoTooltip>
</Stack> </Stack>
</Stack> </Stack>
<ThroughputInput
isDatabase={true}
isSharded={true}
onCostAcknowledgeChange={[Function]}
setIsAutoscale={[Function]}
setIsThroughputCapExceeded={[Function]}
setThroughputValue={[Function]}
/>
</div> </div>
</RightPaneForm> </RightPaneForm>
`; `;

View File

@@ -124,7 +124,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
message: message:
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.", "Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
}; };
const confirmDatabase = `Confirm by typing the ${getDatabaseName()} id (name)`; const confirmDatabase = `Confirm by typing the ${getDatabaseName()} id`;
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${getDatabaseName()}?`; const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${getDatabaseName()}?`;
return ( return (
<RightPaneForm {...props}> <RightPaneForm {...props}>
@@ -132,7 +132,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
<div className="panelMainContent"> <div className="panelMainContent">
<div className="confirmDeleteInput"> <div className="confirmDeleteInput">
<span className="mandatoryStar">* </span> <span className="mandatoryStar">* </span>
<Text variant="small">{confirmDatabase}</Text> <Text variant="small">Confirm by typing the {getDatabaseName()} id</Text>
<TextField <TextField
id="confirmDatabaseId" id="confirmDatabaseId"
data-test="Input:confirmDatabaseId" data-test="Input:confirmDatabaseId"

View File

@@ -1,27 +1,22 @@
import {
AuthError as msalAuthError,
BrowserAuthErrorMessage as msalBrowserAuthErrorMessage,
} from "@azure/msal-browser";
import { import {
Checkbox, Checkbox,
ChoiceGroup, ChoiceGroup,
DefaultButton,
IChoiceGroupOption, IChoiceGroupOption,
ISpinButtonStyles, ISpinButtonStyles,
IToggleStyles, IToggleStyles,
Icon,
MessageBar,
MessageBarType,
Position, Position,
SpinButton, SpinButton,
Toggle, Toggle,
TooltipHost,
} from "@fluentui/react"; } from "@fluentui/react";
import { Accordion, AccordionHeader, AccordionItem, AccordionPanel, makeStyles } from "@fluentui/react-components";
import { AuthType } from "AuthType";
import * as Constants from "Common/Constants"; import * as Constants from "Common/Constants";
import { SplitterDirection } from "Common/Splitter"; import { SplitterDirection } from "Common/Splitter";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip"; import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { Platform, configContext } from "ConfigContext"; import { Platform, configContext } from "ConfigContext";
import { useDialog } from "Explorer/Controls/Dialog";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { deleteAllStates } from "Shared/AppStatePersistenceUtility";
import { import {
DefaultRUThreshold, DefaultRUThreshold,
LocalStorageUtility, LocalStorageUtility,
@@ -32,16 +27,16 @@ import {
} from "Shared/StorageUtility"; } from "Shared/StorageUtility";
import * as StringUtility from "Shared/StringUtility"; import * as StringUtility from "Shared/StringUtility";
import { updateUserContext, userContext } from "UserContext"; import { updateUserContext, userContext } from "UserContext";
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils"; import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
import { getReadOnlyKeys, listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
import create, { UseStore } from "zustand";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import { AuthType } from "AuthType";
import create, { UseStore } from "zustand";
import { getReadOnlyKeys, listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
export interface DataPlaneRbacState { export interface DataPlaneRbacState {
dataPlaneRbacEnabled: boolean; dataPlaneRbacEnabled: boolean;
@@ -55,39 +50,6 @@ export interface DataPlaneRbacState {
type DataPlaneRbacStore = UseStore<Partial<DataPlaneRbacState>>; type DataPlaneRbacStore = UseStore<Partial<DataPlaneRbacState>>;
const useStyles = makeStyles({
bulletList: {
listStyleType: "disc",
paddingLeft: "20px",
},
container: {
display: "flex",
flexDirection: "column",
height: "100%",
},
firstItem: {
flex: "1",
},
header: {
marginRight: "5px",
},
headerIcon: {
paddingTop: "4px",
cursor: "pointer",
},
settingsSectionContainer: {
paddingLeft: "15px",
},
settingsSectionDescription: {
paddingBottom: "10px",
fontSize: "12px",
},
subHeader: {
marginRight: "5px",
fontSize: "12px",
},
});
export const useDataPlaneRbac: DataPlaneRbacStore = create(() => ({ export const useDataPlaneRbac: DataPlaneRbacStore = create(() => ({
dataPlaneRbacEnabled: false, dataPlaneRbacEnabled: false,
})); }));
@@ -111,6 +73,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
? LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled) ? LocalStorageUtility.getEntryString(StorageKey.DataPlaneRbacEnabled)
: Constants.RBACOptions.setAutomaticRBACOption, : Constants.RBACOptions.setAutomaticRBACOption,
); );
const [showDataPlaneRBACWarning, setShowDataPlaneRBACWarning] = useState<boolean>(false);
const [ruThresholdEnabled, setRUThresholdEnabled] = useState<boolean>(isRUThresholdEnabled()); const [ruThresholdEnabled, setRUThresholdEnabled] = useState<boolean>(isRUThresholdEnabled());
const [ruThreshold, setRUThreshold] = useState<number>(getRUThreshold()); const [ruThreshold, setRUThreshold] = useState<number>(getRUThreshold());
@@ -170,30 +133,16 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
const [copilotSampleDBEnabled, setCopilotSampleDBEnabled] = useState<boolean>( const [copilotSampleDBEnabled, setCopilotSampleDBEnabled] = useState<boolean>(
LocalStorageUtility.getEntryString(StorageKey.CopilotSampleDBEnabled) === "true", LocalStorageUtility.getEntryString(StorageKey.CopilotSampleDBEnabled) === "true",
); );
const styles = useStyles();
const explorerVersion = configContext.gitSha; const explorerVersion = configContext.gitSha;
const isEmulator = configContext.platform === Platform.Emulator;
const shouldShowQueryPageOptions = userContext.apiType === "SQL"; const shouldShowQueryPageOptions = userContext.apiType === "SQL";
const showRetrySettings = const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin";
(userContext.apiType === "SQL" || userContext.apiType === "Tables" || userContext.apiType === "Gremlin") && const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
!isEmulator; const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin" && !isEmulator; const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled();
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin" && !isEmulator;
const shouldShowParallelismOption = userContext.apiType !== "Gremlin" && !isEmulator;
const showEnableEntraIdRbac =
userContext.apiType === "SQL" &&
userContext.authType === AuthType.AAD &&
configContext.platform !== Platform.Fabric &&
!isEmulator;
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled() && !isEmulator;
const shouldShowCopilotSampleDBOption = const shouldShowCopilotSampleDBOption =
userContext.apiType === "SQL" && userContext.apiType === "SQL" &&
useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotEnabled &&
useDatabases.getState().sampleDataResourceTokenCollection && useDatabases.getState().sampleDataResourceTokenCollection;
!isEmulator;
const handlerOnSubmit = async () => { const handlerOnSubmit = async () => {
setIsExecuting(true); setIsExecuting(true);
@@ -204,18 +153,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage); 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); LocalStorageUtility.setEntryString(StorageKey.DataPlaneRbacEnabled, enableDataPlaneRBACOption);
if ( if (
enableDataPlaneRBACOption === Constants.RBACOptions.setTrueRBACOption || enableDataPlaneRBACOption === Constants.RBACOptions.setTrueRBACOption ||
@@ -224,29 +161,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
) { ) {
updateUserContext({ updateUserContext({
dataPlaneRbacEnabled: true, dataPlaneRbacEnabled: true,
hasDataPlaneRbacSettingChanged: true,
}); });
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true }); useDataPlaneRbac.setState({ dataPlaneRbacEnabled: true });
try {
const aadToken = await acquireMsalTokenForAccount(userContext.databaseAccount, true);
updateUserContext({ aadToken: aadToken });
useDataPlaneRbac.setState({ aadTokenUpdated: true });
} catch (authError) {
if (
authError instanceof msalAuthError &&
authError.errorCode === msalBrowserAuthErrorMessage.popUpWindowError.code
) {
logConsoleError(
`We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease enable pop-ups for this site and click on "Login for Entra ID" button`,
);
} else {
logConsoleError(
`"Failed to acquire authorization token automatically. Please click on "Login for Entra ID" button to enable Entra ID RBAC operations`,
);
}
}
} else { } else {
updateUserContext({ updateUserContext({
dataPlaneRbacEnabled: false, dataPlaneRbacEnabled: false,
hasDataPlaneRbacSettingChanged: true,
}); });
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext; const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
if (!userContext.features.enableAadDataPlane && !userContext.masterKey) { if (!userContext.features.enableAadDataPlane && !userContext.masterKey) {
@@ -271,7 +192,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
useDataPlaneRbac.setState({ dataPlaneRbacEnabled: false }); useDataPlaneRbac.setState({ dataPlaneRbacEnabled: false });
} }
} }
}
LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled); LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled);
LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled); LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled);
@@ -387,6 +307,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
option: IChoiceGroupOption, option: IChoiceGroupOption,
): void => { ): void => {
setEnableDataPlaneRBACOption(option.key); setEnableDataPlaneRBACOption(option.key);
const shouldShowWarning =
(option.key === Constants.RBACOptions.setTrueRBACOption ||
(option.key === Constants.RBACOptions.setAutomaticRBACOption &&
userContext.databaseAccount.properties.disableLocalAuth === true)) &&
!useDataPlaneRbac.getState().aadTokenUpdated;
setShowDataPlaneRBACWarning(shouldShowWarning);
}; };
const handleOnRUThresholdToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => { const handleOnRUThresholdToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
@@ -501,19 +428,18 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
return ( return (
<RightPaneForm {...genericPaneProps}> <RightPaneForm {...genericPaneProps}>
<div className={`paneMainContent ${styles.container}`}> <div className="paneMainContent">
<Accordion className={`customAccordion ${styles.firstItem}`}>
{shouldShowQueryPageOptions && ( {shouldShowQueryPageOptions && (
<AccordionItem value="1"> <div className="settingsSection">
<AccordionHeader> <div className="settingsSectionPart">
<div className={styles.header}>Page Options</div> <fieldset>
</AccordionHeader> <legend id="pageOptions" className="settingsSectionLabel legendLabel">
<AccordionPanel> Page Options
<div className={styles.settingsSectionContainer}> </legend>
<div className={styles.settingsSectionDescription}> <InfoTooltip>
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many
many query results per page. query results per page.
</div> </InfoTooltip>
<ChoiceGroup <ChoiceGroup
ariaLabelledBy="pageOptions" ariaLabelledBy="pageOptions"
selectedKey={pageOption} selectedKey={pageOption}
@@ -521,15 +447,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
styles={choiceButtonStyles} styles={choiceButtonStyles}
onChange={handleOnPageOptionChange} onChange={handleOnPageOptionChange}
/> />
</fieldset>
</div> </div>
<div className={`tabs ${styles.settingsSectionContainer}`}> <div className="tabs settingsSectionPart">
{isCustomPageOptionSelected() && ( {isCustomPageOptionSelected() && (
<div className="tabcontent"> <div className="tabcontent">
<div className={styles.settingsSectionDescription}> <div className="settingsSectionLabel">
Query results per page{" "} Query results per page
<InfoTooltip className={styles.headerIcon}> <InfoTooltip>Enter the number of query results that should be shown per page.</InfoTooltip>
Enter the number of query results that should be shown per page.
</InfoTooltip>
</div> </div>
<SpinButton <SpinButton
@@ -549,19 +474,21 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</div> </div>
)} )}
</div> </div>
</AccordionPanel> </div>
</AccordionItem>
)} )}
{showEnableEntraIdRbac && ( {userContext.apiType === "SQL" && userContext.authType === AuthType.AAD && (
<AccordionItem value="2"> <>
<AccordionHeader> <div className="settingsSection">
<div className={styles.header}>Enable Entra ID RBAC</div> <div className="settingsSectionPart">
</AccordionHeader> <fieldset>
<AccordionPanel> <legend id="enableDataPlaneRBACOptions" className="settingsSectionLabel legendLabel">
<div className={styles.settingsSectionContainer}> Enable Entra ID RBAC
<div className={styles.settingsSectionDescription}> </legend>
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra ID <TooltipHost
RBAC. content={
<>
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra
ID RBAC.
<a <a
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer" href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
target="_blank" target="_blank"
@@ -570,7 +497,22 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{" "} {" "}
Learn more{" "} Learn more{" "}
</a> </a>
</div> </>
}
>
<Icon iconName="Info" ariaLabel="Info tooltip" className="panelInfoIcon" tabIndex={0} />
</TooltipHost>
{showDataPlaneRBACWarning && configContext.platform === Platform.Portal && (
<MessageBar
messageBarType={MessageBarType.warning}
isMultiline={true}
onDismiss={() => setShowDataPlaneRBACWarning(false)}
dismissButtonAriaLabel="Close"
>
Please click on &quot;Login for Entra ID RBAC&quot; button prior to performing Entra ID RBAC
operations
</MessageBar>
)}
<ChoiceGroup <ChoiceGroup
ariaLabelledBy="enableDataPlaneRBACOptions" ariaLabelledBy="enableDataPlaneRBACOptions"
options={dataPlaneRBACOptionsList} options={dataPlaneRBACOptionsList}
@@ -578,22 +520,58 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
selectedKey={enableDataPlaneRBACOption} selectedKey={enableDataPlaneRBACOption}
onChange={handleOnDataPlaneRBACOptionChange} onChange={handleOnDataPlaneRBACOptionChange}
/> />
</fieldset>
</div> </div>
</AccordionPanel> </div>
</AccordionItem> </>
)} )}
{userContext.apiType === "SQL" && !isEmulator && ( {userContext.apiType === "SQL" && (
<> <>
<AccordionItem value="3"> <div className="settingsSection">
<AccordionHeader> <div className="settingsSectionPart">
<div className={styles.header}>Query Timeout</div> <div>
</AccordionHeader> <legend id="ruThresholdLabel" className="settingsSectionLabel legendLabel">
<AccordionPanel> RU Threshold
<div className={styles.settingsSectionContainer}> </legend>
<div className={styles.settingsSectionDescription}> <InfoTooltip>If a query exceeds a configured RU threshold, the query will be aborted.</InfoTooltip>
When a query reaches a specified time limit, a popup with an option to cancel the query will show
unless automatic cancellation has been enabled.
</div> </div>
<div>
<Toggle
styles={toggleStyles}
label="Enable RU threshold"
onChange={handleOnRUThresholdToggleChange}
defaultChecked={ruThresholdEnabled}
/>
</div>
{ruThresholdEnabled && (
<div>
<SpinButton
label="RU Threshold (RU)"
labelPosition={Position.top}
defaultValue={(ruThreshold || DefaultRUThreshold).toString()}
min={1}
step={1000}
onChange={handleOnRUThresholdSpinButtonChange}
incrementButtonAriaLabel="Increase value by 1000"
decrementButtonAriaLabel="Decrease value by 1000"
styles={spinButtonStyles}
/>
</div>
)}
</div>
</div>
<div className="settingsSection">
<div className="settingsSectionPart">
<div>
<legend id="queryTimeoutLabel" className="settingsSectionLabel legendLabel">
Query Timeout
</legend>
<InfoTooltip>
When a query reaches a specified time limit, a popup with an option to cancel the query will show
unless automatic cancellation has been enabled
</InfoTooltip>
</div>
<div>
<Toggle <Toggle
styles={toggleStyles} styles={toggleStyles}
label="Enable query timeout" label="Enable query timeout"
@@ -602,7 +580,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
/> />
</div> </div>
{queryTimeoutEnabled && ( {queryTimeoutEnabled && (
<div className={styles.settingsSectionContainer}> <div>
<SpinButton <SpinButton
label="Query timeout (ms)" label="Query timeout (ms)"
labelPosition={Position.top} labelPosition={Position.top}
@@ -622,52 +600,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
/> />
</div> </div>
)} )}
</AccordionPanel>
</AccordionItem>
<AccordionItem value="4">
<AccordionHeader>
<div className={styles.header}>RU Limit</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
If a query exceeds a configured RU limit, the query will be aborted.
</div> </div>
<Toggle
styles={toggleStyles}
label="Enable RU limit"
onChange={handleOnRUThresholdToggleChange}
defaultChecked={ruThresholdEnabled}
/>
</div> </div>
{ruThresholdEnabled && ( <div className="settingsSection">
<div className={styles.settingsSectionContainer}> <div className="settingsSectionPart">
<SpinButton <div>
label="RU Limit (RU)" <legend id="defaultQueryResultsView" className="settingsSectionLabel legendLabel">
labelPosition={Position.top} Default Query Results View
defaultValue={(ruThreshold || DefaultRUThreshold).toString()} </legend>
min={1} <InfoTooltip>Select the default view to use when displaying query results.</InfoTooltip>
step={1000}
onChange={handleOnRUThresholdSpinButtonChange}
incrementButtonAriaLabel="Increase value by 1000"
decrementButtonAriaLabel="Decrease value by 1000"
styles={spinButtonStyles}
/>
</div>
)}
</AccordionPanel>
</AccordionItem>
<AccordionItem value="5">
<AccordionHeader>
<div className={styles.header}>Default Query Results View</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
Select the default view to use when displaying query results.
</div> </div>
<div>
<ChoiceGroup <ChoiceGroup
ariaLabelledBy="defaultQueryResultsView" ariaLabelledBy="defaultQueryResultsView"
selectedKey={defaultQueryResultsView} selectedKey={defaultQueryResultsView}
@@ -676,25 +619,21 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
onChange={handleOnDefaultQueryResultsViewChange} onChange={handleOnDefaultQueryResultsViewChange}
/> />
</div> </div>
</AccordionPanel> </div>
</AccordionItem> </div>
</> </>
)} )}
{showRetrySettings && ( <div className="settingsSection">
<AccordionItem value="6"> <div className="settingsSectionPart">
<AccordionHeader> <div className="settingsSectionLabel">
<div className={styles.header}>Retry Settings</div> Retry Settings
</AccordionHeader> <InfoTooltip>Retry policy associated with throttled requests during CosmosDB queries.</InfoTooltip>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
Retry policy associated with throttled requests during CosmosDB queries.
</div> </div>
<div> <div>
<span className={styles.subHeader}>Max retry attempts</span> <legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel">
<InfoTooltip className={styles.headerIcon}> Max retry attempts
Max number of retries to be performed for a request. Default value 9. </legend>
</InfoTooltip> <InfoTooltip>Max number of retries to be performed for a request. Default value 9.</InfoTooltip>
</div> </div>
<SpinButton <SpinButton
labelPosition={Position.top} labelPosition={Position.top}
@@ -710,10 +649,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
styles={spinButtonStyles} styles={spinButtonStyles}
/> />
<div> <div>
<span className={styles.subHeader}>Fixed retry interval (ms)</span> <legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel">
<InfoTooltip className={styles.headerIcon}> Fixed retry interval (ms)
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned </legend>
as part of the response. Default value is 0 milliseconds. <InfoTooltip>
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> </InfoTooltip>
</div> </div>
<SpinButton <SpinButton
@@ -730,8 +671,10 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
styles={spinButtonStyles} styles={spinButtonStyles}
/> />
<div> <div>
<span className={styles.subHeader}>Max wait time (s)</span> <legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel">
<InfoTooltip className={styles.headerIcon}> Max wait time (s)
</legend>
<InfoTooltip>
Max wait time in seconds to wait for a request while the retries are happening. Default value 30 Max wait time in seconds to wait for a request while the retries are happening. Default value 30
seconds. seconds.
</InfoTooltip> </InfoTooltip>
@@ -750,18 +693,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
styles={spinButtonStyles} styles={spinButtonStyles}
/> />
</div> </div>
</AccordionPanel> </div>
</AccordionItem> <div className="settingsSection">
)} <div className="settingsSectionPart settingsSectionInlineCheckbox">
{!isEmulator && ( <div className="settingsSectionLabel">
<AccordionItem value="7"> Enable container pagination
<AccordionHeader> <InfoTooltip>
<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. Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
</InfoTooltip>
</div> </div>
<Checkbox <Checkbox
styles={{ styles={{
@@ -771,23 +710,20 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
ariaLabel="Enable container pagination" ariaLabel="Enable container pagination"
checked={containerPaginationEnabled} checked={containerPaginationEnabled}
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)} onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
label="Enable container pagination"
/> />
</div> </div>
</AccordionPanel>
</AccordionItem>
)}
{shouldShowCrossPartitionOption && (
<AccordionItem value="8">
<AccordionHeader>
<div className={styles.header}>Enable cross-partition query</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
Send more than one request while executing a query. More than one request is necessary if the query
is not scoped to single partition key value.
</div> </div>
{shouldShowCrossPartitionOption && (
<div className="settingsSection">
<div className="settingsSectionPart settingsSectionInlineCheckbox">
<div className="settingsSectionLabel">
Enable cross-partition query
<InfoTooltip>
Send more than one request while executing a query. More than one request is necessary if the query is
not scoped to single partition key value.
</InfoTooltip>
</div>
<Checkbox <Checkbox
styles={{ styles={{
label: { padding: 0 }, label: { padding: 0 },
@@ -796,24 +732,22 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
ariaLabel="Enable cross partition query" ariaLabel="Enable cross partition query"
checked={crossPartitionQueryEnabled} checked={crossPartitionQueryEnabled}
onChange={() => setCrossPartitionQueryEnabled(!crossPartitionQueryEnabled)} onChange={() => setCrossPartitionQueryEnabled(!crossPartitionQueryEnabled)}
label="Enable cross-partition query"
/> />
</div> </div>
</AccordionPanel> </div>
</AccordionItem>
)} )}
{shouldShowParallelismOption && ( {shouldShowParallelismOption && (
<AccordionItem value="9"> <div className="settingsSection">
<AccordionHeader> <div className="settingsSectionPart">
<div className={styles.header}>Max degree of parallelism</div> <div className="settingsSectionLabel">
</AccordionHeader> Max degree of parallelism
<AccordionPanel> <InfoTooltip>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
Gets or sets the number of concurrent operations run client side during parallel query execution. A Gets or sets the number of concurrent operations run client side during parallel query execution. A
positive property value limits the number of concurrent operations to the set value. If it is set to positive property value limits the number of concurrent operations to the set value. If it is set to
less than 0, the system automatically decides the number of concurrent operations to run. less than 0, the system automatically decides the number of concurrent operations to run.
</InfoTooltip>
</div> </div>
<SpinButton <SpinButton
min={-1} min={-1}
step={1} step={1}
@@ -821,32 +755,26 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
role="textbox" role="textbox"
id="max-degree" id="max-degree"
value={"" + maxDegreeOfParallelism} value={"" + maxDegreeOfParallelism}
onIncrement={(newValue) => onIncrement={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism)}
setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism) onDecrement={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) - 1 || maxDegreeOfParallelism)}
}
onDecrement={(newValue) =>
setMaxDegreeOfParallelism(parseInt(newValue) - 1 || maxDegreeOfParallelism)
}
onValidate={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) || maxDegreeOfParallelism)} onValidate={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) || maxDegreeOfParallelism)}
ariaLabel="Max degree of parallelism" ariaLabel="Max degree of parallelism"
label="Max degree of parallelism"
/> />
</div> </div>
</AccordionPanel> </div>
</AccordionItem>
)} )}
{shouldShowPriorityLevelOption && ( {shouldShowPriorityLevelOption && (
<AccordionItem value="10"> <div className="settingsSection">
<AccordionHeader> <div className="settingsSectionPart">
<div className={styles.header}>Priority Level</div> <fieldset>
</AccordionHeader> <legend id="priorityLevel" className="settingsSectionLabel legendLabel">
<AccordionPanel> Priority Level
<div className={styles.settingsSectionContainer}> </legend>
<div className={styles.settingsSectionDescription}> <InfoTooltip>
Sets the priority level for data-plane requests from Data Explorer when using Priority-Based Sets the priority level for data-plane requests from Data Explorer when using Priority-Based
Execution. If &quot;None&quot; is selected, Data Explorer will not specify priority level, and the Execution. If &quot;None&quot; is selected, Data Explorer will not specify priority level, and the
server-side default priority level will be used. server-side default priority level will be used.
</div> </InfoTooltip>
<ChoiceGroup <ChoiceGroup
ariaLabelledBy="priorityLevel" ariaLabelledBy="priorityLevel"
selectedKey={priorityLevel} selectedKey={priorityLevel}
@@ -854,21 +782,21 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
styles={choiceButtonStyles} styles={choiceButtonStyles}
onChange={handleOnPriorityLevelOptionChange} onChange={handleOnPriorityLevelOptionChange}
/> />
</fieldset>
</div>
</div> </div>
</AccordionPanel>
</AccordionItem>
)} )}
{shouldShowGraphAutoVizOption && ( {shouldShowGraphAutoVizOption && (
<AccordionItem value="11"> <div className="settingsSection">
<AccordionHeader> <div className="settingsSectionPart">
<div className={styles.header}>Display Gremlin query results as:&nbsp;</div> <div className="settingsSectionLabel">
</AccordionHeader> Display Gremlin query results as:&nbsp;
<AccordionPanel> <InfoTooltip>
<div className={styles.settingsSectionContainer}> Select Graph to automatically visualize the query results as a Graph or JSON to display the results as
<div className={styles.settingsSectionDescription}> JSON.
Select Graph to automatically visualize the query results as a Graph or JSON to display the results </InfoTooltip>
as JSON.
</div> </div>
<ChoiceGroup <ChoiceGroup
selectedKey={graphAutoVizDisabled} selectedKey={graphAutoVizDisabled}
options={graphAutoOptionList} options={graphAutoOptionList}
@@ -876,21 +804,20 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
aria-label="Graph Auto-visualization" aria-label="Graph Auto-visualization"
/> />
</div> </div>
</AccordionPanel> </div>
</AccordionItem>
)} )}
{shouldShowCopilotSampleDBOption && ( {shouldShowCopilotSampleDBOption && (
<AccordionItem value="12"> <div className="settingsSection">
<AccordionHeader> <div className="settingsSectionPart settingsSectionInlineCheckbox">
<div className={styles.header}>Enable sample database</div> <div className="settingsSectionLabel">
</AccordionHeader> Enable sample database
<AccordionPanel> <InfoTooltip>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
This is a sample database and collection with synthetic product data you can use to explore using This is a sample database and collection with synthetic product data you can use to explore using
NoSQL queries and Query Advisor. This will appear as another database in the Data Explorer UI, and NoSQL queries and Query Advisor. This will appear as another database in the Data Explorer UI, and is
is created by, and maintained by Microsoft at no cost to you. created by, and maintained by Microsoft at no cost to you.
</InfoTooltip>
</div> </div>
<Checkbox <Checkbox
styles={{ styles={{
label: { padding: 0 }, label: { padding: 0 },
@@ -899,42 +826,10 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
ariaLabel="Enable sample db for Query Advisor" ariaLabel="Enable sample db for Query Advisor"
checked={copilotSampleDBEnabled} checked={copilotSampleDBEnabled}
onChange={handleSampleDatabaseChange} onChange={handleSampleDatabaseChange}
label="Enable sample database"
/> />
</div> </div>
</AccordionPanel> </div>
</AccordionItem>
)} )}
</Accordion>
<div className="settingsSection">
<div className="settingsSectionPart">
<DefaultButton
onClick={() => {
useDialog.getState().showOkCancelModalDialog(
"Clear History",
undefined,
"Are you sure you want to proceed?",
() => deleteAllStates(),
"Cancel",
undefined,
<>
<span>
This action will clear the all customizations for this account in this browser, including:
</span>
<ul className={styles.bulletList}>
<li>Reset your customized tab layout, including the splitter positions</li>
<li>Erase your table column preferences, including any custom columns</li>
<li>Clear your filter history</li>
</ul>
</>,
);
}}
>
Clear History
</DefaultButton>
</div>
</div>
<div className="settingsSection"> <div className="settingsSection">
<div className="settingsSectionPart"> <div className="settingsSectionPart">
<div className="settingsSectionLabel">Explorer Version</div> <div className="settingsSectionLabel">Explorer Version</div>

View File

@@ -8,30 +8,24 @@ exports[`Settings Pane should render Default properly 1`] = `
submitButtonText="Apply" submitButtonText="Apply"
> >
<div <div
className="paneMainContent ___133e6fg_0000000 f22iagw f1vx9l62 f1l02sjl" className="paneMainContent"
> >
<Accordion
className="customAccordion ___1uf6361_0000000 fz7g6wx"
>
<AccordionItem
value="1"
>
<AccordionHeader>
<div <div
className="___15c001r_0000000 fq02s40" className="settingsSection"
>
<div
className="settingsSectionPart"
>
<fieldset>
<legend
className="settingsSectionLabel legendLabel"
id="pageOptions"
> >
Page Options Page Options
</div> </legend>
</AccordionHeader> <InfoTooltip>
<AccordionPanel>
<div
className="___1dfa554_0000000 fo7qwa0"
>
<div
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
>
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page. Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page.
</div> </InfoTooltip>
<StyledChoiceGroup <StyledChoiceGroup
ariaLabelledBy="pageOptions" ariaLabelledBy="pageOptions"
onChange={[Function]} onChange={[Function]}
@@ -72,21 +66,19 @@ exports[`Settings Pane should render Default properly 1`] = `
} }
} }
/> />
</fieldset>
</div> </div>
<div <div
className="tabs ___1dfa554_0000000 fo7qwa0" className="tabs settingsSectionPart"
> >
<div <div
className="tabcontent" className="tabcontent"
> >
<div <div
className="___10gar1i_0000000 f1fow5ox f1ugzwwg" className="settingsSectionLabel"
> >
Query results per page Query results per page
<InfoTooltip>
<InfoTooltip
className="___vtc5hy0_0000000 f10ra9hq f1k6fduh"
>
Enter the number of query results that should be shown per page. Enter the number of query results that should be shown per page.
</InfoTooltip> </InfoTooltip>
</div> </div>
@@ -104,71 +96,28 @@ exports[`Settings Pane should render Default properly 1`] = `
/> />
</div> </div>
</div> </div>
</AccordionPanel>
</AccordionItem>
<AccordionItem
value="3"
>
<AccordionHeader>
<div
className="___15c001r_0000000 fq02s40"
>
Query Timeout
</div> </div>
</AccordionHeader>
<AccordionPanel>
<div <div
className="___1dfa554_0000000 fo7qwa0" className="settingsSection"
> >
<div <div
className="___10gar1i_0000000 f1fow5ox f1ugzwwg" className="settingsSectionPart"
> >
When a query reaches a specified time limit, a popup with an option to cancel the query will show unless automatic cancellation has been enabled. <div>
</div> <legend
<StyledToggleBase className="settingsSectionLabel legendLabel"
defaultChecked={false} id="ruThresholdLabel"
label="Enable query timeout" >
onChange={[Function]} RU Threshold
styles={ </legend>
{ <InfoTooltip>
"container": {}, If a query exceeds a configured RU threshold, the query will be aborted.
"label": { </InfoTooltip>
"display": "block",
"fontSize": 12,
"fontWeight": 400,
},
"pill": {},
"root": {},
"text": {},
"thumb": {},
}
}
/>
</div>
</AccordionPanel>
</AccordionItem>
<AccordionItem
value="4"
>
<AccordionHeader>
<div
className="___15c001r_0000000 fq02s40"
>
RU Limit
</div>
</AccordionHeader>
<AccordionPanel>
<div
className="___1dfa554_0000000 fo7qwa0"
>
<div
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
>
If a query exceeds a configured RU limit, the query will be aborted.
</div> </div>
<div>
<StyledToggleBase <StyledToggleBase
defaultChecked={true} defaultChecked={true}
label="Enable RU limit" label="Enable RU threshold"
onChange={[Function]} onChange={[Function]}
styles={ styles={
{ {
@@ -186,14 +135,12 @@ exports[`Settings Pane should render Default properly 1`] = `
} }
/> />
</div> </div>
<div <div>
className="___1dfa554_0000000 fo7qwa0"
>
<StyledSpinButton <StyledSpinButton
decrementButtonAriaLabel="Decrease value by 1000" decrementButtonAriaLabel="Decrease value by 1000"
defaultValue="5000" defaultValue="5000"
incrementButtonAriaLabel="Increase value by 1000" incrementButtonAriaLabel="Increase value by 1000"
label="RU Limit (RU)" label="RU Threshold (RU)"
labelPosition={0} labelPosition={0}
min={1} min={1}
onChange={[Function]} onChange={[Function]}
@@ -216,27 +163,66 @@ exports[`Settings Pane should render Default properly 1`] = `
} }
/> />
</div> </div>
</AccordionPanel> </div>
</AccordionItem> </div>
<AccordionItem
value="5"
>
<AccordionHeader>
<div <div
className="___15c001r_0000000 fq02s40" className="settingsSection"
>
<div
className="settingsSectionPart"
>
<div>
<legend
className="settingsSectionLabel legendLabel"
id="queryTimeoutLabel"
>
Query Timeout
</legend>
<InfoTooltip>
When a query reaches a specified time limit, a popup with an option to cancel the query will show unless automatic cancellation has been enabled
</InfoTooltip>
</div>
<div>
<StyledToggleBase
defaultChecked={false}
label="Enable query timeout"
onChange={[Function]}
styles={
{
"container": {},
"label": {
"display": "block",
"fontSize": 12,
"fontWeight": 400,
},
"pill": {},
"root": {},
"text": {},
"thumb": {},
}
}
/>
</div>
</div>
</div>
<div
className="settingsSection"
>
<div
className="settingsSectionPart"
>
<div>
<legend
className="settingsSectionLabel legendLabel"
id="defaultQueryResultsView"
> >
Default Query Results View Default Query Results View
</div> </legend>
</AccordionHeader> <InfoTooltip>
<AccordionPanel>
<div
className="___1dfa554_0000000 fo7qwa0"
>
<div
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
>
Select the default view to use when displaying query results. Select the default view to use when displaying query results.
</InfoTooltip>
</div> </div>
<div>
<StyledChoiceGroup <StyledChoiceGroup
ariaLabelledBy="defaultQueryResultsView" ariaLabelledBy="defaultQueryResultsView"
onChange={[Function]} onChange={[Function]}
@@ -252,7 +238,7 @@ exports[`Settings Pane should render Default properly 1`] = `
}, },
] ]
} }
selectedKey="horizontal" selectedKey="vertical"
styles={ styles={
{ {
"flexContainer": [ "flexContainer": [
@@ -278,36 +264,30 @@ exports[`Settings Pane should render Default properly 1`] = `
} }
/> />
</div> </div>
</AccordionPanel> </div>
</AccordionItem> </div>
<AccordionItem
value="6"
>
<AccordionHeader>
<div <div
className="___15c001r_0000000 fq02s40" className="settingsSection"
>
<div
className="settingsSectionPart"
>
<div
className="settingsSectionLabel"
> >
Retry Settings Retry Settings
</div> <InfoTooltip>
</AccordionHeader>
<AccordionPanel>
<div
className="___1dfa554_0000000 fo7qwa0"
>
<div
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
>
Retry policy associated with throttled requests during CosmosDB queries. Retry policy associated with throttled requests during CosmosDB queries.
</InfoTooltip>
</div> </div>
<div> <div>
<span <legend
className="___zls1px0_0000000 fq02s40 f1ugzwwg" className="settingsSectionLabel legendLabel"
id="queryRetryAttemptsLabel"
> >
Max retry attempts Max retry attempts
</span> </legend>
<InfoTooltip <InfoTooltip>
className="___vtc5hy0_0000000 f10ra9hq f1k6fduh"
>
Max number of retries to be performed for a request. Default value 9. Max number of retries to be performed for a request. Default value 9.
</InfoTooltip> </InfoTooltip>
</div> </div>
@@ -340,14 +320,13 @@ exports[`Settings Pane should render Default properly 1`] = `
value="9" value="9"
/> />
<div> <div>
<span <legend
className="___zls1px0_0000000 fq02s40 f1ugzwwg" className="settingsSectionLabel legendLabel"
id="queryRetryAttemptsLabel"
> >
Fixed retry interval (ms) Fixed retry interval (ms)
</span> </legend>
<InfoTooltip <InfoTooltip>
className="___vtc5hy0_0000000 f10ra9hq f1k6fduh"
>
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned as part of the response. Default value is 0 milliseconds. 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> </InfoTooltip>
</div> </div>
@@ -380,14 +359,13 @@ exports[`Settings Pane should render Default properly 1`] = `
value="0" value="0"
/> />
<div> <div>
<span <legend
className="___zls1px0_0000000 fq02s40 f1ugzwwg" className="settingsSectionLabel legendLabel"
id="queryRetryAttemptsLabel"
> >
Max wait time (s) Max wait time (s)
</span> </legend>
<InfoTooltip <InfoTooltip>
className="___vtc5hy0_0000000 f10ra9hq f1k6fduh"
>
Max wait time in seconds to wait for a request while the retries are happening. Default value 30 seconds. Max wait time in seconds to wait for a request while the retries are happening. Default value 30 seconds.
</InfoTooltip> </InfoTooltip>
</div> </div>
@@ -420,32 +398,25 @@ exports[`Settings Pane should render Default properly 1`] = `
value="30" value="30"
/> />
</div> </div>
</AccordionPanel> </div>
</AccordionItem>
<AccordionItem
value="7"
>
<AccordionHeader>
<div <div
className="___15c001r_0000000 fq02s40" className="settingsSection"
>
<div
className="settingsSectionPart settingsSectionInlineCheckbox"
>
<div
className="settingsSectionLabel"
> >
Enable container pagination Enable container pagination
</div> <InfoTooltip>
</AccordionHeader>
<AccordionPanel>
<div
className="___1dfa554_0000000 fo7qwa0"
>
<div
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
>
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order. Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
</InfoTooltip>
</div> </div>
<StyledCheckboxBase <StyledCheckboxBase
ariaLabel="Enable container pagination" ariaLabel="Enable container pagination"
checked={false} checked={false}
className="padding" className="padding"
label="Enable container pagination"
onChange={[Function]} onChange={[Function]}
styles={ styles={
{ {
@@ -456,32 +427,25 @@ exports[`Settings Pane should render Default properly 1`] = `
} }
/> />
</div> </div>
</AccordionPanel> </div>
</AccordionItem>
<AccordionItem
value="8"
>
<AccordionHeader>
<div <div
className="___15c001r_0000000 fq02s40" className="settingsSection"
>
<div
className="settingsSectionPart settingsSectionInlineCheckbox"
>
<div
className="settingsSectionLabel"
> >
Enable cross-partition query Enable cross-partition query
</div> <InfoTooltip>
</AccordionHeader>
<AccordionPanel>
<div
className="___1dfa554_0000000 fo7qwa0"
>
<div
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
>
Send more than one request while executing a query. More than one request is necessary if the query is not scoped to single partition key value. Send more than one request while executing a query. More than one request is necessary if the query is not scoped to single partition key value.
</InfoTooltip>
</div> </div>
<StyledCheckboxBase <StyledCheckboxBase
ariaLabel="Enable cross partition query" ariaLabel="Enable cross partition query"
checked={false} checked={false}
className="padding" className="padding"
label="Enable cross-partition query"
onChange={[Function]} onChange={[Function]}
styles={ styles={
{ {
@@ -492,32 +456,25 @@ exports[`Settings Pane should render Default properly 1`] = `
} }
/> />
</div> </div>
</AccordionPanel> </div>
</AccordionItem>
<AccordionItem
value="9"
>
<AccordionHeader>
<div <div
className="___15c001r_0000000 fq02s40" className="settingsSection"
>
<div
className="settingsSectionPart"
>
<div
className="settingsSectionLabel"
> >
Max degree of parallelism Max degree of parallelism
</div> <InfoTooltip>
</AccordionHeader>
<AccordionPanel>
<div
className="___1dfa554_0000000 fo7qwa0"
>
<div
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
>
Gets or sets the number of concurrent operations run client side during parallel query execution. A positive property value limits the number of concurrent operations to the set value. If it is set to less than 0, the system automatically decides the number of concurrent operations to run. Gets or sets the number of concurrent operations run client side during parallel query execution. A positive property value limits the number of concurrent operations to the set value. If it is set to less than 0, the system automatically decides the number of concurrent operations to run.
</InfoTooltip>
</div> </div>
<StyledSpinButton <StyledSpinButton
ariaLabel="Max degree of parallelism" ariaLabel="Max degree of parallelism"
className="textfontclr" className="textfontclr"
id="max-degree" id="max-degree"
label="Max degree of parallelism"
min={-1} min={-1}
onDecrement={[Function]} onDecrement={[Function]}
onIncrement={[Function]} onIncrement={[Function]}
@@ -527,21 +484,6 @@ exports[`Settings Pane should render Default properly 1`] = `
value="6" value="6"
/> />
</div> </div>
</AccordionPanel>
</AccordionItem>
</Accordion>
<div
className="settingsSection"
>
<div
className="settingsSectionPart"
>
<CustomizedDefaultButton
onClick={[Function]}
>
Clear History
</CustomizedDefaultButton>
</div>
</div> </div>
<div <div
className="settingsSection" className="settingsSection"
@@ -569,39 +511,30 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
submitButtonText="Apply" submitButtonText="Apply"
> >
<div <div
className="paneMainContent ___133e6fg_0000000 f22iagw f1vx9l62 f1l02sjl" className="paneMainContent"
> >
<Accordion
className="customAccordion ___1uf6361_0000000 fz7g6wx"
>
<AccordionItem
value="6"
>
<AccordionHeader>
<div <div
className="___15c001r_0000000 fq02s40" className="settingsSection"
>
<div
className="settingsSectionPart"
>
<div
className="settingsSectionLabel"
> >
Retry Settings Retry Settings
</div> <InfoTooltip>
</AccordionHeader>
<AccordionPanel>
<div
className="___1dfa554_0000000 fo7qwa0"
>
<div
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
>
Retry policy associated with throttled requests during CosmosDB queries. Retry policy associated with throttled requests during CosmosDB queries.
</InfoTooltip>
</div> </div>
<div> <div>
<span <legend
className="___zls1px0_0000000 fq02s40 f1ugzwwg" className="settingsSectionLabel legendLabel"
id="queryRetryAttemptsLabel"
> >
Max retry attempts Max retry attempts
</span> </legend>
<InfoTooltip <InfoTooltip>
className="___vtc5hy0_0000000 f10ra9hq f1k6fduh"
>
Max number of retries to be performed for a request. Default value 9. Max number of retries to be performed for a request. Default value 9.
</InfoTooltip> </InfoTooltip>
</div> </div>
@@ -634,14 +567,13 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
value="9" value="9"
/> />
<div> <div>
<span <legend
className="___zls1px0_0000000 fq02s40 f1ugzwwg" className="settingsSectionLabel legendLabel"
id="queryRetryAttemptsLabel"
> >
Fixed retry interval (ms) Fixed retry interval (ms)
</span> </legend>
<InfoTooltip <InfoTooltip>
className="___vtc5hy0_0000000 f10ra9hq f1k6fduh"
>
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned as part of the response. Default value is 0 milliseconds. 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> </InfoTooltip>
</div> </div>
@@ -674,14 +606,13 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
value="0" value="0"
/> />
<div> <div>
<span <legend
className="___zls1px0_0000000 fq02s40 f1ugzwwg" className="settingsSectionLabel legendLabel"
id="queryRetryAttemptsLabel"
> >
Max wait time (s) Max wait time (s)
</span> </legend>
<InfoTooltip <InfoTooltip>
className="___vtc5hy0_0000000 f10ra9hq f1k6fduh"
>
Max wait time in seconds to wait for a request while the retries are happening. Default value 30 seconds. Max wait time in seconds to wait for a request while the retries are happening. Default value 30 seconds.
</InfoTooltip> </InfoTooltip>
</div> </div>
@@ -714,32 +645,25 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
value="30" value="30"
/> />
</div> </div>
</AccordionPanel> </div>
</AccordionItem>
<AccordionItem
value="7"
>
<AccordionHeader>
<div <div
className="___15c001r_0000000 fq02s40" className="settingsSection"
>
<div
className="settingsSectionPart settingsSectionInlineCheckbox"
>
<div
className="settingsSectionLabel"
> >
Enable container pagination Enable container pagination
</div> <InfoTooltip>
</AccordionHeader>
<AccordionPanel>
<div
className="___1dfa554_0000000 fo7qwa0"
>
<div
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
>
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order. Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
</InfoTooltip>
</div> </div>
<StyledCheckboxBase <StyledCheckboxBase
ariaLabel="Enable container pagination" ariaLabel="Enable container pagination"
checked={false} checked={false}
className="padding" className="padding"
label="Enable container pagination"
onChange={[Function]} onChange={[Function]}
styles={ styles={
{ {
@@ -750,26 +674,20 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
} }
/> />
</div> </div>
</AccordionPanel> </div>
</AccordionItem>
<AccordionItem
value="11"
>
<AccordionHeader>
<div <div
className="___15c001r_0000000 fq02s40" className="settingsSection"
>
<div
className="settingsSectionPart"
>
<div
className="settingsSectionLabel"
> >
Display Gremlin query results as:  Display Gremlin query results as: 
</div> <InfoTooltip>
</AccordionHeader>
<AccordionPanel>
<div
className="___1dfa554_0000000 fo7qwa0"
>
<div
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
>
Select Graph to automatically visualize the query results as a Graph or JSON to display the results as JSON. Select Graph to automatically visualize the query results as a Graph or JSON to display the results as JSON.
</InfoTooltip>
</div> </div>
<StyledChoiceGroup <StyledChoiceGroup
aria-label="Graph Auto-visualization" aria-label="Graph Auto-visualization"
@@ -789,21 +707,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
selectedKey="false" selectedKey="false"
/> />
</div> </div>
</AccordionPanel>
</AccordionItem>
</Accordion>
<div
className="settingsSection"
>
<div
className="settingsSectionPart"
>
<CustomizedDefaultButton
onClick={[Function]}
>
Clear History
</CustomizedDefaultButton>
</div>
</div> </div>
<div <div
className="settingsSection" className="settingsSection"

View File

@@ -1,156 +0,0 @@
import {
Button,
Checkbox,
CheckboxOnChangeData,
InputOnChangeData,
makeStyles,
SearchBox,
SearchBoxChangeEvent,
Text,
} from "@fluentui/react-components";
import { configContext } from "ConfigContext";
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
import { CosmosFluentProvider, getPlatformTheme } from "Explorer/Theme/ThemeUtil";
import React from "react";
import { useSidePanel } from "../../../hooks/useSidePanel";
const useColumnSelectionStyles = makeStyles({
paneContainer: {
height: "100%",
display: "flex",
},
searchBox: {
width: "100%",
},
checkboxContainer: {
display: "flex",
flexDirection: "column",
flex: 1,
},
checkboxLabel: {
padding: "4px 8px",
marginBottom: "0px",
},
});
export interface TableColumnSelectionPaneProps {
columnDefinitions: ColumnDefinition[];
selectedColumnIds: string[];
onSelectionChange: (newSelectedColumnIds: string[]) => void;
defaultSelection: string[];
}
export const TableColumnSelectionPane: React.FC<TableColumnSelectionPaneProps> = ({
columnDefinitions,
selectedColumnIds,
onSelectionChange,
defaultSelection,
}: TableColumnSelectionPaneProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const originalSelectedColumnIds = React.useMemo(() => selectedColumnIds, []);
const [columnSearchText, setColumnSearchText] = React.useState<string>("");
const [newSelectedColumnIds, setNewSelectedColumnIds] = React.useState<string[]>(originalSelectedColumnIds);
const styles = useColumnSelectionStyles();
const selectedColumnIdsSet = new Set(newSelectedColumnIds);
const onCheckedValueChange = (id: string, checkedData?: CheckboxOnChangeData): void => {
const checked = checkedData?.checked;
if (checked === "mixed" || checked === undefined) {
return;
}
if (checked) {
selectedColumnIdsSet.add(id);
} else {
/* selectedColumnIds may contain ids that are not in columnDefinitions, because the selected
* ids may have been loaded from persistence, but don't exist in the current retrieved documents.
*/
if (
Array.from(selectedColumnIdsSet).filter((id) => columnDefinitions.find((def) => def.id === id) !== undefined)
.length === 1 &&
selectedColumnIdsSet.has(id)
) {
// Don't allow unchecking the last column
return;
}
selectedColumnIdsSet.delete(id);
}
setNewSelectedColumnIds([...selectedColumnIdsSet]);
};
const onSave = (): void => {
onSelectionChange(newSelectedColumnIds);
closeSidePanel();
};
const onSearchChange: (event: SearchBoxChangeEvent, data: InputOnChangeData) => void = (_, data) =>
// eslint-disable-next-line react/prop-types
setColumnSearchText(data.value);
const theme = getPlatformTheme(configContext.platform);
// Filter and move partition keys to the top
const columnDefinitionList = columnDefinitions
.filter((def) => !columnSearchText || def.label.toLowerCase().includes(columnSearchText.toLowerCase()))
.sort((a, b) => {
const ID = "id";
// "id" always at the top, then partition keys, then everything else sorted
if (a.id === ID) {
return b.id === ID ? 0 : -1;
} else if (b.id === ID) {
return a.id === ID ? 0 : 1;
} else if (a.isPartitionKey && !b.isPartitionKey) {
return -1;
} else if (b.isPartitionKey && !a.isPartitionKey) {
return 1;
} else {
return a.label.localeCompare(b.label);
}
});
return (
<div className={styles.paneContainer}>
<CosmosFluentProvider>
<div className="panelFormWrapper">
<div className="panelMainContent" style={{ display: "flex", flexDirection: "column" }}>
<Text>Select which columns to display in your view of items in your container.</Text>
<div /* Wrap <SearchBox> to avoid margin-bottom set by panelMainContent css */>
<SearchBox
className={styles.searchBox}
value={columnSearchText}
onChange={onSearchChange}
placeholder="Search fields"
/>
</div>
<div className={styles.checkboxContainer}>
{columnDefinitionList.map((columnDefinition) => (
<Checkbox
style={{ marginBottom: 0 }}
key={columnDefinition.id}
label={{
className: styles.checkboxLabel,
children: `${columnDefinition.label}${columnDefinition.isPartitionKey ? " (partition key)" : ""}`,
}}
checked={selectedColumnIdsSet.has(columnDefinition.id)}
onChange={(_, data) => onCheckedValueChange(columnDefinition.id, data)}
/>
))}
</div>
<Button appearance="secondary" size="small" onClick={() => setNewSelectedColumnIds(defaultSelection)}>
Reset
</Button>
</div>
<div className="panelFooter" style={{ display: "flex", gap: theme.spacingHorizontalS }}>
<Button appearance="primary" onClick={onSave}>
Save
</Button>
<Button appearance="secondary" onClick={closeSidePanel}>
Cancel
</Button>
</div>
</div>
</CosmosFluentProvider>
</div>
);
};

View File

@@ -2,7 +2,7 @@ import "@testing-library/jest-dom";
import { RenderResult, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { RenderResult, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels"; import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
import React from "react"; import React from "react";
import { VectorEmbeddingPoliciesComponent } from "./VectorEmbeddingPoliciesComponent"; import { AddVectorEmbeddingPolicyForm } from "./AddVectorEmbeddingPolicyForm";
const mockVectorEmbedding: VectorEmbedding[] = [ const mockVectorEmbedding: VectorEmbedding[] = [
{ path: "/vector1", dataType: "float32", distanceFunction: "euclidean", dimensions: 0 }, { path: "/vector1", dataType: "float32", distanceFunction: "euclidean", dimensions: 0 },
@@ -17,9 +17,9 @@ describe("AddVectorEmbeddingPolicyForm", () => {
beforeEach(() => { beforeEach(() => {
component = render( component = render(
<VectorEmbeddingPoliciesComponent <AddVectorEmbeddingPolicyForm
vectorEmbeddings={mockVectorEmbedding} vectorEmbedding={mockVectorEmbedding}
vectorIndexes={mockVectorIndex} vectorIndex={mockVectorIndex}
onVectorEmbeddingChange={mockOnVectorEmbeddingChange} onVectorEmbeddingChange={mockOnVectorEmbeddingChange}
/>, />,
); );
@@ -36,7 +36,7 @@ describe("AddVectorEmbeddingPolicyForm", () => {
}); });
test("calls onDelete when delete button is clicked", async () => { test("calls onDelete when delete button is clicked", async () => {
const deleteButton = component.container.querySelector("#delete-Vector-embedding-1"); const deleteButton = component.container.querySelector("#delete-vector-policy-1");
fireEvent.click(deleteButton); fireEvent.click(deleteButton);
expect(mockOnVectorEmbeddingChange).toHaveBeenCalled(); expect(mockOnVectorEmbeddingChange).toHaveBeenCalled();
expect(screen.queryByText("Vector embedding 1")).toBeNull(); expect(screen.queryByText("Vector embedding 1")).toBeNull();
@@ -49,19 +49,21 @@ describe("AddVectorEmbeddingPolicyForm", () => {
test("validates input correctly", async () => { test("validates input correctly", async () => {
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "" } }); fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "" } });
await waitFor(() => expect(screen.getByText("Path should not be empty")).toBeInTheDocument(), { await waitFor(() => expect(screen.getByText("Vector embedding path should not be empty")).toBeInTheDocument(), {
timeout: 1500, timeout: 1500,
}); });
await waitFor( await waitFor(
() => () =>
expect(screen.getByText("Dimension must be greater than 0 and less than or equal 4096")).toBeInTheDocument(), expect(
screen.getByText("Vector embedding dimension must be greater than 0 and less than or equal 4096"),
).toBeInTheDocument(),
{ {
timeout: 1500, timeout: 1500,
}, },
); );
fireEvent.change(component.container.querySelector("#vector-policy-dimension-1"), { target: { value: "4096" } }); fireEvent.change(component.container.querySelector("#vector-policy-dimension-1"), { target: { value: "4096" } });
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "/vector1" } }); fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "/vector1" } });
await waitFor(() => expect(screen.queryByText("Path should not be empty")).toBeNull(), { await waitFor(() => expect(screen.queryByText("Vector embedding path should not be empty")).toBeNull(), {
timeout: 1500, timeout: 1500,
}); });
await waitFor( await waitFor(

View File

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

View File

@@ -106,7 +106,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
horizontal={true} horizontal={true}
> >
<StyledCheckboxBase <StyledCheckboxBase
checked={false} checked={true}
label="Share throughput across containers" label="Share throughput across containers"
onChange={[Function]} onChange={[Function]}
styles={ styles={
@@ -137,6 +137,14 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
/> />
</StyledTooltipHostBase> </StyledTooltipHostBase>
</Stack> </Stack>
<ThroughputInput
isDatabase={true}
isSharded={true}
onCostAcknowledgeChange={[Function]}
setIsAutoscale={[Function]}
setIsThroughputCapExceeded={[Function]}
setThroughputValue={[Function]}
/>
</Stack> </Stack>
<Separator <Separator
className="panelSeparator" className="panelSeparator"
@@ -255,14 +263,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
</CustomizedDefaultButton> </CustomizedDefaultButton>
</Stack> </Stack>
</Stack> </Stack>
<ThroughputInput
isDatabase={false}
isSharded={true}
onCostAcknowledgeChange={[Function]}
setIsAutoscale={[Function]}
setIsThroughputCapExceeded={[Function]}
setThroughputValue={[Function]}
/>
<Stack> <Stack>
<Stack <Stack
horizontal={true} horizontal={true}
@@ -308,25 +308,41 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
</Stack> </Stack>
<Stack <Stack
className="panelGroupSpacing" className="panelGroupSpacing"
>
<Stack
horizontal={true}
> >
<Text <Text
className="panelTextBold" className="panelTextBold"
variant="small" variant="small"
> >
Analytical store
</Text>
<StyledTooltipHostBase
content={
<Text <Text
variant="small" variant="small"
> >
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads. Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.
<StyledLinkBase <StyledLinkBase
aria-label="Learn more about analytical store."
href="https://aka.ms/analytical-store-overview" href="https://aka.ms/analytical-store-overview"
target="_blank" target="_blank"
> >
Learn more Learn more
</StyledLinkBase> </StyledLinkBase>
</Text> </Text>
</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>
<Stack <Stack
horizontal={true} horizontal={true}
verticalAlign="center" verticalAlign="center"
@@ -384,7 +400,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
. Enable Synapse Link for this Cosmos DB account. . Enable Synapse Link for this Cosmos DB account.
<StyledLinkBase <StyledLinkBase
aria-label="Learn more about Azure Synapse Link."
className="capacitycalculator-link" className="capacitycalculator-link"
href="https://aka.ms/cosmosdb-synapselink" href="https://aka.ms/cosmosdb-synapselink"
target="_blank" target="_blank"

View File

@@ -361,11 +361,13 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
<span <span
className="css-113" className="css-113"
> >
Confirm by typing the Database id (name) Confirm by typing the
Database
id
</span> </span>
</Text> </Text>
<StyledTextFieldBase <StyledTextFieldBase
ariaLabel="Confirm by typing the Database id (name)" ariaLabel="Confirm by typing the Database id"
autoFocus={true} autoFocus={true}
data-test="Input:confirmDatabaseId" data-test="Input:confirmDatabaseId"
id="confirmDatabaseId" id="confirmDatabaseId"
@@ -380,7 +382,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
} }
> >
<TextFieldBase <TextFieldBase
ariaLabel="Confirm by typing the Database id (name)" ariaLabel="Confirm by typing the Database id"
autoFocus={true} autoFocus={true}
data-test="Input:confirmDatabaseId" data-test="Input:confirmDatabaseId"
deferredValidationTime={200} deferredValidationTime={200}
@@ -675,7 +677,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
> >
<input <input
aria-invalid={false} aria-invalid={false}
aria-label="Confirm by typing the Database id (name)" aria-label="Confirm by typing the Database id"
autoFocus={true} autoFocus={true}
className="ms-TextField-field field-117" className="ms-TextField-field field-117"
data-test="Input:confirmDatabaseId" data-test="Input:confirmDatabaseId"

View File

@@ -171,7 +171,7 @@ export const QueryCopilotCarousel: React.FC<QueryCopilotCarouselProps> = ({
the query builder. the query builder.
</Text> </Text>
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 24 }}>Database Id</Text> <Text style={{ fontSize: 13, fontWeight: 600, marginTop: 24 }}>Database Id</Text>
<Text style={{ fontSize: 13 }}>CopilotSampleDB</Text> <Text style={{ fontSize: 13 }}>CopilotSampleDb</Text>
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database throughput (autoscale)</Text> <Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database throughput (autoscale)</Text>
<Text style={{ fontSize: 13 }}>Autoscale</Text> <Text style={{ fontSize: 13 }}>Autoscale</Text>
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database Max RU/s</Text> <Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database Max RU/s</Text>

View File

@@ -79,13 +79,9 @@ export const QueryCopilotFeedbackModal = ({
readOnly readOnly
/> />
<Text style={{ fontSize: 12, marginBottom: 14 }}> <Text style={{ fontSize: 12, marginBottom: 14 }}>
Microsoft will process the feedback you submit pursuant to your organizations instructions in order to By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the{" "}
improve your and your organizations 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://go.microsoft.com/fwlink/?LinkId=521839" target="_blank"> <Link href="https://privacy.microsoft.com/privacystatement" target="_blank">
Privacy statement Privacy statement
</Link> </Link>
}{" "} }{" "}

View File

@@ -99,10 +99,10 @@ exports[`Query Copilot Feedback Modal snapshot test shoud render and match snaps
} }
} }
> >
Microsoft will process the feedback you submit pursuant to your organizations instructions in order to improve your and your organizations 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 By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
<StyledLinkBase <StyledLinkBase
href="https://go.microsoft.com/fwlink/?LinkId=521839" href="https://privacy.microsoft.com/privacystatement"
target="_blank" target="_blank"
> >
Privacy statement Privacy statement
@@ -236,10 +236,10 @@ exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`]
} }
} }
> >
Microsoft will process the feedback you submit pursuant to your organizations instructions in order to improve your and your organizations 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 By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
<StyledLinkBase <StyledLinkBase
href="https://go.microsoft.com/fwlink/?LinkId=521839" href="https://privacy.microsoft.com/privacystatement"
target="_blank" target="_blank"
> >
Privacy statement Privacy statement
@@ -373,10 +373,10 @@ exports[`Query Copilot Feedback Modal snapshot test should close on cancel click
} }
} }
> >
Microsoft will process the feedback you submit pursuant to your organizations instructions in order to improve your and your organizations 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 By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
<StyledLinkBase <StyledLinkBase
href="https://go.microsoft.com/fwlink/?LinkId=521839" href="https://privacy.microsoft.com/privacystatement"
target="_blank" target="_blank"
> >
Privacy statement Privacy statement
@@ -510,10 +510,10 @@ exports[`Query Copilot Feedback Modal snapshot test should get user unput 1`] =
} }
} }
> >
Microsoft will process the feedback you submit pursuant to your organizations instructions in order to improve your and your organizations 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 By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
<StyledLinkBase <StyledLinkBase
href="https://go.microsoft.com/fwlink/?LinkId=521839" href="https://privacy.microsoft.com/privacystatement"
target="_blank" target="_blank"
> >
Privacy statement Privacy statement
@@ -647,10 +647,10 @@ exports[`Query Copilot Feedback Modal snapshot test should not render dont show
} }
} }
> >
Microsoft will process the feedback you submit pursuant to your organizations instructions in order to improve your and your organizations 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 By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
<StyledLinkBase <StyledLinkBase
href="https://go.microsoft.com/fwlink/?LinkId=521839" href="https://privacy.microsoft.com/privacystatement"
target="_blank" target="_blank"
> >
Privacy statement Privacy statement
@@ -784,10 +784,10 @@ exports[`Query Copilot Feedback Modal snapshot test should render dont show agai
} }
} }
> >
Microsoft will process the feedback you submit pursuant to your organizations instructions in order to improve your and your organizations 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 By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
<StyledLinkBase <StyledLinkBase
href="https://go.microsoft.com/fwlink/?LinkId=521839" href="https://privacy.microsoft.com/privacystatement"
target="_blank" target="_blank"
> >
Privacy statement Privacy statement
@@ -936,10 +936,10 @@ exports[`Query Copilot Feedback Modal snapshot test should submit submission 1`]
} }
} }
> >
Microsoft will process the feedback you submit pursuant to your organizations instructions in order to improve your and your organizations 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 By pressing submit, your feedback will be used to improve Microsoft products and services. Please see the
<StyledLinkBase <StyledLinkBase
href="https://go.microsoft.com/fwlink/?LinkId=521839" href="https://privacy.microsoft.com/privacystatement"
target="_blank" target="_blank"
> >
Privacy statement Privacy statement

View File

@@ -1,5 +1,4 @@
import { MinimalQueryIterator } from "Common/IteratorUtilities"; import { MinimalQueryIterator } from "Common/IteratorUtilities";
import QueryError from "Common/QueryError";
import { QueryResults } from "Contracts/ViewModels"; import { QueryResults } from "Contracts/ViewModels";
import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces"; import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { guid } from "Explorer/Tables/Utilities"; import { guid } from "Explorer/Tables/Utilities";
@@ -29,7 +28,7 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
showSamplePrompts: false, showSamplePrompts: false,
queryIterator: undefined, queryIterator: undefined,
queryResults: undefined, queryResults: undefined,
errors: [], errorMessage: "",
isSamplePromptsOpen: false, isSamplePromptsOpen: false,
showPromptTeachingBubble: true, showPromptTeachingBubble: true,
showDeletePopup: false, showDeletePopup: false,
@@ -65,7 +64,7 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
setShowSamplePrompts: (showSamplePrompts: boolean) => set({ showSamplePrompts }), setShowSamplePrompts: (showSamplePrompts: boolean) => set({ showSamplePrompts }),
setQueryIterator: (queryIterator: MinimalQueryIterator | undefined) => set({ queryIterator }), setQueryIterator: (queryIterator: MinimalQueryIterator | undefined) => set({ queryIterator }),
setQueryResults: (queryResults: QueryResults | undefined) => set({ queryResults }), setQueryResults: (queryResults: QueryResults | undefined) => set({ queryResults }),
setErrors: (errors: QueryError[]) => set({ errors }), setErrorMessage: (errorMessage: string) => set({ errorMessage }),
setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => set({ isSamplePromptsOpen }), setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => set({ isSamplePromptsOpen }),
setShowPromptTeachingBubble: (showPromptTeachingBubble: boolean) => set({ showPromptTeachingBubble }), setShowPromptTeachingBubble: (showPromptTeachingBubble: boolean) => set({ showPromptTeachingBubble }),
setShowDeletePopup: (showDeletePopup: boolean) => set({ showDeletePopup }), setShowDeletePopup: (showDeletePopup: boolean) => set({ showDeletePopup }),

View File

@@ -18,9 +18,8 @@ import {
Text, Text,
TextField, TextField,
} from "@fluentui/react"; } from "@fluentui/react";
import { HttpStatusCodes, NormalizedEventKey } from "Common/Constants"; import { HttpStatusCodes } from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils"; import { handleError } from "Common/ErrorHandlingUtils";
import QueryError, { QueryErrorSeverity } from "Common/QueryError";
import { createUri } from "Common/UrlUtility"; import { createUri } from "Common/UrlUtility";
import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup"; import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup"; import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup";
@@ -28,8 +27,6 @@ import {
SuggestedPrompt, SuggestedPrompt,
getSampleDatabaseSuggestedPrompts, getSampleDatabaseSuggestedPrompts,
getSuggestedPrompts, getSuggestedPrompts,
readPromptHistory,
savePromptHistory,
} from "Explorer/QueryCopilot/QueryCopilotUtilities"; } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { SubmitFeedback, allocatePhoenixContainer } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { SubmitFeedback, allocatePhoenixContainer } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces"; import { GenerateSQLQueryResponse, QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
@@ -37,7 +34,7 @@ import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/Shared/
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import React, { useMemo, useRef, useState } from "react"; import React, { useRef, useState } from "react";
import HintIcon from "../../../images/Hint.svg"; import HintIcon from "../../../images/Hint.svg";
import RecentIcon from "../../../images/Recent.svg"; import RecentIcon from "../../../images/Recent.svg";
import errorIcon from "../../../images/close-black.svg"; import errorIcon from "../../../images/close-black.svg";
@@ -73,9 +70,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
}: QueryCopilotPromptProps): JSX.Element => { }: QueryCopilotPromptProps): JSX.Element => {
const [copilotTeachingBubbleVisible, setCopilotTeachingBubbleVisible] = useState<boolean>(false); const [copilotTeachingBubbleVisible, setCopilotTeachingBubbleVisible] = useState<boolean>(false);
const inputEdited = useRef(false); const inputEdited = useRef(false);
const itemRefs = useRef([]);
const searchInputRef = useRef(null);
const copyQueryRef = useRef(null);
const { const {
openFeedbackModal, openFeedbackModal,
hideFeedbackModalForLikedQueries, hideFeedbackModalForLikedQueries,
@@ -111,10 +105,10 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
setShowErrorMessageBar, setShowErrorMessageBar,
setGeneratedQueryComments, setGeneratedQueryComments,
setQueryResults, setQueryResults,
setErrors, setErrorMessage,
errors, errorMessage,
} = useCopilotStore(); } = useCopilotStore();
const [focusedIndex, setFocusedIndex] = useState(-1);
const sampleProps: SamplePromptsProps = { const sampleProps: SamplePromptsProps = {
isSamplePromptsOpen: isSamplePromptsOpen, isSamplePromptsOpen: isSamplePromptsOpen,
setIsSamplePromptsOpen: setIsSamplePromptsOpen, setIsSamplePromptsOpen: setIsSamplePromptsOpen,
@@ -133,20 +127,20 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
document.body.removeChild(queryElement); document.body.removeChild(queryElement);
setshowCopyPopup(true); setshowCopyPopup(true);
copyQueryRef.current.focus();
setTimeout(() => { setTimeout(() => {
setshowCopyPopup(false); setshowCopyPopup(false);
}, 6000); }, 6000);
}; };
const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected(); const isSampleCopilotActive = useSelectedNode.getState().isQueryCopilotCollectionSelected();
const [histories, setHistories] = useState<string[]>(() => readPromptHistory(userContext.databaseAccount)); const cachedHistoriesString = localStorage.getItem(`${userContext.databaseAccount?.id}-queryCopilotHistories`);
const cachedHistories = cachedHistoriesString?.split("|");
const [histories, setHistories] = useState<string[]>(cachedHistories || []);
const suggestedPrompts: SuggestedPrompt[] = isSampleCopilotActive const suggestedPrompts: SuggestedPrompt[] = isSampleCopilotActive
? getSampleDatabaseSuggestedPrompts() ? getSampleDatabaseSuggestedPrompts()
: getSuggestedPrompts(); : getSuggestedPrompts();
const [filteredHistories, setFilteredHistories] = useState<string[]>(histories); const [filteredHistories, setFilteredHistories] = useState<string[]>(histories);
const [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState<SuggestedPrompt[]>(suggestedPrompts); const [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState<SuggestedPrompt[]>(suggestedPrompts);
const { UpArrow, DownArrow, Enter } = NormalizedEventKey;
const handleUserPromptChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleUserPromptChange = (event: React.ChangeEvent<HTMLInputElement>) => {
inputEdited.current = true; inputEdited.current = true;
@@ -174,7 +168,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)]; const newHistories = [formattedUserPrompt, ...updatedHistories.slice(0, 2)];
setHistories(newHistories); setHistories(newHistories);
savePromptHistory(userContext.databaseAccount, newHistories); localStorage.setItem(`${userContext.databaseAccount.id}-queryCopilotHistories`, newHistories.join("|"));
}; };
const resetMessageStates = (): void => { const resetMessageStates = (): void => {
@@ -185,7 +179,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
const resetQueryResults = (): void => { const resetQueryResults = (): void => {
setQueryResults(null); setQueryResults(null);
setErrors([]); setErrorMessage("");
}; };
const generateSQLQuery = async (): Promise<void> => { const generateSQLQuery = async (): Promise<void> => {
@@ -249,12 +243,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
handleError(JSON.stringify(generateSQLQueryResponse), "copilotTooManyRequestError"); handleError(JSON.stringify(generateSQLQueryResponse), "copilotTooManyRequestError");
useTabs.getState().setIsQueryErrorThrown(true); useTabs.getState().setIsQueryErrorThrown(true);
setShowErrorMessageBar(true); setShowErrorMessageBar(true);
setErrors([ setErrorMessage("Ratelimit exceeded 5 per 1 minute. Please try again after sometime");
new QueryError(
"Ratelimit exceeded 5 per 1 minute. Please try again after sometime",
QueryErrorSeverity.Error,
),
]);
TelemetryProcessor.traceFailure(Action.QueryGenerationFromCopilotPrompt, { TelemetryProcessor.traceFailure(Action.QueryGenerationFromCopilotPrompt, {
databaseName: databaseId, databaseName: databaseId,
collectionId: containerId, collectionId: containerId,
@@ -307,43 +296,12 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
if (isGeneratingQuery === null) { if (isGeneratingQuery === null) {
return " "; return " ";
} else if (isGeneratingQuery) { } else if (isGeneratingQuery) {
return "Thinking"; return "Content is loading";
} else { } else {
return "Content is updated"; return "Content is updated";
} }
}; };
const openSamplePrompts = () => {
inputEdited.current = true;
setShowSamplePrompts(true);
};
const totalSuggestions = useMemo(
() => [...filteredSuggestedPrompts, ...filteredHistories],
[filteredSuggestedPrompts, filteredHistories],
);
const handleKeyDownForInput = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === DownArrow) {
setFocusedIndex(0);
itemRefs.current[0]?.current?.focus();
} else if (event.key === Enter && userPrompt) {
inputEdited.current = true;
startGenerateQueryProcess();
}
};
const handleKeyDownForItem = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === UpArrow && focusedIndex > 0) {
itemRefs.current[focusedIndex - 1].current?.focus();
setFocusedIndex((prevIndex) => prevIndex - 1);
} else if (event.key === DownArrow && focusedIndex < totalSuggestions.length - 1) {
itemRefs.current[focusedIndex + 1].current?.focus();
setFocusedIndex((prevIndex) => prevIndex + 1);
}
};
React.useEffect(() => {
itemRefs.current = totalSuggestions.map(() => React.createRef());
}, [totalSuggestions]);
React.useEffect(() => { React.useEffect(() => {
useTabs.getState().setIsQueryErrorThrown(false); useTabs.getState().setIsQueryErrorThrown(false);
}, []); }, []);
@@ -373,14 +331,23 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
id="naturalLanguageInput" id="naturalLanguageInput"
value={userPrompt} value={userPrompt}
onChange={handleUserPromptChange} onChange={handleUserPromptChange}
onClick={openSamplePrompts} onClick={() => {
onFocus={() => setShowSamplePrompts(true)} inputEdited.current = true;
elementRef={searchInputRef} setShowSamplePrompts(true);
onKeyDown={handleKeyDownForInput} }}
onKeyDown={(e) => {
if (e.key === "Enter" && userPrompt) {
inputEdited.current = true;
startGenerateQueryProcess();
}
}}
style={{ lineHeight: 30 }} style={{ lineHeight: 30 }}
styles={{ styles={{
root: { width: "100%" }, root: { width: "100%" },
suffix: { background: "none", padding: 0 }, suffix: {
background: "none",
padding: 0,
},
fieldGroup: { fieldGroup: {
borderRadius: 4, borderRadius: 4,
borderColor: "#D1D1D1", borderColor: "#D1D1D1",
@@ -393,8 +360,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
}, },
}} }}
disabled={isGeneratingQuery} disabled={isGeneratingQuery}
autoComplete="list" autoComplete="off"
aria-expanded={showSamplePrompts}
placeholder="Ask a question in natural language and well generate the query for you." placeholder="Ask a question in natural language and well generate the query for you."
aria-labelledby="copilot-textfield-label" aria-labelledby="copilot-textfield-label"
onRenderSuffix={() => { onRenderSuffix={() => {
@@ -402,7 +368,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
<IconButton <IconButton
iconProps={{ iconName: "Send" }} iconProps={{ iconName: "Send" }}
disabled={isGeneratingQuery || !userPrompt.trim()} disabled={isGeneratingQuery || !userPrompt.trim()}
allowDisabledFocus={true}
style={{ background: "none" }} style={{ background: "none" }}
onClick={() => startGenerateQueryProcess()} onClick={() => startGenerateQueryProcess()}
aria-label="Send" aria-label="Send"
@@ -467,8 +432,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
setShowSamplePrompts(false); setShowSamplePrompts(false);
inputEdited.current = true; inputEdited.current = true;
}} }}
elementRef={itemRefs.current[i]}
onKeyDown={handleKeyDownForItem}
onRenderIcon={() => <Image src={RecentIcon} styles={{ root: { overflow: "unset" } }} />} onRenderIcon={() => <Image src={RecentIcon} styles={{ root: { overflow: "unset" } }} />}
styles={promptStyles} styles={promptStyles}
> >
@@ -491,16 +454,14 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
> >
Suggested Prompts Suggested Prompts
</Text> </Text>
{filteredSuggestedPrompts.map((prompt, index) => ( {filteredSuggestedPrompts.map((prompt) => (
<DefaultButton <DefaultButton
key={prompt.id} key={prompt.id}
elementRef={itemRefs.current[filteredHistories.length + index]}
onClick={() => { onClick={() => {
setUserPrompt(prompt.text); setUserPrompt(prompt.text);
setShowSamplePrompts(false); setShowSamplePrompts(false);
inputEdited.current = true; inputEdited.current = true;
}} }}
onKeyDown={handleKeyDownForItem}
onRenderIcon={() => <Image src={HintIcon} />} onRenderIcon={() => <Image src={HintIcon} />}
styles={promptStyles} styles={promptStyles}
> >
@@ -553,9 +514,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
</Link> </Link>
{showErrorMessageBar && ( {showErrorMessageBar && (
<MessageBar messageBarType={MessageBarType.error}> <MessageBar messageBarType={MessageBarType.error}>
{errors.length > 0 {errorMessage ? errorMessage : "We ran into an error and were not able to execute query."}
? errors[0].message
: "We ran into an error and were not able to execute query."}
</MessageBar> </MessageBar>
)} )}
{showInvalidQueryMessageBar && ( {showInvalidQueryMessageBar && (
@@ -679,7 +638,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
)} )}
<CommandBarButton <CommandBarButton
className="copyQuery" className="copyQuery"
elementRef={copyQueryRef}
onClick={copyGeneratedCode} onClick={copyGeneratedCode}
iconProps={{ iconName: "Copy" }} iconProps={{ iconName: "Copy" }}
style={{ fontSize: 12, transition: "background-color 0.3s ease", height: "100%" }} style={{ fontSize: 12, transition: "background-color 0.3s ease", height: "100%" }}
@@ -710,9 +668,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
)} )}
</Stack> </Stack>
)} )}
{(showFeedbackBar || isGeneratingQuery) && (
<span role="alert" className="screenReaderOnly" aria-label={getAriaLabel()} />
)}
{isGeneratingQuery && ( {isGeneratingQuery && (
<ProgressIndicator <ProgressIndicator
label="Thinking..." label="Thinking..."

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