Compare commits

..

25 Commits

Author SHA1 Message Date
Senthamil Sindhu
05f2bf8360 Run npm lint 2024-07-08 13:40:25 -07:00
Senthamil Sindhu
23e81b5216 Fix Quickstart issue 2024-07-08 13:31:36 -07:00
Senthamil Sindhu
ab5239df09 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-07-08 13:27:24 -07:00
Senthamil Sindhu
3e48393fbb Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-07-03 00:07:17 -07:00
Senthamil Sindhu
0079a9147f Resolved merge conflict 2024-07-01 16:22:04 -07:00
Senthamil Sindhu
912688dc14 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-06-27 11:00:31 -07:00
Senthamil Sindhu
8849526fab Merge branch 'add-dp-rbac' of https://github.com/Azure/cosmos-explorer 2024-06-19 15:20:20 -07:00
Senthamil Sindhu
24af64a66d Add additional changes for Portal RBAC functionality 2024-06-19 15:05:14 -07:00
Senthamil Sindhu
be871737ad Support data plane RBAC 2024-06-14 12:45:21 -07:00
Senthamil Sindhu
4d8bb5c3ea Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-06-14 12:18:14 -07:00
Senthamil Sindhu
10a8505b9a Support data plane RBAC 2024-06-14 12:12:30 -07:00
Senthamil Sindhu
ef7c2fe2f7 Remove dev endpoint 2024-04-10 11:59:57 -07:00
Senthamil Sindhu
4c7aca95e1 Merge branch 'users/aisayas/mp-cp-activate-prod' of https://github.com/Azure/cosmos-explorer into users/sindhuba/activate-prod 2024-04-09 12:27:51 -07:00
Senthamil Sindhu
2243ad895a Remove prod endpoint 2024-04-09 12:16:13 -07:00
Senthamil Sindhu
b2d5f91fe1 Remove prod 2024-04-09 11:22:17 -07:00
Asier Isayas
a712193477 fix pr check tests 2024-04-09 11:43:24 -04:00
Senthamil Sindhu
5ee411693c Add prod endpoint 2024-04-09 08:41:47 -07:00
Asier Isayas
16c7b2567b fix bug that blocked local mongo proxy and cassandra proxy development 2024-04-09 11:39:11 -04:00
Senthamil Sindhu
78d9a0cd8d Revert code 2024-04-08 16:20:40 -07:00
Senthamil Sindhu
c6ad538559 Run npm format and tests 2024-04-08 15:58:10 -07:00
Senthamil Sindhu
2bc09a6efe Add CP Prod endpoint 2024-04-08 15:37:19 -07:00
Senthamil Sindhu
d3a3033b25 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-04-08 15:32:50 -07:00
Asier Isayas
6bdc714e11 activate Mongo Proxy and Cassandra Proxy in Prod 2024-04-08 16:52:09 -04:00
Senthamil Sindhu
5042f28229 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2024-03-25 15:11:53 -07:00
Senthamil Sindhu
e1430fd06f Fix API endpoint for CassandraProxy query API 2024-03-18 10:25:17 -07:00
456 changed files with 37021 additions and 82369 deletions

View File

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

View File

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

View File

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

View File

@@ -23,6 +23,8 @@ src/Common/MongoUtility.ts
src/Common/NotificationsClientBase.ts src/Common/NotificationsClientBase.ts
src/Common/QueriesClient.ts src/Common/QueriesClient.ts
src/Common/Splitter.ts src/Common/Splitter.ts
src/Controls/Heatmap/Heatmap.test.ts
src/Controls/Heatmap/Heatmap.ts
src/Definitions/datatables.d.ts src/Definitions/datatables.d.ts
src/Definitions/gif.d.ts src/Definitions/gif.d.ts
src/Definitions/globals.d.ts src/Definitions/globals.d.ts

View File

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

View File

@@ -83,7 +83,7 @@ jobs:
- run: npm ci - run: npm ci
- run: npm run build:contracts - run: npm run build:contracts
- name: Restore Build Cache - name: Restore Build Cache
uses: actions/cache@v4 uses: actions/cache@v2
with: with:
path: .cache path: .cache
key: ${{ runner.os }}-build-cache key: ${{ runner.os }}-build-cache
@@ -92,20 +92,18 @@ jobs:
NODE_OPTIONS: "--max-old-space-size=4096" NODE_OPTIONS: "--max-old-space-size=4096"
- run: cp -r ./Contracts ./dist/contracts - run: cp -r ./Contracts ./dist/contracts
- run: cp -r ./configs ./dist/configs - run: cp -r ./configs ./dist/configs
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v3
with: with:
name: dist name: dist
path: dist/ path: dist/
- name: "Az CLI login"
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.PREVIEW_SUBSCRIPTION_ID }}
- name: Upload build to preview blob storage - name: Upload build to preview blob storage
run: az storage blob upload-batch -d '$web' -s 'dist' --account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --auth-mode login --overwrite true run: az storage blob upload-batch -d '$web' -s 'dist' --account-name cosmosexplorerpreview --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --account-key="${PREVIEW_STORAGE_KEY}" --overwrite true
env:
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
- name: Upload preview config to blob storage - name: Upload preview config to blob storage
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --auth-mode login --overwrite true run: az storage blob upload -c '$web' -f ./preview/config.json --account-name cosmosexplorerpreview --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}" --overwrite true
env:
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
nuget: nuget:
name: Publish Nuget name: Publish Nuget
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
@@ -115,21 +113,21 @@ jobs:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }} AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
steps: steps:
- uses: nuget/setup-nuget@v1
with:
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
- name: Download Dist Folder - name: Download Dist Folder
uses: actions/download-artifact@v4 uses: actions/download-artifact@v3
with: with:
name: dist name: dist
- run: cp ./configs/prod.json config.json - run: cp ./configs/prod.json config.json
- run: dotnet nuget add source "$NUGET_SOURCE" --name "ADO" --username "jawelton@microsoft.com" --password "$AZURE_DEVOPS_PAT" --store-password-in-clear-text - run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
- run: dotnet pack DataExplorer.proj /p:PackageVersion="2.0.0-github-${GITHUB_SHA}" - run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
- run: dotnet nuget push "bin/Release/*.nupkg" --skip-duplicate --api-key Az --source="$NUGET_SOURCE" - run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
- run: dotnet nuget remove source "ADO" - uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4 name: packages
name: Upload package to Artifacts
with: with:
name: prod-package path: "*.nupkg"
path: "bin/Release/*.nupkg"
nugetmpac: nugetmpac:
name: Publish Nuget MPAC name: Publish Nuget MPAC
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
@@ -139,21 +137,22 @@ jobs:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }} AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
steps: steps:
- uses: nuget/setup-nuget@v1
with:
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
- name: Download Dist Folder - name: Download Dist Folder
uses: actions/download-artifact@v4 uses: actions/download-artifact@v3
with: with:
name: dist name: dist
- run: cp ./configs/mpac.json config.json - run: cp ./configs/mpac.json config.json
- run: sed -i 's/Azure.Cosmos.DB.Data.Explorer/Azure.Cosmos.DB.Data.Explorer.MPAC/g' DataExplorer.nuspec - run: sed -i 's/Azure.Cosmos.DB.Data.Explorer/Azure.Cosmos.DB.Data.Explorer.MPAC/g' DataExplorer.nuspec
- run: dotnet nuget add source "$NUGET_SOURCE" --name "ADO" --username "jawelton@microsoft.com" --password "$AZURE_DEVOPS_PAT" --store-password-in-clear-text - run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
- run: dotnet pack DataExplorer.proj /p:PackageVersion="2.0.0-github-${GITHUB_SHA}" - run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
- run: dotnet nuget push "bin/Release/*.nupkg" --skip-duplicate --api-key Az --source="$NUGET_SOURCE" - run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
- run: dotnet nuget remove source "ADO" - uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4 name: packages
name: Upload package to Artifacts
with: with:
name: mpac-package path: "*.nupkg"
path: "bin/Release/*.nupkg"
playwright-tests: playwright-tests:
name: "Run Playwright Tests (Shard ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }})" name: "Run Playwright Tests (Shard ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }})"
@@ -164,49 +163,31 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
shardTotal: [16] shardTotal: [8]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: "Az CLI login"
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Use Node.js 18.x - name: Use Node.js 18.x
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 18.x node-version: 18.x
- run: npm ci - run: npm ci
- run: npx playwright install --with-deps - run: npx playwright install --with-deps
- name: "Az CLI login"
uses: Azure/login@v2
with:
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
# We can't use MSAL within playwright so we acquire tokens prior to running the tests
- name: "Acquire RBAC tokens for test accounts"
uses: azure/cli@v2
with:
azcliversion: latest
inlineScript: |
NOSQL_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_TOKEN"
echo NOSQL_TESTACCOUNT_TOKEN=$NOSQL_TESTACCOUNT_TOKEN >> $GITHUB_ENV
NOSQL_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-readonly.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_READONLY_TESTACCOUNT_TOKEN"
echo NOSQL_READONLY_TESTACCOUNT_TOKEN=$NOSQL_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
TABLE_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-tables.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$TABLE_TESTACCOUNT_TOKEN"
echo TABLE_TESTACCOUNT_TOKEN=$TABLE_TESTACCOUNT_TOKEN >> $GITHUB_ENV
GREMLIN_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-gremlin.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$GREMLIN_TESTACCOUNT_TOKEN"
echo GREMLIN_TESTACCOUNT_TOKEN=$GREMLIN_TESTACCOUNT_TOKEN >> $GITHUB_ENV
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}} - name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3 run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
- name: Upload blob report to GitHub Actions Artifacts - name: Upload blob report to GitHub Actions Artifacts
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"
@@ -216,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

View File

@@ -27,7 +27,7 @@ jobs:
- name: "Az CLI login" - name: "Az CLI login"
uses: azure/login@v1 uses: azure/login@v1
with: with:
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }} client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

5
.npmrc
View File

@@ -1,4 +1 @@
save-exact=true save-exact=true
# Ignore peer dependency conflicts
force=true # TODO: Remove this when we update to React 17 or higher!

View File

@@ -1,9 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<NoBuild>true</NoBuild>
<IncludeBuildOutput>false</IncludeBuildOutput>
<NuspecFile>DataExplorer.nuspec</NuspecFile>
<NuspecProperties>version=$(PackageVersion)</NuspecProperties>
</PropertyGroup>
</Project>

View File

@@ -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

7
canvas/README.md Normal file
View File

