mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-25 03:41:19 +00:00
Compare commits
3 Commits
missing_pk
...
NPS_Dialog
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b7cbfbf23 | ||
|
|
76913d42f1 | ||
|
|
7ab2bd38f4 |
@@ -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
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# Install packages once, to prime the node_modules directory.
|
|
||||||
npm ci
|
|
||||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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)
|
||||||
|
|||||||
103
.github/workflows/ci.yml
vendored
103
.github/workflows/ci.yml
vendored
@@ -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 }})"
|
||||||
@@ -186,9 +185,9 @@ jobs:
|
|||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: blob-report-${{ matrix.shardIndex }}
|
name: blob-report-${{ matrix.shardIndex }}
|
||||||
path: blob-report
|
path: blob-report
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
merge-playwright-reports:
|
merge-playwright-reports:
|
||||||
name: "Merge Playwright Reports"
|
name: "Merge Playwright Reports"
|
||||||
@@ -198,26 +197,26 @@ jobs:
|
|||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Download blob reports from GitHub Actions Artifacts
|
- name: Download blob reports from GitHub Actions Artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: all-blob-reports
|
path: all-blob-reports
|
||||||
pattern: blob-report-*
|
pattern: blob-report-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Merge into HTML Report
|
- name: Merge into HTML Report
|
||||||
run: npx playwright merge-reports --reporter html ./all-blob-reports
|
run: npx playwright merge-reports --reporter html ./all-blob-reports
|
||||||
|
|
||||||
- name: Upload HTML report
|
- name: Upload HTML report
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: html-report--attempt-${{ github.run_attempt }}
|
name: html-report--attempt-${{ github.run_attempt }}
|
||||||
path: playwright-report
|
path: playwright-report
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
@@ -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>
|
|
||||||
@@ -18,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
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -61,8 +61,6 @@
|
|||||||
|
|
||||||
@GalleryBackgroundColor: #fdfdfd;
|
@GalleryBackgroundColor: #fdfdfd;
|
||||||
|
|
||||||
@LinkColor: #2d6da4;
|
|
||||||
|
|
||||||
//Icons
|
//Icons
|
||||||
@InfoIconColor: #0072c6;
|
@InfoIconColor: #0072c6;
|
||||||
@WarningIconColor: #db7500;
|
@WarningIconColor: #db7500;
|
||||||
@@ -248,10 +246,6 @@
|
|||||||
outline: 1px dashed @FocusColor;
|
outline: 1px dashed @FocusColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.focusedBorder() {
|
|
||||||
border: 1px dashed @FocusColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/************************************************************************************************
|
/************************************************************************************************
|
||||||
Common Toggle Switch
|
Common Toggle Switch
|
||||||
*************************************************************************************************/
|
*************************************************************************************************/
|
||||||
|
|||||||
@@ -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,21 +1906,8 @@ input::-webkit-calendar-picker-indicator::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs-margin {
|
.nav-tabs-margin {
|
||||||
|
padding-top: 5px;
|
||||||
background-color: #f2f2f2;
|
background-color: #f2f2f2;
|
||||||
|
|
||||||
.nav-tabs {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: flex-end;
|
|
||||||
height: 100%;
|
|
||||||
margin-bottom: -0.5px;
|
|
||||||
|
|
||||||
li {
|
|
||||||
// Override the bootstrap defaults here to align with our layout constants.
|
|
||||||
margin-bottom: 0px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navTabHeight {
|
.navTabHeight {
|
||||||
@@ -2373,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2631,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3132,7 +3112,3 @@ a:link {
|
|||||||
background: white;
|
background: white;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebarContainer {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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,16 +65,17 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content,
|
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content,
|
||||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content:hover {
|
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content:hover {
|
||||||
border-bottom: 2px solid @FabricAccentMedium;
|
border-bottom: 2px solid @FabricAccentMedium;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText {
|
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content>.contentWrapper>.tabNavText {
|
||||||
border-bottom: 0px none transparent;
|
border-bottom: 0px none transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,10 +94,10 @@ a:focus {
|
|||||||
padding-bottom: @SmallSpace;
|
padding-bottom: @SmallSpace;
|
||||||
|
|
||||||
.contentWrapper {
|
.contentWrapper {
|
||||||
.statusIconContainer {
|
.statusIconContainer {
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.tabIconSection {
|
.tabIconSection {
|
||||||
.cancelButton {
|
.cancelButton {
|
||||||
@@ -120,6 +119,7 @@ a:focus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.resourceTree {
|
.resourceTree {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
@@ -156,21 +156,25 @@ a:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.selected {
|
.selected {
|
||||||
& > .treeNodeHeader {
|
&>.treeNodeHeader {
|
||||||
background-color: @FabricAccentExtra;
|
background-color: @FabricAccentExtra;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.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%);
|
||||||
}
|
}
|
||||||
|
|||||||
1666
package-lock.json
generated
1666
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -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",
|
||||||
@@ -81,7 +82,7 @@
|
|||||||
"mkdirp": "1.0.4",
|
"mkdirp": "1.0.4",
|
||||||
"monaco-editor": "0.44.0",
|
"monaco-editor": "0.44.0",
|
||||||
"ms": "2.1.3",
|
"ms": "2.1.3",
|
||||||
"p-retry": "6.2.1",
|
"p-retry": "4.6.2",
|
||||||
"patch-package": "8.0.0",
|
"patch-package": "8.0.0",
|
||||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||||
"post-robot": "10.0.42",
|
"post-robot": "10.0.42",
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"PROXY_PATH": "/proxy",
|
"PROXY_PATH": "/proxy",
|
||||||
"msalRedirectURI": "https://dataexplorer-preview.azurewebsites.net/"
|
"msalRedirectURI": "https://cosmos-explorer-preview.azurewebsites.net/"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
37131
preview/package-lock.json
generated
37131
preview/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,16 +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 class FeedbackLabels {
|
|
||||||
public static readonly provideFeedback: string = "Provide feedback";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const QueryCopilotSampleDatabaseId = "CopilotSampleDB";
|
|
||||||
export const QueryCopilotSampleContainerId = "SampleContainer";
|
export const QueryCopilotSampleContainerId = "SampleContainer";
|
||||||
|
|
||||||
export const QueryCopilotSampleContainerSchema = {
|
export const QueryCopilotSampleContainerSchema = {
|
||||||
|
|||||||
@@ -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 = {};
|
||||||
|
|||||||
@@ -1,26 +1,24 @@
|
|||||||
import * as Cosmos from "@azure/cosmos";
|
import * as Cosmos from "@azure/cosmos";
|
||||||
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
|
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
|
||||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
|
||||||
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||||
import { checkDatabaseResourceTokensValidity, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
|
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { 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 { FabricArtifactInfo, updateUserContext, userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { isDataplaneRbacSupported } from "../Utils/APITypeUtils";
|
|
||||||
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;
|
||||||
|
|
||||||
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||||
const { verb, resourceId, resourceType, headers } = requestInfo;
|
const { verb, resourceId, resourceType, headers } = requestInfo;
|
||||||
|
|
||||||
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType);
|
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && userContext.apiType === "SQL";
|
||||||
if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) {
|
if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) {
|
||||||
Logger.logInfo(
|
Logger.logInfo(
|
||||||
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
|
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
|
||||||
@@ -28,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;
|
||||||
}
|
}
|
||||||
@@ -43,7 +41,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
return decodeURIComponent(headers.authorization);
|
return decodeURIComponent(headers.authorization);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFabricMirroredKey()) {
|
if (configContext.platform === Platform.Fabric) {
|
||||||
switch (requestInfo.resourceType) {
|
switch (requestInfo.resourceType) {
|
||||||
case Cosmos.ResourceType.conflicts:
|
case Cosmos.ResourceType.conflicts:
|
||||||
case Cosmos.ResourceType.container:
|
case Cosmos.ResourceType.container:
|
||||||
@@ -55,13 +53,8 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
// User resource tokens
|
// User resource tokens
|
||||||
// TODO userContext.fabricContext.databaseConnectionInfo can be undefined
|
// TODO userContext.fabricContext.databaseConnectionInfo can be undefined
|
||||||
headers[HttpHeaders.msDate] = new Date().toUTCString();
|
headers[HttpHeaders.msDate] = new Date().toUTCString();
|
||||||
const resourceTokens = (
|
const resourceTokens = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
||||||
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
|
checkDatabaseResourceTokensValidity(userContext.fabricContext.databaseConnectionInfo.resourceTokensTimestamp);
|
||||||
).resourceTokenInfo.resourceTokens;
|
|
||||||
checkDatabaseResourceTokensValidity(
|
|
||||||
(userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY])
|
|
||||||
.resourceTokenInfo.resourceTokensTimestamp,
|
|
||||||
);
|
|
||||||
return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
|
return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
|
||||||
|
|
||||||
case Cosmos.ResourceType.none:
|
case Cosmos.ResourceType.none:
|
||||||
@@ -72,9 +65,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
// For now, these operations aren't used, so fetching the authorization token is commented out.
|
// For now, these operations aren't used, so fetching the authorization token is commented out.
|
||||||
// This provider must return a real token to pass validation by the client, so we return the cached resource token
|
// This provider must return a real token to pass validation by the client, so we return the cached resource token
|
||||||
// (which is a valid token, but won't work for these operations).
|
// (which is a valid token, but won't work for these operations).
|
||||||
const resourceTokens2 = (
|
const resourceTokens2 = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
||||||
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
|
|
||||||
).resourceTokenInfo.resourceTokens;
|
|
||||||
return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId);
|
return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId);
|
||||||
|
|
||||||
/* ************** TODO: Uncomment this code if we need to support these operations **************
|
/* ************** TODO: Uncomment this code if we need to support these operations **************
|
||||||
@@ -134,8 +125,8 @@ export async function getTokenFromAuthService(
|
|||||||
resourceId?: string,
|
resourceId?: string,
|
||||||
): Promise<AuthorizationToken> {
|
): Promise<AuthorizationToken> {
|
||||||
try {
|
try {
|
||||||
const host: string = configContext.PORTAL_BACKEND_ENDPOINT;
|
const host = configContext.BACKEND_ENDPOINT;
|
||||||
const response: Response = await _global.fetch(host + "/api/connectionstring/runtimeproxy/authorizationtokens", {
|
const response = await _global.fetch(host + "/api/guest/runtimeproxy/authorizationTokens", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
@@ -147,7 +138,8 @@ export async function getTokenFromAuthService(
|
|||||||
resourceId,
|
resourceId,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const result: AuthorizationToken = await response.json();
|
//TODO I am not sure why we have to parse the JSON again here. fetch should do it for us when we call .json()
|
||||||
|
const result = JSON.parse(await response.json());
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleError(`Failed to get authorization headers for ${resourceType}: ${getErrorMessage(error)}`);
|
logConsoleError(`Failed to get authorization headers for ${resourceType}: ${getErrorMessage(error)}`);
|
||||||
@@ -165,24 +157,13 @@ 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;
|
||||||
_defaultHeaders["x-ms-cosmos-throughput-bucket"] = 1;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.ConnectionString ||
|
userContext.authType === AuthType.ConnectionString ||
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export function getNewDatabaseSharedThroughputDefault(): boolean {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
@@ -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,20 +51,15 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<TextField
|
||||||
<span id={entityProperty} className="screenReaderOnly">
|
label={entityValueLabel && entityValueLabel}
|
||||||
Edit Property {entityProperty} {attributeValueLabel}
|
className="addEntityTextField"
|
||||||
</span>
|
disabled={isEntityValueDisable}
|
||||||
<TextField
|
type={entityValueType}
|
||||||
label={entityValueLabel && entityValueLabel}
|
placeholder={entityValuePlaceholder}
|
||||||
className="addEntityTextField"
|
value={typeof entityValue === "string" ? entityValue : ""}
|
||||||
disabled={isEntityValueDisable}
|
onChange={onEntityValueChange}
|
||||||
type={entityValueType}
|
ariaLabel={attributeValueLabel}
|
||||||
placeholder={entityValuePlaceholder}
|
/>
|
||||||
value={typeof entityValue === "string" ? entityValue : ""}
|
|
||||||
onChange={onEntityValueChange}
|
|
||||||
aria-labelledby={entityProperty}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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];
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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.";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
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";
|
||||||
|
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
||||||
import { updateUserContext } from "../UserContext";
|
import { updateUserContext } from "../UserContext";
|
||||||
import { deleteDocuments, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
|
import {
|
||||||
|
deleteDocument,
|
||||||
|
getEndpoint,
|
||||||
|
getFeatureEndpointOrDefault,
|
||||||
|
queryDocuments,
|
||||||
|
readDocument,
|
||||||
|
updateDocument,
|
||||||
|
} from "./MongoProxyClient";
|
||||||
|
|
||||||
const databaseId = "testDB";
|
const databaseId = "testDB";
|
||||||
|
|
||||||
@@ -64,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);
|
||||||
});
|
});
|
||||||
@@ -76,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),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -100,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);
|
||||||
});
|
});
|
||||||
@@ -112,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),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -136,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);
|
||||||
});
|
});
|
||||||
@@ -148,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),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -172,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);
|
||||||
});
|
});
|
||||||
@@ -184,20 +178,28 @@ 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),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds the correct proxy URL in development", () => {
|
||||||
|
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||||
|
updateDocument(databaseId, collection, documentId, "{}");
|
||||||
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
|
"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),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe("deleteDocuments", () => {
|
describe("deleteDocument", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
resetConfigContext();
|
resetConfigContext();
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
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);
|
||||||
});
|
});
|
||||||
@@ -206,21 +208,18 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct URL", () => {
|
it("builds the correct URL", () => {
|
||||||
deleteDocuments(databaseId, collection, [documentId]);
|
deleteDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/bulkdelete`,
|
"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",
|
deleteDocument(databaseId, collection, documentId);
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
|
||||||
deleteDocuments(databaseId, collection, [documentId]);
|
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/bulkdelete`,
|
"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),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -232,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", () => {
|
||||||
@@ -251,8 +249,35 @@ 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", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetConfigContext();
|
||||||
|
updateConfigContext({
|
||||||
|
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||||
|
});
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
"feature.mongoProxyEndpoint": "https://localhost:12901",
|
||||||
|
"feature.mongoProxyAPIs": "readDocument|createDocument",
|
||||||
|
});
|
||||||
|
const features = extractFeatures(params);
|
||||||
|
updateUserContext({
|
||||||
|
authType: AuthType.AAD,
|
||||||
|
features: features,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a local endpoint", () => {
|
||||||
|
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||||
|
expect(endpoint).toEqual("https://localhost:12901/api/mongo/explorer");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a production endpoint", () => {
|
||||||
|
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
||||||
|
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||||
|
import {
|
||||||
|
allowedMongoProxyEndpoints,
|
||||||
|
allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||||
|
validateEndpoint,
|
||||||
|
} from "Utils/EndpointUtils";
|
||||||
|
import queryString from "querystring";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { configContext } from "../ConfigContext";
|
import { configContext } from "../ConfigContext";
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||||
import { Collection } from "../Contracts/ViewModels";
|
import { Collection } from "../Contracts/ViewModels";
|
||||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||||
|
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 } 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";
|
||||||
|
|
||||||
@@ -60,6 +67,10 @@ export function queryDocuments(
|
|||||||
query: string,
|
query: string,
|
||||||
continuationToken?: string,
|
continuationToken?: string,
|
||||||
): Promise<QueryResponse> {
|
): Promise<QueryResponse> {
|
||||||
|
if (!useMongoProxyEndpoint("resourcelist") || !useMongoProxyEndpoint("queryDocuments")) {
|
||||||
|
return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken);
|
||||||
|
}
|
||||||
|
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
const params = {
|
const params = {
|
||||||
@@ -78,7 +89,7 @@ export function queryDocuments(
|
|||||||
query,
|
query,
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT) || "";
|
const endpoint = getFeatureEndpointOrDefault("resourcelist") || "";
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
...defaultHeaders,
|
...defaultHeaders,
|
||||||
@@ -116,11 +127,76 @@ export function queryDocuments(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function queryDocuments_ToBeDeprecated(
|
||||||
|
databaseId: string,
|
||||||
|
collection: Collection,
|
||||||
|
isResourceList: boolean,
|
||||||
|
query: string,
|
||||||
|
continuationToken?: string,
|
||||||
|
): Promise<QueryResponse> {
|
||||||
|
const { databaseAccount } = userContext;
|
||||||
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
|
const params = {
|
||||||
|
db: databaseId,
|
||||||
|
coll: collection.id(),
|
||||||
|
resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`,
|
||||||
|
rid: collection.rid,
|
||||||
|
rtype: "docs",
|
||||||
|
sid: userContext.subscriptionId,
|
||||||
|
rg: userContext.resourceGroup,
|
||||||
|
dba: databaseAccount.name,
|
||||||
|
pk:
|
||||||
|
collection && collection.partitionKey && !collection.partitionKey.systemKey
|
||||||
|
? collection.partitionKeyProperties?.[0]
|
||||||
|
: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const endpoint = getFeatureEndpointOrDefault("resourcelist") || "";
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
...defaultHeaders,
|
||||||
|
...authHeaders(),
|
||||||
|
[CosmosSDKConstants.HttpHeaders.IsQuery]: "true",
|
||||||
|
[CosmosSDKConstants.HttpHeaders.PopulateQueryMetrics]: "true",
|
||||||
|
[CosmosSDKConstants.HttpHeaders.EnableScanInQuery]: "true",
|
||||||
|
[CosmosSDKConstants.HttpHeaders.EnableCrossPartitionQuery]: "true",
|
||||||
|
[CosmosSDKConstants.HttpHeaders.ParallelizeCrossPartitionQuery]: "true",
|
||||||
|
[HttpHeaders.contentType]: "application/query+json",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (continuationToken) {
|
||||||
|
headers[CosmosSDKConstants.HttpHeaders.Continuation] = continuationToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = isResourceList ? "/resourcelist" : "";
|
||||||
|
|
||||||
|
return window
|
||||||
|
.fetch(`${endpoint}${path}?${queryString.stringify(params)}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ query }),
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return {
|
||||||
|
continuationToken: response.headers.get(CosmosSDKConstants.HttpHeaders.Continuation),
|
||||||
|
documents: (await response.json()).Documents as DataModels.DocumentId[],
|
||||||
|
headers: response.headers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await errorHandling(response, "querying documents", params);
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function readDocument(
|
export function readDocument(
|
||||||
databaseId: string,
|
databaseId: string,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
documentId: DocumentId,
|
documentId: DocumentId,
|
||||||
): Promise<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
|
if (!useMongoProxyEndpoint("readDocument")) {
|
||||||
|
return readDocument_ToBeDeprecated(databaseId, collection, documentId);
|
||||||
|
}
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
const idComponents = documentId.self.split("/");
|
const idComponents = documentId.self.split("/");
|
||||||
@@ -141,7 +217,7 @@ export function readDocument(
|
|||||||
: "",
|
: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(endpoint, {
|
.fetch(endpoint, {
|
||||||
@@ -161,12 +237,61 @@ export function readDocument(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function readDocument_ToBeDeprecated(
|
||||||
|
databaseId: string,
|
||||||
|
collection: Collection,
|
||||||
|
documentId: DocumentId,
|
||||||
|
): Promise<DataModels.DocumentId> {
|
||||||
|
const { databaseAccount } = userContext;
|
||||||
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
|
const idComponents = documentId.self.split("/");
|
||||||
|
const path = idComponents.slice(0, 4).join("/");
|
||||||
|
const rid = encodeURIComponent(idComponents[5]);
|
||||||
|
const params = {
|
||||||
|
db: databaseId,
|
||||||
|
coll: collection.id(),
|
||||||
|
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
||||||
|
rid,
|
||||||
|
rtype: "docs",
|
||||||
|
sid: userContext.subscriptionId,
|
||||||
|
rg: userContext.resourceGroup,
|
||||||
|
dba: databaseAccount.name,
|
||||||
|
pk:
|
||||||
|
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||||
|
? documentId.partitionKeyProperties?.[0]
|
||||||
|
: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||||
|
|
||||||
|
return window
|
||||||
|
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
...defaultHeaders,
|
||||||
|
...authHeaders(),
|
||||||
|
[CosmosSDKConstants.HttpHeaders.PartitionKey]: encodeURIComponent(
|
||||||
|
JSON.stringify(documentId.partitionKeyHeader()),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
return await errorHandling(response, "reading document", params);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function createDocument(
|
export function createDocument(
|
||||||
databaseId: string,
|
databaseId: string,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
partitionKeyProperty: string,
|
partitionKeyProperty: string,
|
||||||
documentContent: unknown,
|
documentContent: unknown,
|
||||||
): Promise<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
|
if (!useMongoProxyEndpoint("createDocument")) {
|
||||||
|
return createDocument_ToBeDeprecated(databaseId, collection, partitionKeyProperty, documentContent);
|
||||||
|
}
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
const params = {
|
const params = {
|
||||||
@@ -183,7 +308,7 @@ export function createDocument(
|
|||||||
documentContent: JSON.stringify(documentContent),
|
documentContent: JSON.stringify(documentContent),
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getFeatureEndpointOrDefault("createDocument");
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/createDocument`, {
|
.fetch(`${endpoint}/createDocument`, {
|
||||||
@@ -203,12 +328,54 @@ export function createDocument(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createDocument_ToBeDeprecated(
|
||||||
|
databaseId: string,
|
||||||
|
collection: Collection,
|
||||||
|
partitionKeyProperty: string,
|
||||||
|
documentContent: unknown,
|
||||||
|
): Promise<DataModels.DocumentId> {
|
||||||
|
const { databaseAccount } = userContext;
|
||||||
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
|
const params = {
|
||||||
|
db: databaseId,
|
||||||
|
coll: collection.id(),
|
||||||
|
resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`,
|
||||||
|
rid: collection.rid,
|
||||||
|
rtype: "docs",
|
||||||
|
sid: userContext.subscriptionId,
|
||||||
|
rg: userContext.resourceGroup,
|
||||||
|
dba: databaseAccount.name,
|
||||||
|
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const endpoint = getFeatureEndpointOrDefault("createDocument");
|
||||||
|
|
||||||
|
return window
|
||||||
|
.fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(documentContent),
|
||||||
|
headers: {
|
||||||
|
...defaultHeaders,
|
||||||
|
...authHeaders(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
return await errorHandling(response, "creating document", params);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function updateDocument(
|
export function updateDocument(
|
||||||
databaseId: string,
|
databaseId: string,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
documentId: DocumentId,
|
documentId: DocumentId,
|
||||||
documentContent: string,
|
documentContent: string,
|
||||||
): Promise<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
|
if (!useMongoProxyEndpoint("updateDocument")) {
|
||||||
|
return updateDocument_ToBeDeprecated(databaseId, collection, documentId, documentContent);
|
||||||
|
}
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
const idComponents = documentId.self.split("/");
|
const idComponents = documentId.self.split("/");
|
||||||
@@ -229,7 +396,7 @@ export function updateDocument(
|
|||||||
: "",
|
: "",
|
||||||
documentContent,
|
documentContent,
|
||||||
};
|
};
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getFeatureEndpointOrDefault("updateDocument");
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(endpoint, {
|
.fetch(endpoint, {
|
||||||
@@ -250,35 +417,79 @@ export function updateDocument(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteDocuments(
|
export function updateDocument_ToBeDeprecated(
|
||||||
databaseId: string,
|
databaseId: string,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
documentIds: DocumentId[],
|
documentId: DocumentId,
|
||||||
): Promise<{
|
documentContent: string,
|
||||||
deletedCount: number;
|
): Promise<DataModels.DocumentId> {
|
||||||
isAcknowledged: boolean;
|
|
||||||
}> {
|
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
|
const idComponents = documentId.self.split("/");
|
||||||
|
const path = idComponents.slice(0, 5).join("/");
|
||||||
|
const rid = encodeURIComponent(idComponents[5]);
|
||||||
|
const params = {
|
||||||
|
db: databaseId,
|
||||||
|
coll: collection.id(),
|
||||||
|
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
||||||
|
rid,
|
||||||
|
rtype: "docs",
|
||||||
|
sid: userContext.subscriptionId,
|
||||||
|
rg: userContext.resourceGroup,
|
||||||
|
dba: databaseAccount.name,
|
||||||
|
pk:
|
||||||
|
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||||
|
? documentId.partitionKeyProperties?.[0]
|
||||||
|
: "",
|
||||||
|
};
|
||||||
|
const endpoint = getFeatureEndpointOrDefault("updateDocument");
|
||||||
|
|
||||||
const rids: string[] = documentIds.map((documentId) => {
|
return window
|
||||||
const idComponents = documentId.self.split("/");
|
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||||
return idComponents[5];
|
method: "PUT",
|
||||||
});
|
body: documentContent,
|
||||||
|
headers: {
|
||||||
|
...defaultHeaders,
|
||||||
|
...authHeaders(),
|
||||||
|
[HttpHeaders.contentType]: ContentType.applicationJson,
|
||||||
|
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
return await errorHandling(response, "updating document", params);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
|
||||||
|
if (!useMongoProxyEndpoint("deleteDocument")) {
|
||||||
|
return deleteDocument_ToBeDeprecated(databaseId, collection, documentId);
|
||||||
|
}
|
||||||
|
const { databaseAccount } = userContext;
|
||||||
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
|
const idComponents = documentId.self.split("/");
|
||||||
|
const path = idComponents.slice(0, 5).join("/");
|
||||||
|
const rid = encodeURIComponent(idComponents[5]);
|
||||||
const params = {
|
const params = {
|
||||||
databaseID: databaseId,
|
databaseID: databaseId,
|
||||||
collectionID: collection.id(),
|
collectionID: collection.id(),
|
||||||
resourceUrl: `${resourceEndpoint}`,
|
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
||||||
resourceIDs: rids,
|
resourceID: rid,
|
||||||
|
resourceType: "docs",
|
||||||
subscriptionID: userContext.subscriptionId,
|
subscriptionID: userContext.subscriptionId,
|
||||||
resourceGroup: userContext.resourceGroup,
|
resourceGroup: userContext.resourceGroup,
|
||||||
databaseAccountName: databaseAccount.name,
|
databaseAccountName: databaseAccount.name,
|
||||||
|
partitionKey:
|
||||||
|
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||||
|
? documentId.partitionKeyProperties?.[0]
|
||||||
|
: "",
|
||||||
};
|
};
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/bulkdelete`, {
|
.fetch(endpoint, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
body: JSON.stringify(params),
|
body: JSON.stringify(params),
|
||||||
headers: {
|
headers: {
|
||||||
@@ -289,16 +500,62 @@ export function deleteDocuments(
|
|||||||
})
|
})
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const result = await response.json();
|
return undefined;
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
return await errorHandling(response, "deleting documents", params);
|
return await errorHandling(response, "deleting document", params);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteDocument_ToBeDeprecated(
|
||||||
|
databaseId: string,
|
||||||
|
collection: Collection,
|
||||||
|
documentId: DocumentId,
|
||||||
|
): Promise<void> {
|
||||||
|
const { databaseAccount } = userContext;
|
||||||
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
|
const idComponents = documentId.self.split("/");
|
||||||
|
const path = idComponents.slice(0, 5).join("/");
|
||||||
|
const rid = encodeURIComponent(idComponents[5]);
|
||||||
|
const params = {
|
||||||
|
db: databaseId,
|
||||||
|
coll: collection.id(),
|
||||||
|
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
||||||
|
rid,
|
||||||
|
rtype: "docs",
|
||||||
|
sid: userContext.subscriptionId,
|
||||||
|
rg: userContext.resourceGroup,
|
||||||
|
dba: databaseAccount.name,
|
||||||
|
pk:
|
||||||
|
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||||
|
? documentId.partitionKeyProperties?.[0]
|
||||||
|
: "",
|
||||||
|
};
|
||||||
|
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
||||||
|
|
||||||
|
return window
|
||||||
|
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
...defaultHeaders,
|
||||||
|
...authHeaders(),
|
||||||
|
[HttpHeaders.contentType]: ContentType.applicationJson,
|
||||||
|
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return await errorHandling(response, "deleting document", params);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMongoCollectionWithProxy(
|
export function createMongoCollectionWithProxy(
|
||||||
params: DataModels.CreateCollectionParams,
|
params: DataModels.CreateCollectionParams,
|
||||||
): Promise<DataModels.Collection> {
|
): Promise<DataModels.Collection> {
|
||||||
|
if (!useMongoProxyEndpoint("createCollectionWithProxy")) {
|
||||||
|
return createMongoCollectionWithProxy_ToBeDeprecated(params);
|
||||||
|
}
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const shardKey: string = params.partitionKey?.paths[0];
|
const shardKey: string = params.partitionKey?.paths[0];
|
||||||
|
|
||||||
@@ -319,7 +576,7 @@ export function createMongoCollectionWithProxy(
|
|||||||
isSharded: !!shardKey,
|
isSharded: !!shardKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy");
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/createCollection`, {
|
.fetch(`${endpoint}/createCollection`, {
|
||||||
@@ -339,6 +596,69 @@ export function createMongoCollectionWithProxy(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createMongoCollectionWithProxy_ToBeDeprecated(
|
||||||
|
params: DataModels.CreateCollectionParams,
|
||||||
|
): Promise<DataModels.Collection> {
|
||||||
|
const { databaseAccount } = userContext;
|
||||||
|
const shardKey: string = params.partitionKey?.paths[0];
|
||||||
|
const mongoParams: DataModels.MongoParameters = {
|
||||||
|
resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint,
|
||||||
|
db: params.databaseId,
|
||||||
|
coll: params.collectionId,
|
||||||
|
pk: shardKey,
|
||||||
|
offerThroughput: params.autoPilotMaxThroughput || params.offerThroughput,
|
||||||
|
cd: params.createNewDatabase,
|
||||||
|
st: params.databaseLevelThroughput,
|
||||||
|
is: !!shardKey,
|
||||||
|
rid: "",
|
||||||
|
rtype: "colls",
|
||||||
|
sid: userContext.subscriptionId,
|
||||||
|
rg: userContext.resourceGroup,
|
||||||
|
dba: databaseAccount.name,
|
||||||
|
isAutoPilot: !!params.autoPilotMaxThroughput,
|
||||||
|
};
|
||||||
|
|
||||||
|
const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy");
|
||||||
|
|
||||||
|
return window
|
||||||
|
.fetch(
|
||||||
|
`${endpoint}/createCollection?${queryString.stringify(
|
||||||
|
mongoParams as unknown as queryString.ParsedUrlQueryInput,
|
||||||
|
)}`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
...defaultHeaders,
|
||||||
|
...authHeaders(),
|
||||||
|
[HttpHeaders.contentType]: "application/json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
return await errorHandling(response, "creating collection", mongoParams);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export function getFeatureEndpointOrDefault(feature: string): string {
|
||||||
|
let endpoint;
|
||||||
|
if (useMongoProxyEndpoint(feature)) {
|
||||||
|
endpoint = configContext.MONGO_PROXY_ENDPOINT;
|
||||||
|
} else {
|
||||||
|
endpoint =
|
||||||
|
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
||||||
|
validateEndpoint(userContext.features.mongoProxyEndpoint, [
|
||||||
|
...allowedMongoProxyEndpoints,
|
||||||
|
...allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||||
|
])
|
||||||
|
? userContext.features.mongoProxyEndpoint
|
||||||
|
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getEndpoint(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
export function getEndpoint(endpoint: string): string {
|
export function getEndpoint(endpoint: string): string {
|
||||||
let url = endpoint + "/api/mongo/explorer";
|
let url = endpoint + "/api/mongo/explorer";
|
||||||
|
|
||||||
@@ -352,10 +672,26 @@ export function getEndpoint(endpoint: string): string {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ThrottlingError extends Error {
|
export function useMongoProxyEndpoint(api: string): boolean {
|
||||||
constructor(message: string) {
|
const activeMongoProxyEndpoints: string[] = [
|
||||||
super(message);
|
MongoProxyEndpoints.Local,
|
||||||
|
MongoProxyEndpoints.Mpac,
|
||||||
|
MongoProxyEndpoints.Prod,
|
||||||
|
MongoProxyEndpoints.Fairfax,
|
||||||
|
];
|
||||||
|
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
|
||||||
|
if (
|
||||||
|
configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Local &&
|
||||||
|
userContext.databaseAccount.properties.ipRules?.length > 0
|
||||||
|
) {
|
||||||
|
canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
canAccessMongoProxy &&
|
||||||
|
configContext.NEW_MONGO_APIS?.includes(api) &&
|
||||||
|
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -367,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);
|
||||||
}
|
}
|
||||||
|
|||||||
39
src/Common/PortalNotifications.ts
Normal file
39
src/Common/PortalNotifications.ts
Normal 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[];
|
||||||
|
};
|
||||||
@@ -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 }),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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),
|
|
||||||
};
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
import { Platform, configContext } from "../ConfigContext";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
|
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
|
||||||
|
|
||||||
export function updateStyles(): void {
|
export function updateStyles(): void {
|
||||||
if (isFabric()) {
|
if (configContext.platform === Platform.Fabric) {
|
||||||
StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh;
|
StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh;
|
||||||
StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium;
|
StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium;
|
||||||
StyleConstants.AccentLight = StyleConstants.FabricAccentLight;
|
StyleConstants.AccentLight = StyleConstants.FabricAccentLight;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos";
|
import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos";
|
||||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import { useDatabases } from "../../Explorer/useDatabases";
|
import { useDatabases } from "../../Explorer/useDatabases";
|
||||||
@@ -25,7 +24,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
|
|||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
let collection: DataModels.Collection;
|
let collection: DataModels.Collection;
|
||||||
if (!isFabricNative() && userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||||
if (params.createNewDatabase) {
|
if (params.createNewDatabase) {
|
||||||
const createDatabaseParams: DataModels.CreateDatabaseParams = {
|
const createDatabaseParams: DataModels.CreateDatabaseParams = {
|
||||||
autoPilotMaxThroughput: params.autoPilotMaxThroughput,
|
autoPilotMaxThroughput: params.autoPilotMaxThroughput,
|
||||||
@@ -100,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: {
|
||||||
@@ -274,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 };
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { deleteCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
import { deleteCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||||
@@ -13,7 +12,7 @@ import { handleError } from "../ErrorHandlingUtils";
|
|||||||
export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
|
export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
|
||||||
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
|
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
|
||||||
try {
|
try {
|
||||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) {
|
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||||
await deleteCollectionWithARM(databaseId, collectionId);
|
await deleteCollectionWithARM(databaseId, collectionId);
|
||||||
} else {
|
} else {
|
||||||
await client().database(databaseId).container(collectionId).delete();
|
await client().database(databaseId).container(collectionId).delete();
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -105,8 +105,6 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
|||||||
? parseInt(resource.softAllowedMaximumThroughput)
|
? parseInt(resource.softAllowedMaximumThroughput)
|
||||||
: resource.softAllowedMaximumThroughput;
|
: resource.softAllowedMaximumThroughput;
|
||||||
|
|
||||||
const throughputBuckets = resource?.throughputBuckets;
|
|
||||||
|
|
||||||
if (autoscaleSettings) {
|
if (autoscaleSettings) {
|
||||||
return {
|
return {
|
||||||
id: offerId,
|
id: offerId,
|
||||||
@@ -116,7 +114,6 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
|||||||
offerReplacePending: resource.offerReplacePending === "true",
|
offerReplacePending: resource.offerReplacePending === "true",
|
||||||
instantMaximumThroughput,
|
instantMaximumThroughput,
|
||||||
softAllowedMaximumThroughput,
|
softAllowedMaximumThroughput,
|
||||||
throughputBuckets,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +125,6 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
|||||||
offerReplacePending: resource.offerReplacePending === "true",
|
offerReplacePending: resource.offerReplacePending === "true",
|
||||||
instantMaximumThroughput,
|
instantMaximumThroughput,
|
||||||
softAllowedMaximumThroughput,
|
softAllowedMaximumThroughput,
|
||||||
throughputBuckets,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { ContainerResponse } from "@azure/cosmos";
|
import { ContainerResponse } from "@azure/cosmos";
|
||||||
import { Queries } from "Common/Constants";
|
import { Queries } from "Common/Constants";
|
||||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { isFabric, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import { FabricArtifactInfo, userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||||
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||||
@@ -17,13 +16,15 @@ import { handleError } from "../ErrorHandlingUtils";
|
|||||||
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
|
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
|
||||||
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
|
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
|
||||||
|
|
||||||
if (isFabricMirroredKey() && userContext.fabricContext?.databaseName === databaseId) {
|
if (
|
||||||
|
configContext.platform === Platform.Fabric &&
|
||||||
|
userContext.fabricContext &&
|
||||||
|
userContext.fabricContext.databaseConnectionInfo.databaseId === databaseId
|
||||||
|
) {
|
||||||
const collections: DataModels.Collection[] = [];
|
const collections: DataModels.Collection[] = [];
|
||||||
const promises: Promise<ContainerResponse>[] = [];
|
const promises: Promise<ContainerResponse>[] = [];
|
||||||
|
|
||||||
for (const collectionResourceId in (
|
for (const collectionResourceId in userContext.fabricContext.databaseConnectionInfo.resourceTokens) {
|
||||||
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
|
|
||||||
).resourceTokenInfo.resourceTokens) {
|
|
||||||
// Dictionary key looks like this: dbs/SampleDB/colls/Container
|
// Dictionary key looks like this: dbs/SampleDB/colls/Container
|
||||||
const resourceIdObj = collectionResourceId.split("/");
|
const resourceIdObj = collectionResourceId.split("/");
|
||||||
const tokenDatabaseId = resourceIdObj[1];
|
const tokenDatabaseId = resourceIdObj[1];
|
||||||
@@ -55,8 +56,7 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
|
|||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
!userContext.features.enableSDKoperations &&
|
!userContext.features.enableSDKoperations &&
|
||||||
userContext.apiType !== "Tables" &&
|
userContext.apiType !== "Tables"
|
||||||
!isFabric()
|
|
||||||
) {
|
) {
|
||||||
return await readCollectionsWithARM(databaseId);
|
return await readCollectionsWithARM(databaseId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
|
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
@@ -11,9 +11,8 @@ import { handleError } from "../ErrorHandlingUtils";
|
|||||||
import { readOfferWithSDK } from "./readOfferWithSDK";
|
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||||
|
|
||||||
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
|
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
|
||||||
if (isFabricMirroredKey() || isFabricNative()) {
|
if (configContext.platform === Platform.Fabric) {
|
||||||
// For Fabric Mirroring, it is slow, because it requests the token and we don't need it.
|
// TODO This works, but is very slow, because it requests the token, so we skip for now
|
||||||
// For Fabric Native, it is not supported.
|
|
||||||
console.error("Skiping readDatabaseOffer for Fabric");
|
console.error("Skiping readDatabaseOffer for Fabric");
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -24,8 +23,7 @@ export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promis
|
|||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
!userContext.features.enableSDKoperations &&
|
!userContext.features.enableSDKoperations &&
|
||||||
userContext.apiType !== "Tables" &&
|
userContext.apiType !== "Tables"
|
||||||
!isFabric()
|
|
||||||
) {
|
) {
|
||||||
return await readDatabaseOfferWithARM(params.databaseId);
|
return await readDatabaseOfferWithARM(params.databaseId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import { FabricArtifactInfo, userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||||
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||||
@@ -15,13 +14,8 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
|||||||
let databases: DataModels.Database[];
|
let databases: DataModels.Database[];
|
||||||
const clearMessage = logConsoleProgress(`Querying databases`);
|
const clearMessage = logConsoleProgress(`Querying databases`);
|
||||||
|
|
||||||
if (
|
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.databaseConnectionInfo.resourceTokens) {
|
||||||
isFabricMirroredKey() &&
|
const tokensData = userContext.fabricContext.databaseConnectionInfo;
|
||||||
(userContext.fabricContext?.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]).resourceTokenInfo
|
|
||||||
.resourceTokens
|
|
||||||
) {
|
|
||||||
const tokensData = (userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY])
|
|
||||||
.resourceTokenInfo;
|
|
||||||
|
|
||||||
const databaseIdsSet = new Set<string>(); // databaseId
|
const databaseIdsSet = new Set<string>(); // databaseId
|
||||||
|
|
||||||
@@ -52,28 +46,13 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
|||||||
}));
|
}));
|
||||||
clearMessage();
|
clearMessage();
|
||||||
return databases;
|
return databases;
|
||||||
} else if (isFabricNative() && userContext.fabricContext?.databaseName) {
|
|
||||||
const databaseId = userContext.fabricContext.databaseName;
|
|
||||||
databases = [
|
|
||||||
{
|
|
||||||
_rid: "",
|
|
||||||
_self: "",
|
|
||||||
_etag: "",
|
|
||||||
_ts: 0,
|
|
||||||
id: databaseId,
|
|
||||||
collections: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
clearMessage();
|
|
||||||
return databases;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
!userContext.features.enableSDKoperations &&
|
!userContext.features.enableSDKoperations &&
|
||||||
userContext.apiType !== "Tables" &&
|
userContext.apiType !== "Tables"
|
||||||
!isFabric()
|
|
||||||
) {
|
) {
|
||||||
databases = await readDatabasesWithARM();
|
databases = await readDatabasesWithARM();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ContainerDefinition, RequestOptions } from "@azure/cosmos";
|
import { ContainerDefinition, RequestOptions } from "@azure/cosmos";
|
||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { Collection } from "../../Contracts/DataModels";
|
import { Collection } from "../../Contracts/DataModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
@@ -37,8 +36,7 @@ export async function updateCollection(
|
|||||||
if (
|
if (
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
!userContext.features.enableSDKoperations &&
|
!userContext.features.enableSDKoperations &&
|
||||||
userContext.apiType !== "Tables" &&
|
userContext.apiType !== "Tables"
|
||||||
!isFabric()
|
|
||||||
) {
|
) {
|
||||||
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
|
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { OfferDefinition, RequestOptions } from "@azure/cosmos";
|
import { OfferDefinition, RequestOptions } from "@azure/cosmos";
|
||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { Offer, SDKOfferDefinition, ThroughputBucket, UpdateOfferParams } from "../../Contracts/DataModels";
|
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import {
|
import {
|
||||||
migrateCassandraKeyspaceToAutoscale,
|
migrateCassandraKeyspaceToAutoscale,
|
||||||
@@ -57,7 +56,7 @@ export const updateOffer = async (params: UpdateOfferParams): Promise<Offer> =>
|
|||||||
const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`);
|
const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) {
|
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||||
if (params.collectionId) {
|
if (params.collectionId) {
|
||||||
updatedOffer = await updateCollectionOfferWithARM(params);
|
updatedOffer = await updateCollectionOfferWithARM(params);
|
||||||
} else if (userContext.apiType === "Tables") {
|
} else if (userContext.apiType === "Tables") {
|
||||||
@@ -360,13 +359,6 @@ const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpd
|
|||||||
body.properties.resource.throughput = params.manualThroughput;
|
body.properties.resource.throughput = params.manualThroughput;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.throughputBuckets) {
|
|
||||||
const throughputBuckets = params.throughputBuckets.filter(
|
|
||||||
(bucket: ThroughputBucket) => bucket.maxThroughputPercentage !== 100,
|
|
||||||
);
|
|
||||||
body.properties.resource.throughputBuckets = throughputBuckets;
|
|
||||||
}
|
|
||||||
|
|
||||||
return body;
|
return body;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,15 +8,16 @@ import {
|
|||||||
import {
|
import {
|
||||||
allowedAadEndpoints,
|
allowedAadEndpoints,
|
||||||
allowedArcadiaEndpoints,
|
allowedArcadiaEndpoints,
|
||||||
|
allowedCassandraProxyEndpoints,
|
||||||
allowedEmulatorEndpoints,
|
allowedEmulatorEndpoints,
|
||||||
allowedGraphEndpoints,
|
allowedGraphEndpoints,
|
||||||
allowedHostedExplorerEndpoints,
|
allowedHostedExplorerEndpoints,
|
||||||
allowedJunoOrigins,
|
allowedJunoOrigins,
|
||||||
|
allowedMongoBackendEndpoints,
|
||||||
|
allowedMongoProxyEndpoints,
|
||||||
allowedMsalRedirectEndpoints,
|
allowedMsalRedirectEndpoints,
|
||||||
defaultAllowedArmEndpoints,
|
defaultAllowedArmEndpoints,
|
||||||
defaultAllowedBackendEndpoints,
|
defaultAllowedBackendEndpoints,
|
||||||
defaultAllowedCassandraProxyEndpoints,
|
|
||||||
defaultAllowedMongoProxyEndpoints,
|
|
||||||
validateEndpoint,
|
validateEndpoint,
|
||||||
} from "Utils/EndpointUtils";
|
} from "Utils/EndpointUtils";
|
||||||
|
|
||||||
@@ -31,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;
|
||||||
@@ -49,10 +48,15 @@ export interface ConfigContext {
|
|||||||
CATALOG_API_KEY: string;
|
CATALOG_API_KEY: string;
|
||||||
ARCADIA_ENDPOINT: string;
|
ARCADIA_ENDPOINT: string;
|
||||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||||
PORTAL_BACKEND_ENDPOINT: string;
|
BACKEND_ENDPOINT?: string;
|
||||||
|
PORTAL_BACKEND_ENDPOINT?: string;
|
||||||
NEW_BACKEND_APIS?: BackendApi[];
|
NEW_BACKEND_APIS?: BackendApi[];
|
||||||
MONGO_PROXY_ENDPOINT: string;
|
MONGO_BACKEND_ENDPOINT?: string;
|
||||||
CASSANDRA_PROXY_ENDPOINT: string;
|
MONGO_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;
|
||||||
@@ -64,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
|
||||||
@@ -73,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$`,
|
||||||
@@ -88,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/",
|
||||||
@@ -106,14 +105,25 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
|
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
|
||||||
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
|
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
|
||||||
JUNO_ENDPOINT: JunoEndpoints.Prod,
|
JUNO_ENDPOINT: JunoEndpoints.Prod,
|
||||||
|
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 {
|
||||||
@@ -150,19 +160,22 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!validateEndpoint(
|
!validateEndpoint(
|
||||||
newContext.MONGO_PROXY_ENDPOINT,
|
newContext.BACKEND_ENDPOINT,
|
||||||
configContext.allowedMongoProxyEndpoints || defaultAllowedMongoProxyEndpoints,
|
configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
delete newContext.BACKEND_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateEndpoint(newContext.MONGO_PROXY_ENDPOINT, allowedMongoProxyEndpoints)) {
|
||||||
delete newContext.MONGO_PROXY_ENDPOINT;
|
delete newContext.MONGO_PROXY_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) {
|
||||||
!validateEndpoint(
|
delete newContext.MONGO_BACKEND_ENDPOINT;
|
||||||
newContext.CASSANDRA_PROXY_ENDPOINT,
|
}
|
||||||
configContext.allowedCassandraProxyEndpoints || defaultAllowedCassandraProxyEndpoints,
|
|
||||||
)
|
if (!validateEndpoint(newContext.CASSANDRA_PROXY_ENDPOINT, allowedCassandraProxyEndpoints)) {
|
||||||
) {
|
|
||||||
delete newContext.CASSANDRA_PROXY_ENDPOINT;
|
delete newContext.CASSANDRA_PROXY_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -275,12 +265,6 @@ export interface Offer {
|
|||||||
offerReplacePending: boolean;
|
offerReplacePending: boolean;
|
||||||
instantMaximumThroughput?: number;
|
instantMaximumThroughput?: number;
|
||||||
softAllowedMaximumThroughput?: number;
|
softAllowedMaximumThroughput?: number;
|
||||||
throughputBuckets?: ThroughputBucket[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ThroughputBucket {
|
|
||||||
id: number;
|
|
||||||
maxThroughputPercentage: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SDKOfferDefinition extends Resource {
|
export interface SDKOfferDefinition extends Resource {
|
||||||
@@ -358,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 {
|
||||||
@@ -372,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;
|
||||||
@@ -403,7 +376,6 @@ export interface UpdateOfferParams {
|
|||||||
collectionId?: string;
|
collectionId?: string;
|
||||||
migrateToAutoPilot?: boolean;
|
migrateToAutoPilot?: boolean;
|
||||||
migrateToManual?: boolean;
|
migrateToManual?: boolean;
|
||||||
throughputBuckets?: ThroughputBucket[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Notification {
|
export interface Notification {
|
||||||
@@ -671,5 +643,3 @@ export interface FeatureRegistration {
|
|||||||
state: string;
|
state: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Tags = { [key: string]: string };
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
export enum FabricMessageTypes {
|
export enum FabricMessageTypes {
|
||||||
GetAuthorizationToken = "GetAuthorizationToken",
|
GetAuthorizationToken = "GetAuthorizationToken",
|
||||||
GetAllResourceTokens = "GetAllResourceTokens",
|
GetAllResourceTokens = "GetAllResourceTokens",
|
||||||
GetAccessToken = "GetAccessToken",
|
|
||||||
Ready = "Ready",
|
Ready = "Ready",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,47 @@
|
|||||||
import { AuthorizationToken } from "./FabricMessageTypes";
|
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||||
|
|
||||||
// This is the version of these messages
|
// This is the version of these messages
|
||||||
export const FABRIC_RPC_VERSION = "FabricMessageV3";
|
export const FABRIC_RPC_VERSION = "2";
|
||||||
|
|
||||||
// Fabric to Data Explorer
|
// Fabric to Data Explorer
|
||||||
|
|
||||||
|
// TODO Deprecated. Remove this section once DE is updated
|
||||||
|
export type FabricMessageV1 =
|
||||||
|
| {
|
||||||
|
type: "newContainer";
|
||||||
|
databaseName: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "initialize";
|
||||||
|
message: {
|
||||||
|
endpoint: string | undefined;
|
||||||
|
databaseId: string | undefined;
|
||||||
|
resourceTokens: unknown | undefined;
|
||||||
|
resourceTokensTimestamp: number | undefined;
|
||||||
|
error: string | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "authorizationToken";
|
||||||
|
message: {
|
||||||
|
id: string;
|
||||||
|
error: string | undefined;
|
||||||
|
data: AuthorizationToken | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "allResourceTokens";
|
||||||
|
message: {
|
||||||
|
id: string;
|
||||||
|
error: string | undefined;
|
||||||
|
endpoint: string | undefined;
|
||||||
|
databaseId: string | undefined;
|
||||||
|
resourceTokens: unknown | undefined;
|
||||||
|
resourceTokensTimestamp: number | undefined;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// -----------------------------
|
||||||
|
|
||||||
export type FabricMessageV2 =
|
export type FabricMessageV2 =
|
||||||
| {
|
| {
|
||||||
type: "newContainer";
|
type: "newContainer";
|
||||||
@@ -31,7 +69,7 @@ export type FabricMessageV2 =
|
|||||||
message: {
|
message: {
|
||||||
id: string;
|
id: string;
|
||||||
error: string | undefined;
|
error: string | undefined;
|
||||||
data: ResourceTokenInfo | undefined;
|
data: FabricDatabaseConnectionInfo | undefined;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -41,81 +79,17 @@ export type FabricMessageV2 =
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FabricMessageV3 =
|
export type CosmosDBTokenResponse = {
|
||||||
| {
|
token: string;
|
||||||
type: "newContainer";
|
date: string;
|
||||||
databaseName: string;
|
};
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "initialize";
|
|
||||||
version: string;
|
|
||||||
id: string;
|
|
||||||
message: InitializeMessageV3<CosmosDbArtifactType>;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "authorizationToken";
|
|
||||||
message: {
|
|
||||||
id: string;
|
|
||||||
error: string | undefined;
|
|
||||||
data: AuthorizationToken | undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "allResourceTokens_v2";
|
|
||||||
message: {
|
|
||||||
id: string;
|
|
||||||
error: string | undefined;
|
|
||||||
data: ResourceTokenInfo | undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "explorerVisible";
|
|
||||||
message: {
|
|
||||||
visible: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "accessToken";
|
|
||||||
message: {
|
|
||||||
id: string;
|
|
||||||
error: string | undefined;
|
|
||||||
data: { accessToken: string };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum CosmosDbArtifactType {
|
export type CosmosDBConnectionInfoResponse = {
|
||||||
MIRRORED_KEY = "MIRRORED_KEY",
|
|
||||||
MIRRORED_AAD = "MIRRORED_AAD",
|
|
||||||
NATIVE = "NATIVE",
|
|
||||||
}
|
|
||||||
export interface ArtifactConnectionInfo {
|
|
||||||
[CosmosDbArtifactType.MIRRORED_KEY]: { connectionId: string };
|
|
||||||
[CosmosDbArtifactType.MIRRORED_AAD]: AccessTokenConnectionInfo;
|
|
||||||
[CosmosDbArtifactType.NATIVE]: AccessTokenConnectionInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AccessTokenConnectionInfo {
|
|
||||||
accessToken: string;
|
|
||||||
databaseName: string;
|
|
||||||
accountEndpoint: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InitializeMessageV3<T extends CosmosDbArtifactType> {
|
|
||||||
connectionId: string;
|
|
||||||
isVisible: boolean;
|
|
||||||
isReadOnly: boolean;
|
|
||||||
artifactType: T;
|
|
||||||
artifactConnectionInfo: ArtifactConnectionInfo[T];
|
|
||||||
}
|
|
||||||
export interface CosmosDBConnectionInfoResponse {
|
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
resourceTokens: Record<string, string> | undefined;
|
resourceTokens: { [resourceId: string]: string };
|
||||||
accessToken: string | undefined;
|
};
|
||||||
isReadOnly: boolean;
|
|
||||||
credentialType: "Key" | "OAuth2" | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ResourceTokenInfo extends CosmosDBConnectionInfoResponse {
|
export interface FabricDatabaseConnectionInfo extends CosmosDBConnectionInfoResponse {
|
||||||
resourceTokensTimestamp: number;
|
resourceTokensTimestamp: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,14 +385,13 @@ 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;
|
||||||
csmEndpoint?: string;
|
csmEndpoint?: string;
|
||||||
dnsSuffix?: string;
|
dnsSuffix?: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
|
extensionEndpoint?: string;
|
||||||
portalBackendEndpoint?: string;
|
portalBackendEndpoint?: string;
|
||||||
mongoProxyEndpoint?: string;
|
mongoProxyEndpoint?: string;
|
||||||
cassandraProxyEndpoint?: string;
|
cassandraProxyEndpoint?: string;
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { configContext, Platform } from "ConfigContext";
|
|
||||||
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
@@ -21,6 +19,7 @@ import * as ViewModels from "../Contracts/ViewModels";
|
|||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
|
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
|
||||||
import { useSidePanel } from "../hooks/useSidePanel";
|
import { useSidePanel } from "../hooks/useSidePanel";
|
||||||
|
import { Platform, configContext } from "./../ConfigContext";
|
||||||
import Explorer from "./Explorer";
|
import Explorer from "./Explorer";
|
||||||
import { useNotebook } from "./Notebook/useNotebook";
|
import { useNotebook } from "./Notebook/useNotebook";
|
||||||
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
|
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
|
||||||
@@ -42,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 (isFabric() && userContext.fabricContext?.isReadOnly) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const items: TreeNodeMenuItem[] = [
|
const items: TreeNodeMenuItem[] = [
|
||||||
{
|
{
|
||||||
iconSrc: AddCollectionIcon,
|
iconSrc: AddCollectionIcon,
|
||||||
@@ -54,18 +49,16 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!isFabricNative() && (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",
|
||||||
});
|
});
|
||||||
@@ -146,18 +139,17 @@ export const createCollectionContextMenuButton = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isFabric() || (isFabric() && !userContext.fabricContext?.isReadOnly)) {
|
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(
|
"Delete " + getCollectionName(),
|
||||||
"Delete " + getCollectionName(),
|
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||||
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
);
|
||||||
);
|
|
||||||
},
|
},
|
||||||
label: `Delete ${getCollectionName()}`,
|
label: `Delete ${getCollectionName()}`,
|
||||||
styleClass: "deleteCollectionMenuItem",
|
styleClass: "deleteCollectionMenuItem",
|
||||||
|
|||||||
@@ -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}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import "@testing-library/jest-dom";
|
|
||||||
|
|
||||||
describe("AddFullTextPolicyForm", () => {
|
|
||||||
//CTODO: add tests
|
|
||||||
it.skip("should render correctly", () => {});
|
|
||||||
});
|
|
||||||
@@ -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)",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
@@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import { AuthType } from "AuthType";
|
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
import { Features } from "Platform/Hosted/extractFeatures";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
|
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
|
||||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||||
@@ -136,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);
|
||||||
@@ -249,42 +248,4 @@ describe("SettingsComponent", () => {
|
|||||||
expect(conflictResolutionPolicy.mode).toEqual(DataModels.ConflictResolutionMode.Custom);
|
expect(conflictResolutionPolicy.mode).toEqual(DataModels.ConflictResolutionMode.Custom);
|
||||||
expect(conflictResolutionPolicy.conflictResolutionProcedure).toEqual(expectSprocPath);
|
expect(conflictResolutionPolicy.conflictResolutionProcedure).toEqual(expectSprocPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should save throughput bucket changes when Save button is clicked", async () => {
|
|
||||||
updateUserContext({
|
|
||||||
apiType: "SQL",
|
|
||||||
features: { enableThroughputBuckets: true } as Features,
|
|
||||||
authType: AuthType.AAD,
|
|
||||||
});
|
|
||||||
|
|
||||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
|
||||||
|
|
||||||
const settingsComponentInstance = wrapper.instance() as SettingsComponent;
|
|
||||||
const isEnabled = settingsComponentInstance["throughputBucketsEnabled"];
|
|
||||||
expect(isEnabled).toBe(true);
|
|
||||||
|
|
||||||
wrapper.setState({
|
|
||||||
isThroughputBucketsSaveable: true,
|
|
||||||
throughputBuckets: [
|
|
||||||
{ id: 1, maxThroughputPercentage: 70 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
await settingsComponentInstance.onSaveClick();
|
|
||||||
|
|
||||||
expect(updateOffer).toHaveBeenCalledWith({
|
|
||||||
databaseId: collection.databaseId,
|
|
||||||
collectionId: collection.id(),
|
|
||||||
currentOffer: expect.any(Object),
|
|
||||||
autopilotThroughput: collection.offer().autoscaleMaxThroughput,
|
|
||||||
manualThroughput: collection.offer().manualThroughput,
|
|
||||||
throughputBuckets: [
|
|
||||||
{ id: 1, maxThroughputPercentage: 70 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(wrapper.state("isThroughputBucketsSaveable")).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,15 +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 {
|
|
||||||
ThroughputBucketsComponent,
|
|
||||||
ThroughputBucketsComponentProps,
|
|
||||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent";
|
|
||||||
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";
|
||||||
@@ -90,8 +86,6 @@ export interface SettingsComponentState {
|
|||||||
wasAutopilotOriginallySet: boolean;
|
wasAutopilotOriginallySet: boolean;
|
||||||
isScaleSaveable: boolean;
|
isScaleSaveable: boolean;
|
||||||
isScaleDiscardable: boolean;
|
isScaleDiscardable: boolean;
|
||||||
throughputBuckets: DataModels.ThroughputBucket[];
|
|
||||||
throughputBucketsBaseline: DataModels.ThroughputBucket[];
|
|
||||||
throughputError: string;
|
throughputError: string;
|
||||||
|
|
||||||
timeToLive: TtlType;
|
timeToLive: TtlType;
|
||||||
@@ -110,14 +104,6 @@ export interface SettingsComponentState {
|
|||||||
changeFeedPolicyBaseline: ChangeFeedPolicyState;
|
changeFeedPolicyBaseline: ChangeFeedPolicyState;
|
||||||
isSubSettingsSaveable: boolean;
|
isSubSettingsSaveable: boolean;
|
||||||
isSubSettingsDiscardable: boolean;
|
isSubSettingsDiscardable: boolean;
|
||||||
isThroughputBucketsSaveable: 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;
|
||||||
@@ -144,6 +130,7 @@ export interface SettingsComponentState {
|
|||||||
conflictResolutionPolicyProcedureBaseline: string;
|
conflictResolutionPolicyProcedureBaseline: string;
|
||||||
isConflictResolutionDirty: boolean;
|
isConflictResolutionDirty: boolean;
|
||||||
|
|
||||||
|
initialNotification: DataModels.Notification;
|
||||||
selectedTab: SettingsV2TabTypes;
|
selectedTab: SettingsV2TabTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,9 +150,7 @@ 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;
|
||||||
private throughputBucketsEnabled: boolean;
|
|
||||||
public mongoDBCollectionResource: MongoDBCollectionResource;
|
public mongoDBCollectionResource: MongoDBCollectionResource;
|
||||||
|
|
||||||
constructor(props: SettingsComponentProps) {
|
constructor(props: SettingsComponentProps) {
|
||||||
@@ -180,13 +165,8 @@ 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;
|
||||||
this.throughputBucketsEnabled =
|
|
||||||
userContext.apiType === "SQL" &&
|
|
||||||
userContext.features.enableThroughputBuckets &&
|
|
||||||
userContext.authType === AuthType.AAD;
|
|
||||||
|
|
||||||
// Mongo container with system partition key still treat as "Fixed"
|
// Mongo container with system partition key still treat as "Fixed"
|
||||||
this.isFixedContainer =
|
this.isFixedContainer =
|
||||||
@@ -205,8 +185,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
wasAutopilotOriginallySet: false,
|
wasAutopilotOriginallySet: false,
|
||||||
isScaleSaveable: false,
|
isScaleSaveable: false,
|
||||||
isScaleDiscardable: false,
|
isScaleDiscardable: false,
|
||||||
throughputBuckets: undefined,
|
|
||||||
throughputBucketsBaseline: undefined,
|
|
||||||
throughputError: undefined,
|
throughputError: undefined,
|
||||||
|
|
||||||
timeToLive: undefined,
|
timeToLive: undefined,
|
||||||
@@ -225,14 +203,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
changeFeedPolicyBaseline: undefined,
|
changeFeedPolicyBaseline: undefined,
|
||||||
isSubSettingsSaveable: false,
|
isSubSettingsSaveable: false,
|
||||||
isSubSettingsDiscardable: false,
|
isSubSettingsDiscardable: false,
|
||||||
isThroughputBucketsSaveable: false,
|
|
||||||
|
|
||||||
vectorEmbeddingPolicy: undefined,
|
|
||||||
vectorEmbeddingPolicyBaseline: undefined,
|
|
||||||
fullTextPolicy: undefined,
|
|
||||||
fullTextPolicyBaseline: undefined,
|
|
||||||
shouldDiscardContainerPolicies: false,
|
|
||||||
isContainerPolicyDirty: false,
|
|
||||||
|
|
||||||
indexingPolicyContent: undefined,
|
indexingPolicyContent: undefined,
|
||||||
indexingPolicyContentBaseline: undefined,
|
indexingPolicyContentBaseline: undefined,
|
||||||
@@ -259,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,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -338,12 +309,10 @@ 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 ||
|
||||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable) ||
|
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable)
|
||||||
this.state.isThroughputBucketsSaveable
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -351,12 +320,10 @@ 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 ||
|
||||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable) ||
|
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable)
|
||||||
this.state.isThroughputBucketsSaveable
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -436,14 +403,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
throughput: this.state.throughputBaseline,
|
throughput: this.state.throughputBaseline,
|
||||||
throughputBuckets: this.state.throughputBucketsBaseline,
|
|
||||||
throughputBucketsBaseline: this.state.throughputBucketsBaseline,
|
|
||||||
timeToLive: this.state.timeToLiveBaseline,
|
timeToLive: this.state.timeToLiveBaseline,
|
||||||
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: [],
|
||||||
@@ -455,14 +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,
|
||||||
isThroughputBucketsSaveable: false,
|
|
||||||
isSubSettingsDiscardable: false,
|
isSubSettingsDiscardable: false,
|
||||||
isContainerPolicyDirty: false,
|
|
||||||
isIndexingPolicyDirty: false,
|
isIndexingPolicyDirty: false,
|
||||||
isMongoIndexingPolicySaveable: false,
|
isMongoIndexingPolicySaveable: false,
|
||||||
isMongoIndexingPolicyDiscardable: false,
|
isMongoIndexingPolicyDiscardable: false,
|
||||||
@@ -490,21 +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 onThroughputBucketsSaveableChange = (isSaveable: boolean): void => {
|
|
||||||
this.setState({ isThroughputBucketsSaveable: isSaveable });
|
|
||||||
};
|
|
||||||
|
|
||||||
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 => {
|
||||||
@@ -592,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 });
|
||||||
|
|
||||||
@@ -751,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();
|
||||||
@@ -773,13 +711,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
] as DataModels.ComputedProperties;
|
] as DataModels.ComputedProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const throughputBuckets = this.offer?.throughputBuckets;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
throughput: offerThroughput,
|
throughput: offerThroughput,
|
||||||
throughputBaseline: offerThroughput,
|
throughputBaseline: offerThroughput,
|
||||||
throughputBuckets,
|
|
||||||
throughputBucketsBaseline: throughputBuckets,
|
|
||||||
changeFeedPolicy: changeFeedPolicy,
|
changeFeedPolicy: changeFeedPolicy,
|
||||||
changeFeedPolicyBaseline: changeFeedPolicy,
|
changeFeedPolicyBaseline: changeFeedPolicy,
|
||||||
timeToLive: timeToLive,
|
timeToLive: timeToLive,
|
||||||
@@ -792,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,
|
||||||
@@ -867,10 +797,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.setState({ throughput: newThroughput, throughputError });
|
this.setState({ throughput: newThroughput, throughputError });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onThroughputBucketChange = (throughputBuckets: DataModels.ThroughputBucket[]): void => {
|
|
||||||
this.setState({ throughputBuckets });
|
|
||||||
};
|
|
||||||
|
|
||||||
private onAutoPilotSelected = (isAutoPilotSelected: boolean): void =>
|
private onAutoPilotSelected = (isAutoPilotSelected: boolean): void =>
|
||||||
this.setState({ isAutoPilotSelected: isAutoPilotSelected });
|
this.setState({ isAutoPilotSelected: isAutoPilotSelected });
|
||||||
|
|
||||||
@@ -930,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
|
||||||
@@ -952,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 =
|
||||||
@@ -994,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();
|
||||||
@@ -1004,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,
|
||||||
@@ -1061,24 +979,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.throughputBucketsEnabled && this.state.isThroughputBucketsSaveable) {
|
|
||||||
const updatedOffer: DataModels.Offer = await updateOffer({
|
|
||||||
databaseId: this.collection.databaseId,
|
|
||||||
collectionId: this.collection.id(),
|
|
||||||
currentOffer: this.collection.offer(),
|
|
||||||
autopilotThroughput: this.collection.offer().autoscaleMaxThroughput
|
|
||||||
? this.collection.offer().autoscaleMaxThroughput
|
|
||||||
: undefined,
|
|
||||||
manualThroughput: this.collection.offer().manualThroughput
|
|
||||||
? this.collection.offer().manualThroughput
|
|
||||||
: undefined,
|
|
||||||
throughputBuckets: this.state.throughputBuckets,
|
|
||||||
});
|
|
||||||
this.collection.offer(updatedOffer);
|
|
||||||
this.offer = updatedOffer;
|
|
||||||
this.setState({ isThroughputBucketsSaveable: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.isScaleSaveable) {
|
if (this.state.isScaleSaveable) {
|
||||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||||
databaseId: this.collection.databaseId,
|
databaseId: this.collection.databaseId,
|
||||||
@@ -1152,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,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1193,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,
|
||||||
@@ -1259,19 +1145,16 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange,
|
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
const throughputBucketsComponentProps: ThroughputBucketsComponentProps = {
|
|
||||||
currentBuckets: this.state.throughputBuckets,
|
|
||||||
throughputBucketsBaseline: this.state.throughputBucketsBaseline,
|
|
||||||
onBucketsChange: this.onThroughputBucketChange,
|
|
||||||
onSaveableChange: this.onThroughputBucketsSaveableChange,
|
|
||||||
};
|
|
||||||
|
|
||||||
const partitionKeyComponentProps: PartitionKeyComponentProps = {
|
const partitionKeyComponentProps: PartitionKeyComponentProps = {
|
||||||
database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId),
|
database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId),
|
||||||
collection: this.collection,
|
collection: this.collection,
|
||||||
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({
|
||||||
@@ -1285,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} />,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1328,13 +1211,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.throughputBucketsEnabled) {
|
|
||||||
tabs.push({
|
|
||||||
tab: SettingsV2TabTypes.ThroughputBucketsTab,
|
|
||||||
content: <ThroughputBucketsComponent {...throughputBucketsComponentProps} />,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const pivotProps: IPivotProps = {
|
const pivotProps: IPivotProps = {
|
||||||
onLinkClick: this.onPivotChange,
|
onLinkClick: this.onPivotChange,
|
||||||
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import "@testing-library/jest-dom";
|
|
||||||
|
|
||||||
describe("ContainerPolicyComponent", () => {
|
|
||||||
//CTODO: add tests
|
|
||||||
it.skip("should render correctly", () => {});
|
|
||||||
});
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -1,177 +0,0 @@
|
|||||||
import "@testing-library/jest-dom";
|
|
||||||
import { fireEvent, render, screen } from "@testing-library/react";
|
|
||||||
import React from "react";
|
|
||||||
import { ThroughputBucketsComponent } from "./ThroughputBucketsComponent";
|
|
||||||
|
|
||||||
describe("ThroughputBucketsComponent", () => {
|
|
||||||
const mockOnBucketsChange = jest.fn();
|
|
||||||
const mockOnSaveableChange = jest.fn();
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
currentBuckets: [
|
|
||||||
{ id: 1, maxThroughputPercentage: 50 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
],
|
|
||||||
throughputBucketsBaseline: [
|
|
||||||
{ id: 1, maxThroughputPercentage: 40 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 50 },
|
|
||||||
],
|
|
||||||
onBucketsChange: mockOnBucketsChange,
|
|
||||||
onSaveableChange: mockOnSaveableChange,
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders the correct number of buckets", () => {
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
|
||||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders buckets in the correct order even if input is unordered", () => {
|
|
||||||
const unorderedBuckets = [
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
{ id: 1, maxThroughputPercentage: 50 },
|
|
||||||
];
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={unorderedBuckets} />);
|
|
||||||
|
|
||||||
const bucketLabels = screen.getAllByText(/Group \d+/).map((el) => el.textContent);
|
|
||||||
expect(bucketLabels).toEqual(["Group 1 (Data Explorer Query Bucket)", "Group 2", "Group 3", "Group 4", "Group 5"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders all provided buckets even if they exceed the max default bucket count", () => {
|
|
||||||
const oversizedBuckets = [
|
|
||||||
{ id: 1, maxThroughputPercentage: 50 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
{ id: 3, maxThroughputPercentage: 70 },
|
|
||||||
{ id: 4, maxThroughputPercentage: 80 },
|
|
||||||
{ id: 5, maxThroughputPercentage: 90 },
|
|
||||||
{ id: 6, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 7, maxThroughputPercentage: 40 },
|
|
||||||
];
|
|
||||||
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={oversizedBuckets} />);
|
|
||||||
|
|
||||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(7);
|
|
||||||
|
|
||||||
expect(screen.getByDisplayValue("50")).toBeInTheDocument();
|
|
||||||
expect(screen.getByDisplayValue("60")).toBeInTheDocument();
|
|
||||||
expect(screen.getByDisplayValue("70")).toBeInTheDocument();
|
|
||||||
expect(screen.getByDisplayValue("80")).toBeInTheDocument();
|
|
||||||
expect(screen.getByDisplayValue("90")).toBeInTheDocument();
|
|
||||||
expect(screen.getByDisplayValue("100")).toBeInTheDocument();
|
|
||||||
expect(screen.getByDisplayValue("40")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("calls onBucketsChange when a bucket value changes", () => {
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
|
||||||
const input = screen.getByDisplayValue("50");
|
|
||||||
fireEvent.change(input, { target: { value: "70" } });
|
|
||||||
|
|
||||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
|
||||||
{ id: 1, maxThroughputPercentage: 70 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
{ id: 3, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 4, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 5, maxThroughputPercentage: 100 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("triggers onSaveableChange when values change", () => {
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
|
||||||
const input = screen.getByDisplayValue("50");
|
|
||||||
fireEvent.change(input, { target: { value: "80" } });
|
|
||||||
|
|
||||||
expect(mockOnSaveableChange).toHaveBeenCalledWith(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("updates state consistently after multiple changes to different buckets", () => {
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
|
||||||
|
|
||||||
const input1 = screen.getByDisplayValue("50");
|
|
||||||
fireEvent.change(input1, { target: { value: "70" } });
|
|
||||||
|
|
||||||
const input2 = screen.getByDisplayValue("60");
|
|
||||||
fireEvent.change(input2, { target: { value: "80" } });
|
|
||||||
|
|
||||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
|
||||||
{ id: 1, maxThroughputPercentage: 70 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 80 },
|
|
||||||
{ id: 3, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 4, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 5, maxThroughputPercentage: 100 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("resets to baseline when currentBuckets are reset", () => {
|
|
||||||
const { rerender } = render(<ThroughputBucketsComponent {...defaultProps} />);
|
|
||||||
const input1 = screen.getByDisplayValue("50");
|
|
||||||
fireEvent.change(input1, { target: { value: "70" } });
|
|
||||||
|
|
||||||
rerender(<ThroughputBucketsComponent {...defaultProps} currentBuckets={defaultProps.throughputBucketsBaseline} />);
|
|
||||||
|
|
||||||
expect(screen.getByDisplayValue("40")).toBeInTheDocument();
|
|
||||||
expect(screen.getByDisplayValue("50")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not call onBucketsChange when value remains unchanged", () => {
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
|
||||||
const input = screen.getByDisplayValue("50");
|
|
||||||
fireEvent.change(input, { target: { value: "50" } });
|
|
||||||
|
|
||||||
expect(mockOnBucketsChange).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("disables input and slider when maxThroughputPercentage is 100", () => {
|
|
||||||
render(
|
|
||||||
<ThroughputBucketsComponent
|
|
||||||
{...defaultProps}
|
|
||||||
currentBuckets={[
|
|
||||||
{ id: 1, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 50 },
|
|
||||||
]}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const disabledInputs = screen.getAllByDisplayValue("100");
|
|
||||||
expect(disabledInputs.length).toBeGreaterThan(0);
|
|
||||||
expect(disabledInputs[0]).toBeDisabled();
|
|
||||||
|
|
||||||
const sliders = screen.getAllByRole("slider");
|
|
||||||
expect(sliders.length).toBeGreaterThan(0);
|
|
||||||
expect(sliders[0]).toHaveAttribute("aria-disabled", "true");
|
|
||||||
expect(sliders[1]).toHaveAttribute("aria-disabled", "false");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("toggles bucket value between 50 and 100 with switch", () => {
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
|
||||||
const toggles = screen.getAllByRole("switch");
|
|
||||||
|
|
||||||
fireEvent.click(toggles[0]);
|
|
||||||
|
|
||||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
|
||||||
{ id: 1, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
{ id: 3, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 4, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 5, maxThroughputPercentage: 100 },
|
|
||||||
]);
|
|
||||||
|
|
||||||
fireEvent.click(toggles[0]);
|
|
||||||
|
|
||||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
|
||||||
{ id: 1, maxThroughputPercentage: 50 },
|
|
||||||
{ id: 2, maxThroughputPercentage: 60 },
|
|
||||||
{ id: 3, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 4, maxThroughputPercentage: 100 },
|
|
||||||
{ id: 5, maxThroughputPercentage: 100 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("ensures default buckets are used when no buckets are provided", () => {
|
|
||||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={[]} />);
|
|
||||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(5);
|
|
||||||
expect(screen.getAllByDisplayValue("100")).toHaveLength(5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import { Label, Slider, Stack, TextField, Toggle } from "@fluentui/react";
|
|
||||||
import { ThroughputBucket } from "Contracts/DataModels";
|
|
||||||
import React, { FC, useEffect, useState } from "react";
|
|
||||||
import { isDirty } from "../../SettingsUtils";
|
|
||||||
|
|
||||||
const MAX_BUCKET_SIZES = 5;
|
|
||||||
|
|
||||||
const DEFAULT_BUCKETS = Array.from({ length: MAX_BUCKET_SIZES }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
maxThroughputPercentage: 100,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export interface ThroughputBucketsComponentProps {
|
|
||||||
currentBuckets: ThroughputBucket[];
|
|
||||||
throughputBucketsBaseline: ThroughputBucket[];
|
|
||||||
onBucketsChange: (updatedBuckets: ThroughputBucket[]) => void;
|
|
||||||
onSaveableChange: (isSaveable: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = ({
|
|
||||||
currentBuckets,
|
|
||||||
throughputBucketsBaseline,
|
|
||||||
onBucketsChange,
|
|
||||||
onSaveableChange,
|
|
||||||
}) => {
|
|
||||||
const getThroughputBuckets = (buckets: ThroughputBucket[]): ThroughputBucket[] => {
|
|
||||||
if (!buckets || buckets.length === 0) {
|
|
||||||
return DEFAULT_BUCKETS;
|
|
||||||
}
|
|
||||||
const maxBuckets = Math.max(DEFAULT_BUCKETS.length, buckets.length);
|
|
||||||
const adjustedDefaultBuckets = Array.from({ length: maxBuckets }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
maxThroughputPercentage: 100,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return adjustedDefaultBuckets.map(
|
|
||||||
(defaultBucket) => buckets?.find((bucket) => bucket.id === defaultBucket.id) || defaultBucket,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const [throughputBuckets, setThroughputBuckets] = useState<ThroughputBucket[]>(getThroughputBuckets(currentBuckets));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setThroughputBuckets(getThroughputBuckets(currentBuckets));
|
|
||||||
onSaveableChange(false);
|
|
||||||
}, [currentBuckets]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const isChanged = isDirty(throughputBuckets, getThroughputBuckets(throughputBucketsBaseline));
|
|
||||||
onSaveableChange(isChanged);
|
|
||||||
}, [throughputBuckets]);
|
|
||||||
|
|
||||||
const handleBucketChange = (id: number, newValue: number) => {
|
|
||||||
const updatedBuckets = throughputBuckets.map((bucket) =>
|
|
||||||
bucket.id === id ? { ...bucket, maxThroughputPercentage: newValue } : bucket,
|
|
||||||
);
|
|
||||||
setThroughputBuckets(updatedBuckets);
|
|
||||||
const settingsChanged = isDirty(updatedBuckets, throughputBuckets);
|
|
||||||
settingsChanged && onBucketsChange(updatedBuckets);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onToggle = (id: number, checked: boolean) => {
|
|
||||||
handleBucketChange(id, checked ? 50 : 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack tokens={{ childrenGap: "m" }} styles={{ root: { width: "70%", maxWidth: 700 } }}>
|
|
||||||
<Label>Throughput Buckets</Label>
|
|
||||||
<Stack>
|
|
||||||
{throughputBuckets?.map((bucket) => (
|
|
||||||
<Stack key={bucket.id} horizontal tokens={{ childrenGap: 8 }} verticalAlign="center">
|
|
||||||
<Slider
|
|
||||||
min={1}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
value={bucket.maxThroughputPercentage}
|
|
||||||
onChange={(newValue) => handleBucketChange(bucket.id, newValue)}
|
|
||||||
showValue={false}
|
|
||||||
label={`Group ${bucket.id}${bucket.id === 1 ? " (Data Explorer Query Bucket)" : ""}`}
|
|
||||||
styles={{ root: { flex: 2, maxWidth: 400 } }}
|
|
||||||
disabled={bucket.maxThroughputPercentage === 100}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
value={bucket.maxThroughputPercentage.toString()}
|
|
||||||
onChange={(event, newValue) => handleBucketChange(bucket.id, parseInt(newValue || "0", 10))}
|
|
||||||
type="number"
|
|
||||||
suffix="%"
|
|
||||||
styles={{
|
|
||||||
fieldGroup: { width: 80 },
|
|
||||||
}}
|
|
||||||
disabled={bucket.maxThroughputPercentage === 100}
|
|
||||||
/>
|
|
||||||
<Toggle
|
|
||||||
onText="Active"
|
|
||||||
offText="Inactive"
|
|
||||||
checked={bucket.maxThroughputPercentage !== 100}
|
|
||||||
onChange={(event, checked) => onToggle(bucket.id, checked)}
|
|
||||||
styles={{ root: { marginBottom: 0 }, text: { fontSize: 12 } }}
|
|
||||||
></Toggle>
|
|
||||||
</Stack>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -17,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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
`;
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -4,15 +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
|
|
||||||
| DataModels.ThroughputBucket[];
|
|
||||||
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";
|
||||||
@@ -56,12 +48,6 @@ export enum SettingsV2TabTypes {
|
|||||||
PartitionKeyTab,
|
PartitionKeyTab,
|
||||||
ComputedPropertiesTab,
|
ComputedPropertiesTab,
|
||||||
ContainerVectorPolicyTab,
|
ContainerVectorPolicyTab,
|
||||||
ThroughputBucketsTab,
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ContainerPolicyTabTypes {
|
|
||||||
VectorPolicyTab,
|
|
||||||
FullTextPolicyTab,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IsComponentDirtyResult {
|
export interface IsComponentDirtyResult {
|
||||||
@@ -168,9 +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)";
|
||||||
case SettingsV2TabTypes.ThroughputBucketsTab:
|
|
||||||
return "Throughput Buckets";
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown tab ${tab}`);
|
throw new Error(`Unknown tab ${tab}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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,23 +34,10 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
setIsThroughputCapExceeded,
|
setIsThroughputCapExceeded,
|
||||||
onCostAcknowledgeChange,
|
onCostAcknowledgeChange,
|
||||||
}: ThroughputInputProps) => {
|
}: ThroughputInputProps) => {
|
||||||
let defaultThroughput: number;
|
|
||||||
const workloadType: Constants.WorkloadType = getWorkloadType();
|
|
||||||
|
|
||||||
if (
|
|
||||||
isFreeTier ||
|
|
||||||
isQuickstart ||
|
|
||||||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType)
|
|
||||||
) {
|
|
||||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput1K;
|
|
||||||
} else if (workloadType === Constants.WorkloadType.Production) {
|
|
||||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput10K;
|
|
||||||
} else {
|
|
||||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput4K;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
|
const [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);
|
||||||
@@ -61,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;
|
||||||
@@ -170,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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
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";
|
||||||
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||||
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { IGalleryItem } from "Juno/JunoClient";
|
import { IGalleryItem } from "Juno/JunoClient";
|
||||||
import { isFabricMirrored, isFabricMirroredKey, scheduleRefreshFabricToken } 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";
|
||||||
@@ -43,7 +42,7 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
|||||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
||||||
import { useSidePanel } from "../hooks/useSidePanel";
|
import { useSidePanel } from "../hooks/useSidePanel";
|
||||||
import { ReactTabKind, useTabs } from "../hooks/useTabs";
|
import { useTabs } from "../hooks/useTabs";
|
||||||
import "./ComponentRegisterer";
|
import "./ComponentRegisterer";
|
||||||
import { DialogProps, useDialog } from "./Controls/Dialog";
|
import { DialogProps, useDialog } from "./Controls/Dialog";
|
||||||
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
||||||
@@ -187,10 +186,6 @@ export default class Explorer {
|
|||||||
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
|
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFabricMirrored()) {
|
|
||||||
useTabs.getState().closeReactTab(ReactTabKind.Home);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.refreshExplorer();
|
this.refreshExplorer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,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) {
|
||||||
@@ -282,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(
|
||||||
@@ -351,8 +394,8 @@ export default class Explorer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public onRefreshResourcesClick = async (): Promise<void> => {
|
public onRefreshResourcesClick = async (): Promise<void> => {
|
||||||
if (isFabricMirroredKey()) {
|
if (configContext.platform === Platform.Fabric) {
|
||||||
scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases());
|
scheduleRefreshDatabaseResourceToken(true).then(() => this.refreshAllDatabases());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1076,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 {
|
||||||
@@ -1107,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,15 +1174,11 @@ export default class Explorer {
|
|||||||
await this.initNotebooks(userContext.databaseAccount);
|
await this.initNotebooks(userContext.databaseAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.refreshSampleData();
|
await this.refreshSampleData();
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
@@ -1156,27 +1195,26 @@ export default class Explorer {
|
|||||||
.setCopilotSampleDBEnabled(copilotEnabled && copilotUserDBEnabled && copilotSampleDBEnabled);
|
.setCopilotSampleDBEnabled(copilotEnabled && copilotUserDBEnabled && copilotSampleDBEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
public refreshSampleData(): void {
|
public async refreshSampleData(): Promise<void> {
|
||||||
if (!userContext.sampleDataConnectionInfo) {
|
try {
|
||||||
|
if (!userContext.sampleDataConnectionInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const collection: DataModels.Collection = await readSampleCollection();
|
||||||
|
if (!collection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const databaseId = userContext.sampleDataConnectionInfo?.databaseId;
|
||||||
|
if (!databaseId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
|
||||||
|
useDatabases.setState({ sampleDataResourceTokenCollection });
|
||||||
|
} catch (error) {
|
||||||
|
Logger.logError(getErrorMessage(error), "Explorer");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const databaseId = userContext.sampleDataConnectionInfo?.databaseId;
|
|
||||||
if (!databaseId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
readSampleCollection()
|
|
||||||
.then((collection: DataModels.Collection) => {
|
|
||||||
if (!collection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
|
|
||||||
useDatabases.setState({ sampleDataResourceTokenCollection });
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
Logger.logError(getErrorMessage(error), "Explorer/refreshSampleData");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
.flex-direction(@direction: row);
|
.flex-direction(@direction: row);
|
||||||
padding: 4px 5px;
|
padding: 4px 5px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.valueCol {
|
.valueCol {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
@@ -59,10 +63,6 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customTrashIcon {
|
|
||||||
padding-top: 33px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rightPaneTrashIconImg {
|
.rightPaneTrashIconImg {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,11 +142,10 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
|||||||
<div className="labelCol">
|
<div className="labelCol">
|
||||||
<TextField
|
<TextField
|
||||||
className="edgeInput"
|
className="edgeInput"
|
||||||
label={index === 0 && "Key"}
|
|
||||||
type="text"
|
type="text"
|
||||||
id="propertyKeyNewVertexPane"
|
id="propertyKeyNewVertexPane"
|
||||||
componentRef={input}
|
componentRef={input}
|
||||||
required
|
aria-required="true"
|
||||||
placeholder="Key"
|
placeholder="Key"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
aria-label={`Enter value for propery ${index + 1}`}
|
aria-label={`Enter value for propery ${index + 1}`}
|
||||||
@@ -154,11 +153,11 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
|||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onKeyChange(event, index)}
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onKeyChange(event, index)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<span className="mandatoryStar">* </span>
|
||||||
|
|
||||||
<div className="valueCol">
|
<div className="valueCol">
|
||||||
<TextField
|
<TextField
|
||||||
className="edgeInput"
|
className="edgeInput"
|
||||||
label={index === 0 && "Value"}
|
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Value"
|
placeholder="Value"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
@@ -170,8 +169,6 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
|||||||
<div>
|
<div>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
role="combobox"
|
role="combobox"
|
||||||
label={index === 0 && "Type"}
|
|
||||||
ariaLabel="Type"
|
|
||||||
placeholder="Select an option"
|
placeholder="Select an option"
|
||||||
defaultSelectedKey={data.values[0].type}
|
defaultSelectedKey={data.values[0].type}
|
||||||
style={{ width: 100 }}
|
style={{ width: 100 }}
|
||||||
@@ -184,7 +181,7 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
|||||||
</div>
|
</div>
|
||||||
<div className="actionCol">
|
<div className="actionCol">
|
||||||
<div
|
<div
|
||||||
className={`rightPaneTrashIcon rightPaneBtns ${index === 0 && "customTrashIcon"}`}
|
className="rightPaneTrashIcon rightPaneBtns"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="button"
|
role="button"
|
||||||
aria-label={`Delete ${data.key}`}
|
aria-label={`Delete ${data.key}`}
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
|
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
|
||||||
import { StyleConstants } from "../../../Common/StyleConstants";
|
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||||
|
import { Platform, configContext } from "../../../ConfigContext";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
@@ -93,18 +93,19 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootStyle = isFabric()
|
const rootStyle =
|
||||||
? {
|
configContext.platform === Platform.Fabric
|
||||||
root: {
|
? {
|
||||||
backgroundColor: "transparent",
|
root: {
|
||||||
padding: "2px 8px 0px 8px",
|
backgroundColor: "transparent",
|
||||||
},
|
padding: "2px 8px 0px 8px",
|
||||||
}
|
},
|
||||||
: {
|
}
|
||||||
root: {
|
: {
|
||||||
backgroundColor: backgroundColor,
|
root: {
|
||||||
},
|
backgroundColor: backgroundColor,
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
|
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
|
||||||
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);
|
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);
|
||||||
|
|||||||
@@ -37,25 +37,21 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Now that Tables API supports dataplane RBAC, calling createStaticCommandBarButtons will enable the
|
it("Button should not be visible for Tables API", () => {
|
||||||
// Entra ID Login button, which causes this test to fail due to "Invalid hook call.". This seems to be
|
updateUserContext({
|
||||||
// unsupported in jest and needs to be tested with react-hooks-testing-library.
|
databaseAccount: {
|
||||||
//
|
properties: {
|
||||||
// it("Button should not be visible for Tables API", () => {
|
capabilities: [{ name: "EnableTable" }],
|
||||||
// updateUserContext({
|
},
|
||||||
// databaseAccount: {
|
} as DatabaseAccount,
|
||||||
// properties: {
|
});
|
||||||
// capabilities: [{ name: "EnableTable" }],
|
|
||||||
// },
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
// } as DatabaseAccount,
|
const enableAzureSynapseLinkBtn = buttons.find(
|
||||||
// });
|
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||||
//
|
);
|
||||||
// const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
||||||
// const enableAzureSynapseLinkBtn = buttons.find(
|
});
|
||||||
// (button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
|
||||||
// );
|
|
||||||
// expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
|
||||||
//});
|
|
||||||
|
|
||||||
it("Button should not be visible for Cassandra API", () => {
|
it("Button should not be visible for Cassandra API", () => {
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
|
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
|
||||||
@@ -62,7 +61,7 @@ export function createStaticCommandBarButtons(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDataplaneRbacSupported(userContext.apiType)) {
|
if (userContext.apiType === "SQL") {
|
||||||
const [loginButtonProps, setLoginButtonProps] = useState<CommandButtonComponentProps | undefined>(undefined);
|
const [loginButtonProps, setLoginButtonProps] = useState<CommandButtonComponentProps | undefined>(undefined);
|
||||||
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
|
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
|
||||||
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
||||||
@@ -168,18 +167,22 @@ 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,
|
? []
|
||||||
iconAlt: "Settings",
|
: [
|
||||||
onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
|
{
|
||||||
commandButtonLabel: undefined,
|
iconSrc: SettingsIcon,
|
||||||
ariaLabel: "Settings",
|
iconAlt: "Settings",
|
||||||
tooltipText: "Settings",
|
onCommandClick: () =>
|
||||||
hasPopup: true,
|
useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
|
||||||
disabled: false,
|
commandButtonLabel: undefined,
|
||||||
},
|
ariaLabel: "Settings",
|
||||||
];
|
tooltipText: "Settings",
|
||||||
|
hasPopup: true,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const showOpenFullScreen =
|
const showOpenFullScreen =
|
||||||
configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin";
|
configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin";
|
||||||
|
|||||||
@@ -37,10 +37,6 @@
|
|||||||
background-color:@NotificationHigh;
|
background-color:@NotificationHigh;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
|
||||||
.focusedBorder();
|
|
||||||
}
|
|
||||||
|
|
||||||
.statusBar {
|
.statusBar {
|
||||||
.dataTypeIcons {
|
.dataTypeIcons {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -81,6 +81,10 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setElememntRef = (element: HTMLElement): void => {
|
||||||
|
this.consoleHeaderElement = element;
|
||||||
|
};
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
const numInProgress = this.state.allConsoleData.filter(
|
const numInProgress = this.state.allConsoleData.filter(
|
||||||
(data: ConsoleData) => data.type === ConsoleDataType.InProgress,
|
(data: ConsoleData) => data.type === ConsoleDataType.InProgress,
|
||||||
@@ -97,9 +101,7 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
<div
|
<div
|
||||||
className="notificationConsoleHeader"
|
className="notificationConsoleHeader"
|
||||||
id="notificationConsoleHeader"
|
id="notificationConsoleHeader"
|
||||||
role="button"
|
ref={this.setElememntRef}
|
||||||
aria-label="Console"
|
|
||||||
aria-expanded={this.props.isConsoleExpanded}
|
|
||||||
onClick={() => this.expandCollapseConsole()}
|
onClick={() => this.expandCollapseConsole()}
|
||||||
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
|
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -107,15 +109,15 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
<div className="statusBar">
|
<div className="statusBar">
|
||||||
<span className="dataTypeIcons">
|
<span className="dataTypeIcons">
|
||||||
<span className="notificationConsoleHeaderIconWithData">
|
<span className="notificationConsoleHeaderIconWithData">
|
||||||
<img src={LoadingIcon} alt="In progress items" />
|
<img src={LoadingIcon} alt="in progress items" />
|
||||||
<span className="numInProgress">{numInProgress}</span>
|
<span className="numInProgress">{numInProgress}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="notificationConsoleHeaderIconWithData">
|
<span className="notificationConsoleHeaderIconWithData">
|
||||||
<img src={ErrorBlackIcon} alt="Error items" />
|
<img src={ErrorBlackIcon} alt="error items" />
|
||||||
<span className="numErroredItems">{numErroredItems}</span>
|
<span className="numErroredItems">{numErroredItems}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="notificationConsoleHeaderIconWithData">
|
<span className="notificationConsoleHeaderIconWithData">
|
||||||
<img src={infoBubbleIcon} alt="Info items" />
|
<img src={infoBubbleIcon} alt="info items" />
|
||||||
<span className="numInfoItems">{numInfoItems}</span>
|
<span className="numInfoItems">{numInfoItems}</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -127,10 +129,16 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="expandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton">
|
<div
|
||||||
|
className="expandCollapseButton"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={"console button" + (this.props.isConsoleExpanded ? " expanded" : " collapsed")}
|
||||||
|
aria-expanded={!this.props.isConsoleExpanded}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
|
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
|
||||||
alt={this.props.isConsoleExpanded ? "Collapse icon" : "Expand icon"}
|
alt={this.props.isConsoleExpanded ? "ChevronDownIcon" : "ChevronUpIcon"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -139,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:"
|
||||||
@@ -250,6 +258,9 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onConsoleWasExpanded = (): void => {
|
private onConsoleWasExpanded = (): void => {
|
||||||
|
if (this.props.isConsoleExpanded && this.consoleHeaderElement) {
|
||||||
|
this.consoleHeaderElement.focus();
|
||||||
|
}
|
||||||
useNotificationConsole.getState().setConsoleAnimationFinished(true);
|
useNotificationConsole.getState().setConsoleAnimationFinished(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,10 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
className="notificationConsoleContainer"
|
className="notificationConsoleContainer"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-expanded={false}
|
|
||||||
aria-label="Console"
|
|
||||||
className="notificationConsoleHeader"
|
className="notificationConsoleHeader"
|
||||||
id="notificationConsoleHeader"
|
id="notificationConsoleHeader"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onKeyDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -24,7 +21,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="In progress items"
|
alt="in progress items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -37,7 +34,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Error items"
|
alt="error items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -50,7 +47,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Info items"
|
alt="info items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -74,11 +71,14 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
aria-expanded={true}
|
||||||
|
aria-label="console button collapsed"
|
||||||
className="expandCollapseButton"
|
className="expandCollapseButton"
|
||||||
data-test="NotificationConsole/ExpandCollapseButton"
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Expand icon"
|
alt="ChevronUpIcon"
|
||||||
src=""
|
src=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,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"
|
||||||
@@ -175,13 +174,10 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
className="notificationConsoleContainer"
|
className="notificationConsoleContainer"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-expanded={false}
|
|
||||||
aria-label="Console"
|
|
||||||
className="notificationConsoleHeader"
|
className="notificationConsoleHeader"
|
||||||
id="notificationConsoleHeader"
|
id="notificationConsoleHeader"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onKeyDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -194,7 +190,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="In progress items"
|
alt="in progress items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -207,7 +203,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Error items"
|
alt="error items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -220,7 +216,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Info items"
|
alt="info items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -246,11 +242,14 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
aria-expanded={true}
|
||||||
|
aria-label="console button collapsed"
|
||||||
className="expandCollapseButton"
|
className="expandCollapseButton"
|
||||||
data-test="NotificationConsole/ExpandCollapseButton"
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Expand icon"
|
alt="ChevronUpIcon"
|
||||||
src=""
|
src=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -281,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"
|
||||||
|
|||||||
@@ -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));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 {
|
||||||
if (LocalStorageUtility.hasItem(StorageKey.MostRecentActivity)) {
|
private static readonly schemaVersion: string = "2";
|
||||||
const oldDataSchemaVersion: string = "2";
|
private static itemsMaxNumber: number = 5;
|
||||||
const rawData = LocalStorageUtility.getEntryString(StorageKey.MostRecentActivity);
|
private storedData: StoredData;
|
||||||
if (rawData) {
|
constructor() {
|
||||||
const oldData = JSON.parse(rawData);
|
// Retrieve from local storage
|
||||||
if (oldData.schemaVersion === oldDataSchemaVersion) {
|
if (LocalStorageUtility.hasItem(StorageKey.MostRecentActivity)) {
|
||||||
const itemsMap: Record<string, Item[]> = oldData.itemsMap;
|
const rawData = LocalStorageUtility.getEntryString(StorageKey.MostRecentActivity);
|
||||||
Object.keys(itemsMap).forEach((accountId: string) => {
|
|
||||||
const accountName = accountId.split("/").pop();
|
if (!rawData) {
|
||||||
if (accountName) {
|
this.storedData = MostRecentActivity.createEmptyData();
|
||||||
saveState(
|
} else {
|
||||||
{
|
try {
|
||||||
componentName: AppStateComponentNames.MostRecentActivity,
|
this.storedData = JSON.parse(rawData);
|
||||||
globalAccountName: accountName,
|
} catch (e) {
|
||||||
},
|
console.error("Unable to parse stored most recent activity. Use empty data:", rawData);
|
||||||
itemsMap[accountId].map((item) => {
|
this.storedData = MostRecentActivity.createEmptyData();
|
||||||
if ((item.type as unknown as number) === 0) {
|
}
|
||||||
item.type = Type.OpenCollection;
|
|
||||||
} else if ((item.type as unknown as number) === 1) {
|
// If version doesn't match or schema broke, nuke it!
|
||||||
item.type = Type.OpenNotebook;
|
if (
|
||||||
}
|
!this.storedData.hasOwnProperty("schemaVersion") ||
|
||||||
return item;
|
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);
|
||||||
|
}
|
||||||
|
// Don't save if empty
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
// if (!accountId) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Remove duplicate
|
||||||
|
MostRecentActivity.removeDuplicate(newItem, this.storedData.itemsMap[accountId]);
|
||||||
|
|
||||||
|
this.storedData.itemsMap[accountId] = this.storedData.itemsMap[accountId] || [];
|
||||||
|
this.storedData.itemsMap[accountId].unshift(newItem);
|
||||||
|
this.cleanupItems(accountId);
|
||||||
|
this.saveToLocalStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getItems(accountId: string): Item[] {
|
||||||
|
return this.storedData.itemsMap[accountId] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public collectionWasOpened(accountId: string, { id, databaseId }: Pick<CollectionBase, "id" | "databaseId">) {
|
||||||
|
const collectionId = id();
|
||||||
|
this.addItem(accountId, {
|
||||||
|
type: Type.OpenCollection,
|
||||||
|
databaseId,
|
||||||
|
collectionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public notebookWasItemOpened(accountId: string, { name, path }: Pick<NotebookContentItem, "name" | "path">) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = -1;
|
||||||
|
for (let i = 0; i < itemsArray.length; i++) {
|
||||||
|
const currentItem = itemsArray[i];
|
||||||
|
if (JSON.stringify(currentItem) === JSON.stringify(item)) {
|
||||||
|
index = i;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove old data
|
if (index !== -1) {
|
||||||
LocalStorageUtility.removeEntry(StorageKey.MostRecentActivity);
|
itemsArray.splice(index, 1);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addItem = (accountName: string, newItem: Item): void => {
|
|
||||||
// When debugging, accountId is "undefined": most recent activity cannot be saved by account. Uncomment to disable.
|
|
||||||
// if (!accountId) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
let items =
|
|
||||||
(loadState({
|
|
||||||
componentName: AppStateComponentNames.MostRecentActivity,
|
|
||||||
globalAccountName: accountName,
|
|
||||||
}) as Item[]) || [];
|
|
||||||
|
|
||||||
// Remove duplicate
|
|
||||||
items = removeDuplicate(newItem, items);
|
|
||||||
|
|
||||||
items.unshift(newItem);
|
|
||||||
items = cleanupItems(items, accountName);
|
|
||||||
saveState(
|
|
||||||
{
|
|
||||||
componentName: AppStateComponentNames.MostRecentActivity,
|
|
||||||
globalAccountName: accountName,
|
|
||||||
},
|
|
||||||
items,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getItems = (accountName: string): Item[] => {
|
|
||||||
if (!accountName) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
(loadState({
|
|
||||||
componentName: AppStateComponentNames.MostRecentActivity,
|
|
||||||
globalAccountName: accountName,
|
|
||||||
}) as Item[]) || []
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const collectionWasOpened = (
|
|
||||||
accountName: string,
|
|
||||||
{ id, databaseId }: Pick<CollectionBase, "id" | "databaseId">,
|
|
||||||
) => {
|
|
||||||
if (accountName === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const collectionId = id();
|
|
||||||
addItem(accountName, {
|
|
||||||
type: Type.OpenCollection,
|
|
||||||
databaseId,
|
|
||||||
collectionId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const clear = (accountName: string): void => {
|
|
||||||
if (!accountName) {
|
|
||||||
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;
|
|
||||||
for (let i = 0; i < result.length; i++) {
|
|
||||||
const currentItem = result[i];
|
|
||||||
|
|
||||||
if (
|
|
||||||
JSON.stringify(sortObjectKeys(currentItem as unknown as Record<string, unknown>)) ===
|
|
||||||
JSON.stringify(sortObjectKeys(item as unknown as Record<string, unknown>))
|
|
||||||
) {
|
|
||||||
index = i;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index !== -1) {
|
/**
|
||||||
result.splice(index, 1);
|
* Remove unknown types
|
||||||
|
* Limit items to max number
|
||||||
|
*/
|
||||||
|
private cleanupItems(accountId: string): void {
|
||||||
|
if (!this.storedData.itemsMap.hasOwnProperty(accountId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemsArray = this.storedData.itemsMap[accountId]
|
||||||
|
.filter((item) => item.type in Type)
|
||||||
|
.slice(0, MostRecentActivity.itemsMaxNumber);
|
||||||
|
if (itemsArray.length === 0) {
|
||||||
|
delete this.storedData.itemsMap[accountId];
|
||||||
|
} else {
|
||||||
|
this.storedData.itemsMap[accountId] = itemsArray;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
export const mostRecentActivity = new MostRecentActivity();
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove unknown types
|
|
||||||
* Limit items to max number
|
|
||||||
* Modifies the array.
|
|
||||||
*/
|
|
||||||
const cleanupItems = (items: Item[], accountName: string): Item[] => {
|
|
||||||
if (accountName === undefined) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemsArray = items.filter((item) => item.type in Type).slice(0, itemsMaxNumber);
|
|
||||||
if (itemsArray.length === 0) {
|
|
||||||
deleteState({
|
|
||||||
componentName: AppStateComponentNames.MostRecentActivity,
|
|
||||||
globalAccountName: accountName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return itemsArray;
|
|
||||||
};
|
|
||||||
|
|
||||||
migrateOldData();
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Notebook container related stuff
|
* Notebook container related stuff
|
||||||
*/
|
*/
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
import promiseRetry, { AbortError, Options } from "p-retry";
|
import promiseRetry, { AbortError } from "p-retry";
|
||||||
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook, PoolIdType } from "../../Common/Constants";
|
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook, PoolIdType } from "../../Common/Constants";
|
||||||
@@ -19,7 +19,7 @@ export class NotebookContainerClient {
|
|||||||
private clearReconnectionAttemptMessage? = () => {};
|
private clearReconnectionAttemptMessage? = () => {};
|
||||||
private isResettingWorkspace: boolean;
|
private isResettingWorkspace: boolean;
|
||||||
private phoenixClient: PhoenixClient;
|
private phoenixClient: PhoenixClient;
|
||||||
private retryOptions: Options;
|
private retryOptions: promiseRetry.Options;
|
||||||
private scheduleTimerId: NodeJS.Timeout;
|
private scheduleTimerId: NodeJS.Timeout;
|
||||||
|
|
||||||
constructor(private onConnectionLost: () => void) {
|
constructor(private onConnectionLost: () => void) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user