Compare commits
2 Commits
users/rbac
...
p2_copilot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7c9edf1d4 | ||
|
|
3283d4073b |
@@ -1,10 +0,0 @@
|
|||||||
# NOTE: Prettier reads EditorConfig settings, so be careful adjusting settings here and assuming they'll only affect your editor ;).
|
|
||||||
|
|
||||||
# top-most EditorConfig file
|
|
||||||
root = true
|
|
||||||
|
|
||||||
[*.yml]
|
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
[*.{js,jsx,ts,tsx}]
|
|
||||||
indent_size = 2
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
playwright.config.ts
|
|
||||||
|
|
||||||
**/node_modules/
|
**/node_modules/
|
||||||
src/**/__mocks__/**/*
|
src/**/__mocks__/**/*
|
||||||
dist/
|
dist/
|
||||||
@@ -91,7 +89,10 @@ src/Explorer/Tables/TableEntityProcessor.ts
|
|||||||
src/Explorer/Tables/Utilities.ts
|
src/Explorer/Tables/Utilities.ts
|
||||||
src/Explorer/Tabs/ConflictsTab.ts
|
src/Explorer/Tabs/ConflictsTab.ts
|
||||||
src/Explorer/Tabs/DatabaseSettingsTab.ts
|
src/Explorer/Tabs/DatabaseSettingsTab.ts
|
||||||
|
src/Explorer/Tabs/DocumentsTab.test.ts
|
||||||
|
src/Explorer/Tabs/DocumentsTab.ts
|
||||||
src/Explorer/Tabs/GraphTab.ts
|
src/Explorer/Tabs/GraphTab.ts
|
||||||
|
src/Explorer/Tabs/MongoDocumentsTab.ts
|
||||||
src/Explorer/Tabs/NotebookV2Tab.ts
|
src/Explorer/Tabs/NotebookV2Tab.ts
|
||||||
src/Explorer/Tabs/ScriptTabBase.ts
|
src/Explorer/Tabs/ScriptTabBase.ts
|
||||||
src/Explorer/Tabs/TabComponents.ts
|
src/Explorer/Tabs/TabComponents.ts
|
||||||
@@ -127,7 +128,7 @@ src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx
|
|||||||
src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx
|
src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx
|
||||||
src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx
|
src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx
|
||||||
src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx
|
src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx
|
||||||
src/Explorer/Controls/TreeComponent/LegacyTreeComponent.tsx
|
src/Explorer/Controls/TreeComponent/TreeComponent.tsx
|
||||||
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx
|
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx
|
||||||
src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx
|
src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx
|
||||||
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx
|
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx
|
||||||
@@ -144,5 +145,4 @@ src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx
|
|||||||
src/Explorer/Tree/ResourceTreeAdapter.tsx
|
src/Explorer/Tree/ResourceTreeAdapter.tsx
|
||||||
__mocks__/monaco-editor.ts
|
__mocks__/monaco-editor.ts
|
||||||
src/Explorer/Tree/ResourceTree.tsx
|
src/Explorer/Tree/ResourceTree.tsx
|
||||||
src/Utils/EndpointUtils.ts
|
|
||||||
src/Utils/PriorityBasedExecutionUtils.ts
|
src/Utils/PriorityBasedExecutionUtils.ts
|
||||||
@@ -53,9 +53,4 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
settings: {
|
|
||||||
react: {
|
|
||||||
version: "detect",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
196
.github/workflows/ci.yml
vendored
@@ -8,20 +8,17 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
contents: read
|
|
||||||
jobs:
|
jobs:
|
||||||
codemetrics:
|
codemetrics:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: "Log Code Metrics"
|
name: "Log Code Metrics"
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/master'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- name: Use Node.js 18.x
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 14.x
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: node utils/codeMetrics.js
|
- run: node utils/codeMetrics.js
|
||||||
env:
|
env:
|
||||||
@@ -30,11 +27,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: "Compile TypeScript"
|
name: "Compile TypeScript"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- name: Use Node.js 18.x
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 14.x
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run compile
|
- run: npm run compile
|
||||||
- run: npm run compile:strict
|
- run: npm run compile:strict
|
||||||
@@ -42,44 +39,44 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: "Check Format"
|
name: "Check Format"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- name: Use Node.js 18.x
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 14.x
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run format:check
|
- run: npm run format:check
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: "Lint"
|
name: "Lint"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- name: Use Node.js 18.x
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 14.x
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run lint
|
- run: npm run lint
|
||||||
unittest:
|
unittest:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: "Unit Tests"
|
name: "Unit Tests"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- name: Use Node.js 18.x
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 14.x
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run test
|
- run: npm run test
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: "Build"
|
name: "Build"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- name: Use Node.js 18.x
|
- name: Use Node.js 14.x
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 14.x
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build:contracts
|
- run: npm run build:contracts
|
||||||
- name: Restore Build Cache
|
- name: Restore Build Cache
|
||||||
@@ -89,10 +86,10 @@ jobs:
|
|||||||
key: ${{ runner.os }}-build-cache
|
key: ${{ runner.os }}-build-cache
|
||||||
- run: npm run pack:prod
|
- run: npm run pack:prod
|
||||||
env:
|
env:
|
||||||
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@v3
|
- uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
path: dist/
|
path: dist/
|
||||||
@@ -104,6 +101,72 @@ jobs:
|
|||||||
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
|
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:
|
env:
|
||||||
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
|
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
|
||||||
|
endtoendemulator:
|
||||||
|
name: "End To End Emulator Tests"
|
||||||
|
# Temporarily disabled. This test needs to be rewritten in playwright
|
||||||
|
if: false
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Use Node.js 14.x
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 14.x
|
||||||
|
- uses: southpolesteve/cosmos-emulator-github-action@v1
|
||||||
|
- name: End to End Tests
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm start &
|
||||||
|
npm run wait-for-server
|
||||||
|
npx jest -c ./jest.config.e2e.js --detectOpenHandles test/sql/container.spec.ts
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/explorer.html?platform=Emulator"
|
||||||
|
PLATFORM: "Emulator"
|
||||||
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: screenshots
|
||||||
|
path: failed-*
|
||||||
|
endtoend:
|
||||||
|
name: "E2E"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
|
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
test-file:
|
||||||
|
- ./test/cassandra/container.spec.ts
|
||||||
|
- ./test/graph/container.spec.ts
|
||||||
|
- ./test/sql/container.spec.ts
|
||||||
|
- ./test/mongo/container.spec.ts
|
||||||
|
- ./test/mongo/container32.spec.ts
|
||||||
|
- ./test/selfServe/selfServeExample.spec.ts
|
||||||
|
# - ./test/notebooks/upload.spec.ts // TEMP disabled since notebooks service is off
|
||||||
|
- ./test/sql/resourceToken.spec.ts
|
||||||
|
- ./test/tables/container.spec.ts
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Use Node.js 14.x
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 14.x
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm start &
|
||||||
|
- run: npm run wait-for-server
|
||||||
|
- name: ${{ matrix['test-file'] }}
|
||||||
|
run: |
|
||||||
|
# Run tests up to three times
|
||||||
|
for i in $(seq 1 3); do npx jest -c ./jest.config.playwright.js ${{ matrix['test-file'] }} && s=0 && break || s=$? && sleep 1; done; (exit $s)
|
||||||
|
shell: bash
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: screenshots
|
||||||
|
path: screenshots/
|
||||||
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/')
|
||||||
@@ -117,14 +180,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
||||||
- name: Download Dist Folder
|
- name: Download Dist Folder
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
- run: cp ./configs/prod.json config.json
|
- run: cp ./configs/prod.json config.json
|
||||||
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
|
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
|
||||||
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||||
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v2
|
||||||
name: packages
|
name: packages
|
||||||
with:
|
with:
|
||||||
path: "*.nupkg"
|
path: "*.nupkg"
|
||||||
@@ -141,7 +204,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
||||||
- name: Download Dist Folder
|
- name: Download Dist Folder
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist
|
||||||
- run: cp ./configs/mpac.json config.json
|
- run: cp ./configs/mpac.json config.json
|
||||||
@@ -149,74 +212,7 @@ jobs:
|
|||||||
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
|
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
|
||||||
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||||
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v2
|
||||||
name: packages
|
name: packages
|
||||||
with:
|
with:
|
||||||
path: "*.nupkg"
|
path: "*.nupkg"
|
||||||
|
|
||||||
playwright-tests:
|
|
||||||
name: "Run Playwright Tests (Shard ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }})"
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
|
||||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
|
|
||||||
shardTotal: [8]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: "Az CLI login"
|
|
||||||
uses: azure/login@v1
|
|
||||||
with:
|
|
||||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
|
||||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
|
||||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
|
||||||
- name: Use Node.js 18.x
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 18.x
|
|
||||||
- run: npm ci
|
|
||||||
- run: npx playwright install --with-deps
|
|
||||||
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
|
||||||
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
|
||||||
- name: Upload blob report to GitHub Actions Artifacts
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: blob-report-${{ matrix.shardIndex }}
|
|
||||||
path: blob-report
|
|
||||||
retention-days: 1
|
|
||||||
|
|
||||||
merge-playwright-reports:
|
|
||||||
name: "Merge Playwright Reports"
|
|
||||||
# Merge reports after playwright-tests, even if some shards have failed
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
needs: [playwright-tests]
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 18
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Download blob reports from GitHub Actions Artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
path: all-blob-reports
|
|
||||||
pattern: blob-report-*
|
|
||||||
merge-multiple: true
|
|
||||||
|
|
||||||
- name: Merge into HTML Report
|
|
||||||
run: npx playwright merge-reports --reporter html ./all-blob-reports
|
|
||||||
|
|
||||||
- name: Upload HTML report
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: html-report--attempt-${{ github.run_attempt }}
|
|
||||||
path: playwright-report
|
|
||||||
retention-days: 14
|
|
||||||
19
.github/workflows/cleanup.yml
vendored
@@ -9,10 +9,6 @@ on:
|
|||||||
# Once every hour
|
# Once every hour
|
||||||
- cron: "0 15 * * *"
|
- cron: "0 15 * * *"
|
||||||
|
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||||
jobs:
|
jobs:
|
||||||
# This workflow contains a single job called "build"
|
# This workflow contains a single job called "build"
|
||||||
@@ -20,20 +16,13 @@ jobs:
|
|||||||
name: "Cleanup Test Database Accounts"
|
name: "Cleanup Test Database Accounts"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
|
||||||
|
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
- name: Use Node.js 14.x
|
||||||
- name: "Az CLI login"
|
|
||||||
uses: azure/login@v1
|
|
||||||
with:
|
|
||||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
|
||||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
|
||||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
|
||||||
|
|
||||||
- name: Use Node.js 18.x
|
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 14.x
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: node utils/cleanupDBs.js
|
- run: node utils/cleanupDBs.js
|
||||||
|
|||||||
6
.gitignore
vendored
@@ -16,8 +16,4 @@ Contracts/*
|
|||||||
.env
|
.env
|
||||||
failure.png
|
failure.png
|
||||||
screenshots/*
|
screenshots/*
|
||||||
GettingStarted-ignore*.ipynb
|
GettingStarted-ignore*.ipynb
|
||||||
/test-results/
|
|
||||||
/playwright-report/
|
|
||||||
/blob-report/
|
|
||||||
/playwright/.cache/
|
|
||||||
4
.vscode/settings.json
vendored
@@ -20,8 +20,8 @@
|
|||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit",
|
"source.fixAll.eslint": true,
|
||||||
"source.organizeImports": "explicit"
|
"source.organizeImports": true
|
||||||
},
|
},
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com",
|
||||||
"isTerminalEnabled": true,
|
"isTerminalEnabled" : true,
|
||||||
"isPhoenixEnabled": true
|
"isPhoenixEnabled" : true
|
||||||
}
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg id="uuid-f8d4d392-7c12-4bd9-baff-66fbf7814b91" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
|
|
||||||
<path d="m3.802,14.032c.388.242,1.033.511,1.715.511.621,0,1.198-.18,1.676-.487,0,0,.001,0,.002-.001l1.805-1.128v4.073c-.286,0-.574-.078-.824-.234l-4.374-2.734Z" fill="#225086"/>
|
|
||||||
<path d="m7.853,1.507L.353,9.967c-.579.654-.428,1.642.323,2.111,0,0,2.776,1.735,3.126,1.954.388.242,1.033.511,1.715.511.621,0,1.198-.18,1.676-.487,0,0,.001,0,.002-.001l1.805-1.128-4.364-2.728,4.365-4.924V1s0,0,0,0c-.424,0-.847.169-1.147.507Z" fill="#6df"/>
|
|
||||||
<polygon points="4.636 10.199 4.688 10.231 9 12.927 9.001 12.927 9.001 12.927 9.001 5.276 9 5.275 4.636 10.199" fill="#cbf8ff"/>
|
|
||||||
<path d="m17.324,12.078c.751-.469.902-1.457.323-2.111l-4.921-5.551c-.397-.185-.842-.291-1.313-.291-.925,0-1.752.399-2.302,1.026l-.109.123h0s4.364,4.924,4.364,4.924h0s0,0,0,0l-4.365,2.728v4.073c.287,0,.573-.078.823-.234l7.5-4.688Z" fill="#074793"/>
|
|
||||||
<path d="m9.001,1v4.275s.109-.123.109-.123c.55-.627,1.377-1.026,2.302-1.026.472,0,.916.107,1.313.291l-2.579-2.909c-.299-.338-.723-.507-1.146-.507Z" fill="#0294e4"/>
|
|
||||||
<polygon points="13.365 10.199 13.365 10.199 13.365 10.199 9.001 5.276 9.001 12.926 13.365 10.199" fill="#96bcc2"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,3 +0,0 @@
|
|||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M7.31299 1.26164C7.69849 0.897163 8.30151 0.897163 8.68701 1.26164L13.5305 5.84098C13.8302 6.12431 14 6.51853 14 6.93094V12.5002C14 13.3286 13.3284 14.0002 12.5 14.0002H10.5C9.67157 14.0002 9 13.3286 9 12.5002V10.0002C9 9.72407 8.77614 9.50021 8.5 9.50021H7.5C7.22386 9.50021 7 9.72407 7 10.0002V12.5002C7 13.3286 6.32843 14.0002 5.5 14.0002H3.5C2.67157 14.0002 2 13.3286 2 12.5002V6.93094C2 6.51853 2.1698 6.12431 2.46948 5.84098L7.31299 1.26164ZM8 1.98828L3.15649 6.56762C3.0566 6.66207 3 6.79347 3 6.93094V12.5002C3 12.7763 3.22386 13.0002 3.5 13.0002H5.5C5.77614 13.0002 6 12.7763 6 12.5002V10.0002C6 9.17179 6.67157 8.50022 7.5 8.50022H8.5C9.32843 8.50022 10 9.17179 10 10.0002V12.5002C10 12.7763 10.2239 13.0002 10.5 13.0002H12.5C12.7761 13.0002 13 12.7763 13 12.5002V6.93094C13 6.79347 12.9434 6.66207 12.8435 6.56762L8 1.98828Z" fill="#0078D4" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 968 B |
|
Before Width: | Height: | Size: 160 B |
|
Before Width: | Height: | Size: 148 B |
|
Before Width: | Height: | Size: 201 B |
|
Before Width: | Height: | Size: 158 B |
|
Before Width: | Height: | Size: 146 B |
13
jest-playwright.config.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const isCI = require("is-ci");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
exitOnPageError: false,
|
||||||
|
launchOptions: {
|
||||||
|
headless: isCI,
|
||||||
|
slowMo: 10,
|
||||||
|
timeout: 60000,
|
||||||
|
},
|
||||||
|
contextOptions: {
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -31,7 +31,7 @@ module.exports = {
|
|||||||
coveragePathIgnorePatterns: ["/node_modules/"],
|
coveragePathIgnorePatterns: ["/node_modules/"],
|
||||||
|
|
||||||
// A list of reporter names that Jest uses when writing coverage reports
|
// A list of reporter names that Jest uses when writing coverage reports
|
||||||
coverageReporters: ["json", "text", "cobertura", "lcov"],
|
coverageReporters: ["json", "text", "cobertura"],
|
||||||
|
|
||||||
// An object that configures minimum threshold enforcement for coverage results
|
// An object that configures minimum threshold enforcement for coverage results
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
@@ -76,10 +76,6 @@ module.exports = {
|
|||||||
"^dnd-core$": "dnd-core/dist/cjs",
|
"^dnd-core$": "dnd-core/dist/cjs",
|
||||||
"^react-dnd$": "react-dnd/dist/cjs",
|
"^react-dnd$": "react-dnd/dist/cjs",
|
||||||
"^react-dnd-html5-backend$": "react-dnd-html5-backend/dist/cjs",
|
"^react-dnd-html5-backend$": "react-dnd-html5-backend/dist/cjs",
|
||||||
"d3-force": "<rootDir>/node_modules/d3-force/dist/d3-force.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-zoom": "<rootDir>/node_modules/d3-zoom/dist/d3-zoom.min.js",
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 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,6 +130,7 @@ module.exports = {
|
|||||||
|
|
||||||
// The test environment that will be used for testing
|
// The test environment that will be used for testing
|
||||||
// testEnvironment: "jest-environment-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
|
||||||
|
|||||||
7
jest.config.playwright.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
preset: "jest-playwright-preset",
|
||||||
|
testMatch: ["<rootDir>/test/**/*.spec.[jt]s?(x)"],
|
||||||
|
setupFiles: ["dotenv/config"],
|
||||||
|
testEnvironment: "./test/playwrightEnv.js",
|
||||||
|
setupFilesAfterEnv: ["expect-playwright"],
|
||||||
|
};
|
||||||
@@ -130,7 +130,6 @@
|
|||||||
@ActiveTabWidth: 141px;
|
@ActiveTabWidth: 141px;
|
||||||
@TabsHeight: 30px;
|
@TabsHeight: 30px;
|
||||||
@TabsWidth: 140px;
|
@TabsWidth: 140px;
|
||||||
@ContentWrapper: 111px;
|
|
||||||
@StatusIconContainerSize: 18px;
|
@StatusIconContainerSize: 18px;
|
||||||
@LoadingErrorIconSize: 14px;
|
@LoadingErrorIconSize: 14px;
|
||||||
@ErrorIconContainer: 16px;
|
@ErrorIconContainer: 16px;
|
||||||
@@ -148,7 +147,6 @@
|
|||||||
|
|
||||||
// CommandBar
|
// CommandBar
|
||||||
@CommandBarButtonHeight: 40px;
|
@CommandBarButtonHeight: 40px;
|
||||||
@FabricCommandBarButtonHeight: 34px;
|
|
||||||
|
|
||||||
/**********************************************************************************
|
/**********************************************************************************
|
||||||
Portal Consts
|
Portal Consts
|
||||||
@@ -164,10 +162,9 @@
|
|||||||
/**********************************************************************************/
|
/**********************************************************************************/
|
||||||
|
|
||||||
@FabricFont: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
|
@FabricFont: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
|
||||||
@FabricToolbarIconColor: "brightness(0) saturate(100%) invert(50%) sepia(17%) saturate(1459%) hue-rotate(81deg) brightness(99%) contrast(94%)";
|
|
||||||
|
|
||||||
@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: 0 0 2px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.14);
|
||||||
@FabricBoxMargin: 4px 3px 4px 3px;
|
@FabricBoxMargin: 4px 3px 4px 3px;
|
||||||
|
|
||||||
@FabricAccentMediumHigh: #0c695a;
|
@FabricAccentMediumHigh: #0c695a;
|
||||||
@@ -336,11 +333,4 @@
|
|||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
border-color: @InfoPointerColor transparent;
|
border-color: @InfoPointerColor transparent;
|
||||||
}
|
|
||||||
/*********************************************************************************************************
|
|
||||||
Screen Reader Only
|
|
||||||
**********************************************************************************************************/
|
|
||||||
.screenReaderOnly {
|
|
||||||
position: absolute;
|
|
||||||
left: -9999px;
|
|
||||||
}
|
}
|
||||||
@@ -2264,49 +2264,38 @@ a:link {
|
|||||||
width: 82px;
|
width: 82px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// .tabdocuments .scrollable {
|
.tabdocuments .scrollable {
|
||||||
// height: 100%;
|
height: 100%;
|
||||||
// overflow-y: auto;
|
overflow-y: auto;
|
||||||
// overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
// padding-left: 5px;
|
padding-left: 5px;
|
||||||
// padding-right: 5px;
|
padding-right: 5px;
|
||||||
// width: 100%;
|
width: 100%;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .tabdocuments > .tabdocumentsGridElement {
|
.tabdocuments > .tabdocumentsGridElement {
|
||||||
// width: 50%;
|
width: 50%;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .tabdocuments > .evenlySpacedHeader {
|
.tabdocuments > .evenlySpacedHeader {
|
||||||
// width: 30%;
|
width: 30%;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .tabdocuments.scrollable:focus,
|
.tabdocuments.scrollable:focus,
|
||||||
// .tabdocuments.scrollable:active {
|
.tabdocuments.scrollable:active {
|
||||||
// outline: 1px dotted;
|
outline: 1px dotted;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .tabdocuments .scrollable table td {
|
.tabdocuments .scrollable table td {
|
||||||
// white-space: nowrap;
|
white-space: nowrap;
|
||||||
// overflow: hidden;
|
overflow: hidden;
|
||||||
// text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
// }
|
}
|
||||||
|
|
||||||
.mongoDocumentEditor .monaco-editor.vs .redsquiggly {
|
.mongoDocumentEditor .monaco-editor.vs .redsquiggly {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.monaco-editor .quick-input-list-label {
|
|
||||||
/* Restore some of Monaco's default styles that are clobbered by our global styles */
|
|
||||||
padding: 0;
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.monaco-editor .quick-input-list .highlight {
|
|
||||||
/* Padding in highlighted text within the quick input list breaks the flow of the text */
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
td a {
|
td a {
|
||||||
color: #393939;
|
color: #393939;
|
||||||
}
|
}
|
||||||
@@ -2316,9 +2305,10 @@ td a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.loadMore {
|
.loadMore {
|
||||||
display: block;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
padding-left: 30%;
|
||||||
|
padding-top: 2px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loadMore > a:focus {
|
.loadMore > a:focus {
|
||||||
@@ -2366,9 +2356,9 @@ a:link {
|
|||||||
.tabsManagerContainer {
|
.tabsManagerContainer {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: flex;
|
overflow: hidden;
|
||||||
flex-direction: column;
|
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
@@ -2557,12 +2547,10 @@ a:link {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.filterdivs {
|
.filterdivs {
|
||||||
margin: 10px 0px;
|
padding-top: 15px;
|
||||||
|
height: 45px;
|
||||||
|
margin-bottom: 8px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
input {
|
|
||||||
line-height: 14px; // To correct vertical position of the down arrow of the input
|
|
||||||
outline: none; // Remove the dotted border on focus, because fluent has its own focus style (underlined)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.editFilterContainer {
|
.editFilterContainer {
|
||||||
@@ -2579,18 +2567,6 @@ 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;
|
||||||
@@ -2671,7 +2647,7 @@ a:link {
|
|||||||
width: @ActiveTabWidth;
|
width: @ActiveTabWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText {
|
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .tabNavText {
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
border-bottom: 2px solid rgba(0, 120, 212, 1);
|
border-bottom: 2px solid rgba(0, 120, 212, 1);
|
||||||
}
|
}
|
||||||
@@ -2707,71 +2683,67 @@ a:link {
|
|||||||
width: @TabsWidth;
|
width: @TabsWidth;
|
||||||
border-right: @ButtonBorderWidth solid @BaseMedium;
|
border-right: @ButtonBorderWidth solid @BaseMedium;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
.contentWrapper {
|
|
||||||
.flex-display();
|
|
||||||
width: @ContentWrapper;
|
|
||||||
|
|
||||||
.statusIconContainer {
|
.statusIconContainer {
|
||||||
width: @StatusIconContainerSize;
|
width: @StatusIconContainerSize;
|
||||||
height: @StatusIconContainerSize;
|
height: @StatusIconContainerSize;
|
||||||
margin-left: @SmallSpace;
|
margin-left: @SmallSpace;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
||||||
.errorIconContainer {
|
.errorIconContainer {
|
||||||
width: @ErrorIconContainer;
|
width: @ErrorIconContainer;
|
||||||
height: @ErrorIconContainer;
|
height: @ErrorIconContainer;
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
|
|
||||||
.errorIcon {
|
.errorIcon {
|
||||||
width: @ErrorIconWidth;
|
width: @ErrorIconWidth;
|
||||||
height: @LoadingErrorIconSize;
|
|
||||||
background-image: url(../images/error_no_outline.svg);
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center;
|
|
||||||
background-size: 3px;
|
|
||||||
display: block;
|
|
||||||
margin: 1px 0px 0px 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.errorIconContainer.actionsEnabled {
|
|
||||||
&:hover {
|
|
||||||
.hover();
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
.active();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.errorIconContainer[tabindex]:active {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loadingIcon {
|
|
||||||
width: @LoadingErrorIconSize;
|
|
||||||
height: @LoadingErrorIconSize;
|
height: @LoadingErrorIconSize;
|
||||||
margin: 0px 0px @SmallSpace @SmallSpace;
|
background-image: url(../images/error_no_outline.svg);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-size: 3px;
|
||||||
|
display: block;
|
||||||
|
margin: 1px 0px 0px 6px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabNavText {
|
.errorIconContainer.actionsEnabled {
|
||||||
margin-left: @SmallSpace;
|
&:hover {
|
||||||
margin-right: 2px;
|
.hover();
|
||||||
color: @BaseDark;
|
}
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
&:focus {
|
||||||
white-space: nowrap;
|
.focus();
|
||||||
flex-grow: 1;
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
.active();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorIconContainer[tabindex]:active {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingIcon {
|
||||||
|
width: @LoadingErrorIconSize;
|
||||||
|
height: @LoadingErrorIconSize;
|
||||||
|
margin: 0px 0px @SmallSpace @SmallSpace;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tabNavText {
|
||||||
|
margin-left: @SmallSpace;
|
||||||
|
margin-right: 2px;
|
||||||
|
color: @BaseDark;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.tabIconSection {
|
.tabIconSection {
|
||||||
width: 29px;
|
width: 30px;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
|
|
||||||
@@ -2925,21 +2897,9 @@ a:link {
|
|||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settingsSectionInlineCheckbox {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settingsSectionLabel {
|
.settingsSectionLabel {
|
||||||
margin-bottom: @DefaultSpace;
|
margin-bottom: @DefaultSpace;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
|
||||||
.panelInfoIcon {
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pageOptionsPart {
|
.pageOptionsPart {
|
||||||
|
|||||||
@@ -25,38 +25,33 @@ a:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.resourceTreeAndTabs {
|
.resourceTreeAndTabs {
|
||||||
border-radius: 0px;
|
border-radius: @FabricBoxBorderRadius;
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
margin: @FabricBoxMargin;
|
margin: @FabricBoxMargin;
|
||||||
margin-top: 0px;
|
margin-top: 4px;
|
||||||
margin-bottom: 0px;
|
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabsManagerContainer {
|
.tabsManagerContainer {
|
||||||
background-color: #ffffff
|
background-color: #fafafa
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs-margin {
|
.nav-tabs-margin {
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
background-color: #ffffff
|
background-color: #fafafa
|
||||||
}
|
}
|
||||||
|
|
||||||
.commandBarContainer {
|
.commandBarContainer {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border-radius: @FabricBoxBorderRadius @FabricBoxBorderRadius 0px 0px;
|
border-bottom: none;
|
||||||
|
border-radius: @FabricBoxBorderRadius;
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
margin: @FabricBoxMargin;
|
margin: @FabricBoxMargin;
|
||||||
margin-top: 0px;
|
|
||||||
margin-bottom: 0px;
|
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
padding: 0px;
|
|
||||||
height: 40px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dividerContainer {
|
.dividerContainer {
|
||||||
padding: @SmallSpace 0px @SmallSpace 0px;
|
padding: @SmallSpace 0px @SmallSpace 0px;
|
||||||
height: @FabricCommandBarButtonHeight;
|
|
||||||
.flex-display();
|
.flex-display();
|
||||||
|
|
||||||
span {
|
span {
|
||||||
@@ -75,7 +70,7 @@ a:focus {
|
|||||||
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>.tabNavText {
|
||||||
border-bottom: 0px none transparent;
|
border-bottom: 0px none transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,11 +88,9 @@ a:focus {
|
|||||||
width: calc(@TabsWidth - (@SmallSpace * 2));
|
width: calc(@TabsWidth - (@SmallSpace * 2));
|
||||||
padding-bottom: @SmallSpace;
|
padding-bottom: @SmallSpace;
|
||||||
|
|
||||||
.contentWrapper {
|
.statusIconContainer {
|
||||||
.statusIconContainer {
|
margin-left: 0px;
|
||||||
margin-left: 0px;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabIconSection {
|
.tabIconSection {
|
||||||
.cancelButton {
|
.cancelButton {
|
||||||
@@ -165,10 +158,9 @@ a:focus {
|
|||||||
|
|
||||||
|
|
||||||
.dataExplorerErrorConsoleContainer {
|
.dataExplorerErrorConsoleContainer {
|
||||||
border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius;
|
border-radius: @FabricBoxBorderRadius;
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
margin: @FabricBoxMargin;
|
margin: @FabricBoxMargin;
|
||||||
margin-top: 0px;
|
|
||||||
width: auto;
|
width: auto;
|
||||||
align-self: auto;
|
align-self: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
52811
package-lock.json
generated
116
package.json
@@ -5,19 +5,19 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/arm-cosmosdb": "9.1.0",
|
"@azure/arm-cosmosdb": "9.1.0",
|
||||||
"@azure/cosmos": "4.0.1-beta.3",
|
"@azure/cosmos": "4.0.0",
|
||||||
"@azure/cosmos-language-service": "0.0.5",
|
"@azure/cosmos-language-service": "0.0.5",
|
||||||
"@azure/identity": "1.5.2",
|
"@azure/identity": "1.2.1",
|
||||||
"@azure/ms-rest-nodeauth": "3.1.1",
|
"@azure/ms-rest-nodeauth": "3.0.7",
|
||||||
"@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.112.1",
|
"@fluentui/react": "8.14.3",
|
||||||
"@fluentui/react-components": "9.34.0",
|
"@fluentui/react-components": "9.32.1",
|
||||||
"@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.4.2",
|
||||||
"@nteract/connected-components": "6.8.2",
|
"@nteract/connected-components": "6.8.2",
|
||||||
"@nteract/core": "15.1.0",
|
"@nteract/core": "15.1.0",
|
||||||
"@nteract/data-explorer": "8.0.3",
|
"@nteract/data-explorer": "8.0.3",
|
||||||
@@ -46,44 +46,39 @@
|
|||||||
"@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",
|
|
||||||
"applicationinsights": "1.8.0",
|
"applicationinsights": "1.8.0",
|
||||||
"bootstrap": "3.4.1",
|
"bootstrap": "3.4.1",
|
||||||
"canvas": "file:./canvas",
|
"canvas": "file:./canvas",
|
||||||
"clean-webpack-plugin": "4.0.0",
|
"clean-webpack-plugin": "3.0.0",
|
||||||
"clipboard-copy": "4.0.1",
|
"clipboard-copy": "4.0.1",
|
||||||
"copy-webpack-plugin": "11.0.0",
|
"copy-webpack-plugin": "9.0.1",
|
||||||
"crossroads": "0.12.2",
|
"crossroads": "0.12.2",
|
||||||
"css-element-queries": "1.1.1",
|
"css-element-queries": "1.1.1",
|
||||||
"d3": "7.8.5",
|
"d3": "6.1.1",
|
||||||
"datatables.net-colreorder-dt": "1.7.0",
|
"datatables.net-colreorder-dt": "1.5.1",
|
||||||
"datatables.net-dt": "1.13.8",
|
"datatables.net-dt": "1.10.19",
|
||||||
"date-fns": "1.29.0",
|
"date-fns": "1.29.0",
|
||||||
"dayjs": "1.8.19",
|
"dayjs": "1.8.19",
|
||||||
"dom-to-image": "2.6.0",
|
"dom-to-image": "2.6.0",
|
||||||
"dotenv": "8.2.0",
|
"dotenv": "8.2.0",
|
||||||
"eslint-plugin-jest": "27.4.2",
|
"eslint-plugin-jest": "23.13.2",
|
||||||
"eslint-plugin-react": "7.33.2",
|
"eslint-plugin-react": "7.20.0",
|
||||||
"hasher": "1.2.0",
|
"hasher": "1.2.0",
|
||||||
"html2canvas": "1.0.0-rc.5",
|
"html2canvas": "1.0.0-rc.5",
|
||||||
"i18next": "19.8.4",
|
"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",
|
||||||
"immer": "9.0.6",
|
|
||||||
"immutable": "4.0.0-rc.12",
|
"immutable": "4.0.0-rc.12",
|
||||||
"is-ci": "2.0.0",
|
"is-ci": "2.0.0",
|
||||||
"jquery": "3.7.1",
|
"jquery": "3.5.1",
|
||||||
"jquery-typeahead": "2.11.1",
|
"jquery-typeahead": "2.10.6",
|
||||||
"jquery-ui-dist": "1.13.2",
|
"jquery-ui-dist": "1.12.1",
|
||||||
"knockout": "3.5.1",
|
"knockout": "3.5.1",
|
||||||
"loader-utils": "2.0.3",
|
|
||||||
"mkdirp": "1.0.4",
|
"mkdirp": "1.0.4",
|
||||||
"monaco-editor": "0.44.0",
|
"monaco-editor": "0.18.1",
|
||||||
"ms": "2.1.3",
|
"ms": "2.1.3",
|
||||||
"p-retry": "4.6.2",
|
"p-retry": "4.2.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",
|
||||||
"q": "1.5.1",
|
"q": "1.5.1",
|
||||||
@@ -91,7 +86,7 @@
|
|||||||
"react-animate-height": "2.0.8",
|
"react-animate-height": "2.0.8",
|
||||||
"react-dnd": "14.0.2",
|
"react-dnd": "14.0.2",
|
||||||
"react-dnd-html5-backend": "14.0.0",
|
"react-dnd-html5-backend": "14.0.0",
|
||||||
"react-dom": "16.14.0",
|
"react-dom": "16.13.1",
|
||||||
"react-hotkeys": "2.0.0",
|
"react-hotkeys": "2.0.0",
|
||||||
"react-i18next": "11.8.5",
|
"react-i18next": "11.8.5",
|
||||||
"react-notification-system": "0.2.17",
|
"react-notification-system": "0.2.17",
|
||||||
@@ -99,16 +94,15 @@
|
|||||||
"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-youtube": "9.0.1",
|
"react-youtube": "9.0.1",
|
||||||
"react-window": "1.8.10",
|
"redux": "4.0.4",
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
"rx-jupyter": "5.5.12",
|
"rx-jupyter": "5.5.12",
|
||||||
|
"rxjs": "6.6.3",
|
||||||
"sanitize-html": "2.3.3",
|
"sanitize-html": "2.3.3",
|
||||||
"shell-quote": "1.7.3",
|
"styled-components": "4.3.2",
|
||||||
"styled-components": "5.0.1",
|
|
||||||
"swr": "0.4.0",
|
"swr": "0.4.0",
|
||||||
"terser-webpack-plugin": "5.3.9",
|
"terser-webpack-plugin": "5.1.4",
|
||||||
"tinykeys": "2.1.0",
|
"underscore": "1.9.1",
|
||||||
"underscore": "1.12.1",
|
|
||||||
"utility-types": "3.10.0",
|
"utility-types": "3.10.0",
|
||||||
"zustand": "3.5.0"
|
"zustand": "3.5.0"
|
||||||
},
|
},
|
||||||
@@ -117,20 +111,16 @@
|
|||||||
"@babel/preset-env": "7.9.0",
|
"@babel/preset-env": "7.9.0",
|
||||||
"@babel/preset-react": "7.9.4",
|
"@babel/preset-react": "7.9.4",
|
||||||
"@babel/preset-typescript": "7.9.0",
|
"@babel/preset-typescript": "7.9.0",
|
||||||
"@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",
|
||||||
"@types/crossroads": "0.0.30",
|
"@types/crossroads": "0.0.30",
|
||||||
"@types/d3": "5.9.2",
|
"@types/d3": "5.9.2",
|
||||||
"@types/datatables.net": "1.10.28",
|
|
||||||
"@types/datatables.net-colreorder": "1.4.5",
|
|
||||||
"@types/dom-to-image": "2.6.2",
|
"@types/dom-to-image": "2.6.2",
|
||||||
"@types/enzyme": "3.10.12",
|
"@types/enzyme": "3.10.7",
|
||||||
"@types/enzyme-adapter-react-16": "1.0.9",
|
"@types/enzyme-adapter-react-16": "1.0.6",
|
||||||
"@types/hasher": "0.0.31",
|
"@types/hasher": "0.0.31",
|
||||||
"@types/jest": "26.0.20",
|
"@types/jest": "26.0.20",
|
||||||
"@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",
|
||||||
@@ -139,65 +129,66 @@
|
|||||||
"@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",
|
||||||
"@types/react-window": "1.8.8",
|
|
||||||
"@types/sanitize-html": "1.27.2",
|
"@types/sanitize-html": "1.27.2",
|
||||||
"@types/sinon": "2.3.3",
|
"@types/sinon": "2.3.3",
|
||||||
"@types/styled-components": "5.1.1",
|
"@types/styled-components": "5.1.1",
|
||||||
"@types/underscore": "1.7.36",
|
"@types/underscore": "1.7.36",
|
||||||
"@types/youtube-player": "5.5.6",
|
"@types/youtube-player": "5.5.6",
|
||||||
"@typescript-eslint/eslint-plugin": "6.7.4",
|
"@typescript-eslint/eslint-plugin": "4.22.0",
|
||||||
"@typescript-eslint/parser": "6.7.4",
|
"@typescript-eslint/parser": "4.22.0",
|
||||||
"@webpack-cli/serve": "2.0.5",
|
"@webpack-cli/serve": "1.5.2",
|
||||||
"babel-jest": "24.9.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.3.0",
|
||||||
"create-file-webpack": "1.0.2",
|
"create-file-webpack": "1.0.2",
|
||||||
"css-loader": "6.8.1",
|
"css-loader": "1.0.0",
|
||||||
"enzyme": "3.11.0",
|
"enzyme": "3.11.0",
|
||||||
"enzyme-adapter-react-16": "1.15.8",
|
"enzyme-adapter-react-16": "1.15.5",
|
||||||
"enzyme-to-json": "3.6.2",
|
"enzyme-to-json": "3.6.1",
|
||||||
"eslint": "8.50.0",
|
"eslint": "7.8.1",
|
||||||
"eslint-cli": "1.1.1",
|
"eslint-cli": "1.1.1",
|
||||||
"eslint-plugin-no-null": "1.0.2",
|
"eslint-plugin-no-null": "1.0.2",
|
||||||
"eslint-plugin-prefer-arrow": "1.2.3",
|
"eslint-plugin-prefer-arrow": "1.2.2",
|
||||||
"eslint-plugin-react-hooks": "4.6.0",
|
"eslint-plugin-react-hooks": "4.2.0",
|
||||||
|
"expect-playwright": "0.3.3",
|
||||||
"fast-glob": "3.2.5",
|
"fast-glob": "3.2.5",
|
||||||
|
"file-loader": "2.0.0",
|
||||||
"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.0",
|
||||||
"html-loader": "0.5.5",
|
"html-loader": "0.5.5",
|
||||||
"html-loader-jest": "0.2.1",
|
"html-loader-jest": "0.2.1",
|
||||||
"html-webpack-plugin": "5.5.3",
|
"html-webpack-plugin": "5.3.2",
|
||||||
"jest": "26.6.3",
|
"jest": "26.6.3",
|
||||||
"jest-canvas-mock": "2.3.1",
|
"jest-canvas-mock": "2.3.1",
|
||||||
|
"jest-playwright-preset": "1.5.1",
|
||||||
"jest-react-hooks-shallow": "1.5.1",
|
"jest-react-hooks-shallow": "1.5.1",
|
||||||
"jest-trx-results-processor": "0.0.7",
|
"jest-trx-results-processor": "0.0.7",
|
||||||
"less": "3.8.1",
|
"less": "3.8.1",
|
||||||
"less-loader": "11.1.3",
|
"less-loader": "4.1.0",
|
||||||
"less-vars-loader": "1.1.0",
|
"less-vars-loader": "1.1.0",
|
||||||
"mini-css-extract-plugin": "2.1.0",
|
"mini-css-extract-plugin": "2.1.0",
|
||||||
"monaco-editor-webpack-plugin": "7.1.0",
|
"monaco-editor-webpack-plugin": "1.7.0",
|
||||||
"node-fetch": "2.6.7",
|
"node-fetch": "2.6.1",
|
||||||
|
"playwright": "1.13.0",
|
||||||
"prettier": "3.0.3",
|
"prettier": "3.0.3",
|
||||||
"process": "0.11.10",
|
"process": "0.11.10",
|
||||||
"querystring-es3": "0.2.1",
|
|
||||||
"raw-loader": "0.5.1",
|
"raw-loader": "0.5.1",
|
||||||
"react-dev-utils": "12.0.1",
|
"react-dev-utils": "11.0.4",
|
||||||
"rimraf": "3.0.0",
|
"rimraf": "3.0.0",
|
||||||
"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.22.15",
|
"typedoc": "0.20.36",
|
||||||
"typescript": "4.3.5",
|
"typescript": "4.3.4",
|
||||||
"url-loader": "4.1.1",
|
"url-loader": "1.1.1",
|
||||||
"wait-on": "4.0.2",
|
"wait-on": "4.0.2",
|
||||||
"webpack": "5.88.2",
|
"webpack": "5.47.0",
|
||||||
"webpack-bundle-analyzer": "4.9.1",
|
"webpack-bundle-analyzer": "4.4.2",
|
||||||
"webpack-cli": "5.1.4",
|
"webpack-cli": "4.7.2",
|
||||||
"webpack-dev-server": "4.15.2"
|
"webpack-dev-server": "3.11.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "patch-package",
|
|
||||||
"start": "webpack serve --mode development",
|
"start": "webpack serve --mode development",
|
||||||
"dev": "echo \"WARNING: npm run dev has been deprecated\" && npm run build",
|
"dev": "echo \"WARNING: npm run dev has been deprecated\" && npm run build",
|
||||||
"build:dataExplorer:ci": "npm run build:ci",
|
"build:dataExplorer:ci": "npm run build:ci",
|
||||||
@@ -209,7 +200,6 @@
|
|||||||
"test": "rimraf coverage && jest",
|
"test": "rimraf coverage && jest",
|
||||||
"test:debug": "jest --runInBand",
|
"test:debug": "jest --runInBand",
|
||||||
"test:e2e": "jest -c ./jest.config.playwright.js --detectOpenHandles",
|
"test:e2e": "jest -c ./jest.config.playwright.js --detectOpenHandles",
|
||||||
"test:file": "jest --coverage=false",
|
|
||||||
"watch": "npm run start",
|
"watch": "npm run start",
|
||||||
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
|
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
|
||||||
"build:ase": "gulp build:ase",
|
"build:ase": "gulp build:ase",
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
diff --git a/node_modules/datatables.net-colreorder/types/types.d.ts b/node_modules/datatables.net-colreorder/types/types.d.ts
|
|
||||||
index e5dc283..1930c2b 100644
|
|
||||||
--- a/node_modules/datatables.net-colreorder/types/types.d.ts
|
|
||||||
+++ b/node_modules/datatables.net-colreorder/types/types.d.ts
|
|
||||||
@@ -7,7 +7,7 @@
|
|
||||||
|
|
||||||
/// <reference types="jquery" />
|
|
||||||
|
|
||||||
-import DataTables, {Api} from 'datatables.net';
|
|
||||||
+import DataTables, { Api } from 'datatables.net';
|
|
||||||
|
|
||||||
export default DataTables;
|
|
||||||
|
|
||||||
@@ -40,6 +40,8 @@ declare module 'datatables.net' {
|
|
||||||
/**
|
|
||||||
* Create a new ColReorder instance for the target DataTable
|
|
||||||
*/
|
|
||||||
+ // Ignore this error: error TS7013: Construct signature, which lacks return-type annotation, implicitly has an 'any' return type.
|
|
||||||
+ // @ts-ignore
|
|
||||||
new (dt: Api<any>, settings: boolean | ConfigColReorder);
|
|
||||||
|
|
||||||
/**
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* See https://playwright.dev/docs/test-configuration.
|
|
||||||
*/
|
|
||||||
export default defineConfig({
|
|
||||||
testDir: 'test',
|
|
||||||
fullyParallel: true,
|
|
||||||
forbidOnly: !!process.env.CI,
|
|
||||||
retries: process.env.CI ? 3 : 0,
|
|
||||||
workers: process.env.CI ? 1 : undefined,
|
|
||||||
reporter: process.env.CI ? 'blob' : 'html',
|
|
||||||
timeout: 10 * 60 * 1000,
|
|
||||||
use: {
|
|
||||||
actionTimeout: 5 * 60 * 1000,
|
|
||||||
trace: 'off',
|
|
||||||
video: 'off',
|
|
||||||
screenshot: 'on',
|
|
||||||
testIdAttribute: 'data-test',
|
|
||||||
contextOptions: {
|
|
||||||
ignoreHTTPSErrors: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
expect: {
|
|
||||||
timeout: 5 * 60 * 1000,
|
|
||||||
},
|
|
||||||
|
|
||||||
projects: [
|
|
||||||
{
|
|
||||||
name: 'chromium',
|
|
||||||
use: { ...devices['Desktop Chrome'] },
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: 'firefox',
|
|
||||||
use: { ...devices['Desktop Firefox'] },
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: 'webkit',
|
|
||||||
use: { ...devices['Desktop Safari'] },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
webServer: {
|
|
||||||
command: 'npm run start',
|
|
||||||
url: 'https://127.0.0.1:1234/_ready',
|
|
||||||
timeout: 120 * 1000,
|
|
||||||
ignoreHTTPSErrors: true,
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -88,12 +88,6 @@ export class CapabilityNames {
|
|||||||
public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics";
|
public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics";
|
||||||
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";
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum CapacityMode {
|
|
||||||
Provisioned = "Provisioned",
|
|
||||||
Serverless = "Serverless",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// flight names returned from the portal are always lowercase
|
// flight names returned from the portal are always lowercase
|
||||||
@@ -130,36 +124,7 @@ export enum MongoBackendEndpointType {
|
|||||||
remote,
|
remote,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BackendApi {
|
// TODO: 435619 Add default endpoints per cloud and use regional only when available
|
||||||
public static readonly GenerateToken: string = "GenerateToken";
|
|
||||||
public static readonly PortalSettings: string = "PortalSettings";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PortalBackendEndpoints {
|
|
||||||
public static readonly Development: string = "https://localhost:7235";
|
|
||||||
public static readonly Mpac: string = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
|
|
||||||
public static readonly Prod: string = "https://cdb-ms-prod-pbe.cosmos.azure.com";
|
|
||||||
public static readonly Fairfax: string = "https://cdb-ff-prod-pbe.cosmos.azure.us";
|
|
||||||
public static readonly Mooncake: string = "https://cdb-mc-prod-pbe.cosmos.azure.cn";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MongoProxyEndpoints {
|
|
||||||
public static readonly Local: string = "https://localhost:7238";
|
|
||||||
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 Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
|
|
||||||
public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CassandraProxyEndpoints {
|
|
||||||
public static readonly Development: string = "https://localhost:7240";
|
|
||||||
public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com";
|
|
||||||
public static readonly Prod: string = "https://cdb-ms-prod-cp.cosmos.azure.com";
|
|
||||||
public static readonly Fairfax: string = "https://cdb-ff-prod-cp.cosmos.azure.us";
|
|
||||||
public static readonly Mooncake: string = "https://cdb-mc-prod-cp.cosmos.azure.cn";
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: Remove this when new backend is migrated over
|
|
||||||
export class CassandraBackend {
|
export class CassandraBackend {
|
||||||
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
||||||
public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete";
|
public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete";
|
||||||
@@ -171,17 +136,6 @@ export class CassandraBackend {
|
|||||||
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
|
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CassandraProxyAPIs {
|
|
||||||
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
|
||||||
public static readonly connectionStringCreateOrDeleteApi: string = "api/connectionstring/cassandra/createordelete";
|
|
||||||
public static readonly queryApi: string = "api/cassandra";
|
|
||||||
public static readonly connectionStringQueryApi: string = "api/connectionstring/cassandra";
|
|
||||||
public static readonly keysApi: string = "api/cassandra/keys";
|
|
||||||
public static readonly connectionStringKeysApi: string = "api/connectionstring/cassandra/keys";
|
|
||||||
public static readonly schemaApi: string = "api/cassandra/schema";
|
|
||||||
public static readonly connectionStringSchemaApi: string = "api/connectionstring/cassandra/schema";
|
|
||||||
}
|
|
||||||
|
|
||||||
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";
|
||||||
@@ -191,15 +145,6 @@ export class Queries {
|
|||||||
public static QueryEditorMinHeightRatio: number = 0.1;
|
public static QueryEditorMinHeightRatio: number = 0.1;
|
||||||
public static QueryEditorMaxHeightRatio: number = 0.4;
|
public static QueryEditorMaxHeightRatio: number = 0.4;
|
||||||
public static readonly DefaultMaxDegreeOfParallelism = 6;
|
public static readonly DefaultMaxDegreeOfParallelism = 6;
|
||||||
public static readonly DefaultRetryAttempts = 9;
|
|
||||||
public static readonly DefaultRetryIntervalInMs = 0;
|
|
||||||
public static readonly DefaultMaxWaitTimeInSeconds = 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RBACOptions {
|
|
||||||
public static setAutomaticRBACOption: string = "Automatic";
|
|
||||||
public static setTrueRBACOption: string = "True";
|
|
||||||
public static setFalseRBACOption: string = "False";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SavedQueries {
|
export class SavedQueries {
|
||||||
@@ -261,11 +206,6 @@ export class HttpHeaders {
|
|||||||
public static partitionKey: string = "x-ms-documentdb-partitionkey";
|
public static partitionKey: string = "x-ms-documentdb-partitionkey";
|
||||||
public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput";
|
public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput";
|
||||||
public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot";
|
public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot";
|
||||||
public static xAPIKey: string = "X-API-Key";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ContentType {
|
|
||||||
public static applicationJson: string = "application/json";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ApiType {
|
export class ApiType {
|
||||||
|
|||||||
@@ -28,6 +28,19 @@ describe("tokenProvider", () => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("calls the auth token service if no master key is set", async () => {
|
||||||
|
await tokenProvider(options);
|
||||||
|
expect((window.fetch as any).mock.calls.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not call the auth service if a master key is set", async () => {
|
||||||
|
updateUserContext({
|
||||||
|
masterKey: "foo",
|
||||||
|
});
|
||||||
|
await tokenProvider(options);
|
||||||
|
expect((window.fetch as any).mock.calls.length).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getTokenFromAuthService", () => {
|
describe("getTokenFromAuthService", () => {
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import * as Cosmos from "@azure/cosmos";
|
import * as Cosmos from "@azure/cosmos";
|
||||||
|
import { sendCachedDataMessage } from "Common/MessageHandler";
|
||||||
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
|
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
|
||||||
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
import { AuthorizationToken, MessageTypes } from "Contracts/MessageTypes";
|
||||||
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
|
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
|
||||||
import { listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
|
||||||
import { DatabaseAccountListKeysResult } from "Utils/arm/generatedClients/cosmos/types";
|
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { PriorityLevel } from "../Common/Constants";
|
import { PriorityLevel } from "../Common/Constants";
|
||||||
import { Platform, configContext } from "../ConfigContext";
|
import { Platform, configContext } from "../ConfigContext";
|
||||||
import { updateUserContext, userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
||||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||||
@@ -19,16 +17,7 @@ const _global = typeof self === "undefined" ? window : self;
|
|||||||
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||||
const { verb, resourceId, resourceType, headers } = requestInfo;
|
const { verb, resourceId, resourceType, headers } = requestInfo;
|
||||||
|
|
||||||
const aadDataPlaneFeatureEnabled =
|
if (userContext.features.enableAadDataPlane && userContext.aadToken) {
|
||||||
userContext.features.enableAadDataPlane && userContext.databaseAccount.properties.disableLocalAuth;
|
|
||||||
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && userContext.apiType === "SQL";
|
|
||||||
if (aadDataPlaneFeatureEnabled || (!userContext.features.enableAadDataPlane && dataPlaneRBACOptionEnabled)) {
|
|
||||||
if (!userContext.aadToken) {
|
|
||||||
logConsoleError(
|
|
||||||
`AAD token does not exist. Please use "Login for Entra ID" prior to performing Entra ID RBAC operations`,
|
|
||||||
);
|
|
||||||
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}`;
|
||||||
return authorizationToken;
|
return authorizationToken;
|
||||||
@@ -50,10 +39,9 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
case Cosmos.ResourceType.item:
|
case Cosmos.ResourceType.item:
|
||||||
case Cosmos.ResourceType.pkranges:
|
case Cosmos.ResourceType.pkranges:
|
||||||
// User resource tokens
|
// User resource tokens
|
||||||
// TODO userContext.fabricContext.databaseConnectionInfo can be undefined
|
|
||||||
headers[HttpHeaders.msDate] = new Date().toUTCString();
|
headers[HttpHeaders.msDate] = new Date().toUTCString();
|
||||||
const resourceTokens = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
const resourceTokens = userContext.fabricDatabaseConnectionInfo.resourceTokens;
|
||||||
checkDatabaseResourceTokensValidity(userContext.fabricContext.databaseConnectionInfo.resourceTokensTimestamp);
|
checkDatabaseResourceTokensValidity(userContext.fabricDatabaseConnectionInfo.resourceTokensTimestamp);
|
||||||
return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
|
return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
|
||||||
|
|
||||||
case Cosmos.ResourceType.none:
|
case Cosmos.ResourceType.none:
|
||||||
@@ -61,52 +49,20 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
case Cosmos.ResourceType.offer:
|
case Cosmos.ResourceType.offer:
|
||||||
case Cosmos.ResourceType.user:
|
case Cosmos.ResourceType.user:
|
||||||
case Cosmos.ResourceType.permission:
|
case Cosmos.ResourceType.permission:
|
||||||
// For now, these operations aren't used, so fetching the authorization token is commented out.
|
// User master tokens
|
||||||
// This provider must return a real token to pass validation by the client, so we return the cached resource token
|
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(MessageTypes.GetAuthorizationToken, [
|
||||||
// (which is a valid token, but won't work for these operations).
|
requestInfo,
|
||||||
const resourceTokens2 = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
]);
|
||||||
return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId);
|
console.log("Response from Fabric: ", authorizationToken);
|
||||||
|
headers[HttpHeaders.msDate] = authorizationToken.XDate;
|
||||||
/* ************** TODO: Uncomment this code if we need to support these operations **************
|
return decodeURIComponent(authorizationToken.PrimaryReadWriteToken);
|
||||||
// User master tokens
|
|
||||||
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(
|
|
||||||
FabricMessageTypes.GetAuthorizationToken,
|
|
||||||
[requestInfo],
|
|
||||||
userContext.fabricContext.connectionId,
|
|
||||||
);
|
|
||||||
console.log("Response from Fabric: ", authorizationToken);
|
|
||||||
headers[HttpHeaders.msDate] = authorizationToken.XDate;
|
|
||||||
return decodeURIComponent(authorizationToken.PrimaryReadWriteToken);
|
|
||||||
***********************************************************************************************/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userContext.masterKey) {
|
if (userContext.masterKey) {
|
||||||
// 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, resourceId, resourceType, headers, EmulatorMasterKey);
|
||||||
verb,
|
|
||||||
resourceId,
|
|
||||||
resourceType,
|
|
||||||
headers,
|
|
||||||
userContext.masterKey,
|
|
||||||
);
|
|
||||||
return decodeURIComponent(headers.authorization);
|
return decodeURIComponent(headers.authorization);
|
||||||
} else if (userContext.dataPlaneRbacEnabled == false) {
|
|
||||||
const { databaseAccount: account, subscriptionId, resourceGroup } = userContext;
|
|
||||||
const keys: DatabaseAccountListKeysResult = await listKeys(subscriptionId, resourceGroup, account.name);
|
|
||||||
|
|
||||||
if (keys.primaryMasterKey) {
|
|
||||||
updateUserContext({ masterKey: keys.primaryMasterKey });
|
|
||||||
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
|
||||||
await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(
|
|
||||||
verb,
|
|
||||||
resourceId,
|
|
||||||
resourceType,
|
|
||||||
headers,
|
|
||||||
keys.primaryMasterKey,
|
|
||||||
);
|
|
||||||
return decodeURIComponent(headers.authorization);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userContext.resourceToken) {
|
if (userContext.resourceToken) {
|
||||||
@@ -190,16 +146,10 @@ export function client(): Cosmos.CosmosClient {
|
|||||||
|
|
||||||
const options: Cosmos.CosmosClientOptions = {
|
const options: Cosmos.CosmosClientOptions = {
|
||||||
endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
|
endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
|
||||||
|
key: userContext.masterKey,
|
||||||
tokenProvider,
|
tokenProvider,
|
||||||
userAgentSuffix: "Azure Portal",
|
userAgentSuffix: "Azure Portal",
|
||||||
defaultHeaders: _defaultHeaders,
|
defaultHeaders: _defaultHeaders,
|
||||||
connectionPolicy: {
|
|
||||||
retryOptions: {
|
|
||||||
maxRetryAttemptCount: LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts),
|
|
||||||
fixedRetryIntervalInMilliseconds: LocalStorageUtility.getEntryNumber(StorageKey.RetryInterval),
|
|
||||||
maxWaitTimeInSeconds: LocalStorageUtility.getEntryNumber(StorageKey.MaxWaitTimeInSeconds),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (configContext.PROXY_PATH !== undefined) {
|
if (configContext.PROXY_PATH !== undefined) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
|
||||||
export const getEntityName = (multiple?: boolean): string => {
|
export const getEntityName = (): string => {
|
||||||
if (userContext.apiType === "Mongo") {
|
if (userContext.apiType === "Mongo") {
|
||||||
return multiple ? "documents" : "document";
|
return "document";
|
||||||
}
|
}
|
||||||
|
|
||||||
return multiple ? "items" : "item";
|
return "item";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { QueryOperationOptions } from "@azure/cosmos";
|
|
||||||
import { QueryResults } from "../Contracts/ViewModels";
|
import { QueryResults } from "../Contracts/ViewModels";
|
||||||
|
|
||||||
interface QueryResponse {
|
interface QueryResponse {
|
||||||
@@ -11,17 +10,13 @@ interface QueryResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface MinimalQueryIterator {
|
export interface MinimalQueryIterator {
|
||||||
fetchNext: (queryOperationOptions?: QueryOperationOptions) => Promise<QueryResponse>;
|
fetchNext: () => Promise<QueryResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pick<QueryIterator<any>, "fetchNext">;
|
// Pick<QueryIterator<any>, "fetchNext">;
|
||||||
|
|
||||||
export function nextPage(
|
export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> {
|
||||||
documentsIterator: MinimalQueryIterator,
|
return documentsIterator.fetchNext().then((response) => {
|
||||||
firstItemIndex: number,
|
|
||||||
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
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
jest.mock("./MessageHandler");
|
jest.mock("./MessageHandler");
|
||||||
|
import { LogEntryLevel } from "../Contracts/Diagnostics";
|
||||||
import * as Logger from "./Logger";
|
import * as Logger from "./Logger";
|
||||||
|
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||||
import { sendMessage } from "./MessageHandler";
|
import { sendMessage } from "./MessageHandler";
|
||||||
|
|
||||||
describe("Logger", () => {
|
describe("Logger", () => {
|
||||||
@@ -9,16 +11,16 @@ describe("Logger", () => {
|
|||||||
|
|
||||||
it("should log info messages", () => {
|
it("should log info messages", () => {
|
||||||
Logger.logInfo("Test info", "DocDB");
|
Logger.logInfo("Test info", "DocDB");
|
||||||
expect(sendMessage).toHaveBeenCalled();
|
expect(sendMessage).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should log error messages", () => {
|
it("should log error messages", () => {
|
||||||
Logger.logError("Test error", "DocDB");
|
Logger.logError("Test error", "DocDB");
|
||||||
expect(sendMessage).toHaveBeenCalled();
|
expect(sendMessage).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should log warnings", () => {
|
it("should log warnings", () => {
|
||||||
Logger.logWarning("Test warning", "DocDB");
|
Logger.logWarning("Test warning", "DocDB");
|
||||||
expect(sendMessage).toHaveBeenCalled();
|
expect(sendMessage).toBeCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
|
|
||||||
import Q from "q";
|
import Q from "q";
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||||
@@ -28,24 +27,15 @@ export function handleCachedDataMessage(message: any): void {
|
|||||||
runGarbageCollector();
|
runGarbageCollector();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param messageType
|
|
||||||
* @param params
|
|
||||||
* @param scope Use this string to identify request Useful to distinguish response from different senders
|
|
||||||
* @param timeoutInMs
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function sendCachedDataMessage<TResponseDataModel>(
|
export function sendCachedDataMessage<TResponseDataModel>(
|
||||||
messageType: MessageTypes | FabricMessageTypes,
|
messageType: MessageTypes,
|
||||||
params: Object[],
|
params: Object[],
|
||||||
scope?: string,
|
|
||||||
timeoutInMs?: number,
|
timeoutInMs?: number,
|
||||||
): Q.Promise<TResponseDataModel> {
|
): Q.Promise<TResponseDataModel> {
|
||||||
let cachedDataPromise: CachedDataPromise<TResponseDataModel> = {
|
let cachedDataPromise: CachedDataPromise<TResponseDataModel> = {
|
||||||
deferred: Q.defer<TResponseDataModel>(),
|
deferred: Q.defer<TResponseDataModel>(),
|
||||||
startTime: new Date(),
|
startTime: new Date(),
|
||||||
id: _.uniqueId(scope),
|
id: _.uniqueId(),
|
||||||
};
|
};
|
||||||
RequestMap[cachedDataPromise.id] = cachedDataPromise;
|
RequestMap[cachedDataPromise.id] = cachedDataPromise;
|
||||||
sendMessage({ type: messageType, params: params, id: cachedDataPromise.id });
|
sendMessage({ type: messageType, params: params, id: cachedDataPromise.id });
|
||||||
@@ -57,10 +47,6 @@ export function sendCachedDataMessage<TResponseDataModel>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param data Overwrite the data property of the message
|
|
||||||
*/
|
|
||||||
export function sendMessage(data: any): void {
|
export function sendMessage(data: any): void {
|
||||||
_sendMessage({
|
_sendMessage({
|
||||||
signature: "pcIframe",
|
signature: "pcIframe",
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||||
import {
|
|
||||||
allowedMongoProxyEndpoints,
|
|
||||||
allowedMongoProxyEndpoints_ToBeDeprecated,
|
|
||||||
validateEndpoint,
|
|
||||||
} from "Utils/EndpointUtils";
|
|
||||||
import queryString from "querystring";
|
import queryString from "querystring";
|
||||||
|
import { allowedMongoProxyEndpoints, validateEndpoint } from "Utils/EndpointValidation";
|
||||||
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";
|
||||||
@@ -14,7 +10,7 @@ import DocumentId from "../Explorer/Tree/DocumentId";
|
|||||||
import { hasFlag } from "../Platform/Hosted/extractFeatures";
|
import { hasFlag } from "../Platform/Hosted/extractFeatures";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyEndpoints } from "./Constants";
|
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
||||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||||
import { sendMessage } from "./MessageHandler";
|
import { sendMessage } from "./MessageHandler";
|
||||||
|
|
||||||
@@ -66,73 +62,6 @@ export function queryDocuments(
|
|||||||
isResourceList: boolean,
|
isResourceList: boolean,
|
||||||
query: string,
|
query: string,
|
||||||
continuationToken?: string,
|
continuationToken?: string,
|
||||||
): Promise<QueryResponse> {
|
|
||||||
if (!useMongoProxyEndpoint("resourcelist") || !useMongoProxyEndpoint("queryDocuments")) {
|
|
||||||
return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { databaseAccount } = userContext;
|
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
|
||||||
const params = {
|
|
||||||
databaseID: databaseId,
|
|
||||||
collectionID: collection.id(),
|
|
||||||
resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`,
|
|
||||||
resourceID: collection.rid,
|
|
||||||
resourceType: "docs",
|
|
||||||
subscriptionID: userContext.subscriptionId,
|
|
||||||
resourceGroup: userContext.resourceGroup,
|
|
||||||
databaseAccountName: databaseAccount.name,
|
|
||||||
partitionKey:
|
|
||||||
collection && collection.partitionKey && !collection.partitionKey.systemKey
|
|
||||||
? collection.partitionKeyProperties?.[0]
|
|
||||||
: "",
|
|
||||||
query,
|
|
||||||
};
|
|
||||||
|
|
||||||
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" : "/queryDocuments";
|
|
||||||
|
|
||||||
return window
|
|
||||||
.fetch(`${endpoint}${path}`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(params),
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function queryDocuments_ToBeDeprecated(
|
|
||||||
databaseId: string,
|
|
||||||
collection: Collection,
|
|
||||||
isResourceList: boolean,
|
|
||||||
query: string,
|
|
||||||
continuationToken?: string,
|
|
||||||
): Promise<QueryResponse> {
|
): Promise<QueryResponse> {
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
@@ -193,54 +122,6 @@ export function readDocument(
|
|||||||
databaseId: string,
|
databaseId: string,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
documentId: DocumentId,
|
documentId: DocumentId,
|
||||||
): Promise<DataModels.DocumentId> {
|
|
||||||
if (!useMongoProxyEndpoint("readDocument")) {
|
|
||||||
return readDocument_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, 4).join("/");
|
|
||||||
const rid = encodeURIComponent(idComponents[5]);
|
|
||||||
const params = {
|
|
||||||
databaseID: databaseId,
|
|
||||||
collectionID: collection.id(),
|
|
||||||
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
|
||||||
resourceID: rid,
|
|
||||||
resourceType: "docs",
|
|
||||||
subscriptionID: userContext.subscriptionId,
|
|
||||||
resourceGroup: userContext.resourceGroup,
|
|
||||||
databaseAccountName: databaseAccount.name,
|
|
||||||
partitionKey:
|
|
||||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
|
||||||
? documentId.partitionKeyProperties?.[0]
|
|
||||||
: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
|
||||||
|
|
||||||
return window
|
|
||||||
.fetch(endpoint, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(params),
|
|
||||||
headers: {
|
|
||||||
...defaultHeaders,
|
|
||||||
...authHeaders(),
|
|
||||||
[HttpHeaders.contentType]: ContentType.applicationJson,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(async (response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
return await errorHandling(response, "reading document", params);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readDocument_ToBeDeprecated(
|
|
||||||
databaseId: string,
|
|
||||||
collection: Collection,
|
|
||||||
documentId: DocumentId,
|
|
||||||
): Promise<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
@@ -288,51 +169,6 @@ export function createDocument(
|
|||||||
collection: Collection,
|
collection: Collection,
|
||||||
partitionKeyProperty: string,
|
partitionKeyProperty: string,
|
||||||
documentContent: unknown,
|
documentContent: unknown,
|
||||||
): Promise<DataModels.DocumentId> {
|
|
||||||
if (!useMongoProxyEndpoint("createDocument")) {
|
|
||||||
return createDocument_ToBeDeprecated(databaseId, collection, partitionKeyProperty, documentContent);
|
|
||||||
}
|
|
||||||
const { databaseAccount } = userContext;
|
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
|
||||||
const params = {
|
|
||||||
databaseID: databaseId,
|
|
||||||
collectionID: collection.id(),
|
|
||||||
resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`,
|
|
||||||
resourceID: collection.rid,
|
|
||||||
resourceType: "docs",
|
|
||||||
subscriptionID: userContext.subscriptionId,
|
|
||||||
resourceGroup: userContext.resourceGroup,
|
|
||||||
databaseAccountName: databaseAccount.name,
|
|
||||||
partitionKey:
|
|
||||||
collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
|
|
||||||
documentContent: JSON.stringify(documentContent),
|
|
||||||
};
|
|
||||||
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("createDocument");
|
|
||||||
|
|
||||||
return window
|
|
||||||
.fetch(`${endpoint}/createDocument`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(params),
|
|
||||||
headers: {
|
|
||||||
...defaultHeaders,
|
|
||||||
...authHeaders(),
|
|
||||||
[HttpHeaders.contentType]: ContentType.applicationJson,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(async (response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
return await errorHandling(response, "creating document", params);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDocument_ToBeDeprecated(
|
|
||||||
databaseId: string,
|
|
||||||
collection: Collection,
|
|
||||||
partitionKeyProperty: string,
|
|
||||||
documentContent: unknown,
|
|
||||||
): Promise<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
@@ -372,56 +208,6 @@ export function updateDocument(
|
|||||||
collection: Collection,
|
collection: Collection,
|
||||||
documentId: DocumentId,
|
documentId: DocumentId,
|
||||||
documentContent: string,
|
documentContent: string,
|
||||||
): Promise<DataModels.DocumentId> {
|
|
||||||
if (!useMongoProxyEndpoint("updateDocument")) {
|
|
||||||
return updateDocument_ToBeDeprecated(databaseId, collection, documentId, documentContent);
|
|
||||||
}
|
|
||||||
const { databaseAccount } = userContext;
|
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
|
||||||
const idComponents = documentId.self.split("/");
|
|
||||||
const path = idComponents.slice(0, 5).join("/");
|
|
||||||
const rid = encodeURIComponent(idComponents[5]);
|
|
||||||
const params = {
|
|
||||||
databaseID: databaseId,
|
|
||||||
collectionID: collection.id(),
|
|
||||||
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
|
||||||
resourceID: rid,
|
|
||||||
resourceType: "docs",
|
|
||||||
subscriptionID: userContext.subscriptionId,
|
|
||||||
resourceGroup: userContext.resourceGroup,
|
|
||||||
databaseAccountName: databaseAccount.name,
|
|
||||||
partitionKey:
|
|
||||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
|
||||||
? documentId.partitionKeyProperties?.[0]
|
|
||||||
: "",
|
|
||||||
documentContent,
|
|
||||||
};
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("updateDocument");
|
|
||||||
|
|
||||||
return window
|
|
||||||
.fetch(endpoint, {
|
|
||||||
method: "PUT",
|
|
||||||
body: JSON.stringify(params),
|
|
||||||
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 updateDocument_ToBeDeprecated(
|
|
||||||
databaseId: string,
|
|
||||||
collection: Collection,
|
|
||||||
documentId: DocumentId,
|
|
||||||
documentContent: string,
|
|
||||||
): Promise<DataModels.DocumentId> {
|
): Promise<DataModels.DocumentId> {
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
@@ -451,7 +237,7 @@ export function updateDocument_ToBeDeprecated(
|
|||||||
headers: {
|
headers: {
|
||||||
...defaultHeaders,
|
...defaultHeaders,
|
||||||
...authHeaders(),
|
...authHeaders(),
|
||||||
[HttpHeaders.contentType]: ContentType.applicationJson,
|
[HttpHeaders.contentType]: "application/json",
|
||||||
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -464,53 +250,6 @@ export function updateDocument_ToBeDeprecated(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
|
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
|
||||||
if (!useMongoProxyEndpoint("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 = {
|
|
||||||
databaseID: databaseId,
|
|
||||||
collectionID: collection.id(),
|
|
||||||
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
|
||||||
resourceID: rid,
|
|
||||||
resourceType: "docs",
|
|
||||||
subscriptionID: userContext.subscriptionId,
|
|
||||||
resourceGroup: userContext.resourceGroup,
|
|
||||||
databaseAccountName: databaseAccount.name,
|
|
||||||
partitionKey:
|
|
||||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
|
||||||
? documentId.partitionKeyProperties?.[0]
|
|
||||||
: "",
|
|
||||||
};
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
|
||||||
|
|
||||||
return window
|
|
||||||
.fetch(endpoint, {
|
|
||||||
method: "DELETE",
|
|
||||||
body: JSON.stringify(params),
|
|
||||||
headers: {
|
|
||||||
...defaultHeaders,
|
|
||||||
...authHeaders(),
|
|
||||||
[HttpHeaders.contentType]: ContentType.applicationJson,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(async (response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return await errorHandling(response, "deleting document", params);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deleteDocument_ToBeDeprecated(
|
|
||||||
databaseId: string,
|
|
||||||
collection: Collection,
|
|
||||||
documentId: DocumentId,
|
|
||||||
): Promise<void> {
|
|
||||||
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("/");
|
||||||
@@ -538,7 +277,7 @@ export function deleteDocument_ToBeDeprecated(
|
|||||||
headers: {
|
headers: {
|
||||||
...defaultHeaders,
|
...defaultHeaders,
|
||||||
...authHeaders(),
|
...authHeaders(),
|
||||||
[HttpHeaders.contentType]: ContentType.applicationJson,
|
[HttpHeaders.contentType]: "application/json",
|
||||||
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -552,52 +291,6 @@ export function deleteDocument_ToBeDeprecated(
|
|||||||
|
|
||||||
export function createMongoCollectionWithProxy(
|
export function createMongoCollectionWithProxy(
|
||||||
params: DataModels.CreateCollectionParams,
|
params: DataModels.CreateCollectionParams,
|
||||||
): Promise<DataModels.Collection> {
|
|
||||||
if (!useMongoProxyEndpoint("createCollectionWithProxy")) {
|
|
||||||
return createMongoCollectionWithProxy_ToBeDeprecated(params);
|
|
||||||
}
|
|
||||||
const { databaseAccount } = userContext;
|
|
||||||
const shardKey: string = params.partitionKey?.paths[0];
|
|
||||||
|
|
||||||
const createCollectionParams = {
|
|
||||||
databaseID: params.databaseId,
|
|
||||||
collectionID: params.collectionId,
|
|
||||||
resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint,
|
|
||||||
resourceID: "",
|
|
||||||
resourceType: "colls",
|
|
||||||
subscriptionID: userContext.subscriptionId,
|
|
||||||
resourceGroup: userContext.resourceGroup,
|
|
||||||
databaseAccountName: databaseAccount.name,
|
|
||||||
partitionKey: shardKey,
|
|
||||||
isAutoscale: !!params.autoPilotMaxThroughput,
|
|
||||||
hasSharedThroughput: params.databaseLevelThroughput,
|
|
||||||
offerThroughput: params.autoPilotMaxThroughput || params.offerThroughput,
|
|
||||||
createDatabase: params.createNewDatabase,
|
|
||||||
isSharded: !!shardKey,
|
|
||||||
};
|
|
||||||
|
|
||||||
const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy");
|
|
||||||
|
|
||||||
return window
|
|
||||||
.fetch(`${endpoint}/createCollection`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(createCollectionParams),
|
|
||||||
headers: {
|
|
||||||
...defaultHeaders,
|
|
||||||
...authHeaders(),
|
|
||||||
[HttpHeaders.contentType]: ContentType.applicationJson,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(async (response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
return await errorHandling(response, "creating collection", createCollectionParams);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createMongoCollectionWithProxy_ToBeDeprecated(
|
|
||||||
params: DataModels.CreateCollectionParams,
|
|
||||||
): Promise<DataModels.Collection> {
|
): Promise<DataModels.Collection> {
|
||||||
const { databaseAccount } = userContext;
|
const { databaseAccount } = userContext;
|
||||||
const shardKey: string = params.partitionKey?.paths[0];
|
const shardKey: string = params.partitionKey?.paths[0];
|
||||||
@@ -641,20 +334,13 @@ export function createMongoCollectionWithProxy_ToBeDeprecated(
|
|||||||
return await errorHandling(response, "creating collection", mongoParams);
|
return await errorHandling(response, "creating collection", mongoParams);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFeatureEndpointOrDefault(feature: string): string {
|
export function getFeatureEndpointOrDefault(feature: string): string {
|
||||||
let endpoint;
|
const endpoint =
|
||||||
if (useMongoProxyEndpoint(feature)) {
|
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
||||||
endpoint = configContext.MONGO_PROXY_ENDPOINT;
|
validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints)
|
||||||
} else {
|
? userContext.features.mongoProxyEndpoint
|
||||||
endpoint =
|
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_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);
|
return getEndpoint(endpoint);
|
||||||
}
|
}
|
||||||
@@ -663,37 +349,11 @@ export function getEndpoint(endpoint: string): string {
|
|||||||
let url = endpoint + "/api/mongo/explorer";
|
let url = endpoint + "/api/mongo/explorer";
|
||||||
|
|
||||||
if (userContext.authType === AuthType.EncryptedToken) {
|
if (userContext.authType === AuthType.EncryptedToken) {
|
||||||
if (endpoint === configContext.MONGO_PROXY_ENDPOINT) {
|
url = url.replace("api/mongo", "api/guest/mongo");
|
||||||
url = url.replace("api/mongo", "api/connectionstring/mongo");
|
|
||||||
} else {
|
|
||||||
url = url.replace("api/mongo", "api/guest/mongo");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMongoProxyEndpoint(api: string): boolean {
|
|
||||||
const activeMongoProxyEndpoints: string[] = [
|
|
||||||
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
|
||||||
// It causes problems for TypeScript understanding the types
|
// It causes problems for TypeScript understanding the types
|
||||||
async function errorHandling(response: Response, action: string, params: unknown): Promise<void> {
|
async function errorHandling(response: Response, action: string, params: unknown): Promise<void> {
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import Explorer from "../Explorer/Explorer";
|
import Explorer from "../Explorer/Explorer";
|
||||||
import DocumentId, { IDocumentIdContainer } from "../Explorer/Tree/DocumentId";
|
import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
|
||||||
|
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||||
import { useDatabases } from "../Explorer/useDatabases";
|
import { useDatabases } from "../Explorer/useDatabases";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||||
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
|
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
|
||||||
import { handleError } from "./ErrorHandlingUtils";
|
|
||||||
import { createCollection } from "./dataAccess/createCollection";
|
import { createCollection } from "./dataAccess/createCollection";
|
||||||
import { createDocument } from "./dataAccess/createDocument";
|
import { createDocument } from "./dataAccess/createDocument";
|
||||||
import { deleteDocument } from "./dataAccess/deleteDocument";
|
import { deleteDocument } from "./dataAccess/deleteDocument";
|
||||||
import { queryDocuments } from "./dataAccess/queryDocuments";
|
import { queryDocuments } from "./dataAccess/queryDocuments";
|
||||||
|
import { handleError } from "./ErrorHandlingUtils";
|
||||||
|
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
||||||
|
|
||||||
export class QueriesClient {
|
export class QueriesClient {
|
||||||
private static readonly PartitionKey: DataModels.PartitionKey = {
|
private static readonly PartitionKey: DataModels.PartitionKey = {
|
||||||
@@ -161,10 +162,10 @@ export class QueriesClient {
|
|||||||
{
|
{
|
||||||
partitionKey: QueriesClient.PartitionKey,
|
partitionKey: QueriesClient.PartitionKey,
|
||||||
partitionKeyProperties: ["id"],
|
partitionKeyProperties: ["id"],
|
||||||
} as IDocumentIdContainer,
|
} as DocumentsTab,
|
||||||
query,
|
query,
|
||||||
[query.queryName],
|
[query.queryName],
|
||||||
);
|
); // TODO: Remove DocumentId's dependency on DocumentsTab
|
||||||
const options: any = { partitionKey: query.resourceId };
|
const options: any = { partitionKey: query.resourceId };
|
||||||
return deleteDocument(queriesCollection, documentId)
|
return deleteDocument(queriesCollection, documentId)
|
||||||
.then(
|
.then(
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
|
import { ResourceTree } from "Explorer/Tree/ResourceTree";
|
||||||
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
|
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
|
||||||
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
||||||
import refreshImg from "../../images/refresh-cosmos.svg";
|
import refreshImg from "../../images/refresh-cosmos.svg";
|
||||||
|
import { AuthType } from "../AuthType";
|
||||||
import Explorer from "../Explorer/Explorer";
|
import Explorer from "../Explorer/Explorer";
|
||||||
import { ResourceTree } from "../Explorer/Tree/ResourceTree";
|
import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree";
|
||||||
|
import { ResourceTree2 } from "../Explorer/Tree2/ResourceTree";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { getApiShortDisplayName } from "../Utils/APITypeUtils";
|
import { getApiShortDisplayName } from "../Utils/APITypeUtils";
|
||||||
|
import { Platform, configContext } from "./../ConfigContext";
|
||||||
import { NormalizedEventKey } from "./Constants";
|
import { NormalizedEventKey } from "./Constants";
|
||||||
|
|
||||||
export interface ResourceTreeContainerProps {
|
export interface ResourceTreeContainerProps {
|
||||||
@@ -70,8 +74,12 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{userContext.features.enableKoResourceTree ? (
|
{userContext.authType === AuthType.ResourceToken ? (
|
||||||
|
<ResourceTokenTree />
|
||||||
|
) : userContext.features.enableKoResourceTree ? (
|
||||||
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
|
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
|
||||||
|
) : configContext.platform === Platform.Fabric ? (
|
||||||
|
<ResourceTree2 container={container} />
|
||||||
) : (
|
) : (
|
||||||
<ResourceTree container={container} />
|
<ResourceTree container={container} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -142,11 +142,10 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
|||||||
<Image
|
<Image
|
||||||
{...imageProps}
|
{...imageProps}
|
||||||
src={EditIcon}
|
src={EditIcon}
|
||||||
alt={`Edit ${entityProperty} entity`}
|
alt="editEntity"
|
||||||
onClick={onEditEntity}
|
onClick={onEditEntity}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onKeyPress={handleKeyPress}
|
onKeyPress={handleKeyPress}
|
||||||
role="button"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
@@ -156,12 +155,11 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
|||||||
<Image
|
<Image
|
||||||
{...imageProps}
|
{...imageProps}
|
||||||
src={DeleteIcon}
|
src={DeleteIcon}
|
||||||
alt={`Delete ${entityProperty} entity`}
|
alt="delete entity"
|
||||||
id="deleteEntity"
|
id="deleteEntity"
|
||||||
onClick={onDeleteEntity}
|
onClick={onDeleteEntity}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onKeyPress={handleKeyPressdelete}
|
onKeyPress={handleKeyPressdelete}
|
||||||
role="button"
|
|
||||||
/>
|
/>
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
exports[`getCommonQueryOptions builds the correct default options objects 1`] = `
|
exports[`getCommonQueryOptions builds the correct default options objects 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"disableNonStreamingOrderByQuery": true,
|
|
||||||
"enableScanInQuery": true,
|
"enableScanInQuery": true,
|
||||||
"forceQueryPlan": true,
|
"forceQueryPlan": true,
|
||||||
"maxDegreeOfParallelism": 0,
|
"maxDegreeOfParallelism": 0,
|
||||||
@@ -13,7 +12,6 @@ Object {
|
|||||||
|
|
||||||
exports[`getCommonQueryOptions reads from localStorage 1`] = `
|
exports[`getCommonQueryOptions reads from localStorage 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"disableNonStreamingOrderByQuery": true,
|
|
||||||
"enableScanInQuery": true,
|
"enableScanInQuery": true,
|
||||||
"forceQueryPlan": true,
|
"forceQueryPlan": true,
|
||||||
"maxDegreeOfParallelism": 17,
|
"maxDegreeOfParallelism": 17,
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
|
|||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { getCollectionName } from "../../Utils/APITypeUtils";
|
import { getCollectionName } from "../../Utils/APITypeUtils";
|
||||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
|
||||||
import { createUpdateCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
import { createUpdateCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||||
import { createUpdateGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
import { createUpdateGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||||
import { createUpdateMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
import { createUpdateMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||||
import { createUpdateSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
import { createUpdateSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||||
import { createUpdateTable } from "../../Utils/arm/generatedClients/cosmos/tableResources";
|
import { createUpdateTable } from "../../Utils/arm/generatedClients/cosmos/tableResources";
|
||||||
import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types";
|
import * as ARMTypes 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";
|
||||||
import { createMongoCollectionWithProxy } from "../MongoProxyClient";
|
import { createMongoCollectionWithProxy } from "../MongoProxyClient";
|
||||||
@@ -96,9 +96,6 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
|
|||||||
if (params.uniqueKeyPolicy) {
|
if (params.uniqueKeyPolicy) {
|
||||||
resource.uniqueKeyPolicy = params.uniqueKeyPolicy;
|
resource.uniqueKeyPolicy = params.uniqueKeyPolicy;
|
||||||
}
|
}
|
||||||
if (params.vectorEmbeddingPolicy) {
|
|
||||||
resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = {
|
const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = {
|
||||||
properties: {
|
properties: {
|
||||||
|
|||||||
@@ -1,195 +0,0 @@
|
|||||||
import { ApiType, userContext } from "UserContext";
|
|
||||||
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
|
|
||||||
import {
|
|
||||||
cancel,
|
|
||||||
create,
|
|
||||||
get,
|
|
||||||
listByDatabaseAccount,
|
|
||||||
} from "Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
|
|
||||||
import {
|
|
||||||
CosmosCassandraDataTransferDataSourceSink,
|
|
||||||
CosmosMongoDataTransferDataSourceSink,
|
|
||||||
CosmosSqlDataTransferDataSourceSink,
|
|
||||||
CreateJobRequest,
|
|
||||||
DataTransferJobFeedResults,
|
|
||||||
DataTransferJobGetResults,
|
|
||||||
} from "Utils/arm/generatedClients/dataTransferService/types";
|
|
||||||
import { addToPolling, removeFromPolling, updateDataTransferJob, useDataTransferJobs } from "hooks/useDataTransferJobs";
|
|
||||||
import promiseRetry, { AbortError, FailedAttemptError } from "p-retry";
|
|
||||||
|
|
||||||
export interface DataTransferParams {
|
|
||||||
jobName: string;
|
|
||||||
apiType: ApiType;
|
|
||||||
subscriptionId: string;
|
|
||||||
resourceGroupName: string;
|
|
||||||
accountName: string;
|
|
||||||
sourceDatabaseName: string;
|
|
||||||
sourceCollectionName: string;
|
|
||||||
targetDatabaseName: string;
|
|
||||||
targetCollectionName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getDataTransferJobs = async (
|
|
||||||
subscriptionId: string,
|
|
||||||
resourceGroup: string,
|
|
||||||
accountName: string,
|
|
||||||
): Promise<DataTransferJobGetResults[]> => {
|
|
||||||
let dataTransferJobs: DataTransferJobGetResults[] = [];
|
|
||||||
let dataTransferFeeds: DataTransferJobFeedResults = await listByDatabaseAccount(
|
|
||||||
subscriptionId,
|
|
||||||
resourceGroup,
|
|
||||||
accountName,
|
|
||||||
);
|
|
||||||
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
|
|
||||||
while (dataTransferFeeds?.nextLink) {
|
|
||||||
const nextResponse = await window.fetch(dataTransferFeeds.nextLink, {
|
|
||||||
headers: {
|
|
||||||
Authorization: userContext.authorizationToken,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (nextResponse.ok) {
|
|
||||||
dataTransferFeeds = await nextResponse.json();
|
|
||||||
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dataTransferJobs;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const initiateDataTransfer = async (params: DataTransferParams): Promise<DataTransferJobGetResults> => {
|
|
||||||
const {
|
|
||||||
jobName,
|
|
||||||
apiType,
|
|
||||||
subscriptionId,
|
|
||||||
resourceGroupName,
|
|
||||||
accountName,
|
|
||||||
sourceDatabaseName,
|
|
||||||
sourceCollectionName,
|
|
||||||
targetDatabaseName,
|
|
||||||
targetCollectionName,
|
|
||||||
} = params;
|
|
||||||
const sourcePayload = createPayload(apiType, sourceDatabaseName, sourceCollectionName);
|
|
||||||
const targetPayload = createPayload(apiType, targetDatabaseName, targetCollectionName);
|
|
||||||
const body: CreateJobRequest = {
|
|
||||||
properties: {
|
|
||||||
source: sourcePayload,
|
|
||||||
destination: targetPayload,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return create(subscriptionId, resourceGroupName, accountName, jobName, body);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const pollDataTransferJob = async (
|
|
||||||
jobName: string,
|
|
||||||
subscriptionId: string,
|
|
||||||
resourceGroupName: string,
|
|
||||||
accountName: string,
|
|
||||||
): Promise<unknown> => {
|
|
||||||
const currentPollingJobs = useDataTransferJobs.getState().pollingDataTransferJobs;
|
|
||||||
if (currentPollingJobs.has(jobName)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let clearMessage = NotificationConsoleUtils.logConsoleProgress(`Data transfer job ${jobName} in progress`);
|
|
||||||
return await promiseRetry(
|
|
||||||
() => pollDataTransferJobOperation(jobName, subscriptionId, resourceGroupName, accountName, clearMessage),
|
|
||||||
{
|
|
||||||
retries: 500,
|
|
||||||
maxTimeout: 5000,
|
|
||||||
onFailedAttempt: (error: FailedAttemptError) => {
|
|
||||||
clearMessage();
|
|
||||||
clearMessage = NotificationConsoleUtils.logConsoleProgress(error.message);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const pollDataTransferJobOperation = async (
|
|
||||||
jobName: string,
|
|
||||||
subscriptionId: string,
|
|
||||||
resourceGroupName: string,
|
|
||||||
accountName: string,
|
|
||||||
clearMessage?: () => void,
|
|
||||||
): Promise<DataTransferJobGetResults> => {
|
|
||||||
if (!userContext.authorizationToken) {
|
|
||||||
throw new Error("No authority token provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
addToPolling(jobName);
|
|
||||||
|
|
||||||
const body: DataTransferJobGetResults = await get(subscriptionId, resourceGroupName, accountName, jobName);
|
|
||||||
const status = body?.properties?.status;
|
|
||||||
|
|
||||||
updateDataTransferJob(body);
|
|
||||||
|
|
||||||
if (status === "Cancelled") {
|
|
||||||
removeFromPolling(jobName);
|
|
||||||
clearMessage && clearMessage();
|
|
||||||
const cancelMessage = `Data transfer job ${jobName} cancelled`;
|
|
||||||
NotificationConsoleUtils.logConsoleError(cancelMessage);
|
|
||||||
throw new AbortError(cancelMessage);
|
|
||||||
}
|
|
||||||
if (status === "Failed" || status === "Faulted") {
|
|
||||||
removeFromPolling(jobName);
|
|
||||||
const errorMessage = body?.properties?.error
|
|
||||||
? JSON.stringify(body?.properties?.error)
|
|
||||||
: "Operation could not be completed";
|
|
||||||
const error = new Error(errorMessage);
|
|
||||||
clearMessage && clearMessage();
|
|
||||||
NotificationConsoleUtils.logConsoleError(`Data transfer job ${jobName} failed: ${errorMessage}`);
|
|
||||||
throw new AbortError(error);
|
|
||||||
}
|
|
||||||
if (status === "Completed") {
|
|
||||||
removeFromPolling(jobName);
|
|
||||||
clearMessage && clearMessage();
|
|
||||||
NotificationConsoleUtils.logConsoleInfo(`Data transfer job ${jobName} completed`);
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
const processedCount = body.properties.processedCount;
|
|
||||||
const totalCount = body.properties.totalCount;
|
|
||||||
const retryMessage = `Data transfer job ${jobName} in progress, total count: ${totalCount}, processed count: ${processedCount}`;
|
|
||||||
throw new Error(retryMessage);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cancelDataTransferJob = async (
|
|
||||||
subscriptionId: string,
|
|
||||||
resourceGroupName: string,
|
|
||||||
accountName: string,
|
|
||||||
jobName: string,
|
|
||||||
): Promise<void> => {
|
|
||||||
const cancelResult: DataTransferJobGetResults = await cancel(subscriptionId, resourceGroupName, accountName, jobName);
|
|
||||||
updateDataTransferJob(cancelResult);
|
|
||||||
removeFromPolling(cancelResult?.properties?.jobName);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createPayload = (
|
|
||||||
apiType: ApiType,
|
|
||||||
databaseName: string,
|
|
||||||
containerName: string,
|
|
||||||
):
|
|
||||||
| CosmosSqlDataTransferDataSourceSink
|
|
||||||
| CosmosMongoDataTransferDataSourceSink
|
|
||||||
| CosmosCassandraDataTransferDataSourceSink => {
|
|
||||||
switch (apiType) {
|
|
||||||
case "SQL":
|
|
||||||
return {
|
|
||||||
component: "CosmosDBSql",
|
|
||||||
databaseName: databaseName,
|
|
||||||
containerName: containerName,
|
|
||||||
} as CosmosSqlDataTransferDataSourceSink;
|
|
||||||
case "Mongo":
|
|
||||||
return {
|
|
||||||
component: "CosmosDBMongo",
|
|
||||||
databaseName: databaseName,
|
|
||||||
collectionName: containerName,
|
|
||||||
} as CosmosMongoDataTransferDataSourceSink;
|
|
||||||
case "Cassandra":
|
|
||||||
return {
|
|
||||||
component: "CosmosDBCassandra",
|
|
||||||
keyspaceName: databaseName,
|
|
||||||
tableName: containerName,
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported API type for data transfer: ${apiType}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { BulkOperationType, OperationInput } from "@azure/cosmos";
|
|
||||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
@@ -25,58 +24,3 @@ export const deleteDocument = async (collection: CollectionBase, documentId: Doc
|
|||||||
clearMessage();
|
clearMessage();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Bulk delete documents
|
|
||||||
* @param collection
|
|
||||||
* @param documentId
|
|
||||||
* @returns array of ids that were successfully deleted
|
|
||||||
*/
|
|
||||||
export const deleteDocuments = async (collection: CollectionBase, documentIds: DocumentId[]): Promise<DocumentId[]> => {
|
|
||||||
const nbDocuments = documentIds.length;
|
|
||||||
const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`);
|
|
||||||
try {
|
|
||||||
const v2Container = await client().database(collection.databaseId).container(collection.id());
|
|
||||||
|
|
||||||
// Bulk can only delete 100 documents at a time
|
|
||||||
const BULK_DELETE_LIMIT = 100;
|
|
||||||
const promiseArray = [];
|
|
||||||
|
|
||||||
while (documentIds.length > 0) {
|
|
||||||
const documentIdsChunk = documentIds.splice(0, BULK_DELETE_LIMIT);
|
|
||||||
const operations: OperationInput[] = documentIdsChunk.map((documentId) => ({
|
|
||||||
id: documentId.id(),
|
|
||||||
// bulk delete: if not partition key is specified, do not pass empty array, but undefined
|
|
||||||
partitionKey:
|
|
||||||
documentId.partitionKeyValue &&
|
|
||||||
Array.isArray(documentId.partitionKeyValue) &&
|
|
||||||
documentId.partitionKeyValue.length === 0
|
|
||||||
? undefined
|
|
||||||
: documentId.partitionKeyValue,
|
|
||||||
operationType: BulkOperationType.Delete,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const promise = v2Container.items.bulk(operations).then((bulkResult) => {
|
|
||||||
return documentIdsChunk.filter((_, index) => bulkResult[index].statusCode === 204);
|
|
||||||
});
|
|
||||||
promiseArray.push(promise);
|
|
||||||
}
|
|
||||||
|
|
||||||
const allResult = await Promise.all(promiseArray);
|
|
||||||
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;
|
|
||||||
} catch (error) {
|
|
||||||
handleError(
|
|
||||||
error,
|
|
||||||
"DeleteDocuments",
|
|
||||||
`Error while deleting ${documentIds.length} ${getEntityName(documentIds.length > 1)}`,
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
clearMessage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
|
||||||
import { getCommonQueryOptions } from "./queryDocuments";
|
import { getCommonQueryOptions } from "./queryDocuments";
|
||||||
|
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||||
|
|
||||||
describe("getCommonQueryOptions", () => {
|
describe("getCommonQueryOptions", () => {
|
||||||
it("builds the correct default options objects", () => {
|
it("builds the correct default options objects", () => {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
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";
|
||||||
@@ -27,6 +26,6 @@ export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {
|
|||||||
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
|
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
|
||||||
Queries.itemsPerPage;
|
Queries.itemsPerPage;
|
||||||
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
|
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
|
||||||
options.disableNonStreamingOrderByQuery = !isVectorSearchEnabled();
|
|
||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
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";
|
||||||
@@ -9,13 +8,12 @@ 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, queryOperationOptions);
|
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex);
|
||||||
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;
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
configContext.platform === Platform.Fabric &&
|
configContext.platform === Platform.Fabric &&
|
||||||
userContext.fabricContext &&
|
userContext.fabricDatabaseConnectionInfo &&
|
||||||
userContext.fabricContext.databaseConnectionInfo.databaseId === databaseId
|
userContext.fabricDatabaseConnectionInfo.databaseId === databaseId
|
||||||
) {
|
) {
|
||||||
const collections: DataModels.Collection[] = [];
|
const collections: DataModels.Collection[] = [];
|
||||||
const promises: Promise<ContainerResponse>[] = [];
|
const promises: Promise<ContainerResponse>[] = [];
|
||||||
|
|
||||||
for (const collectionResourceId in userContext.fabricContext.databaseConnectionInfo.resourceTokens) {
|
for (const collectionResourceId in userContext.fabricDatabaseConnectionInfo.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];
|
||||||
|
|||||||
@@ -14,8 +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 (configContext.platform === Platform.Fabric && userContext.fabricContext?.databaseConnectionInfo.resourceTokens) {
|
if (configContext.platform === Platform.Fabric && userContext.fabricDatabaseConnectionInfo?.resourceTokens) {
|
||||||
const tokensData = userContext.fabricContext.databaseConnectionInfo;
|
const tokensData = userContext.fabricDatabaseConnectionInfo;
|
||||||
|
|
||||||
const databaseIdsSet = new Set<string>(); // databaseId
|
const databaseIdsSet = new Set<string>(); // databaseId
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,17 @@
|
|||||||
import {
|
import { JunoEndpoints } from "Common/Constants";
|
||||||
BackendApi,
|
|
||||||
CassandraProxyEndpoints,
|
|
||||||
JunoEndpoints,
|
|
||||||
MongoProxyEndpoints,
|
|
||||||
PortalBackendEndpoints,
|
|
||||||
} from "Common/Constants";
|
|
||||||
import {
|
import {
|
||||||
allowedAadEndpoints,
|
allowedAadEndpoints,
|
||||||
allowedArcadiaEndpoints,
|
allowedArcadiaEndpoints,
|
||||||
allowedCassandraProxyEndpoints,
|
|
||||||
allowedEmulatorEndpoints,
|
allowedEmulatorEndpoints,
|
||||||
allowedGraphEndpoints,
|
allowedGraphEndpoints,
|
||||||
allowedHostedExplorerEndpoints,
|
allowedHostedExplorerEndpoints,
|
||||||
allowedJunoOrigins,
|
allowedJunoOrigins,
|
||||||
allowedMongoBackendEndpoints,
|
allowedMongoBackendEndpoints,
|
||||||
allowedMongoProxyEndpoints,
|
|
||||||
allowedMsalRedirectEndpoints,
|
allowedMsalRedirectEndpoints,
|
||||||
defaultAllowedArmEndpoints,
|
defaultAllowedArmEndpoints,
|
||||||
defaultAllowedBackendEndpoints,
|
defaultAllowedBackendEndpoints,
|
||||||
validateEndpoint,
|
validateEndpoint,
|
||||||
} from "Utils/EndpointUtils";
|
} from "Utils/EndpointValidation";
|
||||||
|
|
||||||
export enum Platform {
|
export enum Platform {
|
||||||
Portal = "Portal",
|
Portal = "Portal",
|
||||||
@@ -42,22 +34,10 @@ export interface ConfigContext {
|
|||||||
ARM_API_VERSION: 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
|
|
||||||
CATALOG_ENDPOINT: string;
|
|
||||||
CATALOG_API_VERSION: string;
|
|
||||||
CATALOG_API_KEY: string;
|
|
||||||
ARCADIA_ENDPOINT: string;
|
ARCADIA_ENDPOINT: string;
|
||||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||||
BACKEND_ENDPOINT?: string;
|
BACKEND_ENDPOINT?: string;
|
||||||
PORTAL_BACKEND_ENDPOINT?: string;
|
|
||||||
NEW_BACKEND_APIS?: BackendApi[];
|
|
||||||
MONGO_BACKEND_ENDPOINT?: string;
|
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;
|
||||||
@@ -86,8 +66,6 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
`^https:\\/\\/.*\\.powerbi\\.com$`,
|
`^https:\\/\\/.*\\.powerbi\\.com$`,
|
||||||
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
||||||
`^https:\\/\\/.*\\.analysis-df\\.windows\\.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/",
|
||||||
@@ -97,31 +75,12 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
ARM_API_VERSION: "2016-06-01",
|
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_API_VERSION: "2023-05-01-preview",
|
|
||||||
CATALOG_API_KEY: "",
|
|
||||||
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
|
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
|
||||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
|
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
|
||||||
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",
|
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.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,
|
|
||||||
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
|
|
||||||
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
|
|
||||||
isTerminalEnabled: false,
|
isTerminalEnabled: false,
|
||||||
isPhoenixEnabled: false,
|
isPhoenixEnabled: false,
|
||||||
};
|
};
|
||||||
@@ -167,18 +126,10 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
delete newContext.BACKEND_ENDPOINT;
|
delete newContext.BACKEND_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.MONGO_PROXY_ENDPOINT, allowedMongoProxyEndpoints)) {
|
|
||||||
delete newContext.MONGO_PROXY_ENDPOINT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) {
|
if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) {
|
||||||
delete newContext.MONGO_BACKEND_ENDPOINT;
|
delete newContext.MONGO_BACKEND_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.CASSANDRA_PROXY_ENDPOINT, allowedCassandraProxyEndpoints)) {
|
|
||||||
delete newContext.CASSANDRA_PROXY_ENDPOINT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.JUNO_ENDPOINT, allowedJunoOrigins)) {
|
if (!validateEndpoint(newContext.JUNO_ENDPOINT, allowedJunoOrigins)) {
|
||||||
delete newContext.JUNO_ENDPOINT;
|
delete newContext.JUNO_ENDPOINT;
|
||||||
}
|
}
|
||||||
@@ -196,12 +147,12 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
|
|
||||||
// Injected for local development. These will be removed in the production bundle by webpack
|
// Injected for local development. These will be removed in the production bundle by webpack
|
||||||
if (process.env.NODE_ENV === "development") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
const port: string = process.env.PORT || "1234";
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
|
BACKEND_ENDPOINT: "https://localhost:" + port,
|
||||||
|
MONGO_BACKEND_ENDPOINT: "https://localhost:" + port,
|
||||||
PROXY_PATH: "/proxy",
|
PROXY_PATH: "/proxy",
|
||||||
EMULATOR_ENDPOINT: "https://localhost:8081",
|
EMULATOR_ENDPOINT: "https://localhost:8081",
|
||||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac,
|
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Mpac,
|
|
||||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Mpac,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import { FabricMessageTypes } from "./FabricMessageTypes";
|
|
||||||
|
|
||||||
// This is the current version of these messages
|
|
||||||
export const DATA_EXPLORER_RPC_VERSION = "3";
|
|
||||||
|
|
||||||
// Data Explorer to Fabric
|
|
||||||
export type DataExploreMessageV3 =
|
|
||||||
| {
|
|
||||||
type: FabricMessageTypes.Ready;
|
|
||||||
id: string;
|
|
||||||
params: [string]; // version
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: FabricMessageTypes.GetAuthorizationToken;
|
|
||||||
id: string;
|
|
||||||
params: GetCosmosTokenMessageOptions[];
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: FabricMessageTypes.GetAllResourceTokens;
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetCosmosTokenMessageOptions = {
|
|
||||||
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
|
|
||||||
resourceType: "" | "dbs" | "colls" | "docs" | "sprocs" | "pkranges";
|
|
||||||
resourceId: string;
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CapacityMode, ConnectionStatusType, ContainerStatusType } from "../Common/Constants";
|
import { ConnectionStatusType, ContainerStatusType } from "../Common/Constants";
|
||||||
|
|
||||||
export interface ArmEntity {
|
export interface ArmEntity {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -35,7 +35,6 @@ export interface DatabaseAccountExtendedProperties {
|
|||||||
ipRules?: IpRule[];
|
ipRules?: IpRule[];
|
||||||
privateEndpointConnections?: unknown[];
|
privateEndpointConnections?: unknown[];
|
||||||
capacity?: { totalThroughputLimit: number };
|
capacity?: { totalThroughputLimit: number };
|
||||||
capacityMode?: CapacityMode;
|
|
||||||
locations?: DatabaseAccountResponseLocation[];
|
locations?: DatabaseAccountResponseLocation[];
|
||||||
postgresqlEndpoint?: string;
|
postgresqlEndpoint?: string;
|
||||||
publicNetworkAccess?: string;
|
publicNetworkAccess?: string;
|
||||||
@@ -158,10 +157,8 @@ export interface Collection extends Resource {
|
|||||||
changeFeedPolicy?: ChangeFeedPolicy;
|
changeFeedPolicy?: ChangeFeedPolicy;
|
||||||
analyticalStorageTtl?: number;
|
analyticalStorageTtl?: number;
|
||||||
geospatialConfig?: GeospatialConfig;
|
geospatialConfig?: GeospatialConfig;
|
||||||
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
|
|
||||||
schema?: ISchema;
|
schema?: ISchema;
|
||||||
requestSchema?: () => void;
|
requestSchema?: () => void;
|
||||||
computedProperties?: ComputedProperties;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionsWithPagination {
|
export interface CollectionsWithPagination {
|
||||||
@@ -196,23 +193,10 @@ export interface IndexingPolicy {
|
|||||||
indexingMode: "consistent" | "lazy" | "none";
|
indexingMode: "consistent" | "lazy" | "none";
|
||||||
includedPaths: any;
|
includedPaths: any;
|
||||||
excludedPaths: any;
|
excludedPaths: any;
|
||||||
compositeIndexes?: any[];
|
compositeIndexes?: any;
|
||||||
spatialIndexes?: any[];
|
spatialIndexes?: any;
|
||||||
vectorIndexes?: VectorIndex[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VectorIndex {
|
|
||||||
path: string;
|
|
||||||
type: "flat" | "diskANN" | "quantizedFlat";
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ComputedProperty {
|
|
||||||
name: string;
|
|
||||||
query: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ComputedProperties = ComputedProperty[];
|
|
||||||
|
|
||||||
export interface PartitionKey {
|
export interface PartitionKey {
|
||||||
paths: string[];
|
paths: string[];
|
||||||
kind: "Hash" | "Range" | "MultiHash";
|
kind: "Hash" | "Range" | "MultiHash";
|
||||||
@@ -341,18 +325,6 @@ export interface CreateCollectionParams {
|
|||||||
partitionKey?: PartitionKey;
|
partitionKey?: PartitionKey;
|
||||||
uniqueKeyPolicy?: UniqueKeyPolicy;
|
uniqueKeyPolicy?: UniqueKeyPolicy;
|
||||||
createMongoWildcardIndex?: boolean;
|
createMongoWildcardIndex?: boolean;
|
||||||
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VectorEmbeddingPolicy {
|
|
||||||
vectorEmbeddings: VectorEmbedding[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VectorEmbedding {
|
|
||||||
dataType: "float16" | "float32" | "uint8" | "int8";
|
|
||||||
dimensions: number;
|
|
||||||
distanceFunction: "euclidean" | "cosine" | "dotproduct";
|
|
||||||
path: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReadDatabaseOfferParams {
|
export interface ReadDatabaseOfferParams {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { MessageTypes } from "Contracts/MessageTypes";
|
||||||
import * as ActionContracts from "./ActionContracts";
|
import * as ActionContracts from "./ActionContracts";
|
||||||
import * as Diagnostics from "./Diagnostics";
|
import * as Diagnostics from "./Diagnostics";
|
||||||
import { MessageTypes } from "./MessageTypes";
|
|
||||||
import * as Versions from "./Versions";
|
import * as Versions from "./Versions";
|
||||||
|
|
||||||
export { ActionContracts, Diagnostics, MessageTypes, Versions };
|
export { ActionContracts, Diagnostics, MessageTypes, Versions };
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
import { AuthorizationToken, MessageTypes } from "./MessageTypes";
|
||||||
|
|
||||||
// This is the version of these messages
|
export type FabricMessage =
|
||||||
export const FABRIC_RPC_VERSION = "2";
|
|
||||||
|
|
||||||
// Fabric to Data Explorer
|
|
||||||
|
|
||||||
// TODO Deprecated. Remove this section once DE is updated
|
|
||||||
export type FabricMessageV1 =
|
|
||||||
| {
|
| {
|
||||||
type: "newContainer";
|
type: "newContainer";
|
||||||
databaseName: string;
|
databaseName: string;
|
||||||
@@ -32,53 +26,38 @@ export type FabricMessageV1 =
|
|||||||
| {
|
| {
|
||||||
type: "allResourceTokens";
|
type: "allResourceTokens";
|
||||||
message: {
|
message: {
|
||||||
id: string;
|
|
||||||
error: string | undefined;
|
|
||||||
endpoint: string | undefined;
|
endpoint: string | undefined;
|
||||||
databaseId: string | undefined;
|
databaseId: string | undefined;
|
||||||
resourceTokens: unknown | undefined;
|
resourceTokens: unknown | undefined;
|
||||||
resourceTokensTimestamp: number | undefined;
|
resourceTokensTimestamp: number | undefined;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
// -----------------------------
|
|
||||||
|
|
||||||
export type FabricMessageV2 =
|
export type DataExploreMessage =
|
||||||
|
| "ready"
|
||||||
| {
|
| {
|
||||||
type: "newContainer";
|
type: MessageTypes.TelemetryInfo;
|
||||||
databaseName: string;
|
data: {
|
||||||
|
action: "LoadDatabases";
|
||||||
|
actionModifier: "success" | "start";
|
||||||
|
defaultExperience: "SQL";
|
||||||
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "initialize";
|
type: MessageTypes.GetAuthorizationToken;
|
||||||
version: string;
|
|
||||||
id: string;
|
id: string;
|
||||||
message: {
|
params: GetCosmosTokenMessageOptions[];
|
||||||
connectionId: string;
|
|
||||||
isVisible: boolean;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "authorizationToken";
|
type: MessageTypes.GetAllResourceTokens;
|
||||||
message: {
|
|
||||||
id: string;
|
|
||||||
error: string | undefined;
|
|
||||||
data: AuthorizationToken | undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "allResourceTokens_v2";
|
|
||||||
message: {
|
|
||||||
id: string;
|
|
||||||
error: string | undefined;
|
|
||||||
data: FabricDatabaseConnectionInfo | undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "explorerVisible";
|
|
||||||
message: {
|
|
||||||
visible: boolean;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetCosmosTokenMessageOptions = {
|
||||||
|
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
|
||||||
|
resourceType: "" | "dbs" | "colls" | "docs" | "sprocs" | "pkranges";
|
||||||
|
resourceId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type CosmosDBTokenResponse = {
|
export type CosmosDBTokenResponse = {
|
||||||
token: string;
|
token: string;
|
||||||
date: string;
|
date: string;
|
||||||
@@ -87,9 +66,12 @@ export type CosmosDBTokenResponse = {
|
|||||||
export type CosmosDBConnectionInfoResponse = {
|
export type CosmosDBConnectionInfoResponse = {
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
resourceTokens: { [resourceId: string]: string };
|
resourceTokens: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface FabricDatabaseConnectionInfo extends CosmosDBConnectionInfoResponse {
|
export interface FabricDatabaseConnectionInfo {
|
||||||
|
endpoint: string;
|
||||||
|
databaseId: string;
|
||||||
|
resourceTokens: { [resourceId: string]: string };
|
||||||
resourceTokensTimestamp: number;
|
resourceTokensTimestamp: number;
|
||||||
}
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
/**
|
|
||||||
* Data Explorer -> Fabric communication.
|
|
||||||
*/
|
|
||||||
export enum FabricMessageTypes {
|
|
||||||
GetAuthorizationToken = "GetAuthorizationToken",
|
|
||||||
GetAllResourceTokens = "GetAllResourceTokens",
|
|
||||||
Ready = "Ready",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthorizationToken {
|
|
||||||
XDate: string;
|
|
||||||
PrimaryReadWriteToken: string;
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Messaging types used with Data Explorer <-> Portal communication,
|
* Messaging types used with Data Explorer <-> Portal communication,
|
||||||
* Hosted <-> Explorer communication
|
* Hosted <-> Explorer communication and Data Explorer -> Fabric communication.
|
||||||
*
|
|
||||||
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
||||||
* WARNING: !!!!!!! YOU CAN ONLY ADD NEW TYPES TO THE END OF THIS ENUM !!!!!!!
|
|
||||||
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
||||||
*
|
|
||||||
* Enum are integers, so inserting or deleting a type will break the communication.
|
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
export enum MessageTypes {
|
export enum MessageTypes {
|
||||||
TelemetryInfo,
|
TelemetryInfo,
|
||||||
@@ -44,9 +37,13 @@ export enum MessageTypes {
|
|||||||
DisplayNPSSurvey,
|
DisplayNPSSurvey,
|
||||||
OpenVCoreMongoNetworkingBlade,
|
OpenVCoreMongoNetworkingBlade,
|
||||||
OpenVCoreMongoConnectionStringsBlade,
|
OpenVCoreMongoConnectionStringsBlade,
|
||||||
GetAuthorizationToken, // unused. Can be removed if the portal uses the same list of enums.
|
|
||||||
GetAllResourceTokens, // unused. Can be removed if the portal uses the same list of enums.
|
// Data Explorer -> Fabric communication
|
||||||
Ready, // unused. Can be removed if the portal uses the same list of enums.
|
GetAuthorizationToken,
|
||||||
OpenCESCVAFeedbackBlade,
|
GetAllResourceTokens,
|
||||||
ActivateTab,
|
}
|
||||||
|
|
||||||
|
export interface AuthorizationToken {
|
||||||
|
XDate: string;
|
||||||
|
PrimaryReadWriteToken: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,6 @@ export interface Collection extends CollectionBase {
|
|||||||
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
||||||
geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
|
geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
|
||||||
documentIds: ko.ObservableArray<DocumentId>;
|
documentIds: ko.ObservableArray<DocumentId>;
|
||||||
computedProperties: ko.Observable<DataModels.ComputedProperties>;
|
|
||||||
|
|
||||||
cassandraKeys: CassandraTableKeys;
|
cassandraKeys: CassandraTableKeys;
|
||||||
cassandraSchema: CassandraTableKey[];
|
cassandraSchema: CassandraTableKey[];
|
||||||
@@ -176,11 +175,6 @@ export interface Collection extends CollectionBase {
|
|||||||
loadTriggers(): Promise<any>;
|
loadTriggers(): Promise<any>;
|
||||||
loadOffer(): Promise<void>;
|
loadOffer(): Promise<void>;
|
||||||
|
|
||||||
showStoredProcedures: ko.Observable<boolean>;
|
|
||||||
showTriggers: ko.Observable<boolean>;
|
|
||||||
showUserDefinedFunctions: ko.Observable<boolean>;
|
|
||||||
showConflicts: ko.Observable<boolean>;
|
|
||||||
|
|
||||||
createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure;
|
createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure;
|
||||||
createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction;
|
createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction;
|
||||||
createTriggerNode(data: TriggerDefinition | SqlTriggerResource): Trigger;
|
createTriggerNode(data: TriggerDefinition | SqlTriggerResource): Trigger;
|
||||||
@@ -329,9 +323,9 @@ export enum DocumentExplorerState {
|
|||||||
noDocumentSelected,
|
noDocumentSelected,
|
||||||
newDocumentValid,
|
newDocumentValid,
|
||||||
newDocumentInvalid,
|
newDocumentInvalid,
|
||||||
existingDocumentNoEdits,
|
exisitingDocumentNoEdits,
|
||||||
existingDocumentDirtyValid,
|
exisitingDocumentDirtyValid,
|
||||||
existingDocumentDirtyInvalid,
|
exisitingDocumentDirtyInvalid,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum IndexingPolicyEditorState {
|
export enum IndexingPolicyEditorState {
|
||||||
@@ -344,9 +338,9 @@ export enum IndexingPolicyEditorState {
|
|||||||
export enum ScriptEditorState {
|
export enum ScriptEditorState {
|
||||||
newInvalid,
|
newInvalid,
|
||||||
newValid,
|
newValid,
|
||||||
existingNoEdits,
|
exisitingNoEdits,
|
||||||
existingDirtyValid,
|
exisitingDirtyValid,
|
||||||
existingDirtyInvalid,
|
exisitingDirtyInvalid,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CollectionTabKind {
|
export enum CollectionTabKind {
|
||||||
@@ -392,11 +386,9 @@ export interface DataExplorerInputsFrame {
|
|||||||
dnsSuffix?: string;
|
dnsSuffix?: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
extensionEndpoint?: string;
|
extensionEndpoint?: string;
|
||||||
portalBackendEndpoint?: string;
|
|
||||||
mongoProxyEndpoint?: string;
|
|
||||||
cassandraProxyEndpoint?: string;
|
|
||||||
subscriptionType?: SubscriptionType;
|
subscriptionType?: SubscriptionType;
|
||||||
quotaId?: string;
|
quotaId?: string;
|
||||||
|
addCollectionDefaultFlight?: string;
|
||||||
isTryCosmosDBSubscription?: boolean;
|
isTryCosmosDBSubscription?: boolean;
|
||||||
loadDatabaseAccountTimestamp?: number;
|
loadDatabaseAccountTimestamp?: number;
|
||||||
sharedThroughputMinimum?: number;
|
sharedThroughputMinimum?: number;
|
||||||
@@ -414,7 +406,6 @@ export interface DataExplorerInputsFrame {
|
|||||||
features?: {
|
features?: {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
feedbackPolicies?: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelfServeFrameInputs {
|
export interface SelfServeFrameInputs {
|
||||||
@@ -425,7 +416,6 @@ export interface SelfServeFrameInputs {
|
|||||||
authorizationToken: string;
|
authorizationToken: string;
|
||||||
csmEndpoint: string;
|
csmEndpoint: string;
|
||||||
flights?: readonly string[];
|
flights?: readonly string[];
|
||||||
catalogAPIKey: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MonacoEditorSettings {
|
export class MonacoEditorSettings {
|
||||||
|
|||||||
@@ -109,7 +109,6 @@ describe("iframe rendering when there is no data", () => {
|
|||||||
theme: 4,
|
theme: 4,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
origin: "http://localhost",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const divElement = `<div id="${Heatmap.elementId}"></div>`;
|
const divElement = `<div id="${Heatmap.elementId}"></div>`;
|
||||||
@@ -130,7 +129,6 @@ describe("iframe rendering when there is no data", () => {
|
|||||||
theme: 2,
|
theme: 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
origin: "http://localhost",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const divElement = `<div id="${Heatmap.elementId}"></div>`;
|
const divElement = `<div id="${Heatmap.elementId}"></div>`;
|
||||||
|
|||||||
1954
src/Definitions/datatables.d.ts
vendored
Normal file
2
src/Definitions/jquery-typescript.d.ts
vendored
@@ -5,7 +5,7 @@
|
|||||||
* https://github.com/running-coder/jquery-typeahead/issues/156
|
* https://github.com/running-coder/jquery-typeahead/issues/156
|
||||||
* TODO: Replace this minimum definition by the official one when it comes out.
|
* TODO: Replace this minimum definition by the official one when it comes out.
|
||||||
*/
|
*/
|
||||||
/// <reference types="jquery" />
|
/// <reference path="jquery.d.ts" />
|
||||||
|
|
||||||
interface JQueryTypeaheadParam {
|
interface JQueryTypeaheadParam {
|
||||||
input: string;
|
input: string;
|
||||||
|
|||||||
2
src/Definitions/jquery-ui.d.ts
vendored
@@ -3,7 +3,7 @@
|
|||||||
// Definitions by: Boris Yankov <https://github.com/borisyankov/>, John Reilly <https://github.com/johnnyreilly>
|
// Definitions by: Boris Yankov <https://github.com/borisyankov/>, John Reilly <https://github.com/johnnyreilly>
|
||||||
// Definitions: https://github.com/borisyankov/DefinitelyTyped
|
// Definitions: https://github.com/borisyankov/DefinitelyTyped
|
||||||
|
|
||||||
/// <reference types="jquery"/>
|
/// <reference path="jquery.d.ts"/>
|
||||||
|
|
||||||
declare namespace JQueryUI {
|
declare namespace JQueryUI {
|
||||||
// Accordion //////////////////////////////////////////////////
|
// Accordion //////////////////////////////////////////////////
|
||||||
|
|||||||
1890
src/Definitions/jquery.d.ts
vendored
Normal file
@@ -1,4 +1,3 @@
|
|||||||
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
@@ -20,6 +19,7 @@ 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 { Platform, configContext } from "./../ConfigContext";
|
||||||
|
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
|
||||||
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";
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ export interface AccordionItemComponentProps {
|
|||||||
isExpanded?: boolean;
|
isExpanded?: boolean;
|
||||||
containerStyles?: React.CSSProperties;
|
containerStyles?: React.CSSProperties;
|
||||||
styles?: React.CSSProperties;
|
styles?: React.CSSProperties;
|
||||||
children: JSX.Element;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AccordionItemComponentState {
|
interface AccordionItemComponentState {
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export interface CollapsiblePanelProps {
|
|||||||
isCollapsed: boolean;
|
isCollapsed: boolean;
|
||||||
onCollapsedChanged: (newValue: boolean) => void;
|
onCollapsedChanged: (newValue: boolean) => void;
|
||||||
collapseToLeft?: boolean;
|
collapseToLeft?: boolean;
|
||||||
children: JSX.Element | JSX.Element[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CollapsiblePanel extends React.Component<CollapsiblePanelProps> {
|
export class CollapsiblePanel extends React.Component<CollapsiblePanelProps> {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ describe("CollapsibleSectionComponent", () => {
|
|||||||
const props: CollapsibleSectionProps = {
|
const props: CollapsibleSectionProps = {
|
||||||
title: "Sample title",
|
title: "Sample title",
|
||||||
isExpandedByDefault: true,
|
isExpandedByDefault: true,
|
||||||
children: <></>,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = shallow(<CollapsibleSectionComponent {...props} />);
|
const wrapper = shallow(<CollapsibleSectionComponent {...props} />);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DirectionalHint, Icon, Label, Stack, TooltipHost } from "@fluentui/react";
|
import { Icon, Label, Stack } 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";
|
||||||
@@ -7,8 +7,6 @@ export interface CollapsibleSectionProps {
|
|||||||
title: string;
|
title: string;
|
||||||
isExpandedByDefault: boolean;
|
isExpandedByDefault: boolean;
|
||||||
onExpand?: () => void;
|
onExpand?: () => void;
|
||||||
children: JSX.Element;
|
|
||||||
tooltipContent?: string | JSX.Element | JSX.Element[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollapsibleSectionState {
|
export interface CollapsibleSectionState {
|
||||||
@@ -27,8 +25,8 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
|||||||
this.setState({ isExpanded: !this.state.isExpanded });
|
this.setState({ isExpanded: !this.state.isExpanded });
|
||||||
};
|
};
|
||||||
|
|
||||||
public componentDidUpdate(_prevProps: CollapsibleSectionProps, prevState: CollapsibleSectionState): void {
|
public componentDidUpdate(): void {
|
||||||
if (!prevState.isExpanded && this.state.isExpanded && this.props.onExpand) {
|
if (this.state.isExpanded && this.props.onExpand) {
|
||||||
this.props.onExpand();
|
this.props.onExpand();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,7 +42,7 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack
|
<Stack
|
||||||
className={"collapsibleSection"}
|
className="collapsibleSection"
|
||||||
horizontal
|
horizontal
|
||||||
verticalAlign="center"
|
verticalAlign="center"
|
||||||
tokens={accordionStackTokens}
|
tokens={accordionStackTokens}
|
||||||
@@ -56,19 +54,6 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
|||||||
>
|
>
|
||||||
<Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} />
|
<Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} />
|
||||||
<Label>{this.props.title}</Label>
|
<Label>{this.props.title}</Label>
|
||||||
{this.props.tooltipContent && (
|
|
||||||
<TooltipHost
|
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
|
||||||
content={this.props.tooltipContent}
|
|
||||||
styles={{
|
|
||||||
root: {
|
|
||||||
marginLeft: "0 !important",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
|
||||||
</TooltipHost>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
{this.state.isExpanded && this.props.children}
|
{this.state.isExpanded && this.props.children}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* React component for Command button component.
|
* React component for Command button component.
|
||||||
*/
|
*/
|
||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
|
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
|
||||||
import { KeyCodes } from "../../../Common/Constants";
|
import { KeyCodes } from "../../../Common/Constants";
|
||||||
@@ -31,7 +30,7 @@ export interface CommandButtonComponentProps {
|
|||||||
/**
|
/**
|
||||||
* Click handler for command button click
|
* Click handler for command button click
|
||||||
*/
|
*/
|
||||||
onCommandClick?: (e: React.SyntheticEvent | KeyboardEvent) => void;
|
onCommandClick: (e: React.SyntheticEvent) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Label for the button
|
* Label for the button
|
||||||
@@ -108,17 +107,10 @@ export interface CommandButtonComponentProps {
|
|||||||
* Vertical bar to divide buttons
|
* Vertical bar to divide buttons
|
||||||
*/
|
*/
|
||||||
isDivider?: boolean;
|
isDivider?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aria-label for the button
|
* Aria-label for the button
|
||||||
*/
|
*/
|
||||||
ariaLabel: string;
|
ariaLabel: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* If specified, a keyboard action that should trigger this button's onCommandClick handler when activated.
|
|
||||||
* If not specified, the button will not be triggerable by keyboard shortcuts.
|
|
||||||
*/
|
|
||||||
keyboardAction?: KeyboardAction;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> {
|
export class CommandButtonComponent extends React.Component<CommandButtonComponentProps> {
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export class DiffEditorViewModel {
|
|||||||
) {
|
) {
|
||||||
this.editorContainer = document.getElementById(this.getEditorId());
|
this.editorContainer = document.getElementById(this.getEditorId());
|
||||||
this.editorContainer.innerHTML = "";
|
this.editorContainer.innerHTML = "";
|
||||||
const options: monaco.editor.IStandaloneDiffEditorConstructionOptions = {
|
const options: monaco.editor.IDiffEditorConstructionOptions = {
|
||||||
lineNumbers: this.params.lineNumbers || "off",
|
lineNumbers: this.params.lineNumbers || "off",
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
ariaLabel: this.params.ariaLabel,
|
ariaLabel: this.params.ariaLabel,
|
||||||
|
|||||||
@@ -52,11 +52,7 @@ class EditorViewModel extends JsonEditorViewModel {
|
|||||||
if (EditorViewModel.providerRegistered.indexOf("sql") < 0) {
|
if (EditorViewModel.providerRegistered.indexOf("sql") < 0) {
|
||||||
const { SqlCompletionItemProvider } = await import("@azure/cosmos-language-service");
|
const { SqlCompletionItemProvider } = await import("@azure/cosmos-language-service");
|
||||||
const monaco = await loadMonaco();
|
const monaco = await loadMonaco();
|
||||||
monaco.languages.registerCompletionItemProvider(
|
monaco.languages.registerCompletionItemProvider("sql", new SqlCompletionItemProvider());
|
||||||
"sql",
|
|
||||||
// TODO cosmos-language-service could be outdated
|
|
||||||
new SqlCompletionItemProvider() as unknown as monaco.languages.CompletionItemProvider,
|
|
||||||
);
|
|
||||||
EditorViewModel.providerRegistered.push("sql");
|
EditorViewModel.providerRegistered.push("sql");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,7 @@ export interface EditorReactProps {
|
|||||||
lineDecorationsWidth?: monaco.editor.IEditorOptions["lineDecorationsWidth"];
|
lineDecorationsWidth?: monaco.editor.IEditorOptions["lineDecorationsWidth"];
|
||||||
minimap?: monaco.editor.IEditorOptions["minimap"];
|
minimap?: monaco.editor.IEditorOptions["minimap"];
|
||||||
scrollBeyondLastLine?: monaco.editor.IEditorOptions["scrollBeyondLastLine"];
|
scrollBeyondLastLine?: monaco.editor.IEditorOptions["scrollBeyondLastLine"];
|
||||||
fontSize?: monaco.editor.IEditorOptions["fontSize"];
|
|
||||||
monacoContainerStyles?: React.CSSProperties;
|
monacoContainerStyles?: React.CSSProperties;
|
||||||
className?: string;
|
|
||||||
spinnerClassName?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EditorReact extends React.Component<EditorReactProps, EditorReactStates> {
|
export class EditorReact extends React.Component<EditorReactProps, EditorReactStates> {
|
||||||
@@ -49,25 +46,9 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate() {
|
public componentDidUpdate(previous: EditorReactProps) {
|
||||||
if (!this.editor) {
|
if (this.props.content !== previous.content) {
|
||||||
return;
|
this.editor?.setValue(this.props.content);
|
||||||
}
|
|
||||||
|
|
||||||
const existingContent = this.editor.getModel().getValue();
|
|
||||||
|
|
||||||
if (this.props.content !== existingContent) {
|
|
||||||
if (this.props.isReadOnly) {
|
|
||||||
this.editor.setValue(this.props.content);
|
|
||||||
} else {
|
|
||||||
this.editor.pushUndoStop();
|
|
||||||
this.editor.executeEdits("", [
|
|
||||||
{
|
|
||||||
range: this.editor.getModel().getFullModelRange(),
|
|
||||||
text: this.props.content,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,11 +59,9 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
|||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{!this.state.showEditor && (
|
{!this.state.showEditor && <Spinner size={SpinnerSize.large} className="spinner" />}
|
||||||
<Spinner size={SpinnerSize.large} className={this.props.spinnerClassName || "spinner"} />
|
|
||||||
)}
|
|
||||||
<div
|
<div
|
||||||
className={this.props.className || "jsonEditor"}
|
className="jsonEditor"
|
||||||
style={this.props.monacoContainerStyles}
|
style={this.props.monacoContainerStyles}
|
||||||
ref={(elt: HTMLElement) => this.setRef(elt)}
|
ref={(elt: HTMLElement) => this.setRef(elt)}
|
||||||
/>
|
/>
|
||||||
@@ -92,14 +71,9 @@ 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;
|
||||||
|
const queryEditorModel = this.editor.getModel();
|
||||||
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.
|
queryEditorModel.onDidChangeContent(() => {
|
||||||
// If a single user input causes BOTH the editor content to change AND the cursor selection to change (which is likely),
|
|
||||||
// then there are some inconsistencies as to which event fires first.
|
|
||||||
// But the editor.onDidChangeModelContent event seems to always fire before the cursor selection event.
|
|
||||||
// (This is NOT true for the model's onDidChangeContent event, which sometimes fires after the cursor selection event.)
|
|
||||||
// If the cursor selection event fires first, then the calling component may re-render the component with old content, so we want to ensure the model content changed event always fires first.
|
|
||||||
this.editor.onDidChangeModelContent(() => {
|
|
||||||
const queryEditorModel = this.editor.getModel();
|
const queryEditorModel = this.editor.getModel();
|
||||||
this.props.onContentChanged(queryEditorModel.getValue());
|
this.props.onContentChanged(queryEditorModel.getValue());
|
||||||
});
|
});
|
||||||
@@ -119,12 +93,12 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
|||||||
* Create the monaco editor and attach to DOM
|
* Create the monaco editor and attach to DOM
|
||||||
*/
|
*/
|
||||||
private async createEditor(createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) {
|
private async createEditor(createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) {
|
||||||
const options: monaco.editor.IStandaloneEditorConstructionOptions = {
|
const options: monaco.editor.IEditorConstructionOptions = {
|
||||||
language: this.props.language,
|
language: this.props.language,
|
||||||
value: this.props.content,
|
value: this.props.content,
|
||||||
readOnly: this.props.isReadOnly,
|
readOnly: this.props.isReadOnly,
|
||||||
ariaLabel: this.props.ariaLabel,
|
ariaLabel: this.props.ariaLabel,
|
||||||
fontSize: this.props.fontSize || 12,
|
fontSize: 12,
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
theme: this.props.theme,
|
theme: this.props.theme,
|
||||||
wordWrap: this.props.wordWrap || "off",
|
wordWrap: this.props.wordWrap || "off",
|
||||||
@@ -137,13 +111,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
|||||||
|
|
||||||
this.rootNode.innerHTML = "";
|
this.rootNode.innerHTML = "";
|
||||||
const monaco = await loadMonaco();
|
const monaco = await loadMonaco();
|
||||||
try {
|
createCallback(monaco?.editor?.create(this.rootNode, options));
|
||||||
createCallback(monaco?.editor?.create(this.rootNode, options));
|
|
||||||
} catch (error) {
|
|
||||||
// This could happen if the parent node suddenly disappears during create()
|
|
||||||
console.error("Unable to create EditorReact", error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.rootNode.innerHTML) {
|
if (this.rootNode.innerHTML) {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export class JsonEditorViewModel extends WaitsForTemplateViewModel {
|
|||||||
protected async createEditor(content: string, createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) {
|
protected async createEditor(content: string, createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) {
|
||||||
this.registerCompletionItemProvider();
|
this.registerCompletionItemProvider();
|
||||||
this.editorContainer = document.getElementById(this.getEditorId());
|
this.editorContainer = document.getElementById(this.getEditorId());
|
||||||
const options: monaco.editor.IStandaloneEditorConstructionOptions = {
|
const options: monaco.editor.IEditorConstructionOptions = {
|
||||||
value: content,
|
value: content,
|
||||||
language: this.getEditorLanguage(),
|
language: this.getEditorLanguage(),
|
||||||
readOnly: this.params.isReadOnly,
|
readOnly: this.params.isReadOnly,
|
||||||
|
|||||||
@@ -29,6 +29,6 @@ describe("CodeOfConduct", () => {
|
|||||||
const wrapper = shallow(<CodeOfConduct {...codeOfConductProps} />);
|
const wrapper = shallow(<CodeOfConduct {...codeOfConductProps} />);
|
||||||
wrapper.find(".genericPaneSubmitBtn").first().simulate("click");
|
wrapper.find(".genericPaneSubmitBtn").first().simulate("click");
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
expect(codeOfConductProps.onAcceptCodeOfConduct).toHaveBeenCalled();
|
expect(codeOfConductProps.onAcceptCodeOfConduct).toBeCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,14 +7,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settingsV2ToolTip {
|
.settingsV2ToolTip {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font: 12px @DataExplorerFont;
|
font: 12px @DataExplorerFont;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.autoPilotSelector span {
|
.autoPilotSelector span {
|
||||||
height: 25px;
|
height: 25px;
|
||||||
font: 14px @DataExplorerFont;
|
font: 14px @DataExplorerFont;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settingsV2TabsContainer {
|
.settingsV2TabsContainer {
|
||||||
@@ -25,14 +25,7 @@
|
|||||||
font-family: @DataExplorerFont;
|
font-family: @DataExplorerFont;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settingsV2Editor {
|
.settingsV2IndexingPolicyEditor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 60vh;
|
height: 60vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settingsV2EditorSpinner {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@ import { updateUserContext } from "../../../UserContext";
|
|||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { CollectionSettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
import { CollectionSettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
||||||
import { SettingsComponent, SettingsComponentProps, SettingsComponentState } from "./SettingsComponent";
|
import { SettingsComponent, SettingsComponentProps, SettingsComponentState } from "./SettingsComponent";
|
||||||
import { TtlType, isDirty } from "./SettingsUtils";
|
import { isDirty, TtlType } from "./SettingsUtils";
|
||||||
import { collection } from "./TestUtils";
|
import { collection } from "./TestUtils";
|
||||||
jest.mock("../../../Common/dataAccess/getIndexTransformationProgress", () => ({
|
jest.mock("../../../Common/dataAccess/getIndexTransformationProgress", () => ({
|
||||||
getIndexTransformationProgress: jest.fn().mockReturnValue(undefined),
|
getIndexTransformationProgress: jest.fn().mockReturnValue(undefined),
|
||||||
@@ -190,8 +190,8 @@ describe("SettingsComponent", () => {
|
|||||||
id: "id",
|
id: "id",
|
||||||
};
|
};
|
||||||
await settingsComponentInstance.onSaveClick();
|
await settingsComponentInstance.onSaveClick();
|
||||||
expect(updateCollection).toHaveBeenCalled();
|
expect(updateCollection).toBeCalled();
|
||||||
expect(updateOffer).toHaveBeenCalled();
|
expect(updateOffer).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("revert resets state values", async () => {
|
it("revert resets state values", async () => {
|
||||||
|
|||||||
@@ -1,15 +1,5 @@
|
|||||||
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react";
|
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react";
|
||||||
import {
|
|
||||||
ComputedPropertiesComponent,
|
|
||||||
ComputedPropertiesComponentProps,
|
|
||||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent";
|
|
||||||
import {
|
|
||||||
ContainerVectorPolicyComponent,
|
|
||||||
ContainerVectorPolicyComponentProps,
|
|
||||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent";
|
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
|
||||||
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import DiscardIcon from "../../../../images/discard.svg";
|
import DiscardIcon from "../../../../images/discard.svg";
|
||||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||||
@@ -28,10 +18,6 @@ import { userContext } from "../../../UserContext";
|
|||||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||||
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import {
|
|
||||||
PartitionKeyComponent,
|
|
||||||
PartitionKeyComponentProps,
|
|
||||||
} from "../../Controls/Settings/SettingsSubComponents/PartitionKeyComponent";
|
|
||||||
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
||||||
import "./SettingsComponent.less";
|
import "./SettingsComponent.less";
|
||||||
@@ -117,11 +103,6 @@ export interface SettingsComponentState {
|
|||||||
indexesToAdd: AddMongoIndexProps[];
|
indexesToAdd: AddMongoIndexProps[];
|
||||||
indexTransformationProgress: number;
|
indexTransformationProgress: number;
|
||||||
|
|
||||||
computedPropertiesContent: DataModels.ComputedProperties;
|
|
||||||
computedPropertiesContentBaseline: DataModels.ComputedProperties;
|
|
||||||
shouldDiscardComputedProperties: boolean;
|
|
||||||
isComputedPropertiesDirty: boolean;
|
|
||||||
|
|
||||||
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode;
|
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode;
|
||||||
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode;
|
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode;
|
||||||
conflictResolutionPolicyPath: string;
|
conflictResolutionPolicyPath: string;
|
||||||
@@ -146,10 +127,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private offer: DataModels.Offer;
|
private offer: DataModels.Offer;
|
||||||
private changeFeedPolicyVisible: boolean;
|
private changeFeedPolicyVisible: boolean;
|
||||||
private isFixedContainer: boolean;
|
private isFixedContainer: boolean;
|
||||||
private shouldShowComputedPropertiesEditor: boolean;
|
|
||||||
private shouldShowIndexingPolicyEditor: boolean;
|
private shouldShowIndexingPolicyEditor: boolean;
|
||||||
private shouldShowPartitionKeyEditor: boolean;
|
|
||||||
private isVectorSearchEnabled: boolean;
|
|
||||||
private totalThroughputUsed: number;
|
private totalThroughputUsed: number;
|
||||||
public mongoDBCollectionResource: MongoDBCollectionResource;
|
public mongoDBCollectionResource: MongoDBCollectionResource;
|
||||||
|
|
||||||
@@ -161,10 +139,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.collection = this.props.settingsTab.collection as ViewModels.Collection;
|
this.collection = this.props.settingsTab.collection as ViewModels.Collection;
|
||||||
this.offer = this.collection?.offer();
|
this.offer = this.collection?.offer();
|
||||||
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
|
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
|
||||||
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
|
|
||||||
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
||||||
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
|
|
||||||
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
|
||||||
|
|
||||||
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
||||||
|
|
||||||
@@ -216,11 +191,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
isMongoIndexingPolicyDiscardable: false,
|
isMongoIndexingPolicyDiscardable: false,
|
||||||
indexTransformationProgress: undefined,
|
indexTransformationProgress: undefined,
|
||||||
|
|
||||||
computedPropertiesContent: undefined,
|
|
||||||
computedPropertiesContentBaseline: undefined,
|
|
||||||
shouldDiscardComputedProperties: false,
|
|
||||||
isComputedPropertiesDirty: false,
|
|
||||||
|
|
||||||
conflictResolutionPolicyMode: undefined,
|
conflictResolutionPolicyMode: undefined,
|
||||||
conflictResolutionPolicyModeBaseline: undefined,
|
conflictResolutionPolicyModeBaseline: undefined,
|
||||||
conflictResolutionPolicyPath: undefined,
|
conflictResolutionPolicyPath: undefined,
|
||||||
@@ -311,7 +281,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.state.isSubSettingsSaveable ||
|
this.state.isSubSettingsSaveable ||
|
||||||
this.state.isIndexingPolicyDirty ||
|
this.state.isIndexingPolicyDirty ||
|
||||||
this.state.isConflictResolutionDirty ||
|
this.state.isConflictResolutionDirty ||
|
||||||
this.state.isComputedPropertiesDirty ||
|
|
||||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable)
|
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -322,7 +291,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.state.isSubSettingsDiscardable ||
|
this.state.isSubSettingsDiscardable ||
|
||||||
this.state.isIndexingPolicyDirty ||
|
this.state.isIndexingPolicyDirty ||
|
||||||
this.state.isConflictResolutionDirty ||
|
this.state.isConflictResolutionDirty ||
|
||||||
this.state.isComputedPropertiesDirty ||
|
|
||||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable)
|
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -427,9 +395,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
isMongoIndexingPolicySaveable: false,
|
isMongoIndexingPolicySaveable: false,
|
||||||
isMongoIndexingPolicyDiscardable: false,
|
isMongoIndexingPolicyDiscardable: false,
|
||||||
isConflictResolutionDirty: false,
|
isConflictResolutionDirty: false,
|
||||||
computedPropertiesContent: this.state.computedPropertiesContentBaseline,
|
|
||||||
shouldDiscardComputedProperties: true,
|
|
||||||
isComputedPropertiesDirty: false,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -549,31 +514,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private onMongoIndexingPolicyDiscardableChange = (isMongoIndexingPolicyDiscardable: boolean): void =>
|
private onMongoIndexingPolicyDiscardableChange = (isMongoIndexingPolicyDiscardable: boolean): void =>
|
||||||
this.setState({ isMongoIndexingPolicyDiscardable });
|
this.setState({ isMongoIndexingPolicyDiscardable });
|
||||||
|
|
||||||
private onComputedPropertiesContentChange = (newComputedProperties: DataModels.ComputedProperties): void =>
|
|
||||||
this.setState({ computedPropertiesContent: newComputedProperties });
|
|
||||||
|
|
||||||
private resetShouldDiscardComputedProperties = (): void => this.setState({ shouldDiscardComputedProperties: false });
|
|
||||||
|
|
||||||
private logComputedPropertiesSuccessMessage = (): void => {
|
|
||||||
if (this.props.settingsTab.onLoadStartKey) {
|
|
||||||
traceSuccess(
|
|
||||||
Action.Tab,
|
|
||||||
{
|
|
||||||
databaseName: this.collection.databaseId,
|
|
||||||
collectionName: this.collection.id(),
|
|
||||||
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle: this.props.settingsTab.tabTitle(),
|
|
||||||
},
|
|
||||||
this.props.settingsTab.onLoadStartKey,
|
|
||||||
);
|
|
||||||
this.props.settingsTab.onLoadStartKey = undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private onComputedPropertiesDirtyChange = (isComputedPropertiesDirty: boolean): void =>
|
|
||||||
this.setState({ isComputedPropertiesDirty: isComputedPropertiesDirty });
|
|
||||||
|
|
||||||
private calculateTotalThroughputUsed = (): void => {
|
private calculateTotalThroughputUsed = (): void => {
|
||||||
this.totalThroughputUsed = 0;
|
this.totalThroughputUsed = 0;
|
||||||
(useDatabases.getState().databases || []).forEach(async (database) => {
|
(useDatabases.getState().databases || []).forEach(async (database) => {
|
||||||
@@ -696,6 +636,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
const indexingPolicyContent = this.collection.indexingPolicy();
|
const indexingPolicyContent = this.collection.indexingPolicy();
|
||||||
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
|
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
|
||||||
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
|
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
|
||||||
|
|
||||||
const conflictResolutionPolicyMode = parseConflictResolutionMode(conflictResolutionPolicy?.mode);
|
const conflictResolutionPolicyMode = parseConflictResolutionMode(conflictResolutionPolicy?.mode);
|
||||||
const conflictResolutionPolicyPath = conflictResolutionPolicy?.conflictResolutionPath;
|
const conflictResolutionPolicyPath = conflictResolutionPolicy?.conflictResolutionPath;
|
||||||
const conflictResolutionPolicyProcedure = parseConflictResolutionProcedure(
|
const conflictResolutionPolicyProcedure = parseConflictResolutionProcedure(
|
||||||
@@ -704,12 +645,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
const geospatialConfigTypeString: string =
|
const geospatialConfigTypeString: string =
|
||||||
(this.collection.geospatialConfig && this.collection.geospatialConfig()?.type) || GeospatialConfigType.Geometry;
|
(this.collection.geospatialConfig && this.collection.geospatialConfig()?.type) || GeospatialConfigType.Geometry;
|
||||||
const geoSpatialConfigType = GeospatialConfigType[geospatialConfigTypeString as keyof typeof GeospatialConfigType];
|
const geoSpatialConfigType = GeospatialConfigType[geospatialConfigTypeString as keyof typeof GeospatialConfigType];
|
||||||
let computedPropertiesContent = this.collection.computedProperties();
|
|
||||||
if (!computedPropertiesContent || computedPropertiesContent.length === 0) {
|
|
||||||
computedPropertiesContent = [
|
|
||||||
{ name: "name_of_property", query: "query_to_compute_property" },
|
|
||||||
] as DataModels.ComputedProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
throughput: offerThroughput,
|
throughput: offerThroughput,
|
||||||
@@ -736,8 +671,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
conflictResolutionPolicyProcedureBaseline: conflictResolutionPolicyProcedure,
|
conflictResolutionPolicyProcedureBaseline: conflictResolutionPolicyProcedure,
|
||||||
geospatialConfigType: geoSpatialConfigType,
|
geospatialConfigType: geoSpatialConfigType,
|
||||||
geospatialConfigTypeBaseline: geoSpatialConfigType,
|
geospatialConfigTypeBaseline: geoSpatialConfigType,
|
||||||
computedPropertiesContent: computedPropertiesContent,
|
|
||||||
computedPropertiesContentBaseline: computedPropertiesContent,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -854,12 +787,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private saveCollectionSettings = async (startKey: number): Promise<void> => {
|
private saveCollectionSettings = async (startKey: number): Promise<void> => {
|
||||||
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
|
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
|
||||||
|
|
||||||
if (
|
if (this.state.isSubSettingsSaveable || this.state.isIndexingPolicyDirty || this.state.isConflictResolutionDirty) {
|
||||||
this.state.isSubSettingsSaveable ||
|
|
||||||
this.state.isIndexingPolicyDirty ||
|
|
||||||
this.state.isConflictResolutionDirty ||
|
|
||||||
this.state.isComputedPropertiesDirty
|
|
||||||
) {
|
|
||||||
let defaultTtl: number;
|
let defaultTtl: number;
|
||||||
switch (this.state.timeToLive) {
|
switch (this.state.timeToLive) {
|
||||||
case TtlType.On:
|
case TtlType.On:
|
||||||
@@ -897,10 +825,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
newCollection.conflictResolutionPolicy = conflictResolutionChanges;
|
newCollection.conflictResolutionPolicy = conflictResolutionChanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.isComputedPropertiesDirty) {
|
|
||||||
newCollection.computedProperties = this.state.computedPropertiesContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedCollection: DataModels.Collection = await updateCollection(
|
const updatedCollection: DataModels.Collection = await updateCollection(
|
||||||
this.collection.databaseId,
|
this.collection.databaseId,
|
||||||
this.collection.id(),
|
this.collection.id(),
|
||||||
@@ -914,7 +838,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.collection.conflictResolutionPolicy(updatedCollection.conflictResolutionPolicy);
|
this.collection.conflictResolutionPolicy(updatedCollection.conflictResolutionPolicy);
|
||||||
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
|
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
|
||||||
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
|
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
|
||||||
this.collection.computedProperties(updatedCollection.computedProperties);
|
|
||||||
|
|
||||||
if (wasIndexingPolicyModified) {
|
if (wasIndexingPolicyModified) {
|
||||||
await this.refreshIndexTransformationProgress();
|
await this.refreshIndexTransformationProgress();
|
||||||
@@ -925,7 +848,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
isSubSettingsDiscardable: false,
|
isSubSettingsDiscardable: false,
|
||||||
isIndexingPolicyDirty: false,
|
isIndexingPolicyDirty: false,
|
||||||
isConflictResolutionDirty: false,
|
isConflictResolutionDirty: false,
|
||||||
isComputedPropertiesDirty: false,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1104,7 +1026,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
indexTransformationProgress: this.state.indexTransformationProgress,
|
indexTransformationProgress: this.state.indexTransformationProgress,
|
||||||
refreshIndexTransformationProgress: this.refreshIndexTransformationProgress,
|
refreshIndexTransformationProgress: this.refreshIndexTransformationProgress,
|
||||||
onIndexingPolicyDirtyChange: this.onIndexingPolicyDirtyChange,
|
onIndexingPolicyDirtyChange: this.onIndexingPolicyDirtyChange,
|
||||||
isVectorSearchEnabled: this.isVectorSearchEnabled,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mongoIndexingPolicyComponentProps: MongoIndexingPolicyComponentProps = {
|
const mongoIndexingPolicyComponentProps: MongoIndexingPolicyComponentProps = {
|
||||||
@@ -1121,16 +1042,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
onMongoIndexingPolicyDiscardableChange: this.onMongoIndexingPolicyDiscardableChange,
|
onMongoIndexingPolicyDiscardableChange: this.onMongoIndexingPolicyDiscardableChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
const computedPropertiesComponentProps: ComputedPropertiesComponentProps = {
|
|
||||||
computedPropertiesContent: this.state.computedPropertiesContent,
|
|
||||||
computedPropertiesContentBaseline: this.state.computedPropertiesContentBaseline,
|
|
||||||
logComputedPropertiesSuccessMessage: this.logComputedPropertiesSuccessMessage,
|
|
||||||
onComputedPropertiesContentChange: this.onComputedPropertiesContentChange,
|
|
||||||
onComputedPropertiesDirtyChange: this.onComputedPropertiesDirtyChange,
|
|
||||||
resetShouldDiscardComputedProperties: this.resetShouldDiscardComputedProperties,
|
|
||||||
shouldDiscardComputedProperties: this.state.shouldDiscardComputedProperties,
|
|
||||||
};
|
|
||||||
|
|
||||||
const conflictResolutionPolicyComponentProps: ConflictResolutionComponentProps = {
|
const conflictResolutionPolicyComponentProps: ConflictResolutionComponentProps = {
|
||||||
collection: this.collection,
|
collection: this.collection,
|
||||||
conflictResolutionPolicyMode: this.state.conflictResolutionPolicyMode,
|
conflictResolutionPolicyMode: this.state.conflictResolutionPolicyMode,
|
||||||
@@ -1145,16 +1056,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange,
|
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
const partitionKeyComponentProps: PartitionKeyComponentProps = {
|
|
||||||
database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId),
|
|
||||||
collection: this.collection,
|
|
||||||
explorer: this.props.settingsTab.getContainer(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const containerVectorPolicyProps: ContainerVectorPolicyComponentProps = {
|
|
||||||
vectorEmbeddingPolicy: this.collection.rawDataModel?.vectorEmbeddingPolicy,
|
|
||||||
};
|
|
||||||
|
|
||||||
const tabs: SettingsV2TabInfo[] = [];
|
const tabs: SettingsV2TabInfo[] = [];
|
||||||
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
@@ -1168,13 +1069,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
content: <SubSettingsComponent {...subSettingsComponentProps} />,
|
content: <SubSettingsComponent {...subSettingsComponentProps} />,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.isVectorSearchEnabled) {
|
|
||||||
tabs.push({
|
|
||||||
tab: SettingsV2TabTypes.ContainerVectorPolicyTab,
|
|
||||||
content: <ContainerVectorPolicyComponent {...containerVectorPolicyProps} />,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.shouldShowIndexingPolicyEditor) {
|
if (this.shouldShowIndexingPolicyEditor) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||||
@@ -1197,20 +1091,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.shouldShowPartitionKeyEditor) {
|
|
||||||
tabs.push({
|
|
||||||
tab: SettingsV2TabTypes.PartitionKeyTab,
|
|
||||||
content: <PartitionKeyComponent {...partitionKeyComponentProps} />,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.shouldShowComputedPropertiesEditor) {
|
|
||||||
tabs.push({
|
|
||||||
tab: SettingsV2TabTypes.ComputedPropertiesTab,
|
|
||||||
content: <ComputedPropertiesComponent {...computedPropertiesComponentProps} />,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const pivotProps: IPivotProps = {
|
const pivotProps: IPivotProps = {
|
||||||
onLinkClick: this.onPivotChange,
|
onLinkClick: this.onPivotChange,
|
||||||
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
getThroughputApplyLongDelayMessage,
|
getThroughputApplyLongDelayMessage,
|
||||||
getThroughputApplyShortDelayMessage,
|
getThroughputApplyShortDelayMessage,
|
||||||
getToolTipContainer,
|
getToolTipContainer,
|
||||||
|
indexingPolicynUnsavedWarningMessage,
|
||||||
manualToAutoscaleDisclaimerElement,
|
manualToAutoscaleDisclaimerElement,
|
||||||
mongoIndexTransformationRefreshingMessage,
|
mongoIndexTransformationRefreshingMessage,
|
||||||
mongoIndexingPolicyAADError,
|
mongoIndexingPolicyAADError,
|
||||||
@@ -38,6 +39,7 @@ class SettingsRenderUtilsTestComponent extends React.Component {
|
|||||||
|
|
||||||
{manualToAutoscaleDisclaimerElement}
|
{manualToAutoscaleDisclaimerElement}
|
||||||
{ttlWarning}
|
{ttlWarning}
|
||||||
|
{indexingPolicynUnsavedWarningMessage}
|
||||||
{updateThroughputDelayedApplyWarningMessage}
|
{updateThroughputDelayedApplyWarningMessage}
|
||||||
|
|
||||||
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||||
|
|||||||
@@ -61,8 +61,6 @@ export interface PriceBreakdown {
|
|||||||
currencySign: string;
|
currencySign: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type editorType = "indexPolicy" | "computedProperties";
|
|
||||||
|
|
||||||
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "windowtext" } };
|
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "windowtext" } };
|
||||||
|
|
||||||
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
||||||
@@ -256,10 +254,9 @@ export const ttlWarning: JSX.Element = (
|
|||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const unsavedEditorWarningMessage = (editor: editorType): JSX.Element => (
|
export const indexingPolicynUnsavedWarningMessage: JSX.Element = (
|
||||||
<Text styles={infoAndToolTipTextStyle}>
|
<Text styles={infoAndToolTipTextStyle}>
|
||||||
You have not saved the latest changes made to your{" "}
|
You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes.
|
||||||
{editor === "indexPolicy" ? "indexing policy" : "computed properties"}. Please click save to confirm the changes.
|
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
import * as DataModels from "Contracts/DataModels";
|
|
||||||
import { shallow } from "enzyme";
|
|
||||||
import React from "react";
|
|
||||||
import { ComputedPropertiesComponent, ComputedPropertiesComponentProps } from "./ComputedPropertiesComponent";
|
|
||||||
|
|
||||||
describe("ComputedPropertiesComponent", () => {
|
|
||||||
const initialComputedPropertiesContent: DataModels.ComputedProperties = [
|
|
||||||
{
|
|
||||||
name: "prop1",
|
|
||||||
query: "query1",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const baseProps: ComputedPropertiesComponentProps = {
|
|
||||||
computedPropertiesContent: initialComputedPropertiesContent,
|
|
||||||
computedPropertiesContentBaseline: initialComputedPropertiesContent,
|
|
||||||
logComputedPropertiesSuccessMessage: () => {
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
onComputedPropertiesContentChange: () => {
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
onComputedPropertiesDirtyChange: () => {
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
resetShouldDiscardComputedProperties: () => {
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
shouldDiscardComputedProperties: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
it("renders", () => {
|
|
||||||
const wrapper = shallow(<ComputedPropertiesComponent {...baseProps} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("computed properties are reset", () => {
|
|
||||||
const wrapper = shallow(<ComputedPropertiesComponent {...baseProps} />);
|
|
||||||
|
|
||||||
const computedPropertiesComponentInstance = wrapper.instance() as ComputedPropertiesComponent;
|
|
||||||
const resetComputedPropertiesEditorMockFn = jest.fn();
|
|
||||||
computedPropertiesComponentInstance.resetComputedPropertiesEditor = resetComputedPropertiesEditorMockFn;
|
|
||||||
|
|
||||||
wrapper.setProps({ shouldDiscardComputedProperties: true });
|
|
||||||
wrapper.update();
|
|
||||||
expect(resetComputedPropertiesEditorMockFn.mock.calls.length).toEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("dirty is set", () => {
|
|
||||||
let computedPropertiesComponent = new ComputedPropertiesComponent(baseProps);
|
|
||||||
expect(computedPropertiesComponent.IsComponentDirty()).toEqual(false);
|
|
||||||
|
|
||||||
const newProps = { ...baseProps, computedPropertiesContent: undefined as DataModels.ComputedProperties };
|
|
||||||
computedPropertiesComponent = new ComputedPropertiesComponent(newProps);
|
|
||||||
expect(computedPropertiesComponent.IsComponentDirty()).toEqual(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import { FontIcon, Link, MessageBar, MessageBarType, Stack, Text } from "@fluentui/react";
|
|
||||||
import * as DataModels from "Contracts/DataModels";
|
|
||||||
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "Explorer/Controls/Settings/SettingsRenderUtils";
|
|
||||||
import { isDirty } from "Explorer/Controls/Settings/SettingsUtils";
|
|
||||||
import { loadMonaco } from "Explorer/LazyMonaco";
|
|
||||||
import * as monaco from "monaco-editor";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
export interface ComputedPropertiesComponentProps {
|
|
||||||
computedPropertiesContent: DataModels.ComputedProperties;
|
|
||||||
computedPropertiesContentBaseline: DataModels.ComputedProperties;
|
|
||||||
logComputedPropertiesSuccessMessage: () => void;
|
|
||||||
onComputedPropertiesContentChange: (newComputedProperties: DataModels.ComputedProperties) => void;
|
|
||||||
onComputedPropertiesDirtyChange: (isComputedPropertiesDirty: boolean) => void;
|
|
||||||
resetShouldDiscardComputedProperties: () => void;
|
|
||||||
shouldDiscardComputedProperties: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ComputedPropertiesComponentState {
|
|
||||||
computedPropertiesContentIsValid: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComputedPropertiesComponent extends React.Component<
|
|
||||||
ComputedPropertiesComponentProps,
|
|
||||||
ComputedPropertiesComponentState
|
|
||||||
> {
|
|
||||||
private shouldCheckComponentIsDirty = true;
|
|
||||||
private computedPropertiesDiv = React.createRef<HTMLDivElement>();
|
|
||||||
private computedPropertiesEditor: monaco.editor.IStandaloneCodeEditor;
|
|
||||||
|
|
||||||
constructor(props: ComputedPropertiesComponentProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
computedPropertiesContentIsValid: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(): void {
|
|
||||||
if (this.props.shouldDiscardComputedProperties) {
|
|
||||||
this.resetComputedPropertiesEditor();
|
|
||||||
this.props.resetShouldDiscardComputedProperties();
|
|
||||||
}
|
|
||||||
this.onComponentUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount(): void {
|
|
||||||
this.resetComputedPropertiesEditor();
|
|
||||||
this.onComponentUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
public resetComputedPropertiesEditor = (): void => {
|
|
||||||
if (!this.computedPropertiesEditor) {
|
|
||||||
this.createComputedPropertiesEditor();
|
|
||||||
} else {
|
|
||||||
const indexingPolicyEditorModel = this.computedPropertiesEditor.getModel();
|
|
||||||
const value: string = JSON.stringify(this.props.computedPropertiesContent, undefined, 4);
|
|
||||||
indexingPolicyEditorModel.setValue(value);
|
|
||||||
}
|
|
||||||
this.onComponentUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
private onComponentUpdate = (): void => {
|
|
||||||
if (!this.shouldCheckComponentIsDirty) {
|
|
||||||
this.shouldCheckComponentIsDirty = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.props.onComputedPropertiesDirtyChange(this.IsComponentDirty());
|
|
||||||
this.shouldCheckComponentIsDirty = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
public IsComponentDirty = (): boolean => {
|
|
||||||
if (
|
|
||||||
isDirty(this.props.computedPropertiesContent, this.props.computedPropertiesContentBaseline) &&
|
|
||||||
this.state.computedPropertiesContentIsValid
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
private async createComputedPropertiesEditor(): Promise<void> {
|
|
||||||
const value: string = JSON.stringify(this.props.computedPropertiesContent, undefined, 4);
|
|
||||||
const monaco = await loadMonaco();
|
|
||||||
this.computedPropertiesEditor = monaco.editor.create(this.computedPropertiesDiv.current, {
|
|
||||||
value: value,
|
|
||||||
language: "json",
|
|
||||||
ariaLabel: "Computed properties",
|
|
||||||
});
|
|
||||||
if (this.computedPropertiesEditor) {
|
|
||||||
const computedPropertiesEditorModel = this.computedPropertiesEditor.getModel();
|
|
||||||
computedPropertiesEditorModel.onDidChangeContent(this.onEditorContentChange.bind(this));
|
|
||||||
this.props.logComputedPropertiesSuccessMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onEditorContentChange = (): void => {
|
|
||||||
const computedPropertiesEditorModel = this.computedPropertiesEditor.getModel();
|
|
||||||
try {
|
|
||||||
const newComputedPropertiesContent = JSON.parse(
|
|
||||||
computedPropertiesEditorModel.getValue(),
|
|
||||||
) as DataModels.ComputedProperties;
|
|
||||||
this.props.onComputedPropertiesContentChange(newComputedPropertiesContent);
|
|
||||||
this.setState({ computedPropertiesContentIsValid: true });
|
|
||||||
} catch (e) {
|
|
||||||
this.setState({ computedPropertiesContentIsValid: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public render(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Stack {...titleAndInputStackProps}>
|
|
||||||
{isDirty(this.props.computedPropertiesContent, this.props.computedPropertiesContentBaseline) && (
|
|
||||||
<MessageBar messageBarType={MessageBarType.warning}>
|
|
||||||
{unsavedEditorWarningMessage("computedProperties")}
|
|
||||||
</MessageBar>
|
|
||||||
)}
|
|
||||||
<Text style={{ marginLeft: "30px", marginBottom: "10px" }}>
|
|
||||||
<Link target="_blank" href="https://aka.ms/computed-properties-preview/">
|
|
||||||
{"Learn more"} <FontIcon iconName="NavigateExternalInline" />
|
|
||||||
</Link>
|
|
||||||
  about how to define computed properties and how to use them.
|
|
||||||
</Text>
|
|
||||||
<div className="settingsV2Editor" tabIndex={0} ref={this.computedPropertiesDiv}></div>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { Stack } from "@fluentui/react";
|
|
||||||
import { VectorEmbeddingPolicy } from "Contracts/DataModels";
|
|
||||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
|
||||||
import { titleAndInputStackProps } from "Explorer/Controls/Settings/SettingsRenderUtils";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export interface ContainerVectorPolicyComponentProps {
|
|
||||||
vectorEmbeddingPolicy: VectorEmbeddingPolicy;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ContainerVectorPolicyComponent: React.FC<ContainerVectorPolicyComponentProps> = ({
|
|
||||||
vectorEmbeddingPolicy,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative" } }}>
|
|
||||||
<EditorReact
|
|
||||||
language={"json"}
|
|
||||||
content={JSON.stringify(vectorEmbeddingPolicy || {}, null, 4)}
|
|
||||||
isReadOnly={true}
|
|
||||||
wordWrap={"on"}
|
|
||||||
ariaLabel={"Container vector policy"}
|
|
||||||
lineNumbers={"on"}
|
|
||||||
scrollBeyondLastLine={false}
|
|
||||||
className={"settingsV2Editor"}
|
|
||||||
spinnerClassName={"settingsV2EditorSpinner"}
|
|
||||||
fontSize={14}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -3,7 +3,7 @@ import * as monaco from "monaco-editor";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as DataModels from "../../../../Contracts/DataModels";
|
import * as DataModels from "../../../../Contracts/DataModels";
|
||||||
import { loadMonaco } from "../../../LazyMonaco";
|
import { loadMonaco } from "../../../LazyMonaco";
|
||||||
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils";
|
import { indexingPolicynUnsavedWarningMessage, titleAndInputStackProps } from "../SettingsRenderUtils";
|
||||||
import { isDirty, isIndexTransforming } from "../SettingsUtils";
|
import { isDirty, isIndexTransforming } from "../SettingsUtils";
|
||||||
import { IndexingPolicyRefreshComponent } from "./IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
import { IndexingPolicyRefreshComponent } from "./IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
||||||
|
|
||||||
@@ -16,7 +16,6 @@ export interface IndexingPolicyComponentProps {
|
|||||||
logIndexingPolicySuccessMessage: () => void;
|
logIndexingPolicySuccessMessage: () => void;
|
||||||
indexTransformationProgress: number;
|
indexTransformationProgress: number;
|
||||||
refreshIndexTransformationProgress: () => Promise<void>;
|
refreshIndexTransformationProgress: () => Promise<void>;
|
||||||
isVectorSearchEnabled?: boolean;
|
|
||||||
onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void;
|
onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,15 +119,10 @@ export class IndexingPolicyComponent extends React.Component<
|
|||||||
indexTransformationProgress={this.props.indexTransformationProgress}
|
indexTransformationProgress={this.props.indexTransformationProgress}
|
||||||
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
|
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
|
||||||
/>
|
/>
|
||||||
{this.props.isVectorSearchEnabled && (
|
|
||||||
<MessageBar messageBarType={MessageBarType.severeWarning}>
|
|
||||||
Container vector policies and vector indexes are not modifiable after container creation
|
|
||||||
</MessageBar>
|
|
||||||
)}
|
|
||||||
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
|
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
|
||||||
<MessageBar messageBarType={MessageBarType.warning}>{unsavedEditorWarningMessage("indexPolicy")}</MessageBar>
|
<MessageBar messageBarType={MessageBarType.warning}>{indexingPolicynUnsavedWarningMessage}</MessageBar>
|
||||||
)}
|
)}
|
||||||
<div className="settingsV2Editor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
|
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { renderToString } from "react-dom/server";
|
|
||||||
import { MongoIndexTypes, MongoNotificationMessage, MongoNotificationType } from "../../SettingsUtils";
|
import { MongoIndexTypes, MongoNotificationMessage, MongoNotificationType } from "../../SettingsUtils";
|
||||||
import { MongoIndexingPolicyComponent, MongoIndexingPolicyComponentProps } from "./MongoIndexingPolicyComponent";
|
import { MongoIndexingPolicyComponent, MongoIndexingPolicyComponentProps } from "./MongoIndexingPolicyComponent";
|
||||||
|
import { renderToString } from "react-dom/server";
|
||||||
|
|
||||||
describe("MongoIndexingPolicyComponent", () => {
|
describe("MongoIndexingPolicyComponent", () => {
|
||||||
const baseProps: MongoIndexingPolicyComponentProps = {
|
const baseProps: MongoIndexingPolicyComponentProps = {
|
||||||
@@ -84,7 +84,7 @@ describe("MongoIndexingPolicyComponent", () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
test.each(cases)(
|
test.each(cases)(
|
||||||
"mongo indexing policy saveable and discardable",
|
"",
|
||||||
(
|
(
|
||||||
notification: MongoNotificationMessage,
|
notification: MongoNotificationMessage,
|
||||||
indexToDropIsPresent: boolean,
|
indexToDropIsPresent: boolean,
|
||||||
@@ -111,10 +111,8 @@ describe("MongoIndexingPolicyComponent", () => {
|
|||||||
);
|
);
|
||||||
if (mongoWarningNotificationMessage) {
|
if (mongoWarningNotificationMessage) {
|
||||||
const elementAsString = renderToString(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage());
|
const elementAsString = renderToString(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage());
|
||||||
// eslint-disable-next-line jest/no-conditional-expect
|
|
||||||
expect(elementAsString).toContain(mongoWarningNotificationMessage);
|
expect(elementAsString).toContain(mongoWarningNotificationMessage);
|
||||||
} else {
|
} else {
|
||||||
// eslint-disable-next-line jest/no-conditional-expect
|
|
||||||
expect(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage()).toBeUndefined();
|
expect(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage()).toBeUndefined();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
addMongoIndexStackProps,
|
addMongoIndexStackProps,
|
||||||
createAndAddMongoIndexStackProps,
|
createAndAddMongoIndexStackProps,
|
||||||
customDetailsListStyles,
|
customDetailsListStyles,
|
||||||
|
indexingPolicynUnsavedWarningMessage,
|
||||||
infoAndToolTipTextStyle,
|
infoAndToolTipTextStyle,
|
||||||
mediumWidthStackStyles,
|
mediumWidthStackStyles,
|
||||||
mongoCompoundIndexNotSupportedMessage,
|
mongoCompoundIndexNotSupportedMessage,
|
||||||
@@ -26,16 +27,15 @@ import {
|
|||||||
onRenderRow,
|
onRenderRow,
|
||||||
separatorStyles,
|
separatorStyles,
|
||||||
subComponentStackProps,
|
subComponentStackProps,
|
||||||
unsavedEditorWarningMessage,
|
|
||||||
} from "../../SettingsRenderUtils";
|
} from "../../SettingsRenderUtils";
|
||||||
import {
|
import {
|
||||||
AddMongoIndexProps,
|
AddMongoIndexProps,
|
||||||
MongoIndexIdField,
|
|
||||||
MongoIndexTypes,
|
|
||||||
MongoNotificationType,
|
|
||||||
getMongoIndexType,
|
getMongoIndexType,
|
||||||
getMongoIndexTypeText,
|
getMongoIndexTypeText,
|
||||||
isIndexTransforming,
|
isIndexTransforming,
|
||||||
|
MongoIndexIdField,
|
||||||
|
MongoIndexTypes,
|
||||||
|
MongoNotificationType,
|
||||||
} from "../../SettingsUtils";
|
} from "../../SettingsUtils";
|
||||||
import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
||||||
import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
|
import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
|
||||||
@@ -297,7 +297,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
|||||||
if (this.getMongoWarningNotificationMessage()) {
|
if (this.getMongoWarningNotificationMessage()) {
|
||||||
warningMessage = this.getMongoWarningNotificationMessage();
|
warningMessage = this.getMongoWarningNotificationMessage();
|
||||||
} else if (this.isMongoIndexingPolicySaveable()) {
|
} else if (this.isMongoIndexingPolicySaveable()) {
|
||||||
warningMessage = unsavedEditorWarningMessage("indexPolicy");
|
warningMessage = indexingPolicynUnsavedWarningMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,216 +0,0 @@
|
|||||||
import {
|
|
||||||
DefaultButton,
|
|
||||||
FontWeights,
|
|
||||||
Link,
|
|
||||||
MessageBar,
|
|
||||||
MessageBarType,
|
|
||||||
PrimaryButton,
|
|
||||||
ProgressIndicator,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
} from "@fluentui/react";
|
|
||||||
import * as React from "react";
|
|
||||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
|
||||||
|
|
||||||
import { handleError } from "Common/ErrorHandlingUtils";
|
|
||||||
import { cancelDataTransferJob, pollDataTransferJob } from "Common/dataAccess/dataTransfers";
|
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane";
|
|
||||||
import {
|
|
||||||
CosmosSqlDataTransferDataSourceSink,
|
|
||||||
DataTransferJobGetResults,
|
|
||||||
} from "Utils/arm/generatedClients/dataTransferService/types";
|
|
||||||
import { refreshDataTransferJobs, useDataTransferJobs } from "hooks/useDataTransferJobs";
|
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
|
||||||
import { userContext } from "../../../../UserContext";
|
|
||||||
|
|
||||||
export interface PartitionKeyComponentProps {
|
|
||||||
database: ViewModels.Database;
|
|
||||||
collection: ViewModels.Collection;
|
|
||||||
explorer: Explorer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ database, collection, explorer }) => {
|
|
||||||
const { dataTransferJobs } = useDataTransferJobs();
|
|
||||||
const [portalDataTransferJob, setPortalDataTransferJob] = React.useState<DataTransferJobGetResults>(null);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const loadDataTransferJobs = refreshDataTransferOperations;
|
|
||||||
loadDataTransferJobs();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const currentJob = findPortalDataTransferJob();
|
|
||||||
setPortalDataTransferJob(currentJob);
|
|
||||||
startPollingforUpdate(currentJob);
|
|
||||||
}, [dataTransferJobs]);
|
|
||||||
|
|
||||||
const isHierarchicalPartitionedContainer = (): boolean => collection.partitionKey?.kind === "MultiHash";
|
|
||||||
|
|
||||||
const getPartitionKeyValue = (): string => {
|
|
||||||
return (collection.partitionKeyProperties || []).map((property) => "/" + property).join(", ");
|
|
||||||
};
|
|
||||||
|
|
||||||
const partitionKeyName = "Partition key";
|
|
||||||
const partitionKeyValue = getPartitionKeyValue();
|
|
||||||
|
|
||||||
const textHeadingStyle = {
|
|
||||||
root: { fontWeight: FontWeights.semibold, fontSize: 16 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const textSubHeadingStyle = {
|
|
||||||
root: { fontWeight: FontWeights.semibold },
|
|
||||||
};
|
|
||||||
|
|
||||||
const startPollingforUpdate = (currentJob: DataTransferJobGetResults) => {
|
|
||||||
if (isCurrentJobInProgress(currentJob)) {
|
|
||||||
const jobName = currentJob?.properties?.jobName;
|
|
||||||
try {
|
|
||||||
pollDataTransferJob(
|
|
||||||
jobName,
|
|
||||||
userContext.subscriptionId,
|
|
||||||
userContext.resourceGroup,
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, "ChangePartitionKey", `Failed to complete data transfer job ${jobName}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelRunningDataTransferJob = async (currentJob: DataTransferJobGetResults) => {
|
|
||||||
await cancelDataTransferJob(
|
|
||||||
userContext.subscriptionId,
|
|
||||||
userContext.resourceGroup,
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
currentJob?.properties?.jobName,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isCurrentJobInProgress = (currentJob: DataTransferJobGetResults) => {
|
|
||||||
const jobStatus = currentJob?.properties?.status;
|
|
||||||
return (
|
|
||||||
jobStatus &&
|
|
||||||
jobStatus !== "Completed" &&
|
|
||||||
jobStatus !== "Cancelled" &&
|
|
||||||
jobStatus !== "Failed" &&
|
|
||||||
jobStatus !== "Faulted"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const refreshDataTransferOperations = async () => {
|
|
||||||
await refreshDataTransferJobs(
|
|
||||||
userContext.subscriptionId,
|
|
||||||
userContext.resourceGroup,
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const findPortalDataTransferJob = (): DataTransferJobGetResults => {
|
|
||||||
return dataTransferJobs.find((feed: DataTransferJobGetResults) => {
|
|
||||||
const sourceSink: CosmosSqlDataTransferDataSourceSink = feed?.properties
|
|
||||||
?.source as CosmosSqlDataTransferDataSourceSink;
|
|
||||||
return sourceSink.databaseName === collection.databaseId && sourceSink.containerName === collection.id();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getProgressDescription = (): string => {
|
|
||||||
const processedCount = portalDataTransferJob?.properties?.processedCount;
|
|
||||||
const totalCount = portalDataTransferJob?.properties?.totalCount;
|
|
||||||
const processedCountString = totalCount > 0 ? `(${processedCount} of ${totalCount} documents processed)` : "";
|
|
||||||
return `${portalDataTransferJob?.properties?.status} ${processedCountString}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const startPartitionkeyChangeWorkflow = () => {
|
|
||||||
useSidePanel
|
|
||||||
.getState()
|
|
||||||
.openSidePanel(
|
|
||||||
"Change partition key",
|
|
||||||
<ChangePartitionKeyPane
|
|
||||||
sourceDatabase={database}
|
|
||||||
sourceCollection={collection}
|
|
||||||
explorer={explorer}
|
|
||||||
onClose={refreshDataTransferOperations}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPercentageComplete = () => {
|
|
||||||
const jobStatus = portalDataTransferJob?.properties?.status;
|
|
||||||
const isCompleted = jobStatus === "Completed";
|
|
||||||
if (isCompleted) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const processedCount = portalDataTransferJob?.properties?.processedCount;
|
|
||||||
const totalCount = portalDataTransferJob?.properties?.totalCount;
|
|
||||||
const isJobInProgress = isCurrentJobInProgress(portalDataTransferJob);
|
|
||||||
return isJobInProgress ? (totalCount > 0 ? processedCount / totalCount : null) : 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack tokens={{ childrenGap: 20 }} styles={{ root: { maxWidth: 600 } }}>
|
|
||||||
<Stack tokens={{ childrenGap: 10 }}>
|
|
||||||
<Text styles={textHeadingStyle}>Change {partitionKeyName.toLowerCase()}</Text>
|
|
||||||
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
|
||||||
<Stack tokens={{ childrenGap: 5 }}>
|
|
||||||
<Text styles={textSubHeadingStyle}>Current {partitionKeyName.toLowerCase()}</Text>
|
|
||||||
<Text styles={textSubHeadingStyle}>Partitioning</Text>
|
|
||||||
</Stack>
|
|
||||||
<Stack tokens={{ childrenGap: 5 }}>
|
|
||||||
<Text>{partitionKeyValue}</Text>
|
|
||||||
<Text>{isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"}</Text>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
<MessageBar messageBarType={MessageBarType.warning}>
|
|
||||||
To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to the
|
|
||||||
source container for the entire duration of the partition key change process.
|
|
||||||
<Link
|
|
||||||
href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work"
|
|
||||||
target="_blank"
|
|
||||||
underline
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
</Link>
|
|
||||||
</MessageBar>
|
|
||||||
<Text>
|
|
||||||
To change the partition key, a new destination container must be created or an existing destination container
|
|
||||||
selected. Data will then be copied to the destination container.
|
|
||||||
</Text>
|
|
||||||
<PrimaryButton
|
|
||||||
styles={{ root: { width: "fit-content" } }}
|
|
||||||
text="Change"
|
|
||||||
onClick={startPartitionkeyChangeWorkflow}
|
|
||||||
disabled={isCurrentJobInProgress(portalDataTransferJob)}
|
|
||||||
/>
|
|
||||||
{portalDataTransferJob && (
|
|
||||||
<Stack>
|
|
||||||
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
|
|
||||||
<Stack
|
|
||||||
horizontal
|
|
||||||
tokens={{ childrenGap: 20 }}
|
|
||||||
styles={{
|
|
||||||
root: {
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProgressIndicator
|
|
||||||
label={portalDataTransferJob?.properties?.jobName}
|
|
||||||
description={getProgressDescription()}
|
|
||||||
percentComplete={getPercentageComplete()}
|
|
||||||
styles={{
|
|
||||||
root: {
|
|
||||||
width: "85%",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
></ProgressIndicator>
|
|
||||||
{isCurrentJobInProgress(portalDataTransferJob) && (
|
|
||||||
<DefaultButton text="Cancel" onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)} />
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -306,7 +306,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
};
|
};
|
||||||
|
|
||||||
const costElement = (): JSX.Element => {
|
const costElement = (): JSX.Element => {
|
||||||
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, false);
|
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, true);
|
||||||
return (
|
return (
|
||||||
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
|
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
|
||||||
{newThroughput && newThroughputCostElement()}
|
{newThroughput && newThroughputCostElement()}
|
||||||
|
|||||||
@@ -917,7 +917,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
|||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
0.0080
|
0.012
|
||||||
/hr
|
/hr
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -929,7 +929,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
|||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
0.19
|
0.29
|
||||||
/day
|
/day
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -941,7 +941,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
|||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
5.84
|
8.76
|
||||||
/mo
|
/mo
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -1354,7 +1354,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
|||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
0.0080
|
0.012
|
||||||
/hr
|
/hr
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -1366,7 +1366,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
|||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
0.19
|
0.29
|
||||||
/day
|
/day
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -1378,7 +1378,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
|||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
5.84
|
8.76
|
||||||
/mo
|
/mo
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`ComputedPropertiesComponent renders 1`] = `
|
|
||||||
<Stack
|
|
||||||
tokens={
|
|
||||||
Object {
|
|
||||||
"childrenGap": 5,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"marginBottom": "10px",
|
|
||||||
"marginLeft": "30px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StyledLinkBase
|
|
||||||
href="https://aka.ms/computed-properties-preview/"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
|
|
||||||
<FontIcon
|
|
||||||
iconName="NavigateExternalInline"
|
|
||||||
/>
|
|
||||||
</StyledLinkBase>
|
|
||||||
about how to define computed properties and how to use them.
|
|
||||||
</Text>
|
|
||||||
<div
|
|
||||||
className="settingsV2Editor"
|
|
||||||
tabIndex={0}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
`;
|
|
||||||
@@ -12,7 +12,7 @@ exports[`IndexingPolicyComponent renders 1`] = `
|
|||||||
refreshIndexTransformationProgress={[Function]}
|
refreshIndexTransformationProgress={[Function]}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="settingsV2Editor"
|
className="settingsV2IndexingPolicyEditor"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
|||||||
import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
||||||
|
|
||||||
const zeroValue = 0;
|
const zeroValue = 0;
|
||||||
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy | DataModels.ComputedProperties;
|
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy;
|
||||||
export const TtlOff = "off";
|
export const TtlOff = "off";
|
||||||
export const TtlOn = "on";
|
export const TtlOn = "on";
|
||||||
export const TtlOnNoDefault = "on-nodefault";
|
export const TtlOnNoDefault = "on-nodefault";
|
||||||
@@ -45,9 +45,6 @@ export enum SettingsV2TabTypes {
|
|||||||
ConflictResolutionTab,
|
ConflictResolutionTab,
|
||||||
SubSettingsTab,
|
SubSettingsTab,
|
||||||
IndexingPolicyTab,
|
IndexingPolicyTab,
|
||||||
PartitionKeyTab,
|
|
||||||
ComputedPropertiesTab,
|
|
||||||
ContainerVectorPolicyTab,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IsComponentDirtyResult {
|
export interface IsComponentDirtyResult {
|
||||||
@@ -149,12 +146,6 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
|
|||||||
return "Settings";
|
return "Settings";
|
||||||
case SettingsV2TabTypes.IndexingPolicyTab:
|
case SettingsV2TabTypes.IndexingPolicyTab:
|
||||||
return "Indexing Policy";
|
return "Indexing Policy";
|
||||||
case SettingsV2TabTypes.PartitionKeyTab:
|
|
||||||
return "Partition Keys (preview)";
|
|
||||||
case SettingsV2TabTypes.ComputedPropertiesTab:
|
|
||||||
return "Computed Properties";
|
|
||||||
case SettingsV2TabTypes.ContainerVectorPolicyTab:
|
|
||||||
return "Container Vector Policy (preview)";
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown tab ${tab}`);
|
throw new Error(`Unknown tab ${tab}`);
|
||||||
}
|
}
|
||||||
@@ -208,49 +199,3 @@ export const getMongoIndexTypeText = (index: MongoIndexTypes): string => {
|
|||||||
export const isIndexTransforming = (indexTransformationProgress: number): boolean =>
|
export const isIndexTransforming = (indexTransformationProgress: number): boolean =>
|
||||||
// index transformation progress can be 0
|
// index transformation progress can be 0
|
||||||
indexTransformationProgress !== undefined && indexTransformationProgress !== 100;
|
indexTransformationProgress !== undefined && indexTransformationProgress !== 100;
|
||||||
|
|
||||||
export const getPartitionKeyName = (apiType: string, isLowerCase?: boolean): string => {
|
|
||||||
const partitionKeyName = apiType === "Mongo" ? "Shard key" : "Partition key";
|
|
||||||
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getPartitionKeyTooltipText = (apiType: string): string => {
|
|
||||||
if (apiType === "Mongo") {
|
|
||||||
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. It’s critical to choose a field that will evenly distribute your data.";
|
|
||||||
}
|
|
||||||
let tooltipText = `The ${getPartitionKeyName(
|
|
||||||
apiType,
|
|
||||||
true,
|
|
||||||
)} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`;
|
|
||||||
if (apiType === "SQL") {
|
|
||||||
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
|
|
||||||
}
|
|
||||||
return tooltipText;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getPartitionKeySubtext = (partitionKeyDefault: boolean, apiType: string): string => {
|
|
||||||
if (partitionKeyDefault && (apiType === "SQL" || apiType === "Mongo")) {
|
|
||||||
const subtext = "For small workloads, the item ID is a suitable choice for the partition key.";
|
|
||||||
return subtext;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getPartitionKeyPlaceHolder = (apiType: string, index?: number): string => {
|
|
||||||
switch (apiType) {
|
|
||||||
case "Mongo":
|
|
||||||
return "e.g., categoryId";
|
|
||||||
case "Gremlin":
|
|
||||||
return "e.g., /address";
|
|
||||||
case "SQL":
|
|
||||||
return `${
|
|
||||||
index === undefined
|
|
||||||
? "Required - first partition key e.g., /TenantId"
|
|
||||||
: index === 0
|
|
||||||
? "second partition key e.g., /UserId"
|
|
||||||
: "third partition key e.g., /SessionId"
|
|
||||||
}`;
|
|
||||||
default:
|
|
||||||
return "e.g., /address/zipCode";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -40,12 +40,6 @@ export const collection = {
|
|||||||
version: 2,
|
version: 2,
|
||||||
},
|
},
|
||||||
partitionKeyProperties: ["partitionKey"],
|
partitionKeyProperties: ["partitionKey"],
|
||||||
computedProperties: ko.observable<DataModels.ComputedProperties>([
|
|
||||||
{
|
|
||||||
name: "queryName",
|
|
||||||
query: "query",
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
readSettings: () => {
|
readSettings: () => {
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
Object {
|
Object {
|
||||||
"analyticalStorageTtl": [Function],
|
"analyticalStorageTtl": [Function],
|
||||||
"changeFeedPolicy": [Function],
|
"changeFeedPolicy": [Function],
|
||||||
"computedProperties": [Function],
|
|
||||||
"conflictResolutionPolicy": [Function],
|
"conflictResolutionPolicy": [Function],
|
||||||
"container": Explorer {
|
"container": Explorer {
|
||||||
"_isInitializingNotebooks": false,
|
"_isInitializingNotebooks": false,
|
||||||
|
"_resetNotebookWorkspace": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
@@ -103,10 +103,10 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
Object {
|
Object {
|
||||||
"analyticalStorageTtl": [Function],
|
"analyticalStorageTtl": [Function],
|
||||||
"changeFeedPolicy": [Function],
|
"changeFeedPolicy": [Function],
|
||||||
"computedProperties": [Function],
|
|
||||||
"conflictResolutionPolicy": [Function],
|
"conflictResolutionPolicy": [Function],
|
||||||
"container": Explorer {
|
"container": Explorer {
|
||||||
"_isInitializingNotebooks": false,
|
"_isInitializingNotebooks": false,
|
||||||
|
"_resetNotebookWorkspace": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
@@ -196,7 +196,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"indexingMode": "consistent",
|
"indexingMode": "consistent",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isVectorSearchEnabled={false}
|
|
||||||
logIndexingPolicySuccessMessage={[Function]}
|
logIndexingPolicySuccessMessage={[Function]}
|
||||||
onIndexingPolicyContentChange={[Function]}
|
onIndexingPolicyContentChange={[Function]}
|
||||||
onIndexingPolicyDirtyChange={[Function]}
|
onIndexingPolicyDirtyChange={[Function]}
|
||||||
@@ -205,131 +204,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
shouldDiscardIndexingPolicy={false}
|
shouldDiscardIndexingPolicy={false}
|
||||||
/>
|
/>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
<PivotItem
|
|
||||||
headerText="Partition Keys (preview)"
|
|
||||||
itemKey="PartitionKeyTab"
|
|
||||||
key="PartitionKeyTab"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"marginTop": 20,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<PartitionKeyComponent
|
|
||||||
collection={
|
|
||||||
Object {
|
|
||||||
"analyticalStorageTtl": [Function],
|
|
||||||
"changeFeedPolicy": [Function],
|
|
||||||
"computedProperties": [Function],
|
|
||||||
"conflictResolutionPolicy": [Function],
|
|
||||||
"container": Explorer {
|
|
||||||
"_isInitializingNotebooks": false,
|
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
|
||||||
"isTabsContentExpanded": [Function],
|
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
|
||||||
"onRefreshResourcesClick": [Function],
|
|
||||||
"phoenixClient": PhoenixClient {
|
|
||||||
"armResourceId": undefined,
|
|
||||||
"retryOptions": Object {
|
|
||||||
"maxTimeout": 5000,
|
|
||||||
"minTimeout": 5000,
|
|
||||||
"retries": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"provideFeedbackEmail": [Function],
|
|
||||||
"queriesClient": QueriesClient {
|
|
||||||
"container": [Circular],
|
|
||||||
},
|
|
||||||
"refreshNotebookList": [Function],
|
|
||||||
"resourceTree": ResourceTreeAdapter {
|
|
||||||
"container": [Circular],
|
|
||||||
"copyNotebook": [Function],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"databaseId": "test",
|
|
||||||
"defaultTtl": [Function],
|
|
||||||
"geospatialConfig": [Function],
|
|
||||||
"getDatabase": [Function],
|
|
||||||
"id": [Function],
|
|
||||||
"indexingPolicy": [Function],
|
|
||||||
"offer": [Function],
|
|
||||||
"partitionKey": Object {
|
|
||||||
"kind": "hash",
|
|
||||||
"paths": Array [],
|
|
||||||
"version": 2,
|
|
||||||
},
|
|
||||||
"partitionKeyProperties": Array [
|
|
||||||
"partitionKey",
|
|
||||||
],
|
|
||||||
"readSettings": [Function],
|
|
||||||
"uniqueKeyPolicy": Object {},
|
|
||||||
"usageSizeInKB": [Function],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
explorer={
|
|
||||||
Explorer {
|
|
||||||
"_isInitializingNotebooks": false,
|
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
|
||||||
"isTabsContentExpanded": [Function],
|
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
|
||||||
"onRefreshResourcesClick": [Function],
|
|
||||||
"phoenixClient": PhoenixClient {
|
|
||||||
"armResourceId": undefined,
|
|
||||||
"retryOptions": Object {
|
|
||||||
"maxTimeout": 5000,
|
|
||||||
"minTimeout": 5000,
|
|
||||||
"retries": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"provideFeedbackEmail": [Function],
|
|
||||||
"queriesClient": QueriesClient {
|
|
||||||
"container": [Circular],
|
|
||||||
},
|
|
||||||
"refreshNotebookList": [Function],
|
|
||||||
"resourceTree": ResourceTreeAdapter {
|
|
||||||
"container": [Circular],
|
|
||||||
"copyNotebook": [Function],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PivotItem>
|
|
||||||
<PivotItem
|
|
||||||
headerText="Computed Properties"
|
|
||||||
itemKey="ComputedPropertiesTab"
|
|
||||||
key="ComputedPropertiesTab"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"marginTop": 20,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ComputedPropertiesComponent
|
|
||||||
computedPropertiesContent={
|
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"name": "queryName",
|
|
||||||
"query": "query",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
computedPropertiesContentBaseline={
|
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"name": "queryName",
|
|
||||||
"query": "query",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
logComputedPropertiesSuccessMessage={[Function]}
|
|
||||||
onComputedPropertiesContentChange={[Function]}
|
|
||||||
onComputedPropertiesDirtyChange={[Function]}
|
|
||||||
resetShouldDiscardComputedProperties={[Function]}
|
|
||||||
shouldDiscardComputedProperties={false}
|
|
||||||
/>
|
|
||||||
</PivotItem>
|
|
||||||
</StyledPivot>
|
</StyledPivot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -99,6 +99,18 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
</StyledLinkBase>
|
</StyledLinkBase>
|
||||||
.
|
.
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
|
"fontSize": 14,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes.
|
||||||
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
id="updateThroughputDelayedApplyWarningMessage"
|
id="updateThroughputDelayedApplyWarningMessage"
|
||||||
styles={
|
styles={
|
||||||
|
|||||||
@@ -14,17 +14,7 @@
|
|||||||
.throughputInputSpacing > :not(:last-child) {
|
.throughputInputSpacing > :not(:last-child) {
|
||||||
margin-bottom: @DefaultSpace;
|
margin-bottom: @DefaultSpace;
|
||||||
}
|
}
|
||||||
|
.capacitycalculator-link:focus{
|
||||||
.capacitycalculator-link:focus {
|
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.outlineNone{
|
|
||||||
outline: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copyQuery:focus::after,
|
|
||||||
.deleteQuery:focus::after {
|
|
||||||
outline: none !important;
|
|
||||||
}
|
|
||||||
@@ -23,12 +23,12 @@ describe("ThroughputInput Pane", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should switch mode properly", () => {
|
it("should switch mode properly", () => {
|
||||||
wrapper.find('[id="Manual-input"]').simulate("change");
|
wrapper.find('[aria-label="Manual database throughput"]').simulate("change");
|
||||||
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe(
|
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe(
|
||||||
"Container throughput (400 - unlimited RU/s)",
|
"Container throughput (400 - unlimited RU/s)",
|
||||||
);
|
);
|
||||||
|
|
||||||
wrapper.find('[id="Autoscale-input"]').simulate("change");
|
wrapper.find('[aria-label="Autoscale database throughput"]').simulate("change");
|
||||||
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe("Container throughput (autoscale)");
|
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe("Container throughput (autoscale)");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
<input
|
<input
|
||||||
id="Autoscale-input"
|
id="Autoscale-input"
|
||||||
className="throughputInputRadioBtn"
|
className="throughputInputRadioBtn"
|
||||||
aria-label={`${getThroughputLabelText()} Autoscale`}
|
aria-label="Autoscale database throughput"
|
||||||
aria-required={true}
|
aria-required={true}
|
||||||
checked={isAutoscaleSelected}
|
checked={isAutoscaleSelected}
|
||||||
type="radio"
|
type="radio"
|
||||||
@@ -204,7 +204,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
<input
|
<input
|
||||||
id="Manual-input"
|
id="Manual-input"
|
||||||
className="throughputInputRadioBtn"
|
className="throughputInputRadioBtn"
|
||||||
aria-label={`${getThroughputLabelText()} Manual`}
|
aria-label="Manual database throughput"
|
||||||
checked={!isAutoscaleSelected}
|
checked={!isAutoscaleSelected}
|
||||||
type="radio"
|
type="radio"
|
||||||
aria-required={true}
|
aria-required={true}
|
||||||
@@ -223,7 +223,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
<Text variant="small" aria-label="capacity calculator of azure cosmos db">
|
<Text variant="small" aria-label="capacity calculator of azure cosmos db">
|
||||||
Estimate your required RU/s with{" "}
|
Estimate your required RU/s with{" "}
|
||||||
<Link
|
<Link
|
||||||
className="underlinedLink outlineNone"
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://cosmos.azure.com/capacitycalculator/"
|
href="https://cosmos.azure.com/capacitycalculator/"
|
||||||
aria-label="capacity calculator of azure cosmos db"
|
aria-label="capacity calculator of azure cosmos db"
|
||||||
@@ -272,22 +271,11 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
<Stack className="throughputInputSpacing">
|
<Stack className="throughputInputSpacing">
|
||||||
<Text variant="small" aria-label="ruDescription">
|
<Text variant="small" aria-label="ruDescription">
|
||||||
Estimate your required RU/s with
|
Estimate your required RU/s with
|
||||||
<Link
|
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/" aria-label="capacityLink">
|
||||||
className="underlinedLink"
|
|
||||||
target="_blank"
|
|
||||||
href="https://cosmos.azure.com/capacitycalculator/"
|
|
||||||
aria-label="Capacity calculator"
|
|
||||||
>
|
|
||||||
capacity calculator
|
capacity calculator
|
||||||
</Link>
|
</Link>
|
||||||
.
|
.
|
||||||
</Text>
|
</Text>
|
||||||
<Stack horizontal>
|
|
||||||
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }} aria-label="maxRUDescription">
|
|
||||||
{isDatabase ? "Database" : getCollectionName()} Required RU/s
|
|
||||||
</Text>
|
|
||||||
<InfoTooltip>{getAutoScaleTooltip()}</InfoTooltip>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<TooltipHost
|
<TooltipHost
|
||||||
directionalHint={DirectionalHint.topLeftEdge}
|
directionalHint={DirectionalHint.topLeftEdge}
|
||||||
@@ -308,7 +296,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
min={SharedConstants.CollectionCreation.DefaultCollectionRUs400}
|
min={SharedConstants.CollectionCreation.DefaultCollectionRUs400}
|
||||||
max={userContext.isTryCosmosDBSubscription ? Constants.TryCosmosExperience.maxRU : Infinity}
|
max={userContext.isTryCosmosDBSubscription ? Constants.TryCosmosExperience.maxRU : Infinity}
|
||||||
value={throughput.toString()}
|
value={throughput.toString()}
|
||||||
ariaLabel={`${isDatabase ? "Database" : getCollectionName()} Required RU/s`}
|
aria-label="Max request units per second"
|
||||||
required={true}
|
required={true}
|
||||||
errorMessage={throughputError}
|
errorMessage={throughputError}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -18,17 +18,17 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
horizontal={true}
|
horizontal={true}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ms-Stack css-109"
|
className="ms-Stack css-53"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="mandatoryStar"
|
className="mandatoryStar"
|
||||||
key=".0:$.$.0"
|
key=".0:$.0"
|
||||||
>
|
>
|
||||||
*
|
*
|
||||||
</span>
|
</span>
|
||||||
<Text
|
<Text
|
||||||
aria-label="Throughput header"
|
aria-label="Throughput header"
|
||||||
key=".0:$.$.1"
|
key=".0:$.1"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"fontWeight": 600,
|
"fontWeight": 600,
|
||||||
@@ -39,7 +39,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-label="Throughput header"
|
aria-label="Throughput header"
|
||||||
className="css-110"
|
className="css-54"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"fontWeight": 600,
|
"fontWeight": 600,
|
||||||
@@ -51,7 +51,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
</Text>
|
</Text>
|
||||||
<InfoTooltip
|
<InfoTooltip
|
||||||
key=".0:$.$.2"
|
key=".0:$.2"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<StyledTooltipHostBase
|
<StyledTooltipHostBase
|
||||||
@@ -336,13 +336,12 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ms-TooltipHost root-111"
|
className="ms-TooltipHost root-55"
|
||||||
onBlurCapture={[Function]}
|
onBlurCapture={[Function]}
|
||||||
onFocusCapture={[Function]}
|
onFocusCapture={[Function]}
|
||||||
onKeyDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
onMouseLeave={[Function]}
|
onMouseLeave={[Function]}
|
||||||
role="none"
|
|
||||||
>
|
>
|
||||||
<StyledIconBase
|
<StyledIconBase
|
||||||
ariaLabel="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
|
ariaLabel="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
|
||||||
@@ -632,7 +631,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
|
aria-label="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
|
||||||
className="panelInfoIcon root-114"
|
className="panelInfoIcon root-57"
|
||||||
data-icon-name="Info"
|
data-icon-name="Info"
|
||||||
role="img"
|
role="img"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -641,24 +640,6 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
</i>
|
</i>
|
||||||
</IconBase>
|
</IconBase>
|
||||||
</StyledIconBase>
|
</StyledIconBase>
|
||||||
<div
|
|
||||||
hidden={true}
|
|
||||||
id="tooltip0"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"border": 0,
|
|
||||||
"height": 1,
|
|
||||||
"margin": -1,
|
|
||||||
"overflow": "hidden",
|
|
||||||
"padding": 0,
|
|
||||||
"position": "absolute",
|
|
||||||
"whiteSpace": "nowrap",
|
|
||||||
"width": 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</TooltipHostBase>
|
</TooltipHostBase>
|
||||||
</StyledTooltipHostBase>
|
</StyledTooltipHostBase>
|
||||||
@@ -671,14 +652,14 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
verticalAlign="center"
|
verticalAlign="center"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ms-Stack css-115"
|
className="ms-Stack css-58"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
key=".0:$.$.0"
|
key=".0:$.0"
|
||||||
role="radiogroup"
|
role="radiogroup"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-label="Container throughput (autoscale) Autoscale"
|
aria-label="Autoscale database throughput"
|
||||||
aria-required={true}
|
aria-required={true}
|
||||||
checked={true}
|
checked={true}
|
||||||
className="throughputInputRadioBtn"
|
className="throughputInputRadioBtn"
|
||||||
@@ -695,7 +676,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
Autoscale
|
Autoscale
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
aria-label="Container throughput (autoscale) Manual"
|
aria-label="Manual database throughput"
|
||||||
aria-required={true}
|
aria-required={true}
|
||||||
checked={false}
|
checked={false}
|
||||||
className="throughputInputRadioBtn"
|
className="throughputInputRadioBtn"
|
||||||
@@ -718,28 +699,26 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
className="throughputInputSpacing"
|
className="throughputInputSpacing"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ms-Stack throughputInputSpacing css-116"
|
className="ms-Stack throughputInputSpacing css-59"
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
aria-label="capacity calculator of azure cosmos db"
|
aria-label="capacity calculator of azure cosmos db"
|
||||||
key=".0:$.$.0"
|
key=".0:$.0"
|
||||||
variant="small"
|
variant="small"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-label="capacity calculator of azure cosmos db"
|
aria-label="capacity calculator of azure cosmos db"
|
||||||
className="css-110"
|
className="css-54"
|
||||||
>
|
>
|
||||||
Estimate your required RU/s with
|
Estimate your required RU/s with
|
||||||
|
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
aria-label="capacity calculator of azure cosmos db"
|
aria-label="capacity calculator of azure cosmos db"
|
||||||
className="underlinedLink outlineNone"
|
|
||||||
href="https://cosmos.azure.com/capacitycalculator/"
|
href="https://cosmos.azure.com/capacitycalculator/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<LinkBase
|
<LinkBase
|
||||||
aria-label="capacity calculator of azure cosmos db"
|
aria-label="capacity calculator of azure cosmos db"
|
||||||
className="underlinedLink outlineNone"
|
|
||||||
href="https://cosmos.azure.com/capacitycalculator/"
|
href="https://cosmos.azure.com/capacitycalculator/"
|
||||||
styles={[Function]}
|
styles={[Function]}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -1019,7 +998,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="capacity calculator of azure cosmos db"
|
aria-label="capacity calculator of azure cosmos db"
|
||||||
className="ms-Link underlinedLink outlineNone root-117"
|
className="ms-Link root-60"
|
||||||
href="https://cosmos.azure.com/capacitycalculator/"
|
href="https://cosmos.azure.com/capacitycalculator/"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -1033,14 +1012,14 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
</Text>
|
</Text>
|
||||||
<Stack
|
<Stack
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
key=".0:$.$.1"
|
key=".0:$.1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ms-Stack css-109"
|
className="ms-Stack css-53"
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
aria-label="maxRUDescription"
|
aria-label="maxRUDescription"
|
||||||
key=".0:$.$.0"
|
key=".0:$.0"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"fontWeight": 600,
|
"fontWeight": 600,
|
||||||
@@ -1051,7 +1030,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-label="maxRUDescription"
|
aria-label="maxRUDescription"
|
||||||
className="css-110"
|
className="css-54"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"fontWeight": 600,
|
"fontWeight": 600,
|
||||||
@@ -1064,7 +1043,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
</Text>
|
</Text>
|
||||||
<InfoTooltip
|
<InfoTooltip
|
||||||
key=".0:$.$.1"
|
key=".0:$.1"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<StyledTooltipHostBase
|
<StyledTooltipHostBase
|
||||||
@@ -1349,13 +1328,12 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ms-TooltipHost root-111"
|
className="ms-TooltipHost root-55"
|
||||||
onBlurCapture={[Function]}
|
onBlurCapture={[Function]}
|
||||||
onFocusCapture={[Function]}
|
onFocusCapture={[Function]}
|
||||||
onKeyDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
onMouseLeave={[Function]}
|
onMouseLeave={[Function]}
|
||||||
role="none"
|
|
||||||
>
|
>
|
||||||
<StyledIconBase
|
<StyledIconBase
|
||||||
ariaLabel="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
|
ariaLabel="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
|
||||||
@@ -1645,7 +1623,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
|
aria-label="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
|
||||||
className="panelInfoIcon root-114"
|
className="panelInfoIcon root-57"
|
||||||
data-icon-name="Info"
|
data-icon-name="Info"
|
||||||
role="img"
|
role="img"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -1654,24 +1632,6 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
</i>
|
</i>
|
||||||
</IconBase>
|
</IconBase>
|
||||||
</StyledIconBase>
|
</StyledIconBase>
|
||||||
<div
|
|
||||||
hidden={true}
|
|
||||||
id="tooltip1"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"border": 0,
|
|
||||||
"height": 1,
|
|
||||||
"margin": -1,
|
|
||||||
"overflow": "hidden",
|
|
||||||
"padding": 0,
|
|
||||||
"position": "absolute",
|
|
||||||
"whiteSpace": "nowrap",
|
|
||||||
"width": 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</TooltipHostBase>
|
</TooltipHostBase>
|
||||||
</StyledTooltipHostBase>
|
</StyledTooltipHostBase>
|
||||||
@@ -1683,7 +1643,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
ariaLabel="Container max RU/s"
|
ariaLabel="Container max RU/s"
|
||||||
errorMessage=""
|
errorMessage=""
|
||||||
id="autoscaleRUValueField"
|
id="autoscaleRUValueField"
|
||||||
key=".0:$.$.2"
|
key=".0:$.2"
|
||||||
max="9007199254740991"
|
max="9007199254740991"
|
||||||
min={1000}
|
min={1000}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
@@ -1993,18 +1953,18 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
value="4000"
|
value="4000"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ms-TextField is-required root-119"
|
className="ms-TextField is-required root-62"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ms-TextField-wrapper"
|
className="ms-TextField-wrapper"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ms-TextField-fieldGroup fieldGroup-120"
|
className="ms-TextField-fieldGroup fieldGroup-63"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-invalid={false}
|
aria-invalid={false}
|
||||||
aria-label="Container max RU/s"
|
aria-label="Container max RU/s"
|
||||||
className="ms-TextField-field field-121"
|
className="ms-TextField-field field-64"
|
||||||
id="autoscaleRUValueField"
|
id="autoscaleRUValueField"
|
||||||
max="9007199254740991"
|
max="9007199254740991"
|
||||||
min={1000}
|
min={1000}
|
||||||
@@ -2023,11 +1983,11 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
</TextFieldBase>
|
</TextFieldBase>
|
||||||
</StyledTextFieldBase>
|
</StyledTextFieldBase>
|
||||||
<Text
|
<Text
|
||||||
key=".0:$.$.3"
|
key=".0:$.3"
|
||||||
variant="small"
|
variant="small"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="css-110"
|
className="css-54"
|
||||||
>
|
>
|
||||||
Your
|
Your
|
||||||
container
|
container
|
||||||
|
|||||||
@@ -1,48 +1,48 @@
|
|||||||
import { shallow } from "enzyme";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { LegacyTreeComponent, LegacyTreeNode, LegacyTreeNodeComponent } from "./LegacyTreeComponent";
|
import { shallow } from "enzyme";
|
||||||
|
import { TreeComponent, TreeNode, TreeNodeComponent } from "./TreeComponent";
|
||||||
|
|
||||||
const buildChildren = (): LegacyTreeNode[] => {
|
const buildChildren = (): TreeNode[] => {
|
||||||
const grandChild11: LegacyTreeNode = {
|
const grandChild11: TreeNode = {
|
||||||
label: "ZgrandChild11",
|
label: "ZgrandChild11",
|
||||||
};
|
};
|
||||||
const grandChild12: LegacyTreeNode = {
|
const grandChild12: TreeNode = {
|
||||||
label: "AgrandChild12",
|
label: "AgrandChild12",
|
||||||
};
|
};
|
||||||
const child1: LegacyTreeNode = {
|
const child1: TreeNode = {
|
||||||
label: "Bchild1",
|
label: "Bchild1",
|
||||||
children: [grandChild11, grandChild12],
|
children: [grandChild11, grandChild12],
|
||||||
};
|
};
|
||||||
|
|
||||||
const child2: LegacyTreeNode = {
|
const child2: TreeNode = {
|
||||||
label: "2child2",
|
label: "2child2",
|
||||||
};
|
};
|
||||||
|
|
||||||
return [child1, child2];
|
return [child1, child2];
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildChildren2 = (): LegacyTreeNode[] => {
|
const buildChildren2 = (): TreeNode[] => {
|
||||||
const grandChild11: LegacyTreeNode = {
|
const grandChild11: TreeNode = {
|
||||||
label: "ZgrandChild11",
|
label: "ZgrandChild11",
|
||||||
};
|
};
|
||||||
const grandChild12: LegacyTreeNode = {
|
const grandChild12: TreeNode = {
|
||||||
label: "AgrandChild12",
|
label: "AgrandChild12",
|
||||||
};
|
};
|
||||||
|
|
||||||
const child1: LegacyTreeNode = {
|
const child1: TreeNode = {
|
||||||
label: "aChild",
|
label: "aChild",
|
||||||
};
|
};
|
||||||
|
|
||||||
const child2: LegacyTreeNode = {
|
const child2: TreeNode = {
|
||||||
label: "bchild",
|
label: "bchild",
|
||||||
children: [grandChild11, grandChild12],
|
children: [grandChild11, grandChild12],
|
||||||
};
|
};
|
||||||
|
|
||||||
const child3: LegacyTreeNode = {
|
const child3: TreeNode = {
|
||||||
label: "cchild",
|
label: "cchild",
|
||||||
};
|
};
|
||||||
|
|
||||||
const child4: LegacyTreeNode = {
|
const child4: TreeNode = {
|
||||||
label: "dchild",
|
label: "dchild",
|
||||||
children: [grandChild11, grandChild12],
|
children: [grandChild11, grandChild12],
|
||||||
};
|
};
|
||||||
@@ -50,7 +50,7 @@ const buildChildren2 = (): LegacyTreeNode[] => {
|
|||||||
return [child1, child2, child3, child4];
|
return [child1, child2, child3, child4];
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("LegacyTreeComponent", () => {
|
describe("TreeComponent", () => {
|
||||||
it("renders a simple tree", () => {
|
it("renders a simple tree", () => {
|
||||||
const root = {
|
const root = {
|
||||||
label: "root",
|
label: "root",
|
||||||
@@ -62,14 +62,14 @@ describe("LegacyTreeComponent", () => {
|
|||||||
className: "tree",
|
className: "tree",
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = shallow(<LegacyTreeComponent {...props} />);
|
const wrapper = shallow(<TreeComponent {...props} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("LegacyTreeNodeComponent", () => {
|
describe("TreeNodeComponent", () => {
|
||||||
it("renders a simple node (sorted children, expanded)", () => {
|
it("renders a simple node (sorted children, expanded)", () => {
|
||||||
const node: LegacyTreeNode = {
|
const node: TreeNode = {
|
||||||
label: "label",
|
label: "label",
|
||||||
id: "id",
|
id: "id",
|
||||||
children: buildChildren(),
|
children: buildChildren(),
|
||||||
@@ -98,12 +98,12 @@ describe("LegacyTreeNodeComponent", () => {
|
|||||||
generation: 12,
|
generation: 12,
|
||||||
paddingLeft: 23,
|
paddingLeft: 23,
|
||||||
};
|
};
|
||||||
const wrapper = shallow(<LegacyTreeNodeComponent {...props} />);
|
const wrapper = shallow(<TreeNodeComponent {...props} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders unsorted children by default", () => {
|
it("renders unsorted children by default", () => {
|
||||||
const node: LegacyTreeNode = {
|
const node: TreeNode = {
|
||||||
label: "label",
|
label: "label",
|
||||||
children: buildChildren(),
|
children: buildChildren(),
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
@@ -113,12 +113,12 @@ describe("LegacyTreeNodeComponent", () => {
|
|||||||
generation: 2,
|
generation: 2,
|
||||||
paddingLeft: 9,
|
paddingLeft: 9,
|
||||||
};
|
};
|
||||||
const wrapper = shallow(<LegacyTreeNodeComponent {...props} />);
|
const wrapper = shallow(<TreeNodeComponent {...props} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not render children by default", () => {
|
it("does not render children by default", () => {
|
||||||
const node: LegacyTreeNode = {
|
const node: TreeNode = {
|
||||||
label: "label",
|
label: "label",
|
||||||
children: buildChildren(),
|
children: buildChildren(),
|
||||||
isAlphaSorted: false,
|
isAlphaSorted: false,
|
||||||
@@ -128,12 +128,12 @@ describe("LegacyTreeNodeComponent", () => {
|
|||||||
generation: 2,
|
generation: 2,
|
||||||
paddingLeft: 9,
|
paddingLeft: 9,
|
||||||
};
|
};
|
||||||
const wrapper = shallow(<LegacyTreeNodeComponent {...props} />);
|
const wrapper = shallow(<TreeNodeComponent {...props} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders sorted children, expanded, leaves and parents separated", () => {
|
it("renders sorted children, expanded, leaves and parents separated", () => {
|
||||||
const node: LegacyTreeNode = {
|
const node: TreeNode = {
|
||||||
label: "label",
|
label: "label",
|
||||||
id: "id",
|
id: "id",
|
||||||
children: buildChildren2(),
|
children: buildChildren2(),
|
||||||
@@ -156,12 +156,12 @@ describe("LegacyTreeNodeComponent", () => {
|
|||||||
generation: 12,
|
generation: 12,
|
||||||
paddingLeft: 23,
|
paddingLeft: 23,
|
||||||
};
|
};
|
||||||
const wrapper = shallow(<LegacyTreeNodeComponent {...props} />);
|
const wrapper = shallow(<TreeNodeComponent {...props} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders loading icon", () => {
|
it("renders loading icon", () => {
|
||||||
const node: LegacyTreeNode = {
|
const node: TreeNode = {
|
||||||
label: "label",
|
label: "label",
|
||||||
children: [],
|
children: [],
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
@@ -172,7 +172,7 @@ describe("LegacyTreeNodeComponent", () => {
|
|||||||
generation: 2,
|
generation: 2,
|
||||||
paddingLeft: 9,
|
paddingLeft: 9,
|
||||||
};
|
};
|
||||||
const wrapper = shallow(<LegacyTreeNodeComponent {...props} />);
|
const wrapper = shallow(<TreeNodeComponent {...props} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
IContextualMenuItemProps,
|
IContextualMenuItemProps,
|
||||||
IContextualMenuProps,
|
IContextualMenuProps,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import AnimateHeight from "react-animate-height";
|
import AnimateHeight from "react-animate-height";
|
||||||
import LoadingIndicator_3Squares from "../../../../images/LoadingIndicator_3Squares.gif";
|
import LoadingIndicator_3Squares from "../../../../images/LoadingIndicator_3Squares.gif";
|
||||||
@@ -23,10 +22,18 @@ import { StyleConstants } from "../../../Common/StyleConstants";
|
|||||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
|
||||||
export interface LegacyTreeNode {
|
export interface TreeNodeMenuItem {
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
iconSrc?: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
styleClass?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TreeNode {
|
||||||
label: string;
|
label: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
children?: LegacyTreeNode[];
|
children?: TreeNode[];
|
||||||
contextMenu?: TreeNodeMenuItem[];
|
contextMenu?: TreeNodeMenuItem[];
|
||||||
iconSrc?: string;
|
iconSrc?: string;
|
||||||
isExpanded?: boolean;
|
isExpanded?: boolean;
|
||||||
@@ -43,37 +50,34 @@ export interface LegacyTreeNode {
|
|||||||
onContextMenuOpen?: () => void;
|
onContextMenuOpen?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LegacyTreeComponentProps {
|
export interface TreeComponentProps {
|
||||||
rootNode: LegacyTreeNode;
|
rootNode: TreeNode;
|
||||||
style?: any;
|
style?: any;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LegacyTreeComponent extends React.Component<LegacyTreeComponentProps> {
|
export class TreeComponent extends React.Component<TreeComponentProps> {
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div style={this.props.style} className={`treeComponent ${this.props.className}`} role="tree">
|
<div style={this.props.style} className={`treeComponent ${this.props.className}`} role="tree">
|
||||||
<LegacyTreeNodeComponent paddingLeft={0} node={this.props.rootNode} generation={0} />
|
<TreeNodeComponent paddingLeft={0} node={this.props.rootNode} generation={0} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tree node is a react component */
|
/* Tree node is a react component */
|
||||||
interface LegacyTreeNodeComponentProps {
|
interface TreeNodeComponentProps {
|
||||||
node: LegacyTreeNode;
|
node: TreeNode;
|
||||||
generation: number;
|
generation: number;
|
||||||
paddingLeft: number;
|
paddingLeft: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LegacyTreeNodeComponentState {
|
interface TreeNodeComponentState {
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
isMenuShowing: boolean;
|
isMenuShowing: boolean;
|
||||||
}
|
}
|
||||||
export class LegacyTreeNodeComponent extends React.Component<
|
export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, TreeNodeComponentState> {
|
||||||
LegacyTreeNodeComponentProps,
|
|
||||||
LegacyTreeNodeComponentState
|
|
||||||
> {
|
|
||||||
private static readonly paddingPerGenerationPx = 16;
|
private static readonly paddingPerGenerationPx = 16;
|
||||||
private static readonly iconOffset = 22;
|
private static readonly iconOffset = 22;
|
||||||
private static readonly transitionDurationMS = 200;
|
private static readonly transitionDurationMS = 200;
|
||||||
@@ -81,7 +85,7 @@ export class LegacyTreeNodeComponent extends React.Component<
|
|||||||
private contextMenuRef = React.createRef<HTMLDivElement>();
|
private contextMenuRef = React.createRef<HTMLDivElement>();
|
||||||
private isExpanded: boolean;
|
private isExpanded: boolean;
|
||||||
|
|
||||||
constructor(props: LegacyTreeNodeComponentProps) {
|
constructor(props: TreeNodeComponentProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.isExpanded = props.node.isExpanded;
|
this.isExpanded = props.node.isExpanded;
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -90,13 +94,13 @@ export class LegacyTreeNodeComponent extends React.Component<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: LegacyTreeNodeComponentProps, prevState: LegacyTreeNodeComponentState) {
|
componentDidUpdate(prevProps: TreeNodeComponentProps, prevState: TreeNodeComponentState) {
|
||||||
// Only call when expand has actually changed
|
// Only call when expand has actually changed
|
||||||
if (this.state.isExpanded !== prevState.isExpanded) {
|
if (this.state.isExpanded !== prevState.isExpanded) {
|
||||||
if (this.state.isExpanded) {
|
if (this.state.isExpanded) {
|
||||||
this.props.node.onExpanded && setTimeout(this.props.node.onExpanded, LegacyTreeNodeComponent.callbackDelayMS);
|
this.props.node.onExpanded && setTimeout(this.props.node.onExpanded, TreeNodeComponent.callbackDelayMS);
|
||||||
} else {
|
} else {
|
||||||
this.props.node.onCollapsed && setTimeout(this.props.node.onCollapsed, LegacyTreeNodeComponent.callbackDelayMS);
|
this.props.node.onCollapsed && setTimeout(this.props.node.onCollapsed, TreeNodeComponent.callbackDelayMS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.props.node.isExpanded !== this.isExpanded) {
|
if (this.props.node.isExpanded !== this.isExpanded) {
|
||||||
@@ -111,18 +115,18 @@ export class LegacyTreeNodeComponent extends React.Component<
|
|||||||
return this.renderNode(this.props.node, this.props.generation);
|
return this.renderNode(this.props.node, this.props.generation);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getSortedChildren(treeNode: LegacyTreeNode): LegacyTreeNode[] {
|
private static getSortedChildren(treeNode: TreeNode): TreeNode[] {
|
||||||
if (!treeNode || !treeNode.children) {
|
if (!treeNode || !treeNode.children) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const compareFct = (a: LegacyTreeNode, b: LegacyTreeNode) => a.label.localeCompare(b.label);
|
const compareFct = (a: TreeNode, b: TreeNode) => a.label.localeCompare(b.label);
|
||||||
|
|
||||||
let unsortedChildren;
|
let unsortedChildren;
|
||||||
if (treeNode.isLeavesParentsSeparate) {
|
if (treeNode.isLeavesParentsSeparate) {
|
||||||
// Separate parents and leave
|
// Separate parents and leave
|
||||||
const parents: LegacyTreeNode[] = treeNode.children.filter((node) => node.children);
|
const parents: TreeNode[] = treeNode.children.filter((node) => node.children);
|
||||||
const leaves: LegacyTreeNode[] = treeNode.children.filter((node) => !node.children);
|
const leaves: TreeNode[] = treeNode.children.filter((node) => !node.children);
|
||||||
|
|
||||||
if (treeNode.isAlphaSorted) {
|
if (treeNode.isAlphaSorted) {
|
||||||
parents.sort(compareFct);
|
parents.sort(compareFct);
|
||||||
@@ -137,18 +141,18 @@ export class LegacyTreeNodeComponent extends React.Component<
|
|||||||
return unsortedChildren;
|
return unsortedChildren;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static isNodeHeaderBlank(node: LegacyTreeNode): boolean {
|
private static isNodeHeaderBlank(node: TreeNode): boolean {
|
||||||
return (node.label === undefined || node.label === null) && !node.contextMenu;
|
return (node.label === undefined || node.label === null) && !node.contextMenu;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderNode(node: LegacyTreeNode, generation: number): JSX.Element {
|
private renderNode(node: TreeNode, generation: number): JSX.Element {
|
||||||
const paddingLeft = generation * LegacyTreeNodeComponent.paddingPerGenerationPx;
|
let paddingLeft = generation * TreeNodeComponent.paddingPerGenerationPx;
|
||||||
let additionalOffsetPx = 15;
|
let additionalOffsetPx = 15;
|
||||||
|
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
const childrenWithSubChildren = node.children.filter((child: LegacyTreeNode) => !!child.children);
|
const childrenWithSubChildren = node.children.filter((child: TreeNode) => !!child.children);
|
||||||
if (childrenWithSubChildren.length > 0) {
|
if (childrenWithSubChildren.length > 0) {
|
||||||
additionalOffsetPx = LegacyTreeNodeComponent.iconOffset;
|
additionalOffsetPx = TreeNodeComponent.iconOffset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,17 +160,16 @@ export class LegacyTreeNodeComponent extends React.Component<
|
|||||||
const showSelected =
|
const showSelected =
|
||||||
this.props.node.isSelected &&
|
this.props.node.isSelected &&
|
||||||
this.props.node.isSelected() &&
|
this.props.node.isSelected() &&
|
||||||
!LegacyTreeNodeComponent.isAnyDescendantSelected(this.props.node);
|
!TreeNodeComponent.isAnyDescendantSelected(this.props.node);
|
||||||
|
|
||||||
const headerStyle: React.CSSProperties = { paddingLeft: this.props.paddingLeft };
|
const headerStyle: React.CSSProperties = { paddingLeft: this.props.paddingLeft };
|
||||||
if (LegacyTreeNodeComponent.isNodeHeaderBlank(node)) {
|
if (TreeNodeComponent.isNodeHeaderBlank(node)) {
|
||||||
headerStyle.height = 0;
|
headerStyle.height = 0;
|
||||||
headerStyle.padding = 0;
|
headerStyle.padding = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-test={`Tree/TreeNode:${node.label}`}
|
|
||||||
className={`${this.props.node.className || ""} main${generation} nodeItem ${showSelected ? "selected" : ""}`}
|
className={`${this.props.node.className || ""} main${generation} nodeItem ${showSelected ? "selected" : ""}`}
|
||||||
onClick={(event: React.MouseEvent<HTMLDivElement>) => this.onNodeClick(event, node)}
|
onClick={(event: React.MouseEvent<HTMLDivElement>) => this.onNodeClick(event, node)}
|
||||||
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) => this.onNodeKeyPress(event, node)}
|
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) => this.onNodeKeyPress(event, node)}
|
||||||
@@ -175,9 +178,9 @@ export class LegacyTreeNodeComponent extends React.Component<
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`treeNodeHeader ${this.state.isMenuShowing ? "showingMenu" : ""}`}
|
className={`treeNodeHeader ${this.state.isMenuShowing ? "showingMenu" : ""}`}
|
||||||
data-test={`Tree/TreeNode/Header:${node.label}`}
|
|
||||||
style={headerStyle}
|
style={headerStyle}
|
||||||
tabIndex={node.children ? -1 : 0}
|
tabIndex={node.children ? -1 : 0}
|
||||||
|
data-test={node.label}
|
||||||
>
|
>
|
||||||
{this.renderCollapseExpandIcon(node)}
|
{this.renderCollapseExpandIcon(node)}
|
||||||
{node.iconSrc && <img className="nodeIcon" src={node.iconSrc} alt="" />}
|
{node.iconSrc && <img className="nodeIcon" src={node.iconSrc} alt="" />}
|
||||||
@@ -192,13 +195,10 @@ export class LegacyTreeNodeComponent extends React.Component<
|
|||||||
<img className="loadingIcon" src={LoadingIndicator_3Squares} hidden={!this.props.node.isLoading} />
|
<img className="loadingIcon" src={LoadingIndicator_3Squares} hidden={!this.props.node.isLoading} />
|
||||||
</div>
|
</div>
|
||||||
{node.children && (
|
{node.children && (
|
||||||
<AnimateHeight
|
<AnimateHeight duration={TreeNodeComponent.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}>
|
||||||
duration={LegacyTreeNodeComponent.transitionDurationMS}
|
|
||||||
height={this.state.isExpanded ? "auto" : 0}
|
|
||||||
>
|
|
||||||
<div className="nodeChildren" data-test={node.label} role="group">
|
<div className="nodeChildren" data-test={node.label} role="group">
|
||||||
{LegacyTreeNodeComponent.getSortedChildren(node).map((childNode: LegacyTreeNode) => (
|
{TreeNodeComponent.getSortedChildren(node).map((childNode: TreeNode) => (
|
||||||
<LegacyTreeNodeComponent
|
<TreeNodeComponent
|
||||||
key={`${childNode.label}-${generation + 1}-${childNode.timestamp}`}
|
key={`${childNode.label}-${generation + 1}-${childNode.timestamp}`}
|
||||||
node={childNode}
|
node={childNode}
|
||||||
generation={generation + 1}
|
generation={generation + 1}
|
||||||
@@ -216,14 +216,12 @@ export class LegacyTreeNodeComponent extends React.Component<
|
|||||||
* Recursive: is the node or any descendant selected
|
* Recursive: is the node or any descendant selected
|
||||||
* @param node
|
* @param node
|
||||||
*/
|
*/
|
||||||
private static isAnyDescendantSelected(node: LegacyTreeNode): boolean {
|
private static isAnyDescendantSelected(node: TreeNode): boolean {
|
||||||
return (
|
return (
|
||||||
node.children &&
|
node.children &&
|
||||||
node.children.reduce(
|
node.children.reduce(
|
||||||
(previous: boolean, child: LegacyTreeNode) =>
|
(previous: boolean, child: TreeNode) =>
|
||||||
previous ||
|
previous || (child.isSelected && child.isSelected()) || TreeNodeComponent.isAnyDescendantSelected(child),
|
||||||
(child.isSelected && child.isSelected()) ||
|
|
||||||
LegacyTreeNodeComponent.isAnyDescendantSelected(child),
|
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -234,10 +232,10 @@ export class LegacyTreeNodeComponent extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onRightClick = (): void => {
|
private onRightClick = (): void => {
|
||||||
this.contextMenuRef.current.firstChild.dispatchEvent(LegacyTreeNodeComponent.createClickEvent());
|
this.contextMenuRef.current.firstChild.dispatchEvent(TreeNodeComponent.createClickEvent());
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderContextMenuButton(node: LegacyTreeNode): JSX.Element {
|
private renderContextMenuButton(node: TreeNode): JSX.Element {
|
||||||
const menuItemLabel = "More";
|
const menuItemLabel = "More";
|
||||||
const buttonStyles: Partial<IButtonStyles> = {
|
const buttonStyles: Partial<IButtonStyles> = {
|
||||||
rootFocused: { outline: `1px dashed ${StyleConstants.FocusColor}` },
|
rootFocused: { outline: `1px dashed ${StyleConstants.FocusColor}` },
|
||||||
@@ -249,7 +247,7 @@ export class LegacyTreeNodeComponent extends React.Component<
|
|||||||
name="More"
|
name="More"
|
||||||
title="More"
|
title="More"
|
||||||
className="treeMenuEllipsis"
|
className="treeMenuEllipsis"
|
||||||
ariaLabel={`${menuItemLabel} options`}
|
ariaLabel={menuItemLabel}
|
||||||
menuIconProps={{
|
menuIconProps={{
|
||||||
iconName: menuItemLabel,
|
iconName: menuItemLabel,
|
||||||
styles: { root: { fontSize: "18px", fontWeight: "bold" } },
|
styles: { root: { fontSize: "18px", fontWeight: "bold" } },
|
||||||
@@ -265,9 +263,9 @@ export class LegacyTreeNodeComponent extends React.Component<
|
|||||||
onMenuDismissed: (contextualMenu?: IContextualMenuProps) => this.setState({ isMenuShowing: false }),
|
onMenuDismissed: (contextualMenu?: IContextualMenuProps) => this.setState({ isMenuShowing: false }),
|
||||||
contextualMenuItemAs: (props: IContextualMenuItemProps) => (
|
contextualMenuItemAs: (props: IContextualMenuItemProps) => (
|
||||||
<div
|
<div
|
||||||
data-test={`Tree/TreeNode/MenuItem:${props.item.text}`}
|
data-test={`treeComponentMenuItemContainer`}
|
||||||
className="treeComponentMenuItemContainer"
|
className="treeComponentMenuItemContainer"
|
||||||
onContextMenu={(e) => e.target.dispatchEvent(LegacyTreeNodeComponent.createClickEvent())}
|
onContextMenu={(e) => e.target.dispatchEvent(TreeNodeComponent.createClickEvent())}
|
||||||
>
|
>
|
||||||
{props.item.onRenderIcon()}
|
{props.item.onRenderIcon()}
|
||||||
<span
|
<span
|
||||||
@@ -299,7 +297,7 @@ export class LegacyTreeNodeComponent extends React.Component<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderCollapseExpandIcon(node: LegacyTreeNode): JSX.Element {
|
private renderCollapseExpandIcon(node: TreeNode): JSX.Element {
|
||||||
if (!node.children || !node.label) {
|
if (!node.children || !node.label) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
@@ -316,12 +314,12 @@ export class LegacyTreeNodeComponent extends React.Component<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onNodeClick = (event: React.MouseEvent<HTMLDivElement>, node: LegacyTreeNode): void => {
|
private onNodeClick = (event: React.MouseEvent<HTMLDivElement>, node: TreeNode): void => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
const isExpanded = !this.state.isExpanded;
|
const isExpanded = !this.state.isExpanded;
|
||||||
// Prevent collapsing if node header is blank
|
// Prevent collapsing if node header is blank
|
||||||
if (!(LegacyTreeNodeComponent.isNodeHeaderBlank(node) && !isExpanded)) {
|
if (!(TreeNodeComponent.isNodeHeaderBlank(node) && !isExpanded)) {
|
||||||
this.setState({ isExpanded });
|
this.setState({ isExpanded });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -329,14 +327,14 @@ export class LegacyTreeNodeComponent extends React.Component<
|
|||||||
this.props.node.onClick && this.props.node.onClick(this.state.isExpanded);
|
this.props.node.onClick && this.props.node.onClick(this.state.isExpanded);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onNodeKeyPress = (event: React.KeyboardEvent<HTMLDivElement>, node: LegacyTreeNode): void => {
|
private onNodeKeyPress = (event: React.KeyboardEvent<HTMLDivElement>, node: TreeNode): void => {
|
||||||
if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) {
|
if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
this.props.node.onClick && this.props.node.onClick(this.state.isExpanded);
|
this.props.node.onClick && this.props.node.onClick(this.state.isExpanded);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onCollapseExpandIconKeyPress = (event: React.KeyboardEvent<HTMLDivElement>, node: LegacyTreeNode): void => {
|
private onCollapseExpandIconKeyPress = (event: React.KeyboardEvent<HTMLDivElement>, node: TreeNode): void => {
|
||||||
if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) {
|
if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
import { TreeItem, TreeItemLayout, tokens } from "@fluentui/react-components";
|
|
||||||
import PromiseSource from "Utils/PromiseSource";
|
|
||||||
import { mount, shallow } from "enzyme";
|
|
||||||
import React from "react";
|
|
||||||
import { act } from "react-dom/test-utils";
|
|
||||||
import { TreeNode, TreeNodeComponent } from "./TreeNodeComponent";
|
|
||||||
|
|
||||||
function generateTestNode(id: string, additionalProps?: Partial<TreeNode>): TreeNode {
|
|
||||||
const node: TreeNode = {
|
|
||||||
id,
|
|
||||||
label: `${id}Label`,
|
|
||||||
className: `${id}Class`,
|
|
||||||
iconSrc: `${id}Icon`,
|
|
||||||
onClick: jest.fn().mockName(`${id}Click`),
|
|
||||||
...additionalProps,
|
|
||||||
};
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("TreeNodeComponent", () => {
|
|
||||||
it("renders a single node", () => {
|
|
||||||
const node = generateTestNode("root");
|
|
||||||
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
|
|
||||||
expect(component).toMatchSnapshot();
|
|
||||||
|
|
||||||
// The "click" handler is actually attached to onOpenChange, with a type of "Click".
|
|
||||||
component
|
|
||||||
.find(TreeItem)
|
|
||||||
.props()
|
|
||||||
.onOpenChange(null!, { open: true, value: "borp", target: null!, event: null!, type: "Click" });
|
|
||||||
expect(node.onClick).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders a node with a menu", () => {
|
|
||||||
const node = generateTestNode("root", {
|
|
||||||
contextMenu: [
|
|
||||||
{
|
|
||||||
label: "enabledItem",
|
|
||||||
onClick: jest.fn().mockName("enabledItemClick"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "disabledItem",
|
|
||||||
onClick: jest.fn().mockName("disabledItemClick"),
|
|
||||||
isDisabled: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
|
|
||||||
expect(component).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders a loading spinner if the node is loading", async () => {
|
|
||||||
const loading = new PromiseSource();
|
|
||||||
const node = generateTestNode("root", {
|
|
||||||
onExpanded: () => loading.promise,
|
|
||||||
});
|
|
||||||
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
component
|
|
||||||
.find(TreeItem)
|
|
||||||
.props()
|
|
||||||
.onOpenChange(null!, { open: true, value: "borp", target: null!, event: null!, type: "ExpandIconClick" });
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(component).toMatchSnapshot("loading");
|
|
||||||
await loading.resolveAndWait();
|
|
||||||
expect(component).toMatchSnapshot("loaded");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders single selected leaf node as selected", () => {
|
|
||||||
const node = generateTestNode("root", {
|
|
||||||
isSelected: () => true,
|
|
||||||
});
|
|
||||||
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
|
|
||||||
expect(component.find(TreeItemLayout).props().style?.backgroundColor).toStrictEqual(
|
|
||||||
tokens.colorNeutralBackground1Selected,
|
|
||||||
);
|
|
||||||
expect(component).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders selected parent node as selected if no descendant nodes are selected", () => {
|
|
||||||
const node = generateTestNode("root", {
|
|
||||||
isSelected: () => true,
|
|
||||||
children: [
|
|
||||||
generateTestNode("child1", {
|
|
||||||
children: [generateTestNode("grandchild1"), generateTestNode("grandchild2")],
|
|
||||||
}),
|
|
||||||
generateTestNode("child2"),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
|
|
||||||
expect(component.find(TreeItemLayout).props().style?.backgroundColor).toStrictEqual(
|
|
||||||
tokens.colorNeutralBackground1Selected,
|
|
||||||
);
|
|
||||||
expect(component).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders selected parent node as unselected if any descendant node is selected", () => {
|
|
||||||
const node = generateTestNode("root", {
|
|
||||||
isSelected: () => true,
|
|
||||||
children: [
|
|
||||||
generateTestNode("child1", {
|
|
||||||
children: [
|
|
||||||
generateTestNode("grandchild1", {
|
|
||||||
isSelected: () => true,
|
|
||||||
}),
|
|
||||||
generateTestNode("grandchild2"),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
generateTestNode("child2"),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
|
|
||||||
expect(component.find(TreeItemLayout).props().style?.backgroundColor).toBeUndefined();
|
|
||||||
expect(component).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders an icon if the node has one", () => {
|
|
||||||
const node = generateTestNode("root", {
|
|
||||||
iconSrc: "the-icon.svg",
|
|
||||||
});
|
|
||||||
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
|
|
||||||
expect(component).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders a node as expandable if it has empty, but defined, children array", () => {
|
|
||||||
const node = generateTestNode("root", {
|
|
||||||
isLoading: true,
|
|
||||||
children: [
|
|
||||||
generateTestNode("child1", {
|
|
||||||
children: [],
|
|
||||||
}),
|
|
||||||
generateTestNode("child2"),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
|
|
||||||
expect(component).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not render children if the node is loading", () => {
|
|
||||||
const node = generateTestNode("root", {
|
|
||||||
isLoading: true,
|
|
||||||
children: [
|
|
||||||
generateTestNode("child1", {
|
|
||||||
children: [generateTestNode("grandchild1"), generateTestNode("grandchild2")],
|
|
||||||
}),
|
|
||||||
generateTestNode("child2"),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
const component = shallow(<TreeNodeComponent node={node} treeNodeId={node.id} />);
|
|
||||||
expect(component).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fully renders a tree", () => {
|
|
||||||
const child3Loading = new PromiseSource();
|
|
||||||
const node = generateTestNode("root", {
|
|
||||||
isSelected: () => true,
|
|
||||||
children: [
|
|
||||||
generateTestNode("child1", {
|
|
||||||
children: [
|
|
||||||
generateTestNode("grandchild1", {
|
|
||||||
iconSrc: "grandchild1Icon.svg",
|
|
||||||
isSelected: () => true,
|
|
||||||
}),
|
|
||||||
generateTestNode("grandchild2"),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
generateTestNode("child2Loading", {
|
|
||||||
isLoading: true,
|
|
||||||
children: [generateTestNode("grandchild3NotRendered")],
|
|
||||||
}),
|
|
||||||
generateTestNode("child3Expanding", {
|
|
||||||
onExpanded: () => child3Loading.promise,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
const component = mount(<TreeNodeComponent node={node} treeNodeId={node.id} />);
|
|
||||||
|
|
||||||
// Find and expand the child3Expanding node
|
|
||||||
const expandingChild = component.find(TreeItem).filterWhere((n) => n.props().value === "root/child3ExpandingLabel");
|
|
||||||
act(() => {
|
|
||||||
expandingChild.props().onOpenChange(null!, {
|
|
||||||
open: true,
|
|
||||||
value: "root/child3ExpandingLabel",
|
|
||||||
target: null!,
|
|
||||||
event: null!,
|
|
||||||
type: "Click",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(component).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
import {
|
|
||||||
Button,
|
|
||||||
Menu,
|
|
||||||
MenuItem,
|
|
||||||
MenuList,
|
|
||||||
MenuOpenChangeData,
|
|
||||||
MenuOpenEvent,
|
|
||||||
MenuPopover,
|
|
||||||
MenuTrigger,
|
|
||||||
Spinner,
|
|
||||||
Tree,
|
|
||||||
TreeItem,
|
|
||||||
TreeItemLayout,
|
|
||||||
TreeOpenChangeData,
|
|
||||||
TreeOpenChangeEvent,
|
|
||||||
} from "@fluentui/react-components";
|
|
||||||
import { MoreHorizontal20Regular } from "@fluentui/react-icons";
|
|
||||||
import { tokens } from "@fluentui/react-theme";
|
|
||||||
import * as React from "react";
|
|
||||||
import { useCallback } from "react";
|
|
||||||
|
|
||||||
export interface TreeNodeMenuItem {
|
|
||||||
label: string;
|
|
||||||
onClick: () => void;
|
|
||||||
iconSrc?: string;
|
|
||||||
isDisabled?: boolean;
|
|
||||||
styleClass?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TreeNode {
|
|
||||||
label: string;
|
|
||||||
id?: string;
|
|
||||||
children?: TreeNode[];
|
|
||||||
contextMenu?: TreeNodeMenuItem[];
|
|
||||||
iconSrc?: string;
|
|
||||||
isExpanded?: boolean;
|
|
||||||
className?: string;
|
|
||||||
isAlphaSorted?: boolean;
|
|
||||||
// data?: any; // Piece of data corresponding to this node
|
|
||||||
timestamp?: number;
|
|
||||||
isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves
|
|
||||||
isLoading?: boolean;
|
|
||||||
isScrollable?: boolean;
|
|
||||||
isSelected?: () => boolean;
|
|
||||||
onClick?: () => void; // Only if a leaf, other click will expand/collapse
|
|
||||||
onExpanded?: () => Promise<void>;
|
|
||||||
onCollapsed?: () => void;
|
|
||||||
onContextMenuOpen?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TreeNodeComponentProps {
|
|
||||||
node: TreeNode;
|
|
||||||
className?: string;
|
|
||||||
treeNodeId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Function that returns true if any descendant (at any depth) of this node is selected. */
|
|
||||||
function isAnyDescendantSelected(node: TreeNode): boolean {
|
|
||||||
return (
|
|
||||||
node.children &&
|
|
||||||
node.children.reduce(
|
|
||||||
(previous: boolean, child: TreeNode) =>
|
|
||||||
previous || (child.isSelected && child.isSelected()) || isAnyDescendantSelected(child),
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTreeIcon = (iconSrc: string): JSX.Element => <img src={iconSrc} alt="" style={{ width: 16, height: 16 }} />;
|
|
||||||
|
|
||||||
export const TreeNodeComponent: React.FC<TreeNodeComponentProps> = ({
|
|
||||||
node,
|
|
||||||
treeNodeId,
|
|
||||||
}: TreeNodeComponentProps): JSX.Element => {
|
|
||||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
|
||||||
|
|
||||||
const getSortedChildren = (treeNode: TreeNode): TreeNode[] => {
|
|
||||||
if (!treeNode || !treeNode.children) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const compareFct = (a: TreeNode, b: TreeNode) => a.label.localeCompare(b.label);
|
|
||||||
|
|
||||||
let unsortedChildren;
|
|
||||||
if (treeNode.isLeavesParentsSeparate) {
|
|
||||||
// Separate parents and leave
|
|
||||||
const parents: TreeNode[] = treeNode.children.filter((node) => node.children);
|
|
||||||
const leaves: TreeNode[] = treeNode.children.filter((node) => !node.children);
|
|
||||||
|
|
||||||
if (treeNode.isAlphaSorted) {
|
|
||||||
parents.sort(compareFct);
|
|
||||||
leaves.sort(compareFct);
|
|
||||||
}
|
|
||||||
|
|
||||||
unsortedChildren = parents.concat(leaves);
|
|
||||||
} else {
|
|
||||||
unsortedChildren = treeNode.isAlphaSorted ? treeNode.children.sort(compareFct) : treeNode.children;
|
|
||||||
}
|
|
||||||
|
|
||||||
return unsortedChildren;
|
|
||||||
};
|
|
||||||
|
|
||||||
// A branch node is any node with a defined children array, even if the array is empty.
|
|
||||||
const isBranch = !!node.children;
|
|
||||||
|
|
||||||
const onOpenChange = useCallback(
|
|
||||||
(_: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
|
|
||||||
if (data.type === "Click" && !isBranch && node.onClick) {
|
|
||||||
node.onClick();
|
|
||||||
}
|
|
||||||
if (!node.isExpanded && data.open && node.onExpanded) {
|
|
||||||
// Catch the transition non-expanded to expanded
|
|
||||||
setIsLoading(true);
|
|
||||||
node.onExpanded?.().then(() => setIsLoading(false));
|
|
||||||
} else if (node.isExpanded && !data.open && node.onCollapsed) {
|
|
||||||
// Catch the transition expanded to non-expanded
|
|
||||||
node.onCollapsed?.();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[isBranch, node, setIsLoading],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onMenuOpenChange = useCallback(
|
|
||||||
(e: MenuOpenEvent, data: MenuOpenChangeData) => {
|
|
||||||
if (data.open) {
|
|
||||||
node.onContextMenuOpen?.();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[node],
|
|
||||||
);
|
|
||||||
|
|
||||||
// We show a node as selected if it is selected AND no descendant is selected.
|
|
||||||
// We want to show only the deepest selected node as selected.
|
|
||||||
const isCurrentNodeSelected = node.isSelected && node.isSelected();
|
|
||||||
const shouldShowAsSelected = isCurrentNodeSelected && !isAnyDescendantSelected(node);
|
|
||||||
|
|
||||||
const contextMenuItems = (node.contextMenu ?? []).map((menuItem) => (
|
|
||||||
<MenuItem
|
|
||||||
data-test={`TreeNode/ContextMenuItem:${menuItem.label}`}
|
|
||||||
disabled={menuItem.isDisabled}
|
|
||||||
key={menuItem.label}
|
|
||||||
onClick={menuItem.onClick}
|
|
||||||
>
|
|
||||||
{menuItem.label}
|
|
||||||
</MenuItem>
|
|
||||||
));
|
|
||||||
|
|
||||||
const treeItem = (
|
|
||||||
<TreeItem
|
|
||||||
data-test={`TreeNodeContainer:${treeNodeId}`}
|
|
||||||
value={treeNodeId}
|
|
||||||
itemType={isBranch ? "branch" : "leaf"}
|
|
||||||
onOpenChange={onOpenChange}
|
|
||||||
>
|
|
||||||
<TreeItemLayout
|
|
||||||
className={node.className}
|
|
||||||
data-test={`TreeNode:${treeNodeId}`}
|
|
||||||
actions={
|
|
||||||
contextMenuItems.length > 0 && (
|
|
||||||
<Menu onOpenChange={onMenuOpenChange}>
|
|
||||||
<MenuTrigger disableButtonEnhancement>
|
|
||||||
<Button
|
|
||||||
aria-label="More options"
|
|
||||||
data-test="TreeNode/ContextMenuTrigger"
|
|
||||||
appearance="subtle"
|
|
||||||
icon={<MoreHorizontal20Regular />}
|
|
||||||
/>
|
|
||||||
</MenuTrigger>
|
|
||||||
<MenuPopover data-test={`TreeNode/ContextMenu:${treeNodeId}`}>
|
|
||||||
<MenuList>{contextMenuItems}</MenuList>
|
|
||||||
</MenuPopover>
|
|
||||||
</Menu>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
expandIcon={isLoading ? <Spinner size="extra-tiny" /> : undefined}
|
|
||||||
iconBefore={node.iconSrc && getTreeIcon(node.iconSrc)}
|
|
||||||
style={{
|
|
||||||
backgroundColor: shouldShowAsSelected ? tokens.colorNeutralBackground1Selected : undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{node.label}
|
|
||||||
</TreeItemLayout>
|
|
||||||
{!node.isLoading && node.children?.length > 0 && (
|
|
||||||
<Tree style={{ overflow: node.isScrollable ? "auto" : undefined }}>
|
|
||||||
{getSortedChildren(node).map((childNode: TreeNode) => (
|
|
||||||
<TreeNodeComponent key={childNode.label} node={childNode} treeNodeId={`${treeNodeId}/${childNode.label}`} />
|
|
||||||
))}
|
|
||||||
</Tree>
|
|
||||||
)}
|
|
||||||
</TreeItem>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (contextMenuItems.length === 0) {
|
|
||||||
return treeItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For accessibility, it's highly recommended that any 'actions' also be available in the context menu.
|
|
||||||
// See https://react.fluentui.dev/?path=/docs/components-tree--default#actions
|
|
||||||
return (
|
|
||||||
<Menu positioning="below-end" openOnContext onOpenChange={onMenuOpenChange}>
|
|
||||||
<MenuTrigger disableButtonEnhancement>{treeItem}</MenuTrigger>
|
|
||||||
<MenuPopover>
|
|
||||||
<MenuList>{contextMenuItems}</MenuList>
|
|
||||||
</MenuPopover>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`LegacyTreeComponent renders a simple tree 1`] = `
|
exports[`TreeComponent renders a simple tree 1`] = `
|
||||||
<div
|
<div
|
||||||
className="treeComponent tree"
|
className="treeComponent tree"
|
||||||
role="tree"
|
role="tree"
|
||||||
>
|
>
|
||||||
<LegacyTreeNodeComponent
|
<TreeNodeComponent
|
||||||
generation={0}
|
generation={0}
|
||||||
node={
|
node={
|
||||||
Object {
|
Object {
|
||||||
@@ -33,17 +33,16 @@ exports[`LegacyTreeComponent renders a simple tree 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`LegacyTreeNodeComponent does not render children by default 1`] = `
|
exports[`TreeNodeComponent does not render children by default 1`] = `
|
||||||
<div
|
<div
|
||||||
className=" main2 nodeItem "
|
className=" main2 nodeItem "
|
||||||
data-test="Tree/TreeNode:label"
|
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onKeyPress={[Function]}
|
onKeyPress={[Function]}
|
||||||
role="treeitem"
|
role="treeitem"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="treeNodeHeader "
|
className="treeNodeHeader "
|
||||||
data-test="Tree/TreeNode/Header:label"
|
data-test="label"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"paddingLeft": 9,
|
"paddingLeft": 9,
|
||||||
@@ -103,7 +102,7 @@ exports[`LegacyTreeNodeComponent does not render children by default 1`] = `
|
|||||||
data-test="label"
|
data-test="label"
|
||||||
role="group"
|
role="group"
|
||||||
>
|
>
|
||||||
<LegacyTreeNodeComponent
|
<TreeNodeComponent
|
||||||
generation={3}
|
generation={3}
|
||||||
key="Bchild1-3-undefined"
|
key="Bchild1-3-undefined"
|
||||||
node={
|
node={
|
||||||
@@ -121,7 +120,7 @@ exports[`LegacyTreeNodeComponent does not render children by default 1`] = `
|
|||||||
}
|
}
|
||||||
paddingLeft={32}
|
paddingLeft={32}
|
||||||
/>
|
/>
|
||||||
<LegacyTreeNodeComponent
|
<TreeNodeComponent
|
||||||
generation={3}
|
generation={3}
|
||||||
key="2child2-3-undefined"
|
key="2child2-3-undefined"
|
||||||
node={
|
node={
|
||||||
@@ -136,10 +135,9 @@ exports[`LegacyTreeNodeComponent does not render children by default 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`LegacyTreeNodeComponent renders a simple node (sorted children, expanded) 1`] = `
|
exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`] = `
|
||||||
<div
|
<div
|
||||||
className="nodeClassname main12 nodeItem "
|
className="nodeClassname main12 nodeItem "
|
||||||
data-test="Tree/TreeNode:label"
|
|
||||||
id="id"
|
id="id"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onKeyPress={[Function]}
|
onKeyPress={[Function]}
|
||||||
@@ -147,7 +145,7 @@ exports[`LegacyTreeNodeComponent renders a simple node (sorted children, expande
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="treeNodeHeader "
|
className="treeNodeHeader "
|
||||||
data-test="Tree/TreeNode/Header:label"
|
data-test="label"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"paddingLeft": 23,
|
"paddingLeft": 23,
|
||||||
@@ -174,7 +172,7 @@ exports[`LegacyTreeNodeComponent renders a simple node (sorted children, expande
|
|||||||
onKeyPress={[Function]}
|
onKeyPress={[Function]}
|
||||||
>
|
>
|
||||||
<CustomizedIconButton
|
<CustomizedIconButton
|
||||||
ariaLabel="More options"
|
ariaLabel="More"
|
||||||
className="treeMenuEllipsis"
|
className="treeMenuEllipsis"
|
||||||
menuIconProps={
|
menuIconProps={
|
||||||
Object {
|
Object {
|
||||||
@@ -256,7 +254,7 @@ exports[`LegacyTreeNodeComponent renders a simple node (sorted children, expande
|
|||||||
data-test="label"
|
data-test="label"
|
||||||
role="group"
|
role="group"
|
||||||
>
|
>
|
||||||
<LegacyTreeNodeComponent
|
<TreeNodeComponent
|
||||||
generation={13}
|
generation={13}
|
||||||
key="2child2-13-undefined"
|
key="2child2-13-undefined"
|
||||||
node={
|
node={
|
||||||
@@ -266,7 +264,7 @@ exports[`LegacyTreeNodeComponent renders a simple node (sorted children, expande
|
|||||||
}
|
}
|
||||||
paddingLeft={214}
|
paddingLeft={214}
|
||||||
/>
|
/>
|
||||||
<LegacyTreeNodeComponent
|
<TreeNodeComponent
|
||||||
generation={13}
|
generation={13}
|
||||||
key="Bchild1-13-undefined"
|
key="Bchild1-13-undefined"
|
||||||
node={
|
node={
|
||||||
@@ -289,17 +287,16 @@ exports[`LegacyTreeNodeComponent renders a simple node (sorted children, expande
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`LegacyTreeNodeComponent renders loading icon 1`] = `
|
exports[`TreeNodeComponent renders loading icon 1`] = `
|
||||||
<div
|
<div
|
||||||
className=" main2 nodeItem "
|
className=" main2 nodeItem "
|
||||||
data-test="Tree/TreeNode:label"
|
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onKeyPress={[Function]}
|
onKeyPress={[Function]}
|
||||||
role="treeitem"
|
role="treeitem"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="treeNodeHeader "
|
className="treeNodeHeader "
|
||||||
data-test="Tree/TreeNode/Header:label"
|
data-test="label"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"paddingLeft": 9,
|
"paddingLeft": 9,
|
||||||
@@ -363,10 +360,9 @@ exports[`LegacyTreeNodeComponent renders loading icon 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`LegacyTreeNodeComponent renders sorted children, expanded, leaves and parents separated 1`] = `
|
exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents separated 1`] = `
|
||||||
<div
|
<div
|
||||||
className="nodeClassname main12 nodeItem "
|
className="nodeClassname main12 nodeItem "
|
||||||
data-test="Tree/TreeNode:label"
|
|
||||||
id="id"
|
id="id"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onKeyPress={[Function]}
|
onKeyPress={[Function]}
|
||||||
@@ -374,7 +370,7 @@ exports[`LegacyTreeNodeComponent renders sorted children, expanded, leaves and p
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="treeNodeHeader "
|
className="treeNodeHeader "
|
||||||
data-test="Tree/TreeNode/Header:label"
|
data-test="label"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"paddingLeft": 23,
|
"paddingLeft": 23,
|
||||||
@@ -401,7 +397,7 @@ exports[`LegacyTreeNodeComponent renders sorted children, expanded, leaves and p
|
|||||||
onKeyPress={[Function]}
|
onKeyPress={[Function]}
|
||||||
>
|
>
|
||||||
<CustomizedIconButton
|
<CustomizedIconButton
|
||||||
ariaLabel="More options"
|
ariaLabel="More"
|
||||||
className="treeMenuEllipsis"
|
className="treeMenuEllipsis"
|
||||||
menuIconProps={
|
menuIconProps={
|
||||||
Object {
|
Object {
|
||||||
@@ -474,7 +470,7 @@ exports[`LegacyTreeNodeComponent renders sorted children, expanded, leaves and p
|
|||||||
data-test="label"
|
data-test="label"
|
||||||
role="group"
|
role="group"
|
||||||
>
|
>
|
||||||
<LegacyTreeNodeComponent
|
<TreeNodeComponent
|
||||||
generation={13}
|
generation={13}
|
||||||
key="bchild-13-undefined"
|
key="bchild-13-undefined"
|
||||||
node={
|
node={
|
||||||
@@ -492,7 +488,7 @@ exports[`LegacyTreeNodeComponent renders sorted children, expanded, leaves and p
|
|||||||
}
|
}
|
||||||
paddingLeft={192}
|
paddingLeft={192}
|
||||||
/>
|
/>
|
||||||
<LegacyTreeNodeComponent
|
<TreeNodeComponent
|
||||||
generation={13}
|
generation={13}
|
||||||
key="dchild-13-undefined"
|
key="dchild-13-undefined"
|
||||||
node={
|
node={
|
||||||
@@ -510,7 +506,7 @@ exports[`LegacyTreeNodeComponent renders sorted children, expanded, leaves and p
|
|||||||
}
|
}
|
||||||
paddingLeft={192}
|
paddingLeft={192}
|
||||||
/>
|
/>
|
||||||
<LegacyTreeNodeComponent
|
<TreeNodeComponent
|
||||||
generation={13}
|
generation={13}
|
||||||
key="aChild-13-undefined"
|
key="aChild-13-undefined"
|
||||||
node={
|
node={
|
||||||
@@ -520,7 +516,7 @@ exports[`LegacyTreeNodeComponent renders sorted children, expanded, leaves and p
|
|||||||
}
|
}
|
||||||
paddingLeft={214}
|
paddingLeft={214}
|
||||||
/>
|
/>
|
||||||
<LegacyTreeNodeComponent
|
<TreeNodeComponent
|
||||||
generation={13}
|
generation={13}
|
||||||
key="cchild-13-undefined"
|
key="cchild-13-undefined"
|
||||||
node={
|
node={
|
||||||
@@ -535,17 +531,16 @@ exports[`LegacyTreeNodeComponent renders sorted children, expanded, leaves and p
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`LegacyTreeNodeComponent renders unsorted children by default 1`] = `
|
exports[`TreeNodeComponent renders unsorted children by default 1`] = `
|
||||||
<div
|
<div
|
||||||
className=" main2 nodeItem "
|
className=" main2 nodeItem "
|
||||||
data-test="Tree/TreeNode:label"
|
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onKeyPress={[Function]}
|
onKeyPress={[Function]}
|
||||||
role="treeitem"
|
role="treeitem"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="treeNodeHeader "
|
className="treeNodeHeader "
|
||||||
data-test="Tree/TreeNode/Header:label"
|
data-test="label"
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"paddingLeft": 9,
|
"paddingLeft": 9,
|
||||||
@@ -605,7 +600,7 @@ exports[`LegacyTreeNodeComponent renders unsorted children by default 1`] = `
|
|||||||
data-test="label"
|
data-test="label"
|
||||||
role="group"
|
role="group"
|
||||||
>
|
>
|
||||||
<LegacyTreeNodeComponent
|
<TreeNodeComponent
|
||||||
generation={3}
|
generation={3}
|
||||||
key="Bchild1-3-undefined"
|
key="Bchild1-3-undefined"
|
||||||
node={
|
node={
|
||||||
@@ -623,7 +618,7 @@ exports[`LegacyTreeNodeComponent renders unsorted children by default 1`] = `
|
|||||||
}
|
}
|
||||||
paddingLeft={32}
|
paddingLeft={32}
|
||||||
/>
|
/>
|
||||||
<LegacyTreeNodeComponent
|
<TreeNodeComponent
|
||||||
generation={3}
|
generation={3}
|
||||||
key="2child2-3-undefined"
|
key="2child2-3-undefined"
|
||||||
node={
|
node={
|
||||||