@@ -0,0 +1,7 @@
# Why?
This adds a mock module for `canvas`. Nteract has a ignored require and undeclared dependency on this module. `cavnas` is a server side node module and is not used in browser side code for nteract.
Installing it locally (`npm install canvas`) will resolve the problem, but it is a native module so it is flaky depending on the system, node version, processor arch, etc. This module provides a simpler, more robust solution.
Remove this workaround if [this bug](https://github.com/nteract/any-vega/issues/2) ever gets resolved

1
canvas/index.js Normal file
View File

@@ -0,0 +1 @@
module.exports = {}

11
canvas/package.json Normal file
View File

@@ -0,0 +1,11 @@
{
"name": "canvas",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none">
<path fill="#8CC5E7" d="M21.4679537,3.20617761 C22.1814672,4.67953668 20.0131274,4.83706564 20.1243243,5.49498069 C20.3281853,6.68108108 20.1891892,8.44169884 20.0316602,10.1745174 C19.7629344,13.1119691 21.9590734,20.1451737 17.3814672,22.9714286 C16.5196911,23.5088803 14.4718147,23.8054054 12.4517375,23.8517375 C12.4517375,23.8517375 12.442471,23.8517375 12.442471,23.8517375 C12.442471,23.8517375 12.4332046,23.8517375 12.4332046,23.8517375 C10.4131274,23.8054054 8.08725869,23.5088803 7.22548263,22.9714286 C2.65714286,20.1451737 4.85328185,13.1119691 4.59382239,10.1745174 C4.42702703,8.44169884 4.28803089,6.68108108 4.5011583,5.49498069 C4.61235521,4.83706564 2.44401544,4.68880309 3.15752896,3.20617761 C3.76911197,1.93667954 5.27953668,3.05791506 5.65945946,2.65945946 C7.596139,0.648648649 9.94980695,0.111196911 11.8030888,0.0648648649 C11.988417,0.0648648649 12.8223938,0.0648648649 12.8223938,0.0648648649 C14.6664093,0.157528958 17.0200772,0.657915058 18.9660232,2.65945946 C19.3459459,3.05791506 20.8471042,1.93667954 21.4679537,3.20617761 Z M11.4324324,10.9065637 C11.3490347,10.9436293 11.2100386,11.8517375 11.6362934,11.8980695 C11.9235521,11.9258687 12.7111969,12.0185328 12.8965251,11.8980695 C13.2579151,11.6664093 13.2208494,11.1104247 13.0169884,10.9714286 C12.6741313,10.7490347 11.5250965,10.8602317 11.4324324,10.9065637 Z M9.07876448,4.10501931 C8.12432432,3.99382239 6.52123552,4.88339768 6.28030888,6.77374517 C6.02084942,8.73822394 8.33745174,10.6841699 10.56139,8.73822394 C11.7567568,7.69111969 12.1737452,4.46640927 9.07876448,4.10501931 Z M15.5281853,4.10501931 C12.4332046,4.46640927 12.8501931,7.69111969 14.0455598,8.73822394 C16.2694981,10.6841699 18.5861004,8.73822394 18.3266409,6.77374517 C18.0949807,4.88339768 16.4918919,3.99382239 15.5281853,4.10501931 Z"/>
<path fill="#B8937F" d="M12.3127413,8.98841699 C12.8965251,8.90501931 14.2957529,9.57220077 14.2030888,10.3598456 C14.0918919,11.2772201 10.5984556,11.3976834 10.4131274,10.3042471 C10.3019305,9.63706564 10.8301158,9.21081081 12.3127413,8.98841699 Z M20.1984556,16.3737452 C19.9111969,16.3644788 19.7258687,15.984556 19.7258687,15.7528958 C19.7258687,15.3359073 19.7814672,14.8447876 20.0872587,14.6316602 C20.7173745,14.196139 21.2177606,16.3830116 20.1984556,16.3737452 Z M4.41776062,16.3737452 C3.3984556,16.3830116 3.8988417,14.196139 4.52895753,14.6316602 C4.83474903,14.8447876 4.89034749,15.3359073 4.89034749,15.7528958 C4.89034749,15.984556 4.70501931,16.3644788 4.41776062,16.3737452 Z M18.2617761,23.0918919 C18.4471042,23.3606178 18.4563707,23.5459459 18.1598456,23.6849421 C17.0293436,24.203861 16.019305,23.5088803 16.3992278,23.3142857 C17.2054054,22.9065637 17.7057915,22.2671815 18.2617761,23.0918919 Z M6.35444015,23.184556 C6.91042471,22.3598456 7.41081081,22.9992278 8.21698842,23.4069498 C8.5969112,23.6015444 7.58687259,24.2965251 6.45637066,23.7776062 C6.15984556,23.63861 6.16911197,23.4532819 6.35444015,23.184556 Z"/>
<path fill="#000000" d="M19.7351351,3.42857143 C19.7814672,3.23397683 20.2633205,3.14131274 20.5320463,3.47490347 C20.8563707,3.87335907 20.0594595,4.42007722 20.0223938,4.1976834 C19.9297297,3.5953668 19.6795367,3.62316602 19.7351351,3.42857143 Z M4.88108108,3.42857143 C4.93667954,3.62316602 4.68648649,3.5953668 4.59382239,4.1976834 C4.55675676,4.42007722 3.75984556,3.87335907 4.08416988,3.47490347 C4.34362934,3.14131274 4.82548263,3.23397683 4.88108108,3.42857143 Z M15.7413127,7.94131274 C15.1578953,7.94131274 14.6849421,7.46835949 14.6849421,6.88494208 C14.6849421,6.30152468 15.1578953,5.82857143 15.7413127,5.82857143 C16.3247301,5.82857143 16.7976834,6.30152468 16.7976834,6.88494208 C16.7976834,7.46835949 16.3247301,7.94131274 15.7413127,7.94131274 Z M15.4633205,6.76447876 C15.6475575,6.76447876 15.7969112,6.61512511 15.7969112,6.43088803 C15.7969112,6.24665096 15.6475575,6.0972973 15.4633205,6.0972973 C15.2790834,6.0972973 15.1297297,6.24665096 15.1297297,6.43088803 C15.1297297,6.61512511 15.2790834,6.76447876 15.4633205,6.76447876 Z M11.3583012,9.43320463 C11.4694981,9.00694981 11.8586873,8.86795367 12.1737452,8.85868726 C12.9799228,8.84015444 13.2857143,9.27567568 13.3135135,9.61853282 C13.369112,10.2023166 11.1081081,10.3413127 11.3583012,9.43320463 Z M8.87490347,7.94131274 C8.29148607,7.94131274 7.81853282,7.46835949 7.81853282,6.88494208 C7.81853282,6.30152468 8.29148607,5.82857143 8.87490347,5.82857143 C9.45832088,5.82857143 9.93127413,6.30152468 9.93127413,6.88494208 C9.93127413,7.46835949 9.45832088,7.94131274 8.87490347,7.94131274 Z M9.15289575,6.76447876 C9.33713283,6.76447876 9.48648649,6.61512511 9.48648649,6.43088803 C9.48648649,6.24665096 9.33713283,6.0972973 9.15289575,6.0972973 C8.96865868,6.0972973 8.81930502,6.24665096 8.81930502,6.43088803 C8.81930502,6.61512511 8.96865868,6.76447876 9.15289575,6.76447876 Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<path d="M38.9437824,35.879008 C89.5234256,-13.1200214 170.398168,-11.8028432 219.397197,39.0402357 C224.929346,31.6640377 229.671187,23.4975328 233.095851,15.0675923 C249.165425,64.0666217 258.912543,105.162582 255.224444,137.038295 C253.380395,163.90873 242.842969,189.725423 225.456217,210.273403 C180.145286,264.014274 99.53398,270.863601 45.7931091,225.55267 L45.7931091,225.55267 L44.765,224.638 L44.7103323,224.601984 C44.5420247,224.484832 44.376007,224.362668 44.2124952,224.235492 C43.7219599,223.853965 43.2765312,223.438607 42.8762093,222.995252 L42.732,222.831 L41.0512675,221.3377 C39.4121124,219.93271 37.7729573,218.52772 36.3188215,216.93771 L35.7825547,216.332423 C-13.2164747,165.752779 -11.6358609,84.8780374 38.9437824,35.879008 Z M57.9111486,207.375611 C53.169307,203.687512 46.3199803,204.214383 42.6318814,208.956225 C39.3888978,213.125775 39.4048731,218.924805 42.6798072,222.771269 L42.732,222.831 L44.765,224.638 L44.9644841,224.773953 C49.5691585,227.80174 55.7644273,227.175885 59.2982065,222.896387 L59.4917624,222.654878 C63.1798614,217.913037 62.3895545,211.06371 57.9111486,207.375611 Z M231.778672,28.2393744 C218.60689,55.9001168 185.940871,76.9749681 157.753257,83.5608592 C131.146257,89.8833146 107.963921,84.6146018 83.4644059,94.0982849 C27.6160498,115.436572 28.6697923,181.822354 59.2283268,196.838185 L59.2283268,196.838185 L61.0723763,197.891928 C61.0723763,197.891928 83.1456487,193.50309 104.973663,187.707242 L106.843514,187.207079 C115.561826,184.857554 124.138869,182.296538 131.146257,179.714869 C167.500376,166.279651 207.542593,133.08676 220.714375,94.6251562 C213.865049,134.667374 179.35498,173.392413 144.84491,191.042601 C126.404416,200.526284 112.178891,202.633769 81.883792,213.171195 C78.195693,214.488373 75.297901,215.805551 75.297901,215.805551 C75.6675607,215.754564 76.0372203,215.70481 76.4060145,215.65629 L77.1421925,215.560893 L77.1421925,215.560893 L77.8745239,215.468787 C84.5652297,214.639554 90.5771682,214.224938 90.5771682,214.224938 C133.517178,212.117452 200.956702,226.342977 232.305544,184.45671 C264.444692,141.780136 246.531068,72.7599979 231.778672,28.2393744 Z" fill="#6DB33F">
</path>
<path d="M57.9111486,207.375611 C62.3895545,211.06371 63.1798614,217.913037 59.4917624,222.654878 C55.8036635,227.39672 48.9543368,227.923591 44.2124952,224.235492 C39.4706537,220.547393 38.9437824,213.698066 42.6318814,208.956225 C46.3199803,204.214383 53.169307,203.687512 57.9111486,207.375611 Z M231.778672,28.2393744 C246.531068,72.7599979 264.444692,141.780136 232.305544,184.45671 C200.956702,226.342977 133.517178,212.117452 90.5771682,214.224938 C90.5771682,214.224938 84.5652297,214.639554 77.8745239,215.468787 L77.1421925,215.560893 C76.5300999,215.63902 75.9140004,215.720572 75.297901,215.805551 C75.297901,215.805551 78.195693,214.488373 81.883792,213.171195 C112.178891,202.633769 126.404416,200.526284 144.84491,191.042601 C179.35498,173.392413 213.865049,134.667374 220.714375,94.6251562 C207.542593,133.08676 167.500376,166.279651 131.146257,179.714869 C106.119871,188.935116 61.0723763,197.891928 61.0723763,197.891928 L59.2283268,196.838185 C28.6697923,181.822354 27.6160498,115.436572 83.4644059,94.0982849 C107.963921,84.6146018 131.146257,89.8833146 157.753257,83.5608592 C185.940871,76.9749681 218.60689,55.9001168 231.778672,28.2393744 Z" fill="#FFFFFF">
</path>

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -1 +0,0 @@
<svg width="15" height="15" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" overflow="hidden"><defs><clipPath id="clip0"><rect x="479" y="279" width="15" height="15"/></clipPath><clipPath id="clip1"><rect x="-0.287396" y="-0.171573" width="152381" height="152381"/></clipPath><image width="35" height="35" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAMAAAApB0NrAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAH4UExURQAAAASExCGs7CC//wB9vQB5uxSU1yaz8ySy9AB8uwB8vAB5uxOR1Say8yax8iaw8gB6vAB6uwB7uwB4uROQ1Cax8ySy8QCAvwB3ugB5ugB2uROP1CWw8yav8wCT2QCQ1QCP1wB2uQB1uBOO0yWv8yat8wCP1QCP1QCP1QCO1QCQ1wB1tQB2uAB3uAB1tx+k6SSt8ySt8QCN0wCO1ACM0wBzuQBztAB1twB0tgB0tiSr8SSs8gCM1ACM1ACEywBwtQBxtAB0tgBztQB0sySp8SSr8gCL0wCK0gCL0wCHzwByuABusgBztQBttiOq8gB6wQCI0QCJ0gB7wySn8SOo8gBxtABtsABwtgCFzwCI0gCG0QCJ0SKn8SKn8iKl8QBvsABvsgBvsQBtsABssgCCzACG0QCF0ACDzyKm8QBtrwBusABsrgCAzQCE0ACF0ACF0ACE0CKj8SKl8QBssQBtrwBtrwBsrgBsrwCK1QCBzwCD0ACA0B2d7CGj8QBsrABrrQBwrwCA3wCC0ACCzwCBzwB+zhGM3iGi8SKh8QCAzQCAzgCAzwB9zRCK3iCh8SGh8SCf8QB+zAB/zgB8zRCJ3SCg8SCf7wB+zQB6zBCI3CCe8CCf8CCe8CCf8SCf/wB7zAB4yxGJ3h6c8CCd7yCf7wmA0RqX60C//5CaUeMAAACodFJOUwA8XAho+//ncID/////53iM//////9wCKv/////gCiYILf///+AMPP/70wYw/+3lP+AXP+MKNv/+3uA/3z/+////+NAgP9c9/////+7HP+A///MgP9Y+////7scgP+AeP/////7/+NA/2D/iyTb//t4gP808//vUBjD/7eU/yifIAi3//////+Ar////////4CI/////3B//////+9/CGj7/+dwEDhYBCm1XqwAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAFpSURBVDhPY2AYBaiAkYkZXQgdsLCysXPgV8XJxc3Dy8vHj0eVgKCQsIioqKioGLoMDIhLSEpKSknLyMjIyKLLyckrgJUoCgsLCyspq6ioqKiiKVFT19DUYmDQ1tEFAT19AwMDA0M0NUbGxsbGJqZm5ubm5haWDFbW1tbWVmhqGGxsbW1t7ewdHB2dnBkYXFxdXV1d0NUwuLl7eHh4enn7+DIwMLj4+fn5Yaph8A8IDAwMDBIHsYNDQkJCgtFVMISGhUdERkZGRkUzMDDExMbGxsahK4lPSExKTklNTU1NS2dgiMvIyMhAV5OZlZWVlZ2Tm5eXl5dfwFBYVFRUVIimpriktKycgaGisgoEqmtqa2tr0dUw1NU3gKjGpuaWlpbWtvb29vYOdDUw0NjZ1dXd09vX39c3AV0OASZOmjR5ytSpU6dOQ5dBAtN7ZsycNXvO3HnoEshg/oKFixYvQRdFA0uXLUcXGqEAAH4FV0z+qQbjAAAAAElFTkSuQmCC" preserveAspectRatio="none" id="img2"></image><clipPath id="clip3"><path d="M44291.4 46947.4 187148 46947.4 187148 188823 44291.4 188823Z" fill-rule="evenodd" clip-rule="evenodd"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-479 -279)"><g clip-path="url(#clip1)" transform="matrix(0.000105 0 0 0.000105 479 279)"><g clip-path="url(#clip3)" transform="matrix(1 0 0 1.00692 -44291.4 -47272.4)"><use width="100%" height="100%" xlink:href="#img2" transform="scale(6709.45 6709.45)"></use></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -80,7 +80,6 @@ module.exports = {
"d3-quadtree": "<rootDir>/node_modules/d3-quadtree/dist/d3-quadtree.min.js", "d3-quadtree": "<rootDir>/node_modules/d3-quadtree/dist/d3-quadtree.min.js",
"d3-scale-chromatic": "<rootDir>/node_modules/d3-scale-chromatic/dist/d3-scale-chromatic.min.js", "d3-scale-chromatic": "<rootDir>/node_modules/d3-scale-chromatic/dist/d3-scale-chromatic.min.js",
"d3-zoom": "<rootDir>/node_modules/d3-zoom/dist/d3-zoom.min.js", "d3-zoom": "<rootDir>/node_modules/d3-zoom/dist/d3-zoom.min.js",
uuid: require.resolve("uuid"), // Force module uuid to resolve with the CJS entry point, because Jest does not support package.json.exports. See https://github.com/uuidjs/uuid/issues/451
}, },
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
@@ -134,7 +133,7 @@ module.exports = {
snapshotSerializers: ["enzyme-to-json/serializer"], snapshotSerializers: ["enzyme-to-json/serializer"],
// The test environment that will be used for testing // The test environment that will be used for testing
testEnvironment: "jsdom", // testEnvironment: "jest-environment-jsdom",
modulePaths: ["node_modules", "<rootDir>/src"], modulePaths: ["node_modules", "<rootDir>/src"],
// Options that will be passed to the testEnvironment // Options that will be passed to the testEnvironment
@@ -158,7 +157,7 @@ module.exports = {
// testResultsProcessor: "./trxProcessor.js", // testResultsProcessor: "./trxProcessor.js",
// This option allows use of a custom test runner // This option allows use of a custom test runner
testRunner: "jest-circus/runner", // testRunner: "jasmine2",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// testURL: "http://localhost", // testURL: "http://localhost",
@@ -168,17 +167,13 @@ module.exports = {
// A map from regular expressions to paths to transformers // A map from regular expressions to paths to transformers
transform: { transform: {
"^.+\\.html?$": "jest-html-loader", "^.+\\.html?$": "html-loader-jest",
"^.+\\.[t|j]sx?$": "babel-jest", "^.+\\.[t|j]sx?$": "babel-jest",
"^.+\\.svg$": "<rootDir>/jest/svgTransform.js", "^.+\\.svg$": "<rootDir>/svgTransform.js",
}, },
// 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,
@@ -191,7 +186,4 @@ module.exports = {
// Whether to use watchman for file crawling // Whether to use watchman for file crawling
// watchman: true, // watchman: true,
// TODO: toMatchInlineSnapshot() does not work with prettier 3. Remove when fixed: https://github.com/jestjs/jest/issues/14305
prettierPath: null,
}; };

View File

@@ -61,8 +61,6 @@
@GalleryBackgroundColor: #fdfdfd; @GalleryBackgroundColor: #fdfdfd;
@LinkColor: #2d6da4;
//Icons //Icons
@InfoIconColor: #0072c6; @InfoIconColor: #0072c6;
@WarningIconColor: #db7500; @WarningIconColor: #db7500;
@@ -170,7 +168,7 @@
@FabricBoxBorderRadius: 8px; @FabricBoxBorderRadius: 8px;
@FabricBoxBorderShadow: rgba(0, 0, 0, 0.133) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px 0px; @FabricBoxBorderShadow: rgba(0, 0, 0, 0.133) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px 0px;
@FabricBoxMargin: 4px 8px 4px 8px; @FabricBoxMargin: 4px 3px 4px 3px;
@FabricAccentMediumHigh: #0c695a; @FabricAccentMediumHigh: #0c695a;
@FabricAccentMedium: #117865; @FabricAccentMedium: #117865;
@@ -248,10 +246,6 @@
outline: 1px dashed @FocusColor; outline: 1px dashed @FocusColor;
} }
.focusedBorder() {
border: 1px dashed @FocusColor;
}
/************************************************************************************************ /************************************************************************************************
Common Toggle Switch Common Toggle Switch
*************************************************************************************************/ *************************************************************************************************/

View File

@@ -1830,14 +1830,6 @@ input::-webkit-calendar-picker-indicator::after {
transform: rotate(90deg); transform: rotate(90deg);
} }
.customAccordion button:focus {
.focus();
}
.customAccordion {
margin-top: 1px;
}
.datalist-arrow:after:hover { .datalist-arrow:after:hover {
content: "\276F"; content: "\276F";
position: absolute; position: absolute;
@@ -1914,21 +1906,8 @@ input::-webkit-calendar-picker-indicator::after {
} }
.nav-tabs-margin { .nav-tabs-margin {
padding-top: 8px;
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 {
@@ -2095,6 +2074,14 @@ a:link {
display: inline; display: inline;
} }
.resourceTreeAndTabs {
display: flex;
flex: 1 1 auto;
overflow-x: clip;
overflow-y: auto;
height: 100%;
}
.collectiontitle { .collectiontitle {
font-size: 14px; font-size: 14px;
text-transform: uppercase; text-transform: uppercase;
@@ -2338,6 +2325,11 @@ td a:hover {
outline: 1px dotted; outline: 1px dotted;
} }
#content.active .tabdocuments .scrollable {
height: 100%;
overflow-y: auto;
}
.table-fixed thead { .table-fixed thead {
width: 97%; width: 97%;
padding-left: 18px; padding-left: 18px;
@@ -2373,9 +2365,10 @@ a:link {
.tabsManagerContainer { .tabsManagerContainer {
height: 100%; height: 100%;
flex-grow: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0; // This prevents it to grow past the parent's width if its content is too wide min-height: 300px;
} }
.tabs { .tabs {
@@ -2586,6 +2579,18 @@ a:link {
cursor: pointer; cursor: pointer;
} }
.documentsTab {
.documentsTable {
.documentsTableCell {
border-left: 1px solid @BaseMedium;
height: 100%;
}
.documentsTableHeader {
border-bottom: 1px solid @BaseMedium;
}
}
}
.querydropdown { .querydropdown {
border: 1px solid @BaseMedium; border: 1px solid @BaseMedium;
font-style: normal; font-style: normal;
@@ -2632,7 +2637,7 @@ a:link {
.tabPanesContainer { .tabPanesContainer {
display: flex; display: flex;
flex-grow: 1; height: 100%;
overflow: hidden; overflow: hidden;
} }
@@ -2869,7 +2874,6 @@ a:link {
z-index: 1000; z-index: 1000;
overflow-y: auto; overflow-y: auto;
overflow-x: clip; overflow-x: clip;
min-height: fit-content;
} }
.uniqueIndexesContainer { .uniqueIndexesContainer {
@@ -3133,7 +3137,3 @@ a:link {
background: white; background: white;
height: 100%; height: 100%;
} }
.sidebarContainer {
height: 100%;
}

View File

@@ -20,10 +20,6 @@ a:focus {
text-decoration: underline; text-decoration: underline;
} }
.splashLoaderContainer {
background-color: #f5f5f5;
}
#divExplorer { #divExplorer {
background-color: #f5f5f5; background-color: #f5f5f5;
} }
@@ -31,24 +27,26 @@ a:focus {
.resourceTreeAndTabs { .resourceTreeAndTabs {
border-radius: 0px; border-radius: 0px;
box-shadow: @FabricBoxBorderShadow; box-shadow: @FabricBoxBorderShadow;
margin: @FabricBoxMargin;
margin-top: 0px; margin-top: 0px;
margin-bottom: 0px; margin-bottom: 0px;
background-color: #ffffff; background-color: #ffffff;
} }
.tabsManagerContainer { .tabsManagerContainer {
background-color: #ffffff; background-color: #ffffff
} }
.nav-tabs-margin { .nav-tabs-margin {
padding-top: 5px; padding-top: 8px;
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%);
} }
@@ -210,13 +216,4 @@ a:focus {
.fileImportImg img { .fileImportImg img {
filter: brightness(0) saturate(100%); filter: brightness(0) saturate(100%);
} }
.tabPanesContainer {
overflow: auto !important;
}
.tabs-container {
min-height: 500px;
min-width: 500px;
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,19 @@
.dataResourceTree { .dataResourceTree {
margin-left: @MediumSpace; margin-left: @MediumSpace;
overflow: auto; overflow: auto;
.databaseHeader {
padding: 1px;
font-size: 14px;
}
.collectionHeader {
font-size: 12px;
}
.loadMoreHeader {
color: RGB(5, 99, 193);
}
} }
.notebookResourceTree { .notebookResourceTree {

20550
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,20 +5,21 @@
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@azure/arm-cosmosdb": "9.1.0", "@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "4.5.0", "@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",
"@fluentui/react": "8.119.0", "@fluentui/react": "8.112.1",
"@fluentui/react-components": "9.54.2", "@fluentui/react-components": "9.34.0",
"@jupyterlab/services": "6.0.2", "@jupyterlab/services": "6.0.2",
"@jupyterlab/terminal": "3.0.3", "@jupyterlab/terminal": "3.0.3",
"@microsoft/applicationinsights-web": "2.6.1", "@microsoft/applicationinsights-web": "2.6.1",
"@nteract/commutable": "7.5.1", "@nteract/commutable": "7.5.1",
"@nteract/connected-components": "6.8.2", "@nteract/connected-components": "6.8.2",
"@nteract/core": "15.1.9", "@nteract/core": "15.1.0",
"@nteract/data-explorer": "8.0.3", "@nteract/data-explorer": "8.0.3",
"@nteract/directory-listing": "2.0.6", "@nteract/directory-listing": "2.0.6",
"@nteract/dropdown-menu": "1.0.1", "@nteract/dropdown-menu": "1.0.1",
@@ -41,17 +42,15 @@
"@nteract/transform-vega": "7.0.6", "@nteract/transform-vega": "7.0.6",
"@octokit/rest": "17.9.2", "@octokit/rest": "17.9.2",
"@phosphor/widgets": "1.9.3", "@phosphor/widgets": "1.9.3",
"@testing-library/jest-dom": "6.4.6", "@testing-library/jest-dom": "5.11.9",
"@types/lodash": "4.14.171", "@types/lodash": "4.14.171",
"@types/mkdirp": "1.0.1", "@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.5.7", "@types/node-fetch": "2.5.7",
"@uiw/react-split": "5.9.3",
"@xmldom/xmldom": "0.7.13", "@xmldom/xmldom": "0.7.13",
"@xterm/xterm": "5.5.0",
"@xterm/addon-fit": "0.10.0",
"allotment": "1.20.2",
"applicationinsights": "1.8.0", "applicationinsights": "1.8.0",
"bootstrap": "3.4.1", "bootstrap": "3.4.1",
"canvas": "2.11.2", "canvas": "file:./canvas",
"clean-webpack-plugin": "4.0.0", "clean-webpack-plugin": "4.0.0",
"clipboard-copy": "4.0.1", "clipboard-copy": "4.0.1",
"copy-webpack-plugin": "11.0.0", "copy-webpack-plugin": "11.0.0",
@@ -68,7 +67,7 @@
"eslint-plugin-react": "7.33.2", "eslint-plugin-react": "7.33.2",
"hasher": "1.2.0", "hasher": "1.2.0",
"html2canvas": "1.0.0-rc.5", "html2canvas": "1.0.0-rc.5",
"i18next": "23.11.5", "i18next": "19.8.4",
"i18next-browser-languagedetector": "6.0.1", "i18next-browser-languagedetector": "6.0.1",
"i18next-http-backend": "1.0.23", "i18next-http-backend": "1.0.23",
"iframe-resizer-react": "1.1.0", "iframe-resizer-react": "1.1.0",
@@ -83,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",
@@ -94,13 +93,13 @@
"react-dnd-html5-backend": "14.0.0", "react-dnd-html5-backend": "14.0.0",
"react-dom": "16.14.0", "react-dom": "16.14.0",
"react-hotkeys": "2.0.0", "react-hotkeys": "2.0.0",
"react-i18next": "14.1.2", "react-i18next": "11.8.5",
"react-notification-system": "0.2.17", "react-notification-system": "0.2.17",
"react-redux": "7.1.3", "react-redux": "7.1.3",
"react-splitter-layout": "4.0.0", "react-splitter-layout": "4.0.0",
"react-string-format": "1.0.1", "react-string-format": "1.0.1",
"react-window": "1.8.10",
"react-youtube": "9.0.1", "react-youtube": "9.0.1",
"react-window": "1.8.10",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"rx-jupyter": "5.5.12", "rx-jupyter": "5.5.12",
"sanitize-html": "2.3.3", "sanitize-html": "2.3.3",
@@ -114,11 +113,11 @@
"zustand": "3.5.0" "zustand": "3.5.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.24.7", "@babel/core": "7.9.0",
"@babel/preset-env": "7.24.7", "@babel/preset-env": "7.9.0",
"@babel/preset-react": "7.24.7", "@babel/preset-react": "7.9.4",
"@babel/preset-typescript": "7.24.7", "@babel/preset-typescript": "7.9.0",
"@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",
@@ -130,13 +129,13 @@
"@types/enzyme": "3.10.12", "@types/enzyme": "3.10.12",
"@types/enzyme-adapter-react-16": "1.0.9", "@types/enzyme-adapter-react-16": "1.0.9",
"@types/hasher": "0.0.31", "@types/hasher": "0.0.31",
"@types/jest": "29.5.12", "@types/jest": "26.0.20",
"@types/jquery": "3.5.29", "@types/jquery": "3.5.29",
"@types/node": "12.11.1", "@types/node": "12.11.1",
"@types/post-robot": "10.0.1", "@types/post-robot": "10.0.1",
"@types/q": "1.5.1", "@types/q": "1.5.1",
"@types/react": "17.0.44", "@types/react": "17.0.3",
"@types/react-dom": "17.0.15", "@types/react-dom": "17.0.3",
"@types/react-notification-system": "0.2.39", "@types/react-notification-system": "0.2.39",
"@types/react-redux": "7.1.7", "@types/react-redux": "7.1.7",
"@types/react-splitter-layout": "3.0.1", "@types/react-splitter-layout": "3.0.1",
@@ -149,7 +148,7 @@
"@typescript-eslint/eslint-plugin": "6.7.4", "@typescript-eslint/eslint-plugin": "6.7.4",
"@typescript-eslint/parser": "6.7.4", "@typescript-eslint/parser": "6.7.4",
"@webpack-cli/serve": "2.0.5", "@webpack-cli/serve": "2.0.5",
"babel-jest": "29.7.0", "babel-jest": "24.9.0",
"babel-loader": "8.1.0", "babel-loader": "8.1.0",
"buffer": "5.1.0", "buffer": "5.1.0",
"case-sensitive-paths-webpack-plugin": "2.4.0", "case-sensitive-paths-webpack-plugin": "2.4.0",
@@ -166,15 +165,13 @@
"fast-glob": "3.2.5", "fast-glob": "3.2.5",
"fs-extra": "7.0.0", "fs-extra": "7.0.0",
"html-inline-css-webpack-plugin": "1.11.2", "html-inline-css-webpack-plugin": "1.11.2",
"html-loader": "5.0.0", "html-loader": "0.5.5",
"html-loader-jest": "0.2.1",
"html-webpack-plugin": "5.5.3", "html-webpack-plugin": "5.5.3",
"jest": "29.7.0", "jest": "26.6.3",
"jest-canvas-mock": "2.5.2", "jest-canvas-mock": "2.3.1",
"jest-circus": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jest-html-loader": "1.0.0",
"jest-react-hooks-shallow": "1.5.1", "jest-react-hooks-shallow": "1.5.1",
"jest-trx-results-processor": "3.0.2", "jest-trx-results-processor": "0.0.7",
"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",
@@ -190,8 +187,8 @@
"sinon": "3.2.1", "sinon": "3.2.1",
"style-loader": "0.23.0", "style-loader": "0.23.0",
"ts-loader": "9.2.4", "ts-loader": "9.2.4",
"typedoc": "0.26.2", "typedoc": "0.22.15",
"typescript": "4.9.5", "typescript": "4.3.5",
"url-loader": "4.1.1", "url-loader": "4.1.1",
"wait-on": "4.0.2", "wait-on": "4.0.2",
"webpack": "5.88.2", "webpack": "5.88.2",

View File

@@ -1,13 +0,0 @@
diff --git a/node_modules/@phosphor/virtualdom/lib/index.d.ts b/node_modules/@phosphor/virtualdom/lib/index.d.ts
index 95682b9..73e2daa 100644
--- a/node_modules/@phosphor/virtualdom/lib/index.d.ts
+++ b/node_modules/@phosphor/virtualdom/lib/index.d.ts
@@ -58,7 +58,7 @@ export declare type ElementEventMap = {
ondrop: DragEvent;
ondurationchange: Event;
onemptied: Event;
- onended: MediaStreamErrorEvent;
+ onended: ErrorEvent;
onerror: ErrorEvent;
onfocus: FocusEvent;
oninput: Event;

View File

@@ -0,0 +1,11 @@
diff --git a/node_modules/@uiw/react-split/cjs/index.d.ts b/node_modules/@uiw/react-split/cjs/index.d.ts
index 644bcc3..f794760 100644
--- a/node_modules/@uiw/react-split/cjs/index.d.ts
+++ b/node_modules/@uiw/react-split/cjs/index.d.ts
@@ -56,5 +56,5 @@ export default class Split extends React.Component<SplitProps, SplitState> {
onMouseDown(paneNumber: number, env: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
onDragging(env: Event): void;
onDragEnd(): void;
- render(): import("react/jsx-runtime").JSX.Element;
+ render(): JSX.Element;
}

View File

@@ -1,93 +1,51 @@
import { defineConfig, devices } from "@playwright/test"; import { defineConfig, devices } from '@playwright/test';
/** /**
* See https://playwright.dev/docs/test-configuration. * See https://playwright.dev/docs/test-configuration.
*/ */
export default defineConfig({ export default defineConfig({
testDir: "test", testDir: 'test',
fullyParallel: true, fullyParallel: true,
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
retries: process.env.CI ? 3 : 0, retries: process.env.CI ? 3 : 0,
workers: process.env.CI ? 1 : undefined, workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI ? "blob" : "html", reporter: process.env.CI ? 'blob' : 'html',
timeout: 10 * 60 * 1000, timeout: 10 * 60 * 1000,
use: { use: {
trace: "off", actionTimeout: 5 * 60 * 1000,
video: "off", trace: 'off',
screenshot: "on", video: 'off',
testIdAttribute: "data-test", screenshot: 'on',
testIdAttribute: 'data-test',
contextOptions: { contextOptions: {
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
}, },
}, },
expect: { expect: {
// Many of our expectations take a little longer than the default 5 seconds. timeout: 5 * 60 * 1000,
timeout: 15 * 1000,
}, },
projects: [ projects: [
{ {
name: "chromium", name: 'chromium',
use: { use: { ...devices['Desktop Chrome'] },
...devices["Desktop Chrome"],
launchOptions: {
args: ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"],
},
},
}, },
{ {
name: "firefox", name: 'firefox',
use: { use: { ...devices['Desktop Firefox'] },
...devices["Desktop Firefox"],
launchOptions: {
firefoxUserPrefs: {
"security.fileuri.strict_origin_policy": false,
"network.http.referer.XOriginPolicy": 0,
"network.http.referer.trimmingPolicy": 0,
"privacy.file_unique_origin": false,
"security.csp.enable": false,
"network.cors_preflight.allow_client_cert": true,
"dom.security.https_first": false,
"network.http.cross-origin-embedder-policy": false,
"network.http.cross-origin-opener-policy": false,
"browser.tabs.remote.useCrossOriginPolicy": false,
"browser.tabs.remote.useCORP": false,
},
args: ["--disable-web-security"],
},
},
}, },
{ {
name: "webkit", name: 'webkit',
use: { use: { ...devices['Desktop Safari'] },
...devices["Desktop Safari"],
},
},
{
name: "Google Chrome",
use: {
...devices["Desktop Chrome"],
channel: "chrome",
launchOptions: {
args: ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"],
},
},
},
{
name: "Microsoft Edge",
use: {
...devices["Desktop Edge"],
channel: "msedge",
launchOptions: {
args: ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"],
},
},
}, },
], ],
webServer: { webServer: {
command: "npm run start", command: 'npm run start',
url: "https://127.0.0.1:1234/_ready", url: 'https://127.0.0.1:1234/_ready',
timeout: 120 * 1000, timeout: 120 * 1000,
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,

View File

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

View File

@@ -4,8 +4,8 @@ Cosmos Explorer Preview makes it possible to try a working version of any commit
Initial support is for Hosted (Connection string only) or the Azure Portal. Examples: Initial support is for Hosted (Connection string only) or the Azure Portal. Examples:
Connection string URLs: https://dataexplorer-preview.azurewebsites.net/commit/COMMIT_SHA/hostedExplorer.html Connection string URLs: https://cosmos-explorer-preview.azurewebsites.net/commit/COMMIT_SHA/hostedExplorer.html
Portal URLs: https://ms.portal.azure.com/?dataExplorerSource=https://dataexplorer-preview.azurewebsites.net/commit/COMMIT_SHA/explorer.html#home Portal URLs: https://ms.portal.azure.com/?dataExplorerSource=https://cosmos-explorer-preview.azurewebsites.net/commit/COMMIT_SHA/explorer.html#home
In both cases replace `COMMIT_SHA` with the commit you want to view. It must have already completed its build on GitHub Actions. In both cases replace `COMMIT_SHA` with the commit you want to view. It must have already completed its build on GitHub Actions.

View File

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

View File

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

37133
preview/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,55 @@
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
import { getApiShortDisplayName } from "../Utils/APITypeUtils";
import { NormalizedEventKey } from "./Constants";
export interface CollapsedResourceTreeProps {
toggleLeftPaneExpanded: () => void;
isLeftPaneExpanded: boolean;
}
export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps> = ({
toggleLeftPaneExpanded,
isLeftPaneExpanded,
}: CollapsedResourceTreeProps): JSX.Element => {
const focusButton = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
useEffect(() => {
if (focusButton.current) {
focusButton.current.focus();
}
});
const onKeyPressToggleLeftPaneExpanded = (event: React.KeyboardEvent) => {
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
toggleLeftPaneExpanded();
event.stopPropagation();
}
};
return (
<div id="mini" className={!isLeftPaneExpanded ? "mini toggle-mini" : "hiddenMain"}>
<div className="main-nav nav">
<ul className="nav">
<li
className="resourceTreeCollapse"
id="collapseToggleLeftPaneButton"
role="button"
tabIndex={0}
aria-label={getApiShortDisplayName() + `Expand tree`}
onClick={toggleLeftPaneExpanded}
onKeyPress={onKeyPressToggleLeftPaneExpanded}
ref={focusButton}
>
<span className="leftarrowCollapsed">
<img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" />
</span>
<span className="collectionCollapsed">
<span>{getApiShortDisplayName()}</span>
</span>
</li>
</ul>
</div>
</div>
);
};

View File

@@ -89,7 +89,6 @@ export class CapabilityNames {
public static readonly EnableMongo: string = "EnableMongo"; public static readonly EnableMongo: string = "EnableMongo";
public static readonly EnableServerless: string = "EnableServerless"; public static readonly EnableServerless: string = "EnableServerless";
public static readonly EnableNoSQLVectorSearch: string = "EnableNoSQLVectorSearch"; public static readonly EnableNoSQLVectorSearch: string = "EnableNoSQLVectorSearch";
public static readonly EnableNoSQLFullTextSearch: string = "EnableNoSQLFullTextSearch";
} }
export enum CapacityMode { export enum CapacityMode {
@@ -97,12 +96,6 @@ export enum CapacityMode {
Serverless = "Serverless", Serverless = "Serverless",
} }
export enum WorkloadType {
Learning = "Learning",
DevelopmentTesting = "Development/Testing",
Production = "Production",
None = "None",
}
// flight names returned from the portal are always lowercase // flight names returned from the portal are always lowercase
export class Flights { export class Flights {
public static readonly SettingsV2 = "settingsv2"; public static readonly SettingsV2 = "settingsv2";
@@ -125,7 +118,6 @@ export class AfecFeatures {
export class TagNames { export class TagNames {
public static defaultExperience: string = "defaultExperience"; public static defaultExperience: string = "defaultExperience";
public static WorkloadType: string = "hidden-workload-type";
} }
export class MongoDBAccounts { export class MongoDBAccounts {
@@ -138,6 +130,12 @@ export enum MongoBackendEndpointType {
remote, remote,
} }
export class BackendApi {
public static readonly GenerateToken: string = "GenerateToken";
public static readonly PortalSettings: string = "PortalSettings";
public static readonly AccountRestrictions: string = "AccountRestrictions";
}
export class PortalBackendEndpoints { export class PortalBackendEndpoints {
public static readonly Development: string = "https://localhost:7235"; public static readonly Development: string = "https://localhost:7235";
public static readonly Mpac: string = "https://cdb-ms-mpac-pbe.cosmos.azure.com"; public static readonly Mpac: string = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
@@ -147,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";
@@ -197,15 +183,12 @@ 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";
public static setAutomaticRBACOption: string = "Automatic";
public static setTrueRBACOption: string = "True";
public static setFalseRBACOption: string = "False";
public static itemsPerPage: number = 100; public static itemsPerPage: number = 100;
public static unlimitedItemsPerPage: number = 100; // TODO: Figure out appropriate value so it works for accounts with a large number of partitions public static unlimitedItemsPerPage: number = 100; // TODO: Figure out appropriate value so it works for accounts with a large number of partitions
public static containersPerPage: number = 50; public static containersPerPage: number = 50;
@@ -248,7 +231,6 @@ export class Areas {
public static ShareDialog: string = "Share Access Dialog"; public static ShareDialog: string = "Share Access Dialog";
public static Notebook: string = "Notebook"; public static Notebook: string = "Notebook";
public static Copilot: string = "Copilot"; public static Copilot: string = "Copilot";
public static CloudShell: string = "Cloud Shell";
} }
export class HttpHeaders { export class HttpHeaders {
@@ -305,7 +287,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;
@@ -517,19 +498,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 GlobalSecondaryIndexLabels {
public static readonly NewGlobalSecondaryIndex: string = "New Global Secondary Index";
}
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 = {
@@ -765,10 +734,3 @@ export const ShortenedQueryCopilotSampleContainerSchema = {
userPrompt: "find all products", userPrompt: "find all products",
}; };
export enum MongoGuidRepresentation {
Standard = "Standard",
CSharpLegacy = "CSharpLegacy",
JavaLegacy = "JavaLegacy",
PythonLegacy = "PythonLegacy",
}

View File

@@ -1,7 +1,34 @@
import { PortalBackendEndpoints } from "Common/Constants"; import { ResourceType } from "@azure/cosmos";
import { configContext, Platform, resetConfigContext, updateConfigContext } from "../ConfigContext"; import { Platform, resetConfigContext, updateConfigContext } from "../ConfigContext";
import { updateUserContext } from "../UserContext"; import { updateUserContext } from "../UserContext";
import { endpoint, getTokenFromAuthService, requestPlugin } from "./CosmosClient"; import { endpoint, getTokenFromAuthService, requestPlugin, tokenProvider } from "./CosmosClient";
describe("tokenProvider", () => {
const options = {
verb: "GET" as any,
path: "/",
resourceId: "",
resourceType: "dbs" as ResourceType,
headers: {},
getAuthorizationTokenUsingMasterKey: () => "",
};
beforeEach(() => {
updateConfigContext({
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
});
window.fetch = jest.fn().mockImplementation(() => {
return {
json: () => "{}",
headers: new Map(),
};
});
});
afterEach(() => {
jest.restoreAllMocks();
});
});
describe("getTokenFromAuthService", () => { describe("getTokenFromAuthService", () => {
beforeEach(() => { beforeEach(() => {
@@ -21,22 +48,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 +106,7 @@ describe("requestPlugin", () => {
const next = jest.fn(); const next = jest.fn();
updateConfigContext({ updateConfigContext({
platform: Platform.Hosted, platform: Platform.Hosted,
PORTAL_BACKEND_ENDPOINT: "https://localhost:1234", BACKEND_ENDPOINT: "https://localhost:1234",
PROXY_PATH: "/proxy", PROXY_PATH: "/proxy",
}); });
const headers = {}; const headers = {};

View File

@@ -1,15 +1,12 @@
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 { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
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 { 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";
@@ -20,19 +17,27 @@ 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;
if (useDataplaneRbacAuthorization(userContext)) { const aadDataPlaneFeatureEnabled =
Logger.logInfo( userContext.features.enableAadDataPlane && userContext.databaseAccount.properties.disableLocalAuth;
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `, const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && userContext.apiType === "SQL";
"Explorer/tokenProvider", if (aadDataPlaneFeatureEnabled || (!userContext.features.enableAadDataPlane && dataPlaneRBACOptionEnabled)) {
);
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 use "Login for Entra ID" prior to performing Entra ID RBAC operations`,
); );
return null; return null;
} }
const AUTH_PREFIX = `type=aad&ver=1.0&sig=`; const AUTH_PREFIX = `type=aad&ver=1.0&sig=`;
const authorizationToken = `${AUTH_PREFIX}${userContext.aadToken}`; const authorizationToken = `${AUTH_PREFIX}${userContext.aadToken}`;
console.log(`Returning Auth token`);
return authorizationToken;
}
if (userContext.dataPlaneRbacEnabled && userContext.authorizationToken) {
console.log(` Getting Portal Auth token `);
const AUTH_PREFIX = `type=aad&ver=1.0&sig=`;
const authorizationToken = `${AUTH_PREFIX}${userContext.authorizationToken}`;
console.log(`Returning Portal Auth token`);
return authorizationToken; return authorizationToken;
} }
@@ -42,7 +47,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:
@@ -54,13 +59,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:
@@ -71,9 +71,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 **************
@@ -91,7 +89,6 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
} }
if (userContext.masterKey) { if (userContext.masterKey) {
Logger.logInfo(`Master Key exists`, "Explorer/tokenProvider");
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK. // TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
await Cosmos.setAuthorizationTokenHeaderUsingMasterKey( await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(
verb, verb,
@@ -124,11 +121,7 @@ export const endpoint = () => {
const location = _global.parent ? _global.parent.location : _global.location; const location = _global.parent ? _global.parent.location : _global.location;
return configContext.EMULATOR_ENDPOINT || location.origin; return configContext.EMULATOR_ENDPOINT || location.origin;
} }
return ( return userContext.endpoint || userContext?.databaseAccount?.properties?.documentEndpoint;
userContext.selectedRegionalEndpoint ||
userContext.endpoint ||
userContext?.databaseAccount?.properties?.documentEndpoint
);
}; };
export async function getTokenFromAuthService( export async function getTokenFromAuthService(
@@ -137,8 +130,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",
@@ -150,7 +143,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)}`);
@@ -168,24 +162,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 ||
@@ -206,7 +189,6 @@ export function client(): Cosmos.CosmosClient {
userAgentSuffix: "Azure Portal", userAgentSuffix: "Azure Portal",
defaultHeaders: _defaultHeaders, defaultHeaders: _defaultHeaders,
connectionPolicy: { connectionPolicy: {
enableEndpointDiscovery: !userContext.selectedRegionalEndpoint,
retryOptions: { retryOptions: {
maxRetryAttemptCount: LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts), maxRetryAttemptCount: LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts),
fixedRetryIntervalInMilliseconds: LocalStorageUtility.getEntryNumber(StorageKey.RetryInterval), fixedRetryIntervalInMilliseconds: LocalStorageUtility.getEntryNumber(StorageKey.RetryInterval),

View File

@@ -1,34 +0,0 @@
import { WorkloadType } from "Common/Constants";
import { getWorkloadType } from "Common/DatabaseAccountUtility";
import { DatabaseAccount, Tags } from "Contracts/DataModels";
import { updateUserContext } from "UserContext";
describe("Database Account Utility", () => {
describe("Workload Type", () => {
beforeEach(() => {
updateUserContext({
databaseAccount: {
tags: {} as Tags,
} as DatabaseAccount,
});
});
it("Workload Type should return Learning", () => {
updateUserContext({
databaseAccount: {
tags: {
"hidden-workload-type": WorkloadType.Learning,
} as Tags,
} as DatabaseAccount,
});
const workloadType: WorkloadType = getWorkloadType();
expect(workloadType).toBe(WorkloadType.Learning);
});
it("Workload Type should return None", () => {
const workloadType: WorkloadType = getWorkloadType();
expect(workloadType).toBe(WorkloadType.None);
});
});
});

View File

@@ -1,6 +1,3 @@
import { TagNames, WorkloadType } from "Common/Constants";
import { Tags } from "Contracts/DataModels";
import { isFabric } from "Platform/Fabric/FabricUtil";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
function isVirtualNetworkFilterEnabled() { function isVirtualNetworkFilterEnabled() {
@@ -18,18 +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;
}
export function isGlobalSecondaryIndexEnabled(): boolean {
return (
!isFabric() && userContext.apiType === "SQL" && userContext.databaseAccount?.properties?.enableMaterializedViews
);
}

View File

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

View File

@@ -10,7 +10,6 @@ export interface TableEntityProps {
isEntityValueDisable?: boolean; isEntityValueDisable?: boolean;
entityTimeValue: string; entityTimeValue: string;
entityValueType: string; entityValueType: string;
entityProperty: string;
onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void; onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
onSelectDate: (date: Date | null | undefined) => void; onSelectDate: (date: Date | null | undefined) => void;
onEntityTimeValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void; onEntityTimeValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
@@ -27,7 +26,6 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
onSelectDate, onSelectDate,
isEntityValueDisable, isEntityValueDisable,
onEntityTimeValueChange, onEntityTimeValueChange,
entityProperty,
}: TableEntityProps): JSX.Element => { }: TableEntityProps): JSX.Element => {
if (isEntityTypeDate) { if (isEntityTypeDate) {
return ( return (
@@ -53,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}
/>
</>
); );
}; };

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,5 @@
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { QueryOperationOptions } from "@azure/cosmos";
import * as Constants from "../Common/Constants";
import { QueryResults } from "../Contracts/ViewModels"; import { QueryResults } from "../Contracts/ViewModels";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
interface QueryResponse { interface QueryResponse {
// [Todo] remove any // [Todo] remove any
@@ -13,15 +11,17 @@ interface QueryResponse {
} }
export interface MinimalQueryIterator { export interface MinimalQueryIterator {
fetchNext: () => Promise<QueryResponse>; fetchNext: (queryOperationOptions?: QueryOperationOptions) => Promise<QueryResponse>;
} }
// Pick<QueryIterator<any>, "fetchNext">; // Pick<QueryIterator<any>, "fetchNext">;
export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> { export function nextPage(
TelemetryProcessor.traceStart(Action.ExecuteQuery); documentsIterator: MinimalQueryIterator,
return documentsIterator.fetchNext().then((response) => { firstItemIndex: number,
TelemetryProcessor.traceSuccess(Action.ExecuteQuery, { dataExplorerArea: Constants.Areas.Tab }); queryOperationOptions?: QueryOperationOptions,
): Promise<QueryResults> {
return documentsIterator.fetchNext(queryOperationOptions).then((response) => {
const documents = response.resources; const documents = response.resources;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const headers = (response as any).headers || {}; // TODO this is a private key. Remove any const headers = (response as any).headers || {}; // TODO this is a private key. Remove any

View File

@@ -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,7 +71,7 @@ describe("MongoProxyClient", () => {
databaseAccount, databaseAccount,
}); });
updateConfigContext({ updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
}); });
window.fetch = jest.fn().mockImplementation(fetchMock); window.fetch = jest.fn().mockImplementation(fetchMock);
}); });
@@ -75,18 +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",
});
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),
); );
}); });
@@ -98,7 +103,7 @@ describe("MongoProxyClient", () => {
databaseAccount, databaseAccount,
}); });
updateConfigContext({ updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
}); });
window.fetch = jest.fn().mockImplementation(fetchMock); window.fetch = jest.fn().mockImplementation(fetchMock);
}); });
@@ -109,18 +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",
});
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),
); );
}); });
@@ -132,7 +135,7 @@ describe("MongoProxyClient", () => {
databaseAccount, databaseAccount,
}); });
updateConfigContext({ updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
}); });
window.fetch = jest.fn().mockImplementation(fetchMock); window.fetch = jest.fn().mockImplementation(fetchMock);
}); });
@@ -143,18 +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",
});
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),
); );
}); });
@@ -166,7 +167,7 @@ describe("MongoProxyClient", () => {
databaseAccount, databaseAccount,
}); });
updateConfigContext({ updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
}); });
window.fetch = jest.fn().mockImplementation(fetchMock); window.fetch = jest.fn().mockImplementation(fetchMock);
}); });
@@ -177,19 +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",
}); });
window.fetch = jest.fn().mockImplementation(fetchMock); window.fetch = jest.fn().mockImplementation(fetchMock);
}); });
@@ -198,20 +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);
});
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),
); );
}); });
@@ -223,13 +231,13 @@ describe("MongoProxyClient", () => {
databaseAccount, databaseAccount,
}); });
updateConfigContext({ updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
}); });
}); });
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", () => {
@@ -241,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");
}); });
}); });
}); });

View File

@@ -1,14 +1,20 @@
import { Constants as CosmosSDKConstants } from "@azure/cosmos"; import { Constants as CosmosSDKConstants } from "@azure/cosmos";
import { getMongoGuidRepresentation } from "Shared/StorageUtility"; 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";
@@ -61,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 = {
@@ -79,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,
@@ -117,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("/");
@@ -140,12 +215,9 @@ export function readDocument(
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
? documentId.partitionKeyProperties?.[0] ? documentId.partitionKeyProperties?.[0]
: "", : "",
clientSettings: {
guidRepresentation: getMongoGuidRepresentation(),
},
}; };
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT); const endpoint = getFeatureEndpointOrDefault("readDocument");
return window return window
.fetch(endpoint, { .fetch(endpoint, {
@@ -165,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 = {
@@ -185,12 +306,9 @@ export function createDocument(
partitionKey: partitionKey:
collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "", collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
documentContent: JSON.stringify(documentContent), documentContent: JSON.stringify(documentContent),
clientSettings: {
guidRepresentation: getMongoGuidRepresentation(),
},
}; };
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT); const endpoint = getFeatureEndpointOrDefault("createDocument");
return window return window
.fetch(`${endpoint}/createDocument`, { .fetch(`${endpoint}/createDocument`, {
@@ -210,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("/");
@@ -235,11 +395,8 @@ export function updateDocument(
? documentId.partitionKeyProperties?.[0] ? documentId.partitionKeyProperties?.[0]
: "", : "",
documentContent, documentContent,
clientSettings: {
guidRepresentation: getMongoGuidRepresentation(),
},
}; };
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT); const endpoint = getFeatureEndpointOrDefault("updateDocument");
return window return window
.fetch(endpoint, { .fetch(endpoint, {
@@ -260,38 +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,
clientSettings: { partitionKey:
guidRepresentation: getMongoGuidRepresentation(), 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: {
@@ -302,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];
@@ -332,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`, {
@@ -352,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";
@@ -365,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
@@ -380,14 +703,6 @@ async function errorHandling(response: Response, action: string, params: unknown
if (response.status === HttpStatusCodes.Forbidden) { if (response.status === HttpStatusCodes.Forbidden) {
sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage }); sendMessage({ type: MessageTypes.ForbiddenError, reason: errorMessage });
return; return;
} else if (
response.status === HttpStatusCodes.BadRequest &&
errorMessage.includes("Error=16500") &&
errorMessage.includes("RetryAfterMs=")
) {
// If throttling is happening, Cosmos DB will return a 400 with a body of:
// A write operation resulted in an error. Error=16500, RetryAfterMs=4, Details='Batch write error.
throw new ThrottlingError(errorMessage);
} }
throw new Error(errorMessage); throw new Error(errorMessage);
} }

View File

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

View File

@@ -1,113 +0,0 @@
import QueryError, { QueryErrorLocation, QueryErrorSeverity } from "Common/QueryError";
describe("QueryError.tryParse", () => {
const testErrorLocationResolver = ({ start, end }: { start: number; end: number }) =>
new QueryErrorLocation(
{ offset: start, lineNumber: start, column: start },
{ offset: end, lineNumber: end, column: end },
);
it("handles a string error", () => {
const error = "error";
const result = QueryError.tryParse(error, testErrorLocationResolver);
expect(result).toEqual([new QueryError("error", QueryErrorSeverity.Error)]);
});
it("handles an error object", () => {
const error = {
message: "error",
severity: "Warning",
location: { start: 0, end: 1 },
code: "code",
};
const result = QueryError.tryParse(error, testErrorLocationResolver);
expect(result).toEqual([
new QueryError(
"error",
QueryErrorSeverity.Warning,
"code",
new QueryErrorLocation({ offset: 0, lineNumber: 0, column: 0 }, { offset: 1, lineNumber: 1, column: 1 }),
),
]);
});
it("handles a JSON message without syntax errors", () => {
const innerError = {
code: "BadRequest",
message: "Your query is bad, and you should feel bad",
};
const message = `Message: ${JSON.stringify(innerError)}\r\nActivity ID: 42`;
const outerError = {
code: "BadRequest",
message,
};
const result = QueryError.tryParse(outerError, testErrorLocationResolver);
expect(result).toEqual([
new QueryError("Your query is bad, and you should feel bad", QueryErrorSeverity.Error, "BadRequest"),
]);
});
// Imitate the value coming from the backend, which has the syntax errors serialized as JSON in the message, along with a prefix and activity id.
it("handles single-nested error", () => {
const errors = [
{
message: "error1",
severity: "Warning",
location: { start: 0, end: 1 },
code: "code1",
},
{
message: "error2",
severity: "Error",
location: { start: 2, end: 3 },
code: "code2",
},
];
const innerError = {
code: "BadRequest",
message: "Your query is bad, and you should feel bad",
errors,
};
const message = `Message: ${JSON.stringify(innerError)}\r\nActivity ID: 42`;
const outerError = {
code: "BadRequest",
message,
};
const result = QueryError.tryParse(outerError, testErrorLocationResolver);
expect(result).toEqual([
new QueryError(
"error1",
QueryErrorSeverity.Warning,
"code1",
new QueryErrorLocation({ offset: 0, lineNumber: 0, column: 0 }, { offset: 1, lineNumber: 1, column: 1 }),
),
new QueryError(
"error2",
QueryErrorSeverity.Error,
"code2",
new QueryErrorLocation({ offset: 2, lineNumber: 2, column: 2 }, { offset: 3, lineNumber: 3, column: 3 }),
),
]);
});
// Imitate another value we've gotten from the backend, which has a doubly-nested JSON payload.
it("handles double-nested error", () => {
const outerError = {
code: "BadRequest",
message:
'{"code":"BadRequest","message":"{\\"errors\\":[{\\"severity\\":\\"Error\\",\\"location\\":{\\"start\\":7,\\"end\\":18},\\"code\\":\\"SC2005\\",\\"message\\":\\"\'nonexistent\' is not a recognized built-in function name.\\"}]}\\r\\nActivityId: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa, Windows/10.0.20348 cosmos-netstandard-sdk/3.18.0"}',
};
const result = QueryError.tryParse(outerError, testErrorLocationResolver);
expect(result).toEqual([
new QueryError(
"'nonexistent' is not a recognized built-in function name.",
QueryErrorSeverity.Error,
"SC2005",
new QueryErrorLocation({ offset: 7, lineNumber: 7, column: 7 }, { offset: 18, lineNumber: 18, column: 18 }),
),
]);
});
});

View File

@@ -1,247 +0,0 @@
import { monaco } from "Explorer/LazyMonaco";
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> = {};
const HELP_LINKS: Record<string, string> = {};
export default class QueryError {
message: string;
helpLink?: string;
constructor(
message: string,
public severity: QueryErrorSeverity,
public code?: string,
public location?: QueryErrorLocation,
helpLink?: string,
) {
// Automatically replace the message with a more Data Explorer-specific message if we have for this error code.
this.message = REPLACEMENT_MESSAGES[code] ? REPLACEMENT_MESSAGES[code](message) : message;
// Automatically set the help link if we have one for this error code.
this.helpLink = helpLink ?? HELP_LINKS[code];
}
getMonacoSeverity(): monaco.MarkerSeverity {
// It's very difficult to use the monaco.MarkerSeverity enum from here, so we'll just use the numbers directly.
// See: https://microsoft.github.io/monaco-editor/typedoc/enums/MarkerSeverity.html
switch (this.severity) {
case QueryErrorSeverity.Error:
return 8;
case QueryErrorSeverity.Warning:
return 4;
default:
return 2; // Info
}
}
/** Attempts to parse a query error from a string or object.
*
* @param error The error to parse.
* @returns An array of query errors if the error could be parsed, or null otherwise.
*/
static tryParse(
error: unknown,
locationResolver?: (location: { start: number; end: number }) => QueryErrorLocation,
): QueryError[] {
locationResolver =
locationResolver ||
(({ start, end }) => new QueryErrorLocation(new ErrorPosition(start), new ErrorPosition(end)));
const errors = QueryError.tryParseObject(error, locationResolver);
if (errors !== null) {
return errors;
}
const errorMessage = error as string;
// Map some well known messages to richer errors
const knownError = knownErrors[errorMessage];
if (knownError) {
return [knownError];
} else {
return [new QueryError(errorMessage, QueryErrorSeverity.Error)];
}
}
static read(
error: unknown,
locationResolver: (location: { start: number; end: number }) => QueryErrorLocation,
): QueryError | null {
if (typeof error !== "object" || error === null) {
return null;
}
const message = "message" in error && typeof error.message === "string" ? error.message : undefined;
if (!message) {
return null; // Invalid error (no message).
}
const severity =
"severity" in error && typeof error.severity === "string"
? (error.severity as QueryErrorSeverity)
: QueryErrorSeverity.Error;
const location =
"location" in error && typeof error.location === "object"
? locationResolver(error.location as { start: number; end: number })
: undefined;
const code = "code" in error && typeof error.code === "string" ? error.code : undefined;
return new QueryError(message, severity, code, location);
}
private static tryParseObject(
error: unknown,
locationResolver: (location: { start: number; end: number }) => QueryErrorLocation,
): QueryError[] | null {
let message: string | undefined;
if (typeof error === "object" && "message" in error && typeof error.message === "string") {
message = error.message;
} else {
// Unsupported error format.
return null;
}
// Some newer backends produce a message that contains a doubly-nested JSON payload.
// In this case, the message we get is a fully-complete JSON object we can parse.
// So let's try that first
if (message.startsWith("{") && message.endsWith("}")) {
let outer: unknown = undefined;
try {
outer = JSON.parse(message);
if (typeof outer === "object" && "message" in outer && typeof outer.message === "string") {
message = outer.message;
}
} catch (e) {
// Just continue if the parsing fails. We'll use the fallback logic below.
}
}
const lines = message.split("\n");
message = lines[0].trim();
if (message.startsWith("Message: ")) {
message = message.substring("Message: ".length);
}
let parsed: unknown;
try {
parsed = JSON.parse(message);
} catch (e) {
// The message doesn't contain a nested error.
return [QueryError.read(error, locationResolver)];
}
if (typeof parsed === "object") {
if ("errors" in parsed && Array.isArray(parsed.errors)) {
return parsed.errors.map((e) => QueryError.read(e, locationResolver)).filter((e) => e !== null);
}
return [QueryError.read(parsed, locationResolver)];
}
return null;
}
}
const knownErrors: Record<string, QueryError> = {
"User aborted query.": new QueryError("User aborted query.", QueryErrorSeverity.Warning),
};

View File

@@ -0,0 +1,82 @@
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
import refreshImg from "../../images/refresh-cosmos.svg";
import Explorer from "../Explorer/Explorer";
import { ResourceTree } from "../Explorer/Tree/ResourceTree";
import { userContext } from "../UserContext";
import { getApiShortDisplayName } from "../Utils/APITypeUtils";
import { NormalizedEventKey } from "./Constants";
export interface ResourceTreeContainerProps {
toggleLeftPaneExpanded: () => void;
isLeftPaneExpanded: boolean;
container: Explorer;
}
export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps> = ({
toggleLeftPaneExpanded,
isLeftPaneExpanded,
container,
}: ResourceTreeContainerProps): JSX.Element => {
const focusButton = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
useEffect(() => {
if (isLeftPaneExpanded) {
if (focusButton.current) {
focusButton.current.focus();
}
}
});
const onKeyPressToggleLeftPaneExpanded = (event: React.KeyboardEvent) => {
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
toggleLeftPaneExpanded();
event.stopPropagation();
}
};
return (
<div id="main" className={isLeftPaneExpanded ? "main" : "hiddenMain"}>
{/* Collections Window - - Start */}
<div id="mainslide" className="flexContainer">
{/* Collections Window Title/Command Bar - Start */}
<div className="collectiontitle">
<div className="coltitle">
<span className="titlepadcol">{getApiShortDisplayName()}</span>
<div className="float-right">
<span
className="padimgcolrefresh"
data-test="refreshTree"
role="button"
data-bind="click: onRefreshResourcesClick, clickBubble: false, event: { keypress: onRefreshDatabasesKeyPress }"
tabIndex={0}
aria-label={getApiShortDisplayName() + `Refresh tree`}
title="Refresh tree"
>
<img className="refreshcol" src={refreshImg} alt="Refresh Tree" />
</span>
<span
className="padimgcolrefresh1"
id="expandToggleLeftPaneButton"
role="button"
onClick={toggleLeftPaneExpanded}
onKeyPress={onKeyPressToggleLeftPaneExpanded}
tabIndex={0}
aria-label={getApiShortDisplayName() + `Collapse Tree`}
title="Collapse Tree"
ref={focusButton}
>
<img className="refreshcol1" src={arrowLeftImg} alt="Hide" />
</span>
</div>
</div>
</div>
{userContext.features.enableKoResourceTree ? (
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
) : (
<ResourceTree container={container} />
)}
</div>
{/* Collections Window - End */}
</div>
);
};

View File

@@ -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;

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`requestPlugin Emulator builds a url for emulator proxy via webpack 1`] = ` exports[`requestPlugin Emulator builds a url for emulator proxy via webpack 1`] = `
{ Object {
"endpoint": "http://localhost/proxy", "endpoint": "http://localhost/proxy",
"headers": { "headers": Object {
"x-ms-proxy-target": "http://localhost", "x-ms-proxy-target": "http://localhost",
}, },
"path": "/dbs/foo", "path": "/dbs/foo",
@@ -11,9 +11,9 @@ exports[`requestPlugin Emulator builds a url for emulator proxy via webpack 1`]
`; `;
exports[`requestPlugin Hosted builds a proxy URL in development 1`] = ` exports[`requestPlugin Hosted builds a proxy URL in development 1`] = `
{ Object {
"endpoint": "http://localhost/proxy", "endpoint": "http://localhost/proxy",
"headers": { "headers": Object {
"x-ms-proxy-target": "baz", "x-ms-proxy-target": "baz",
}, },
"path": "/dbs/foo", "path": "/dbs/foo",

View File

@@ -1,12 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`nextPage returns results for the next page 1`] = ` exports[`nextPage returns results for the next page 1`] = `
{ Object {
"activityId": "foo", "activityId": "foo",
"documents": [], "documents": Array [],
"firstItemIndex": 11, "firstItemIndex": 11,
"hasMoreResults": false, "hasMoreResults": false,
"headers": {}, "headers": Object {},
"itemCount": 0, "itemCount": 0,
"lastItemIndex": 10, "lastItemIndex": 10,
"requestCharge": 1, "requestCharge": 1,

View File

@@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getCommonQueryOptions builds the correct default options objects 1`] = ` exports[`getCommonQueryOptions builds the correct default options objects 1`] = `
{ Object {
"enableQueryControl": false, "disableNonStreamingOrderByQuery": true,
"enableScanInQuery": true, "enableScanInQuery": true,
"forceQueryPlan": true, "forceQueryPlan": true,
"maxDegreeOfParallelism": 0, "maxDegreeOfParallelism": 0,
@@ -12,8 +12,8 @@ exports[`getCommonQueryOptions builds the correct default options objects 1`] =
`; `;
exports[`getCommonQueryOptions reads from localStorage 1`] = ` exports[`getCommonQueryOptions reads from localStorage 1`] = `
{ Object {
"enableQueryControl": false, "disableNonStreamingOrderByQuery": true,
"enableScanInQuery": true, "enableScanInQuery": true,
"forceQueryPlan": true, "forceQueryPlan": true,
"maxDegreeOfParallelism": 17, "maxDegreeOfParallelism": 17,

View File

@@ -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: {
@@ -273,8 +269,6 @@ const createCollectionWithSDK = async (params: DataModels.CreateCollectionParams
indexingPolicy: params.indexingPolicy || undefined, indexingPolicy: params.indexingPolicy || undefined,
uniqueKeyPolicy: params.uniqueKeyPolicy || undefined, uniqueKeyPolicy: params.uniqueKeyPolicy || undefined,
analyticalStorageTtl: params.analyticalStorageTtl, analyticalStorageTtl: params.analyticalStorageTtl,
vectorEmbeddingPolicy: params.vectorEmbeddingPolicy,
fullTextPolicy: params.fullTextPolicy,
} as ContainerRequest; // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed } as ContainerRequest; // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed
const collectionOptions: RequestOptions = {}; const collectionOptions: RequestOptions = {};
const createDatabaseBody: DatabaseRequest = { id: params.databaseId }; const createDatabaseBody: DatabaseRequest = { id: params.databaseId };

View File

@@ -4,7 +4,6 @@ import * as DataModels from "../../Contracts/DataModels";
import { useDatabases } from "../../Explorer/useDatabases"; import { useDatabases } from "../../Explorer/useDatabases";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { getDatabaseName } from "../../Utils/APITypeUtils"; import { getDatabaseName } from "../../Utils/APITypeUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { createUpdateCassandraKeyspace } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { createUpdateCassandraKeyspace } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { createUpdateGremlinDatabase } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; import { createUpdateGremlinDatabase } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
import { createUpdateMongoDBDatabase } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; import { createUpdateMongoDBDatabase } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
@@ -16,6 +15,7 @@ import {
MongoDBDatabaseCreateUpdateParameters, MongoDBDatabaseCreateUpdateParameters,
SqlDatabaseCreateUpdateParameters, SqlDatabaseCreateUpdateParameters,
} from "../../Utils/arm/generatedClients/cosmos/types"; } from "../../Utils/arm/generatedClients/cosmos/types";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils"; import { handleError } from "../ErrorHandlingUtils";
@@ -152,18 +152,8 @@ async function createDatabaseWithSDK(params: DataModels.CreateDatabaseParams): P
createBody.throughput = params.offerThroughput; createBody.throughput = params.offerThroughput;
} }
} }
let response: DatabaseResponse;
try { const response: DatabaseResponse = await client().databases.create(createBody);
response = await client().databases.create(createBody);
} catch (error) {
if (error.message.includes("Shared throughput database creation is not supported for serverless accounts")) {
createBody.maxThroughput = undefined;
createBody.throughput = undefined;
response = await client().databases.create(createBody);
} else {
throw error;
}
}
return response.resource; return response.resource;
} }

View File

@@ -1,74 +0,0 @@
import { constructRpOptions } from "Common/dataAccess/createCollection";
import { handleError } from "Common/ErrorHandlingUtils";
import { Collection, CreateMaterializedViewsParams as CreateGlobalSecondaryIndexParams } from "Contracts/DataModels";
import { userContext } from "UserContext";
import { createUpdateSqlContainer } from "Utils/arm/generatedClients/cosmos/sqlResources";
import {
CreateUpdateOptions,
SqlContainerResource,
SqlDatabaseCreateUpdateParameters,
} from "Utils/arm/generatedClients/cosmos/types";
import { logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils";
export const createGlobalSecondaryIndex = async (params: CreateGlobalSecondaryIndexParams): Promise<Collection> => {
const clearMessage = logConsoleProgress(
`Creating a new global secondary index ${params.materializedViewId} for database ${params.databaseId}`,
);
const options: CreateUpdateOptions = constructRpOptions(params);
const resource: SqlContainerResource = {
id: params.materializedViewId,
};
if (params.materializedViewDefinition) {
resource.materializedViewDefinition = params.materializedViewDefinition;
}
if (params.analyticalStorageTtl) {
resource.analyticalStorageTtl = params.analyticalStorageTtl;
}
if (params.indexingPolicy) {
resource.indexingPolicy = params.indexingPolicy;
}
if (params.partitionKey) {
resource.partitionKey = params.partitionKey;
}
if (params.uniqueKeyPolicy) {
resource.uniqueKeyPolicy = params.uniqueKeyPolicy;
}
if (params.vectorEmbeddingPolicy) {
resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy;
}
if (params.fullTextPolicy) {
resource.fullTextPolicy = params.fullTextPolicy;
}
const rpPayload: SqlDatabaseCreateUpdateParameters = {
properties: {
resource,
options,
},
};
try {
const createResponse = await createUpdateSqlContainer(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.materializedViewId,
rpPayload,
);
logConsoleInfo(`Successfully created global secondary index ${params.materializedViewId}`);
return createResponse && (createResponse.properties.resource as Collection);
} catch (error) {
handleError(
error,
"CreateGlobalSecondaryIndex",
`Error while creating global secondary index ${params.materializedViewId}`,
);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -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();

View File

@@ -26,24 +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[],
abortSignal: AbortSignal,
): 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());
@@ -66,21 +56,18 @@ export const deleteDocuments = async (
operationType: BulkOperationType.Delete, operationType: BulkOperationType.Delete,
})); }));
const promise = v2Container.items const promise = v2Container.items.bulk(operations).then((bulkResult) => {
.bulk(operations, undefined, { return documentIdsChunk.filter((_, index) => bulkResult[index].statusCode === 204);
abortSignal, });
})
.then((bulkResults) => {
return bulkResults.map((bulkResult, index) => {
const documentId = documentIdsChunk[index];
return { ...bulkResult, documentId };
});
});
promiseArray.push(promise); promiseArray.push(promise);
} }
const allResult = await Promise.all(promiseArray); const allResult = await Promise.all(promiseArray);
const flatAllResult = Array.prototype.concat.apply([], allResult); const flatAllResult = Array.prototype.concat.apply([], allResult);
logConsoleInfo(
`Successfully deleted ${getEntityName(flatAllResult.length > 1)}: ${flatAllResult.length} out of ${nbDocuments}`,
);
// TODO: handle case result.length != nbDocuments
return flatAllResult; return flatAllResult;
} catch (error) { } catch (error) {
handleError( handleError(

View File

@@ -1,4 +1,3 @@
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
@@ -42,7 +41,7 @@ interface MetricsResponse {
} }
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => { export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
if (userContext.authType !== AuthType.AAD || isFabricNative()) { if (userContext.authType !== AuthType.AAD) {
return undefined; return undefined;
} }

View File

@@ -1,4 +1,5 @@
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { Queries } from "../Constants"; import { Queries } from "../Constants";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
@@ -25,7 +26,7 @@ export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {
options.maxItemCount || options.maxItemCount ||
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) || (storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
Queries.itemsPerPage; Queries.itemsPerPage;
options.enableQueryControl = LocalStorageUtility.getEntryBoolean(StorageKey.QueryControlEnabled);
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism); options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
options.disableNonStreamingOrderByQuery = !isVectorSearchEnabled();
return options; return options;
}; };

View File

@@ -1,3 +1,4 @@
import { QueryOperationOptions } from "@azure/cosmos";
import { QueryResults } from "../../Contracts/ViewModels"; import { QueryResults } from "../../Contracts/ViewModels";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { getEntityName } from "../DocumentUtility"; import { getEntityName } from "../DocumentUtility";
@@ -8,12 +9,13 @@ export const queryDocumentsPage = async (
resourceName: string, resourceName: string,
documentsIterator: MinimalQueryIterator, documentsIterator: MinimalQueryIterator,
firstItemIndex: number, firstItemIndex: number,
queryOperationOptions?: QueryOperationOptions,
): Promise<QueryResults> => { ): Promise<QueryResults> => {
const entityName = getEntityName(); const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`); const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
try { try {
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex); const result: QueryResults = await nextPage(documentsIterator, firstItemIndex, queryOperationOptions);
const itemCount = (result.documents && result.documents.length) || 0; const itemCount = (result.documents && result.documents.length) || 0;
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`); logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
return result; return result;

View File

@@ -1,4 +1,3 @@
import { isFabric } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels"; import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
@@ -14,11 +13,6 @@ import { readOfferWithSDK } from "./readOfferWithSDK";
export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise<Offer> => { export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise<Offer> => {
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`); const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
if (isFabric()) {
// Not exposing offers in Fabric
return undefined;
}
try { try {
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
@@ -111,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,
@@ -122,7 +114,6 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
offerReplacePending: resource.offerReplacePending === "true", offerReplacePending: resource.offerReplacePending === "true",
instantMaximumThroughput, instantMaximumThroughput,
softAllowedMaximumThroughput, softAllowedMaximumThroughput,
throughputBuckets,
}; };
} }
@@ -134,7 +125,6 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
offerReplacePending: resource.offerReplacePending === "true", offerReplacePending: resource.offerReplacePending === "true",
instantMaximumThroughput, instantMaximumThroughput,
softAllowedMaximumThroughput, softAllowedMaximumThroughput,
throughputBuckets,
}; };
} }

View File

@@ -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);
} }

View File

@@ -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);
} }

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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;
}; };

View File

@@ -1,16 +1,23 @@
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
import { import {
BackendApi,
CassandraProxyEndpoints,
JunoEndpoints,
MongoProxyEndpoints,
PortalBackendEndpoints,
} from "Common/Constants";
import {
allowedAadEndpoints,
allowedArcadiaEndpoints, allowedArcadiaEndpoints,
allowedCassandraProxyEndpoints,
allowedEmulatorEndpoints, allowedEmulatorEndpoints,
allowedGraphEndpoints,
allowedHostedExplorerEndpoints, allowedHostedExplorerEndpoints,
allowedJunoOrigins, allowedJunoOrigins,
allowedMongoBackendEndpoints,
allowedMongoProxyEndpoints,
allowedMsalRedirectEndpoints, allowedMsalRedirectEndpoints,
defaultAllowedAadEndpoints,
defaultAllowedArmEndpoints, defaultAllowedArmEndpoints,
defaultAllowedBackendEndpoints, defaultAllowedBackendEndpoints,
defaultAllowedCassandraProxyEndpoints,
defaultAllowedGraphEndpoints,
defaultAllowedMongoProxyEndpoints,
validateEndpoint, validateEndpoint,
} from "Utils/EndpointUtils"; } from "Utils/EndpointUtils";
@@ -23,18 +30,16 @@ export enum Platform {
export interface ConfigContext { export interface ConfigContext {
platform: Platform; platform: Platform;
allowedAadEndpoints: ReadonlyArray<string>;
allowedGraphEndpoints: ReadonlyArray<string>;
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;
AAD_ENDPOINT: string; AAD_ENDPOINT: string;
ARM_AUTH_AREA: string;
ARM_ENDPOINT: string; ARM_ENDPOINT: string;
EMULATOR_ENDPOINT?: string; EMULATOR_ENDPOINT?: string;
ARM_API_VERSION: string;
GRAPH_ENDPOINT: string; GRAPH_ENDPOINT: string;
GRAPH_API_VERSION: string; GRAPH_API_VERSION: string;
// This is the endpoint to get offering Ids to be used to fetch prices. Refer to this doc: https://learn.microsoft.com/en-us/rest/api/marketplacecatalog/dataplane/skus/list?view=rest-marketplacecatalog-dataplane-2023-05-01-preview&tabs=HTTP // This is the endpoint to get offering Ids to be used to fetch prices. Refer to this doc: https://learn.microsoft.com/en-us/rest/api/marketplacecatalog/dataplane/skus/list?view=rest-marketplacecatalog-dataplane-2023-05-01-preview&tabs=HTTP
@@ -43,14 +48,22 @@ 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;
MONGO_PROXY_ENDPOINT: string; PORTAL_BACKEND_ENDPOINT?: string;
CASSANDRA_PROXY_ENDPOINT: string; NEW_BACKEND_APIS?: BackendApi[];
MONGO_BACKEND_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[];
PROXY_PATH?: string; PROXY_PATH?: string;
JUNO_ENDPOINT: string; JUNO_ENDPOINT: string;
GITHUB_CLIENT_ID: string; GITHUB_CLIENT_ID: string;
GITHUB_TEST_ENV_CLIENT_ID: string; GITHUB_TEST_ENV_CLIENT_ID: string;
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it. GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
isTerminalEnabled: boolean;
isPhoenixEnabled: boolean; isPhoenixEnabled: boolean;
hostedExplorerURL: string; hostedExplorerURL: string;
armAPIVersion?: string; armAPIVersion?: string;
@@ -60,28 +73,28 @@ export interface ConfigContext {
// Default configuration // Default configuration
let configContext: Readonly<ConfigContext> = { let configContext: Readonly<ConfigContext> = {
platform: Platform.Portal, platform: Platform.Portal,
allowedAadEndpoints: defaultAllowedAadEndpoints,
allowedGraphEndpoints: defaultAllowedGraphEndpoints,
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$`,
`^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`, `^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`,
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`, `^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
`^https:\\/\\/.*\\.powerbi\\.com$`, `^https:\\/\\/.*\\.powerbi\\.com$`,
`^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`, `^https:\\/\\/.*\\.analysis-df\\.net$`,
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
`^https:\\/\\/.*\\.azure-test\\.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/",
AAD_ENDPOINT: "https://login.microsoftonline.com/", AAD_ENDPOINT: "https://login.microsoftonline.com/",
ARM_AUTH_AREA: "https://management.azure.com/",
ARM_ENDPOINT: "https://management.azure.com/", ARM_ENDPOINT: "https://management.azure.com/",
ARM_API_VERSION: "2016-06-01",
GRAPH_ENDPOINT: "https://graph.microsoft.com", GRAPH_ENDPOINT: "https://graph.microsoft.com",
GRAPH_API_VERSION: "1.6", GRAPH_API_VERSION: "1.6",
CATALOG_ENDPOINT: "https://catalogapi.azure.com/", CATALOG_ENDPOINT: "https://catalogapi.azure.com/",
@@ -92,9 +105,24 @@ 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"],
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
isTerminalEnabled: false,
isPhoenixEnabled: false, isPhoenixEnabled: false,
}; };
@@ -110,21 +138,19 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
return; return;
} }
if (!validateEndpoint(newContext.AAD_ENDPOINT, configContext.allowedAadEndpoints || defaultAllowedAadEndpoints)) {
delete newContext.AAD_ENDPOINT;
}
if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) { if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) {
delete newContext.ARM_ENDPOINT; delete newContext.ARM_ENDPOINT;
} }
if (!validateEndpoint(newContext.AAD_ENDPOINT, allowedAadEndpoints)) {
delete newContext.AAD_ENDPOINT;
}
if (!validateEndpoint(newContext.EMULATOR_ENDPOINT, allowedEmulatorEndpoints)) { if (!validateEndpoint(newContext.EMULATOR_ENDPOINT, allowedEmulatorEndpoints)) {
delete newContext.EMULATOR_ENDPOINT; delete newContext.EMULATOR_ENDPOINT;
} }
if ( if (!validateEndpoint(newContext.GRAPH_ENDPOINT, allowedGraphEndpoints)) {
!validateEndpoint(newContext.GRAPH_ENDPOINT, configContext.allowedGraphEndpoints || defaultAllowedGraphEndpoints)
) {
delete newContext.GRAPH_ENDPOINT; delete newContext.GRAPH_ENDPOINT;
} }
@@ -134,28 +160,22 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
if ( if (
!validateEndpoint( !validateEndpoint(
newContext.PORTAL_BACKEND_ENDPOINT, newContext.BACKEND_ENDPOINT,
configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints, configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints,
) )
) { ) {
delete newContext.PORTAL_BACKEND_ENDPOINT; delete newContext.BACKEND_ENDPOINT;
} }
if ( if (!validateEndpoint(newContext.MONGO_PROXY_ENDPOINT, allowedMongoProxyEndpoints)) {
!validateEndpoint(
newContext.MONGO_PROXY_ENDPOINT,
configContext.allowedMongoProxyEndpoints || defaultAllowedMongoProxyEndpoints,
)
) {
delete newContext.MONGO_PROXY_ENDPOINT; delete newContext.MONGO_PROXY_ENDPOINT;
} }
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;
} }

View File

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

View File

@@ -1,8 +1,7 @@
export interface QueryRequestOptions { export interface QueryRequestOptions {
$skipToken?: string; $skipToken?: string;
$top?: number; $top?: number;
$allowPartialScopes: boolean; subscriptions: string[];
subscriptions?: string[];
} }
export interface QueryResponse { export interface QueryResponse {

View File

@@ -18,13 +18,10 @@ export type DataExploreMessageV3 =
| { | {
type: FabricMessageTypes.GetAllResourceTokens; type: FabricMessageTypes.GetAllResourceTokens;
id: string; id: string;
}
| {
type: FabricMessageTypes.OpenSettings;
settingsId: string;
}; };
export interface GetCosmosTokenMessageOptions {
export type GetCosmosTokenMessageOptions = {
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace"; verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
resourceType: "" | "dbs" | "colls" | "docs" | "sprocs" | "pkranges"; resourceType: "" | "dbs" | "colls" | "docs" | "sprocs" | "pkranges";
resourceId: string; resourceId: string;
} };

View File

@@ -6,8 +6,6 @@ export interface ArmEntity {
location: string; location: string;
type: string; type: string;
kind: string; kind: string;
tags?: Tags;
resourceGroup?: string;
} }
export interface DatabaseAccount extends ArmEntity { export interface DatabaseAccount extends ArmEntity {
@@ -33,7 +31,6 @@ export interface DatabaseAccountExtendedProperties {
writeLocations?: DatabaseAccountResponseLocation[]; writeLocations?: DatabaseAccountResponseLocation[];
enableFreeTier?: boolean; enableFreeTier?: boolean;
enableAnalyticalStorage?: boolean; enableAnalyticalStorage?: boolean;
enableMaterializedViews?: boolean;
isVirtualNetworkFilterEnabled?: boolean; isVirtualNetworkFilterEnabled?: boolean;
ipRules?: IpRule[]; ipRules?: IpRule[];
privateEndpointConnections?: unknown[]; privateEndpointConnections?: unknown[];
@@ -44,12 +41,6 @@ export interface DatabaseAccountExtendedProperties {
publicNetworkAccess?: string; publicNetworkAccess?: string;
enablePriorityBasedExecution?: boolean; enablePriorityBasedExecution?: boolean;
vcoreMongoEndpoint?: string; vcoreMongoEndpoint?: string;
apiProperties?: ApiProperties;
}
export interface ApiProperties {
/* Describes the version of the MongoDB account. */
serverVersion?: "3.2" | "3.6" | "4.0" | "4.2" | "5.0" | "6.0" | "7.0";
} }
export interface DatabaseAccountResponseLocation { export interface DatabaseAccountResponseLocation {
@@ -168,12 +159,9 @@ 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;
materializedViews?: MaterializedView[];
materializedViewDefinition?: MaterializedViewDefinition;
} }
export interface CollectionsWithPagination { export interface CollectionsWithPagination {
@@ -211,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";
vectorIndexShardKey?: string[];
indexingSearchListSize?: number;
quantizationByteSize?: number;
}
export interface FullTextIndex {
path: string;
} }
export interface ComputedProperty { export interface ComputedProperty {
@@ -233,17 +213,6 @@ export interface ComputedProperty {
export type ComputedProperties = ComputedProperty[]; export type ComputedProperties = ComputedProperty[];
export interface MaterializedView {
id: string;
_rid: string;
}
export interface MaterializedViewDefinition {
definition: string;
sourceCollectionId: string;
sourceCollectionRid?: string;
}
export interface PartitionKey { export interface PartitionKey {
paths: string[]; paths: string[];
kind: "Hash" | "Range" | "MultiHash"; kind: "Hash" | "Range" | "MultiHash";
@@ -296,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 {
@@ -366,7 +329,9 @@ export interface CreateDatabaseParams {
offerThroughput?: number; offerThroughput?: number;
} }
export interface CreateCollectionParamsBase { export interface CreateCollectionParams {
createNewDatabase: boolean;
collectionId: string;
databaseId: string; databaseId: string;
databaseLevelThroughput: boolean; databaseLevelThroughput: boolean;
offerThroughput?: number; offerThroughput?: number;
@@ -377,17 +342,6 @@ export interface CreateCollectionParamsBase {
uniqueKeyPolicy?: UniqueKeyPolicy; uniqueKeyPolicy?: UniqueKeyPolicy;
createMongoWildcardIndex?: boolean; createMongoWildcardIndex?: boolean;
vectorEmbeddingPolicy?: VectorEmbeddingPolicy; vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
fullTextPolicy?: FullTextPolicy;
}
export interface CreateCollectionParams extends CreateCollectionParamsBase {
createNewDatabase: boolean;
collectionId: string;
}
export interface CreateMaterializedViewsParams extends CreateCollectionParamsBase {
materializedViewId: string;
materializedViewDefinition: MaterializedViewDefinition;
} }
export interface VectorEmbeddingPolicy { export interface VectorEmbeddingPolicy {
@@ -395,22 +349,12 @@ export interface VectorEmbeddingPolicy {
} }
export interface VectorEmbedding { export interface VectorEmbedding {
dataType: "float32" | "uint8" | "int8"; dataType: "float16" | "float32" | "uint8" | "int8";
dimensions: number; dimensions: number;
distanceFunction: "euclidean" | "cosine" | "dotproduct"; distanceFunction: "euclidean" | "cosine" | "dotproduct";
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;
@@ -432,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 {
@@ -700,5 +643,3 @@ export interface FeatureRegistration {
state: string; state: string;
}; };
} }
export type Tags = { [key: string]: string };

View File

@@ -4,9 +4,7 @@
export enum FabricMessageTypes { export enum FabricMessageTypes {
GetAuthorizationToken = "GetAuthorizationToken", GetAuthorizationToken = "GetAuthorizationToken",
GetAllResourceTokens = "GetAllResourceTokens", GetAllResourceTokens = "GetAllResourceTokens",
GetAccessToken = "GetAccessToken",
Ready = "Ready", Ready = "Ready",
OpenSettings = "OpenSettings",
} }
export interface AuthorizationToken { export interface AuthorizationToken {

View File

@@ -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,88 +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 };
};
}
| {
type: "refreshResourceTree";
message: {
id: string;
error: string | undefined;
};
};
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;
} }

View File

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

View File

@@ -1,6 +1,4 @@
import { import {
ItemDefinition,
JSONObject,
QueryMetrics, QueryMetrics,
Resource, Resource,
StoredProcedureDefinition, StoredProcedureDefinition,
@@ -31,11 +29,8 @@ export interface UploadDetailsRecord {
numFailed: number; numFailed: number;
numThrottled: number; numThrottled: number;
errors: string[]; errors: string[];
resources?: ItemDefinition[];
} }
export type BulkInsertResult = Omit<UploadDetailsRecord, "fileName">;
export interface QueryResultsMetadata { export interface QueryResultsMetadata {
hasMoreResults: boolean; hasMoreResults: boolean;
firstItemIndex: number; firstItemIndex: number;
@@ -50,7 +45,6 @@ export interface QueryResults extends QueryResultsMetadata {
roundTrips?: number; roundTrips?: number;
headers?: any; headers?: any;
queryMetrics?: QueryMetrics; queryMetrics?: QueryMetrics;
ruThresholdExceeded?: boolean;
} }
export interface Button { export interface Button {
@@ -104,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 {
@@ -121,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;
@@ -138,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>;
@@ -149,8 +136,6 @@ export interface Collection extends CollectionBase {
geospatialConfig: ko.Observable<DataModels.GeospatialConfig>; geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
documentIds: ko.ObservableArray<DocumentId>; documentIds: ko.ObservableArray<DocumentId>;
computedProperties: ko.Observable<DataModels.ComputedProperties>; computedProperties: ko.Observable<DataModels.ComputedProperties>;
materializedViews: ko.Observable<DataModels.MaterializedView[]>;
materializedViewDefinition: ko.Observable<DataModels.MaterializedViewDefinition>;
cassandraKeys: CassandraTableKeys; cassandraKeys: CassandraTableKeys;
cassandraSchema: CassandraTableKey[]; cassandraSchema: CassandraTableKey[];
@@ -165,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;
@@ -212,12 +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[] }>;
bulkInsertDocuments(documents: JSONObject[]): Promise<{
numSucceeded: number; getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
numFailed: number;
numThrottled: number;
errors: string[];
}>;
} }
/** /**
@@ -337,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 {
@@ -412,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;
@@ -443,7 +415,6 @@ export interface DataExplorerInputsFrame {
[key: string]: string; [key: string]: string;
}; };
feedbackPolicies?: any; feedbackPolicies?: any;
aadToken?: string;
} }
export interface SelfServeFrameInputs { export interface SelfServeFrameInputs {

View File

@@ -0,0 +1,11 @@
<!doctype html>
<html class="no-js" lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="data:," />
</head>
<body>
<div id="heatmap"></div>
</body>
</html>

View File

@@ -0,0 +1,55 @@
@import "../../../less/Common/Constants";
html {
font-family: @DataExplorerFont;
padding: 0px;
margin: 0px;
border: 0px;
overflow: hidden;
position: fixed;
width: 100%;
height: 100%;
}
body {
font-family: @DataExplorerFont;
padding: 0px;
margin: 0px;
border: 0px;
overflow: hidden;
}
#heatmap {
.dark-theme {
color: @BaseLight;
}
.chartTitle {
position: absolute;
top: 5px;
left: 3px;
font-size: 13px;
}
.noDataMessage {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
z-index: 10000;
height: 100%;
width: 100%;
top: 0;
left: 0;
opacity: 0.97;
div {
border-color: rgba(204, 204, 204, 0.8);
box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.12);
padding: 15px 10px;
width: calc(55% - 40px);
font-size: 13px;
text-align: center;
border-width: 1px;
border-style: solid;
}
}
}

View File

@@ -0,0 +1,143 @@
import dayjs from "dayjs";
import { handleMessage, Heatmap, isDarkTheme } from "./Heatmap";
import { PortalTheme } from "./HeatmapDatatypes";
describe("The Heatmap Control", () => {
const dataPoints = {
"1": {
"2019-06-19T00:59:10Z": {
"Normalized Throughput": 0.35,
},
"2019-06-19T00:48:10Z": {
"Normalized Throughput": 0.25,
},
},
};
const chartCaptions = {
chartTitle: "chart title",
yAxisTitle: "YAxisTitle",
tooltipText: "Tooltip text",
timeWindow: 123456789,
};
let heatmap: Heatmap;
const theme: PortalTheme = 1;
const divElement = `<div id="${Heatmap.elementId}"></div>`;
describe("drawHeatmap rendering", () => {
beforeEach(() => {
heatmap = new Heatmap(dataPoints, chartCaptions, theme);
document.body.innerHTML = divElement;
});
afterEach(() => {
document.body.innerHTML = ``;
});
it("should call _getChartSettings when drawHeatmap is invoked", () => {
const _getChartSettings = spyOn<any>(heatmap, "_getChartSettings");
heatmap.drawHeatmap();
expect(_getChartSettings.calls.any()).toBe(true);
});
it("should call _getLayoutSettings when drawHeatmap is invoked", () => {
const _getLayoutSettings = spyOn<any>(heatmap, "_getLayoutSettings");
heatmap.drawHeatmap();
expect(_getLayoutSettings.calls.any()).toBe(true);
});
it("should call _getChartDisplaySettings when drawHeatmap is invoked", () => {
const _getChartDisplaySettings = spyOn<any>(heatmap, "_getChartDisplaySettings");
heatmap.drawHeatmap();
expect(_getChartDisplaySettings.calls.any()).toBe(true);
});
it("drawHeatmap should render a Heatmap inside the div element", () => {
heatmap.drawHeatmap();
expect(document.body.innerHTML).not.toEqual(divElement);
});
});
describe("generateMatrixFromMap", () => {
it("should massage input data to match output expected", () => {
expect(heatmap.generateMatrixFromMap(dataPoints).yAxisPoints).toEqual(["1"]);
expect(heatmap.generateMatrixFromMap(dataPoints).dataPoints).toEqual([[0.25, 0.35]]);
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints.length).toEqual(2);
});
it("should output the date format to ISO8601 string format", () => {
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints[0].slice(10, 11)).toEqual("T");
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints[0].slice(-1)).toEqual("Z");
});
it("should convert the time to the user's local time", () => {
if (dayjs().utcOffset()) {
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints).not.toEqual([
"2019-06-19T00:48:10Z",
"2019-06-19T00:59:10Z",
]);
} else {
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints).toEqual([
"2019-06-19T00:48:10Z",
"2019-06-19T00:59:10Z",
]);
}
});
});
describe("isDarkTheme", () => {
it("isDarkTheme should return the correct result", () => {
expect(isDarkTheme(PortalTheme.dark)).toEqual(true);
expect(isDarkTheme(PortalTheme.azure)).not.toEqual(true);
});
});
});
describe("iframe rendering when there is no data", () => {
afterEach(() => {
document.body.innerHTML = ``;
});
it("should show a no data message with a dark theme", () => {
const data = {
data: {
signature: "pcIframe",
data: {
chartData: {},
chartSettings: {},
theme: 4,
},
},
origin: "http://localhost",
};
const divElement = `<div id="${Heatmap.elementId}"></div>`;
document.body.innerHTML = divElement;
handleMessage(data as MessageEvent);
expect(document.body.innerHTML).toContain("dark-theme");
expect(document.body.innerHTML).toContain("noDataMessage");
});
it("should show a no data message with a white theme", () => {
const data = {
data: {
signature: "pcIframe",
data: {
chartData: {},
chartSettings: {},
theme: 2,
},
},
origin: "http://localhost",
};
const divElement = `<div id="${Heatmap.elementId}"></div>`;
document.body.innerHTML = divElement;
handleMessage(data as MessageEvent);
expect(document.body.innerHTML).not.toContain("dark-theme");
expect(document.body.innerHTML).toContain("noDataMessage");
});
});

View File

@@ -0,0 +1,269 @@
import dayjs from "dayjs";
import * as Plotly from "plotly.js-cartesian-dist-min";
import { sendCachedDataMessage, sendReadyMessage } from "../../Common/MessageHandler";
import { StyleConstants } from "../../Common/StyleConstants";
import { MessageTypes } from "../../Contracts/ExplorerContracts";
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
import "./Heatmap.less";
import {
ChartSettings,
DataPayload,
DisplaySettings,
FontSettings,
HeatmapCaptions,
HeatmapData,
LayoutSettings,
PartitionTimeStampToData,
PortalTheme,
} from "./HeatmapDatatypes";
export class Heatmap {
public static readonly elementId: string = "heatmap";
private _chartData: HeatmapData;
private _heatmapCaptions: HeatmapCaptions;
private _theme: PortalTheme;
private _defaultFontColor: string;
constructor(data: DataPayload, heatmapCaptions: HeatmapCaptions, theme: PortalTheme) {
this._theme = theme;
this._defaultFontColor = StyleConstants.BaseDark;
this._setThemeColorForChart();
this._chartData = this.generateMatrixFromMap(data);
this._heatmapCaptions = heatmapCaptions;
}
private _setThemeColorForChart() {
if (isDarkTheme(this._theme)) {
this._defaultFontColor = StyleConstants.BaseLight;
}
}
private _getFontStyles(size: number = StyleConstants.MediumFontSize, color = "#838383"): FontSettings {
return {
family: StyleConstants.DataExplorerFont,
size,
color,
};
}
public generateMatrixFromMap(data: DataPayload): HeatmapData {
// all keys in data payload, sorted...
const rows: string[] = Object.keys(data).sort((a: string, b: string) => {
if (parseInt(a) < parseInt(b)) {
return -1;
} else {
if (parseInt(a) > parseInt(b)) {
return 1;
} else {
return 0;
}
}
});
const output: HeatmapData = {
yAxisPoints: [],
dataPoints: [],
xAxisPoints: Object.keys(data[rows[0]]).sort((a: string, b: string) => {
if (a < b) {
return -1;
} else {
if (a > b) {
return 1;
} else {
return 0;
}
}
}),
};
// go thru all rows and create 2d matrix for heatmap...
for (let i = 0; i < rows.length; i++) {
output.yAxisPoints.push(rows[i]);
const dataPoints: number[] = [];
for (let a = 0; a < output.xAxisPoints.length; a++) {
const row: PartitionTimeStampToData = data[rows[i]];
dataPoints.push(row[output.xAxisPoints[a]]["Normalized Throughput"]);
}
output.dataPoints.push(dataPoints);
}
for (let a = 0; a < output.xAxisPoints.length; a++) {
const dateTime = output.xAxisPoints[a];
// convert to local users timezone...
const day = dayjs(new Date(dateTime)).format("YYYY-MM-DD");
const hour = dayjs(new Date(dateTime)).format("HH:mm:ss");
// coerce to ISOString format since that is what plotly wants...
output.xAxisPoints[a] = `${day}T${hour}Z`;
}
return output;
}
private _getChartSettings(): ChartSettings[] {
return [
{
z: this._chartData.dataPoints,
type: "heatmap",
zmin: 0,
zmid: 50,
zmax: 100,
colorscale: [
[0.0, "#1FD338"],
[0.1, "#1CAD2F"],
[0.2, "#50A527"],
[0.3, "#719F21"],
[0.4, "#95991B"],
[0.5, "#CE8F11"],
[0.6, "#E27F0F"],
[0.7, "#E46612"],
[0.8, "#E64914"],
[0.9, "#B80016"],
[1.0, "#B80016"],
],
name: "",
hovertemplate: this._heatmapCaptions.tooltipText,
colorbar: {
thickness: 15,
outlinewidth: 0,
tickcolor: StyleConstants.BaseDark,
tickfont: this._getFontStyles(10, this._defaultFontColor),
},
y: this._chartData.yAxisPoints,
x: this._chartData.xAxisPoints,
},
];
}
private _getLayoutSettings(): LayoutSettings {
return {
margin: {
l: 40,
r: 10,
b: 35,
t: 30,
pad: 0,
},
paper_bgcolor: "transparent",
plot_bgcolor: "transparent",
width: 462,
height: 240,
yaxis: {
title: this._heatmapCaptions.yAxisTitle,
titlefont: this._getFontStyles(11),
autorange: true,
showgrid: false,
zeroline: false,
showline: false,
autotick: true,
fixedrange: true,
ticks: "",
showticklabels: false,
},
xaxis: {
fixedrange: true,
title: "*White area in heatmap indicates there is no available data",
titlefont: this._getFontStyles(11),
autorange: true,
showgrid: false,
zeroline: false,
showline: false,
autotick: true,
tickformat: this._heatmapCaptions.timeWindow > 7 ? "%I:%M %p" : "%b %e",
showticklabels: true,
tickfont: this._getFontStyles(10),
},
title: {
text: this._heatmapCaptions.chartTitle,
x: 0.01,
font: this._getFontStyles(13, this._defaultFontColor),
},
};
}
private _getChartDisplaySettings(): DisplaySettings {
return {
/* heatmap can be fully responsive however the min-height needed in that case is greater than the iframe portal height, hence explicit width + height have been set in _getLayoutSettings
responsive: true,*/
displayModeBar: false,
};
}
public drawHeatmap(): void {
// todo - create random elementId generator so multiple heatmaps can be created - ticket # 431469
Plotly.plot(
Heatmap.elementId,
this._getChartSettings(),
this._getLayoutSettings(),
this._getChartDisplaySettings(),
);
const plotDiv: any = document.getElementById(Heatmap.elementId);
plotDiv.on("plotly_click", (data: any) => {
let timeSelected: string = data.points[0].x;
timeSelected = timeSelected.replace(" ", "T");
timeSelected = `${timeSelected}Z`;
let xAxisIndex = 0;
for (let i = 0; i < this._chartData.xAxisPoints.length; i++) {
if (this._chartData.xAxisPoints[i] === timeSelected) {
xAxisIndex = i;
break;
}
}
const output = [];
for (let i = 0; i < this._chartData.dataPoints.length; i++) {
output.push(this._chartData.dataPoints[i][xAxisIndex]);
}
sendCachedDataMessage(MessageTypes.LogInfo, output);
});
}
}
export function isDarkTheme(theme: PortalTheme) {
return theme === PortalTheme.dark;
}
export function handleMessage(event: MessageEvent) {
if (isInvalidParentFrameOrigin(event)) {
return;
}
if (typeof event.data !== "object" || event.data["signature"] !== "pcIframe") {
return;
}
if (
typeof event.data.data !== "object" ||
!("chartData" in event.data.data) ||
!("chartSettings" in event.data.data)
) {
return;
}
Plotly.purge(Heatmap.elementId);
document.getElementById(Heatmap.elementId)!.innerHTML = "";
const data = event.data.data;
const chartData: DataPayload = data.chartData;
const chartSettings: HeatmapCaptions = data.chartSettings;
const chartTheme: PortalTheme = data.theme;
if (Object.keys(chartData).length) {
new Heatmap(chartData, chartSettings, chartTheme).drawHeatmap();
} else {
const chartTitleElement = document.createElement("div");
chartTitleElement.innerHTML = data.chartSettings.chartTitle;
chartTitleElement.classList.add("chartTitle");
const noDataMessageElement = document.createElement("div");
noDataMessageElement.classList.add("noDataMessage");
const noDataMessageContent = document.createElement("div");
noDataMessageContent.innerHTML = data.errorMessage;
noDataMessageElement.appendChild(noDataMessageContent);
if (isDarkTheme(chartTheme)) {
chartTitleElement.classList.add("dark-theme");
noDataMessageElement.classList.add("dark-theme");
noDataMessageContent.classList.add("dark-theme");
}
document.getElementById(Heatmap.elementId)!.appendChild(chartTitleElement);
document.getElementById(Heatmap.elementId)!.appendChild(noDataMessageElement);
}
}
window.addEventListener("message", handleMessage, false);
sendReadyMessage();

View File

@@ -0,0 +1,106 @@
type dataPoint = string | number;
export interface DataPayload {
[id: string]: PartitionTimeStampToData;
}
export enum PortalTheme {
blue = 1,
azure,
light,
dark,
}
export interface HeatmapData {
yAxisPoints: string[];
xAxisPoints: string[];
dataPoints: dataPoint[][];
}
export interface HeatmapCaptions {
chartTitle: string;
yAxisTitle: string;
tooltipText: string;
timeWindow: number;
}
export interface FontSettings {
family: string;
size: number;
color: string;
}
export interface LayoutSettings {
paper_bgcolor?: string;
plot_bgcolor?: string;
margin?: {
l: number;
r: number;
b: number;
t: number;
pad: number;
};
width?: number;
height?: number;
yaxis?: {
fixedrange: boolean;
title: HeatmapCaptions["yAxisTitle"];
titlefont: FontSettings;
autorange: boolean;
showgrid: boolean;
zeroline: boolean;
showline: boolean;
autotick: boolean;
ticks: "";
showticklabels: boolean;
};
xaxis?: {
fixedrange: boolean;
title: string;
titlefont: FontSettings;
autorange: boolean;
showgrid: boolean;
zeroline: boolean;
showline: boolean;
autotick: boolean;
showticklabels: boolean;
tickformat: string;
tickfont: FontSettings;
};
title?: {
text: HeatmapCaptions["chartTitle"];
x: number;
font?: FontSettings;
};
font?: FontSettings;
}
export interface ChartSettings {
z: HeatmapData["dataPoints"];
type: "heatmap";
zmin: number;
zmid: number;
zmax: number;
colorscale: [number, string][];
name: string;
hovertemplate: HeatmapCaptions["tooltipText"];
colorbar: {
thickness: number;
outlinewidth: number;
tickcolor: string;
tickfont: FontSettings;
};
y: HeatmapData["yAxisPoints"];
x: HeatmapData["xAxisPoints"];
}
export interface DisplaySettings {
displayModeBar: boolean;
responsive?: boolean;
}
export interface PartitionTimeStampToData {
[timeSeriesDates: string]: {
[NormalizedThroughput: string]: number;
};
}

View File

@@ -1,13 +1,5 @@
import { GlobalSecondaryIndexLabels } from "Common/Constants";
import { isGlobalSecondaryIndexEnabled } from "Common/DatabaseAccountUtility";
import { configContext, Platform } from "ConfigContext";
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import {
AddGlobalSecondaryIndexPanel,
AddGlobalSecondaryIndexPanelProps,
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
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";
@@ -27,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";
@@ -48,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,
@@ -60,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",
}); });
@@ -103,29 +90,13 @@ export const createCollectionContextMenuButton = (
iconSrc: HostedTerminalIcon, iconSrc: HostedTerminalIcon,
onClick: () => { onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
if (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) { if (useNotebook.getState().isShellEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else { } else {
selectedCollection && selectedCollection.onNewMongoShellClick(); selectedCollection && selectedCollection.onNewMongoShellClick();
} }
}, },
label: label: useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell",
useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell
? "Open Mongo Shell"
: "New Shell",
});
}
if (
(useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) &&
userContext.apiType === "Cassandra"
) {
items.push({
iconSrc: HostedTerminalIcon,
onClick: () => {
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
},
label: "Open Cassandra Shell",
}); });
} }
@@ -158,39 +129,20 @@ 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>) => {
useSelectedNode.getState().setSelectedNode(selectedCollection);
(useSidePanel.getState().getRef = lastFocusedElement),
useSidePanel
.getState()
.openSidePanel(
"Delete " + getCollectionName(),
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
);
},
label: `Delete ${getCollectionName()}`,
styleClass: "deleteCollectionMenuItem",
});
}
if (isGlobalSecondaryIndexEnabled() && !selectedCollection.materializedViewDefinition()) {
items.push({
label: GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex,
onClick: () => { onClick: () => {
const addMaterializedViewPanelProps: AddGlobalSecondaryIndexPanelProps = { useSelectedNode.getState().setSelectedNode(selectedCollection);
explorer: container,
sourceContainer: selectedCollection,
};
useSidePanel useSidePanel
.getState() .getState()
.openSidePanel( .openSidePanel(
GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex, "Delete " + getCollectionName(),
<AddGlobalSecondaryIndexPanel {...addMaterializedViewPanelProps} />, <DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
); );
}, },
label: `Delete ${getCollectionName()}`,
styleClass: "deleteCollectionMenuItem",
}); });
} }

View File

@@ -1,4 +1,4 @@
import { DirectionalHint, Icon, IconButton, Label, Stack, TooltipHost } from "@fluentui/react"; import { DirectionalHint, Icon, Label, Stack, TooltipHost } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import { NormalizedEventKey } from "../../../Common/Constants"; import { NormalizedEventKey } from "../../../Common/Constants";
import { accordionStackTokens } from "../Settings/SettingsRenderUtils"; import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
@@ -9,9 +9,6 @@ export interface CollapsibleSectionProps {
onExpand?: () => void; onExpand?: () => void;
children: JSX.Element; children: JSX.Element;
tooltipContent?: string | JSX.Element | JSX.Element[]; tooltipContent?: string | JSX.Element | JSX.Element[];
showDelete?: boolean;
onDelete?: () => void;
disabled?: boolean;
} }
export interface CollapsibleSectionState { export interface CollapsibleSectionState {
@@ -72,20 +69,6 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} /> <Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
</TooltipHost> </TooltipHost>
)} )}
{this.props.showDelete && (
<Stack.Item style={{ marginLeft: "auto" }}>
<IconButton
disabled={this.props.disabled}
id={`delete-${this.props.title.split(" ").join("-")}`}
iconProps={{ iconName: "Delete" }}
style={{ height: 27, marginRight: "20px" }}
onClick={(event) => {
event.stopPropagation();
this.props.onDelete();
}}
/>
</Stack.Item>
)}
</Stack> </Stack>
{this.state.isExpanded && this.props.children} {this.state.isExpanded && this.props.children}
</> </>

View File

@@ -11,7 +11,7 @@ exports[`CollapsibleSectionComponent renders 1`] = `
role="button" role="button"
tabIndex={0} tabIndex={0}
tokens={ tokens={
{ Object {
"childrenGap": 10, "childrenGap": 10,
} }
} }

View File

@@ -35,7 +35,7 @@ export interface DialogState {
textFieldProps?: TextFieldProps, textFieldProps?: TextFieldProps,
primaryButtonDisabled?: boolean, primaryButtonDisabled?: boolean,
) => void; ) => void;
showOkModalDialog: (title: string, subText: string, linkProps?: LinkProps) => void; showOkModalDialog: (title: string, subText: string) => void;
} }
export const useDialog: UseStore<DialogState> = create((set, get) => ({ export const useDialog: UseStore<DialogState> = create((set, get) => ({
@@ -83,7 +83,7 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
textFieldProps, textFieldProps,
primaryButtonDisabled, primaryButtonDisabled,
}), }),
showOkModalDialog: (title: string, subText: string, linkProps?: LinkProps): void => showOkModalDialog: (title: string, subText: string): void =>
get().openDialog({ get().openDialog({
isModal: true, isModal: true,
title, title,
@@ -94,7 +94,6 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
get().closeDialog(); get().closeDialog();
}, },
onSecondaryButtonClick: undefined, onSecondaryButtonClick: undefined,
linkProps,
}), }),
})); }));
@@ -214,10 +213,8 @@ export const Dialog: FC = () => {
{contentHtml} {contentHtml}
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />} {progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
<DialogFooter> <DialogFooter>
<PrimaryButton {...primaryButtonProps} data-test={`DialogButton:${primaryButtonText}`} /> <PrimaryButton {...primaryButtonProps} />
{secondaryButtonProps && ( {secondaryButtonProps && <DefaultButton {...secondaryButtonProps} />}
<DefaultButton {...secondaryButtonProps} data-test={`DialogButton:${secondaryButtonText}`} />
)}
</DialogFooter> </DialogFooter>
</FluentDialog> </FluentDialog>
) : ( ) : (

View File

@@ -3,37 +3,6 @@ import * as React from "react";
import { loadMonaco, monaco } from "../../LazyMonaco"; import { loadMonaco, monaco } from "../../LazyMonaco";
// import "./EditorReact.less"; // import "./EditorReact.less";
// In development, add a function to window to allow us to get the editor instance for a given element
if (process.env.NODE_ENV === "development") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const win = window as any;
win._monaco_getEditorForElement =
win._monaco_getEditorForElement ||
((element: HTMLElement) => {
const editorId = element.dataset["monacoEditorId"];
if (!editorId || !win.__monaco_editors || typeof win.__monaco_editors !== "object") {
return null;
}
return win.__monaco_editors[editorId];
});
win._monaco_getEditorContentForElement =
win._monaco_getEditorContentForElement ||
((element: HTMLElement) => {
const editor = win._monaco_getEditorForElement(element);
return editor ? editor.getValue() : null;
});
win._monaco_setEditorContentForElement =
win._monaco_setEditorContentForElement ||
((element: HTMLElement, text: string) => {
const editor = win._monaco_getEditorForElement(element);
if (editor) {
editor.setValue(text);
}
});
}
interface EditorReactStates { interface EditorReactStates {
showEditor: boolean; showEditor: boolean;
} }
@@ -42,7 +11,7 @@ export interface EditorReactProps {
content: string; content: string;
isReadOnly: boolean; isReadOnly: boolean;
ariaLabel: string; // Sets what will be read to the user to define the control ariaLabel: string; // Sets what will be read to the user to define the control
onContentSelected?: (selectedContent: string, selection: monaco.Selection) => void; // Called when text is selected onContentSelected?: (selectedContent: string) => void; // Called when text is selected
onContentChanged?: (newContent: string) => void; // Called when text is changed onContentChanged?: (newContent: string) => void; // Called when text is changed
theme?: string; // Monaco editor theme theme?: string; // Monaco editor theme
wordWrap?: monaco.editor.IEditorOptions["wordWrap"]; wordWrap?: monaco.editor.IEditorOptions["wordWrap"];
@@ -55,34 +24,12 @@ export interface EditorReactProps {
monacoContainerStyles?: React.CSSProperties; monacoContainerStyles?: React.CSSProperties;
className?: string; className?: string;
spinnerClassName?: string; spinnerClassName?: string;
modelMarkers?: monaco.editor.IMarkerData[];
enableWordWrapContextMenuItem?: boolean; // Enable/Disable "Word Wrap" context menu item
onWordWrapChanged?: (wordWrap: "on" | "off") => void; // Called when word wrap is changed
} }
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 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;
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 +58,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 +69,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 +82,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 +92,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,27 +109,10 @@ 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);
}, },
); );
} }
if (this.props.enableWordWrapContextMenuItem) {
editor.addAction({
// An unique identifier of the contributed action.
id: "wordwrap",
label: "Toggle Word Wrap",
contextMenuGroupId: EditorReact.VIEWING_OPTIONS_GROUP_ID,
contextMenuOrder: 1,
// Method that will be executed when the action is triggered.
// @param editor The editor instance is passed in as a convenience
run: (ed) => {
const newOption = ed.getOption(this.monacoApi.editor.EditorOption.wordWrap) === "on" ? "off" : "on";
ed.updateOptions({ wordWrap: newOption });
this.props.onWordWrapChanged(newOption);
},
});
}
} }
/** /**
@@ -218,14 +133,12 @@ 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 monaco = await loadMonaco();
try { try {
createCallback(this.monacoApi.editor.create(this.rootNode, options)); createCallback(monaco?.editor?.create(this.rootNode, options));
} catch (error) { } catch (error) {
// This could happen if the parent node suddenly disappears during create() // This could happen if the parent node suddenly disappears during create()
console.error("Unable to create EditorReact", error); console.error("Unable to create EditorReact", error);

View File

@@ -18,7 +18,7 @@ exports[`Feature panel renders all flags 1`] = `
<Stack <Stack
className="options" className="options"
tokens={ tokens={
{ Object {
"childrenGap": 10, "childrenGap": 10,
} }
} }
@@ -27,7 +27,7 @@ exports[`Feature panel renders all flags 1`] = `
horizontal={true} horizontal={true}
horizontalAlign="space-between" horizontalAlign="space-between"
tokens={ tokens={
{ Object {
"childrenGap": 10, "childrenGap": 10,
} }
} }
@@ -52,7 +52,7 @@ exports[`Feature panel renders all flags 1`] = `
horizontal={true} horizontal={true}
horizontalAlign="start" horizontalAlign="start"
tokens={ tokens={
{ Object {
"childrenGap": 10, "childrenGap": 10,
} }
} }
@@ -61,16 +61,16 @@ exports[`Feature panel renders all flags 1`] = `
label="Base Url" label="Base Url"
onChange={[Function]} onChange={[Function]}
options={ options={
[ Array [
{ Object {
"key": "https://localhost:1234/explorer.html", "key": "https://localhost:1234/explorer.html",
"text": "localhost:1234", "text": "localhost:1234",
}, },
{ Object {
"key": "https://cosmos.azure.com/explorer.html", "key": "https://cosmos.azure.com/explorer.html",
"text": "cosmos.azure.com", "text": "cosmos.azure.com",
}, },
{ Object {
"key": "https://portal.azure.com", "key": "https://portal.azure.com",
"text": "portal", "text": "portal",
}, },
@@ -78,8 +78,8 @@ exports[`Feature panel renders all flags 1`] = `
} }
selectedKey="https://localhost:1234/explorer.html" selectedKey="https://localhost:1234/explorer.html"
styles={ styles={
{ Object {
"dropdown": { "dropdown": Object {
"width": 200, "width": 200,
}, },
} }
@@ -89,20 +89,20 @@ exports[`Feature panel renders all flags 1`] = `
label="Platform" label="Platform"
onChange={[Function]} onChange={[Function]}
options={ options={
[ Array [
{ Object {
"key": "Hosted", "key": "Hosted",
"text": "Hosted", "text": "Hosted",
}, },
{ Object {
"key": "Portal", "key": "Portal",
"text": "Portal", "text": "Portal",
}, },
{ Object {
"key": "Emulator", "key": "Emulator",
"text": "Emulator", "text": "Emulator",
}, },
{ Object {
"key": "", "key": "",
"text": "None", "text": "None",
}, },
@@ -110,8 +110,8 @@ exports[`Feature panel renders all flags 1`] = `
} }
selectedKey="Hosted" selectedKey="Hosted"
styles={ styles={
{ Object {
"dropdown": { "dropdown": Object {
"width": 200, "width": 200,
}, },
} }
@@ -208,7 +208,7 @@ exports[`Feature panel renders all flags 1`] = `
<Stack <Stack
horizontal={true} horizontal={true}
tokens={ tokens={
{ Object {
"childrenGap": 10, "childrenGap": 10,
} }
} }
@@ -222,8 +222,8 @@ exports[`Feature panel renders all flags 1`] = `
onChange={[Function]} onChange={[Function]}
placeholder="https://notebookserver" placeholder="https://notebookserver"
styles={ styles={
{ Object {
"fieldGroup": { "fieldGroup": Object {
"width": 300, "width": 300,
}, },
} }
@@ -235,8 +235,8 @@ exports[`Feature panel renders all flags 1`] = `
onChange={[Function]} onChange={[Function]}
placeholder="" placeholder=""
styles={ styles={
{ Object {
"fieldGroup": { "fieldGroup": Object {
"width": 300, "width": 300,
}, },
} }
@@ -248,8 +248,8 @@ exports[`Feature panel renders all flags 1`] = `
onChange={[Function]} onChange={[Function]}
placeholder="" placeholder=""
styles={ styles={
{ Object {
"fieldGroup": { "fieldGroup": Object {
"width": 300, "width": 300,
}, },
} }
@@ -265,8 +265,8 @@ exports[`Feature panel renders all flags 1`] = `
onChange={[Function]} onChange={[Function]}
placeholder="" placeholder=""
styles={ styles={
{ Object {
"fieldGroup": { "fieldGroup": Object {
"width": 300, "width": 300,
}, },
} }
@@ -279,8 +279,8 @@ exports[`Feature panel renders all flags 1`] = `
onChange={[Function]} onChange={[Function]}
placeholder="https://localhost:1234/explorer.html" placeholder="https://localhost:1234/explorer.html"
styles={ styles={
{ Object {
"fieldGroup": { "fieldGroup": Object {
"width": 300, "width": 300,
}, },
} }
@@ -292,8 +292,8 @@ exports[`Feature panel renders all flags 1`] = `
onChange={[Function]} onChange={[Function]}
placeholder="" placeholder=""
styles={ styles={
{ Object {
"fieldGroup": { "fieldGroup": Object {
"width": 300, "width": 300,
}, },
} }

View File

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

View File

@@ -1,239 +0,0 @@
import {
DefaultButton,
Dropdown,
IDropdownOption,
IStyleFunctionOrObject,
ITextFieldStyleProps,
ITextFieldStyles,
Label,
Stack,
TextField,
} from "@fluentui/react";
import { FullTextIndex, FullTextPath, FullTextPolicy } from "Contracts/DataModels";
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
import * as React from "react";
export interface FullTextPoliciesComponentProps {
fullTextPolicy: FullTextPolicy;
onFullTextPathChange: (
fullTextPolicy: FullTextPolicy,
fullTextIndexes: FullTextIndex[],
validationPassed: boolean,
) => void;
discardChanges?: boolean;
onChangesDiscarded?: () => void;
}
export interface FullTextPolicyData {
path: string;
language: string;
pathError: string;
}
const labelStyles = {
root: {
fontSize: 12,
},
};
const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldStyles> = {
fieldGroup: {
height: 27,
},
field: {
fontSize: 12,
padding: "0 8px",
},
};
const dropdownStyles = {
title: {
height: 27,
lineHeight: "24px",
fontSize: 12,
},
dropdown: {
height: 27,
lineHeight: "24px",
},
dropdownItem: {
fontSize: 12,
},
};
export const FullTextPoliciesComponent: React.FunctionComponent<FullTextPoliciesComponentProps> = ({
fullTextPolicy,
onFullTextPathChange,
discardChanges,
onChangesDiscarded,
}): JSX.Element => {
const getFullTextPathError = (path: string, index?: number): string => {
let error = "";
if (!path) {
error = "Full text path should not be empty";
}
if (
index >= 0 &&
fullTextPathData?.find(
(fullTextPath: FullTextPolicyData, dataIndex: number) => dataIndex !== index && fullTextPath.path === path,
)
) {
error = "Full text path is already defined";
}
return error;
};
const initializeData = (fullTextPolicy: FullTextPolicy): FullTextPolicyData[] => {
if (!fullTextPolicy) {
fullTextPolicy = { defaultLanguage: getFullTextLanguageOptions()[0].key as never, fullTextPaths: [] };
}
return fullTextPolicy.fullTextPaths.map((fullTextPath: FullTextPath) => ({
...fullTextPath,
pathError: getFullTextPathError(fullTextPath.path),
}));
};
const [fullTextPathData, setFullTextPathData] = React.useState<FullTextPolicyData[]>(initializeData(fullTextPolicy));
const [defaultLanguage, setDefaultLanguage] = React.useState<string>(
fullTextPolicy ? fullTextPolicy.defaultLanguage : (getFullTextLanguageOptions()[0].key as never),
);
React.useEffect(() => {
propagateData();
}, [fullTextPathData, defaultLanguage]);
React.useEffect(() => {
if (discardChanges) {
setFullTextPathData(initializeData(fullTextPolicy));
setDefaultLanguage(fullTextPolicy.defaultLanguage);
onChangesDiscarded();
}
}, [discardChanges]);
const propagateData = () => {
const newFullTextPolicy: FullTextPolicy = {
defaultLanguage: defaultLanguage,
fullTextPaths: fullTextPathData.map((policy: FullTextPolicyData) => ({
path: policy.path,
language: policy.language,
})),
};
const fullTextIndexes: FullTextIndex[] = fullTextPathData.map((policy) => ({
path: policy.path,
}));
const validationPassed = fullTextPathData.every((policy: FullTextPolicyData) => policy.pathError === "");
onFullTextPathChange(newFullTextPolicy, fullTextIndexes, validationPassed);
};
const onFullTextPathValueChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value.trim();
const fullTextPaths = [...fullTextPathData];
if (!fullTextPaths[index]?.path && !value.startsWith("/")) {
fullTextPaths[index].path = "/" + value;
} else {
fullTextPaths[index].path = value;
}
fullTextPaths[index].pathError = getFullTextPathError(value, index);
setFullTextPathData(fullTextPaths);
};
const onFullTextPathPolicyChange = (index: number, option: IDropdownOption): void => {
const policies = [...fullTextPathData];
policies[index].language = option.key as never;
setFullTextPathData(policies);
};
const onAdd = () => {
setFullTextPathData([
...fullTextPathData,
{
path: "",
language: defaultLanguage,
pathError: getFullTextPathError(""),
},
]);
};
const onDelete = (index: number) => {
const policies = fullTextPathData.filter((_uniqueKey, j) => index !== j);
setFullTextPathData(policies);
};
return (
<Stack tokens={{ childrenGap: 4 }}>
<Stack style={{ marginBottom: 10 }}>
<Label styles={labelStyles}>Default language</Label>
<Dropdown
required={true}
styles={dropdownStyles}
options={getFullTextLanguageOptions()}
selectedKey={defaultLanguage}
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
setDefaultLanguage(option.key as never)
}
></Dropdown>
</Stack>
{fullTextPathData &&
fullTextPathData.length > 0 &&
fullTextPathData.map((fullTextPolicy: FullTextPolicyData, index: number) => (
<CollapsibleSectionComponent
key={index}
isExpandedByDefault={true}
title={`Full text path ${index + 1}`}
showDelete={true}
onDelete={() => onDelete(index)}
>
<Stack horizontal tokens={{ childrenGap: 4 }}>
<Stack
styles={{
root: {
margin: "0 0 6px 20px !important",
paddingLeft: 20,
width: "80%",
borderLeft: "1px solid",
},
}}
>
<Stack>
<Label styles={labelStyles}>Path</Label>
<TextField
id={`full-text-policy-path-${index + 1}`}
required={true}
placeholder="/fullTextPath1"
styles={textFieldStyles}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onFullTextPathValueChange(index, event)}
value={fullTextPolicy.path || ""}
errorMessage={fullTextPolicy.pathError}
/>
</Stack>
<Stack>
<Label styles={labelStyles}>Language</Label>
<Dropdown
required={true}
styles={dropdownStyles}
options={getFullTextLanguageOptions()}
selectedKey={fullTextPolicy.language}
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
onFullTextPathPolicyChange(index, option)
}
></Dropdown>
</Stack>
</Stack>
</Stack>
</CollapsibleSectionComponent>
))}
<DefaultButton id={`add-vector-policy`} styles={{ root: { maxWidth: 170, fontSize: 12 } }} onClick={onAdd}>
Add full text path
</DefaultButton>
</Stack>
);
};
export const getFullTextLanguageOptions = (): IDropdownOption[] => {
return [
{
key: "en-US",
text: "English (US)",
},
];
};

View File

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

View File

@@ -1,315 +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"
data-test={"DocumentsTab/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>
</>
);
};

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