mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-27 21:01:57 +00:00
Compare commits
242 Commits
2819674
...
users/lang
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04c01256a6 | ||
|
|
1795b8e2e9 | ||
|
|
4296b5ae02 | ||
|
|
e8a5658799 | ||
|
|
b4973e8367 | ||
|
|
e23ba02561 | ||
|
|
4b207f3fa6 | ||
|
|
c5b7f599b3 | ||
|
|
6aeac542b1 | ||
|
|
0d22d4ab4d | ||
|
|
0658448b54 | ||
|
|
85352b74a3 | ||
|
|
26645f8360 | ||
|
|
777b695051 | ||
|
|
1f300e32fe | ||
|
|
e81408560e | ||
|
|
ed1e2990d0 | ||
|
|
5e92a0c5d7 | ||
|
|
26b6de4c53 | ||
|
|
f308cabeaa | ||
|
|
e5a82fd356 | ||
|
|
4778183e50 | ||
|
|
b1d9570a95 | ||
|
|
833d677d20 | ||
|
|
2397283649 | ||
|
|
905aa26f27 | ||
|
|
a2556dad06 | ||
|
|
c9398e303b | ||
|
|
038142c180 | ||
|
|
94d3fcb30f | ||
|
|
d3722f2c99 | ||
|
|
5a5e155205 | ||
|
|
9d4a9c0601 | ||
|
|
2226169a71 | ||
|
|
6f35fb5526 | ||
|
|
805a4ae168 | ||
|
|
cc89691da3 | ||
|
|
24860a6842 | ||
|
|
bf6b362610 | ||
|
|
baca7922b4 | ||
|
|
b59ba20ed0 | ||
|
|
7f55de7aa2 | ||
|
|
62c76cc264 | ||
|
|
99d95a4cec | ||
|
|
647cca09b3 | ||
|
|
2c5f4e9666 | ||
|
|
58ae64193f | ||
|
|
1e10273510 | ||
|
|
c141e2612b | ||
|
|
7a179ff34a | ||
|
|
806a0657df | ||
|
|
bc479fb808 | ||
|
|
31773ee73b | ||
|
|
3d1f280378 | ||
|
|
2ef036ee94 | ||
|
|
77c758714d | ||
|
|
bcd8b7229f | ||
|
|
0a1d16de1b | ||
|
|
1e6c40eabf | ||
|
|
70d1dc6f74 | ||
|
|
d07d2c7c0d | ||
|
|
7a1aa89cd1 | ||
|
|
e67c3f6774 | ||
|
|
bd334a118a | ||
|
|
4e71e340e3 | ||
|
|
9efbe7d056 | ||
|
|
ea2ab19518 | ||
|
|
5871c1e2d0 | ||
|
|
5d59c47979 | ||
|
|
fa460bfba2 | ||
|
|
81dccbe5be | ||
|
|
49c3d0f0cb | ||
|
|
375bb5f567 | ||
|
|
e9f83a8efd | ||
|
|
093ddba2db | ||
|
|
dfe79b20f5 | ||
|
|
1021e9c969 | ||
|
|
c30a9681fe | ||
|
|
17754cba05 | ||
|
|
b07fa89a23 | ||
|
|
f1dcf1c548 | ||
|
|
88f38d6522 | ||
|
|
28db549fa1 | ||
|
|
658e2ffe85 | ||
|
|
bea3aa8b55 | ||
|
|
ce0cfed128 | ||
|
|
fe892dcc62 | ||
|
|
380caba5f5 | ||
|
|
c0a79c1e67 | ||
|
|
9945304e18 | ||
|
|
0ce9acdfdf | ||
|
|
62ab0e3e60 | ||
|
|
b096fa9bf8 | ||
|
|
d199311633 | ||
|
|
bf225f91c4 | ||
|
|
4d0b1a6db8 | ||
|
|
e66c8a1b5c | ||
|
|
7e1a738f8e | ||
|
|
dabb91e9e9 | ||
|
|
55df5cb121 | ||
|
|
7570d6b91d | ||
|
|
b8d6a0188a | ||
|
|
e36853c100 | ||
|
|
8c25742304 | ||
|
|
1ba3a6c761 | ||
|
|
996f785aac | ||
|
|
6c67f3b2e5 | ||
|
|
c680481fe0 | ||
|
|
1ee79881ef | ||
|
|
06d4829422 | ||
|
|
416743c548 | ||
|
|
b5d4509d49 | ||
|
|
417ef899f0 | ||
|
|
736731474f | ||
|
|
9b12775151 | ||
|
|
7002da0b51 | ||
|
|
7c5fb1b697 | ||
|
|
06e28ae3e7 | ||
|
|
52c2cfe419 | ||
|
|
b76d83d8e1 | ||
|
|
495296602a | ||
|
|
96ba0a9729 | ||
|
|
6276464e0d | ||
|
|
98c5fe65e6 | ||
|
|
cebf044803 | ||
|
|
f669a99228 | ||
|
|
36736882ee | ||
|
|
19d1e0d1df | ||
|
|
ceeead8458 | ||
|
|
4da3363cf7 | ||
|
|
ff4bc78d6c | ||
|
|
b6e3e5ea1c | ||
|
|
9e9d270b65 | ||
|
|
f56e5e64b9 | ||
|
|
14e5efcebf | ||
|
|
5c3f18f5f8 | ||
|
|
6ebc48ad28 | ||
|
|
298197b1b8 | ||
|
|
81a5b7cb6d | ||
|
|
b023250e67 | ||
|
|
92246144f7 | ||
|
|
a08415e7bc | ||
|
|
b94ce28e96 | ||
|
|
f8f7ea34bd | ||
|
|
cbd5e6bf76 | ||
|
|
618c5ec0fe | ||
|
|
afc82845b5 | ||
|
|
f4bcee5461 | ||
|
|
17207624a9 | ||
|
|
d36e511b18 | ||
|
|
c1a28793ba | ||
|
|
acf5acfdb4 | ||
|
|
7b81767ded | ||
|
|
c12eced120 | ||
|
|
2b15a4d43d | ||
|
|
c220a8b070 | ||
|
|
a5a5a95973 | ||
|
|
e3fab9b5bf | ||
|
|
98000a27f0 | ||
|
|
af664326ea | ||
|
|
a44ed1f45c | ||
|
|
e0cb3da6aa | ||
|
|
6c9673975a | ||
|
|
d35e2a325e | ||
|
|
00a816c488 | ||
|
|
953bef404b | ||
|
|
dfcb771939 | ||
|
|
6925fa8e4e | ||
|
|
7f6338b68b | ||
|
|
db50f42832 | ||
|
|
f533eeb0fc | ||
|
|
3c5d899e47 | ||
|
|
b44778b00a | ||
|
|
1464745659 | ||
|
|
18cc2a4195 | ||
|
|
86f2bc171f | ||
|
|
cabedf4a29 | ||
|
|
5aa6b0abe1 | ||
|
|
f24b0bcf1b | ||
|
|
56408a97d7 | ||
|
|
0df68c4967 | ||
|
|
e09930d9d0 | ||
|
|
da2e874ae6 | ||
|
|
a524138ac9 | ||
|
|
39b0fb9e2c | ||
|
|
ac22e88d9c | ||
|
|
91d9e27049 | ||
|
|
f881f7fd2f | ||
|
|
4c74525b5d | ||
|
|
1a6d8d5357 | ||
|
|
56c0049e9a | ||
|
|
b3837a089d | ||
|
|
e68aaebca6 | ||
|
|
6e1c4fd037 | ||
|
|
0039adf1c2 | ||
|
|
5d4e9d82bb | ||
|
|
47bdc9c426 | ||
|
|
b8457e3bf9 | ||
|
|
533e9c887c | ||
|
|
76ad930930 | ||
|
|
932f211038 | ||
|
|
b480c635ca | ||
|
|
16eb096fdb | ||
|
|
e9571c0f2d | ||
|
|
c9abcc1728 | ||
|
|
a36f3f7922 | ||
|
|
5a64fc2582 | ||
|
|
12366bb645 | ||
|
|
9cebe5f9ba | ||
|
|
5d80ecb462 | ||
|
|
f87611a39d | ||
|
|
a914fd020c | ||
|
|
e43b4eee5c | ||
|
|
8b0b3b07d6 | ||
|
|
f403b086ad | ||
|
|
f8cfc6c21c | ||
|
|
35ca7944ae | ||
|
|
6d98b4a500 | ||
|
|
2d06eef9cc | ||
|
|
31f7178669 | ||
|
|
c0b54f6e84 | ||
|
|
cd3eb5b5b3 | ||
|
|
dbb0324a64 | ||
|
|
e207f3702b | ||
|
|
f496220ed6 | ||
|
|
eb790d09b5 | ||
|
|
323305e485 | ||
|
|
70635e426f | ||
|
|
5a5bf34d4d | ||
|
|
0975591945 | ||
|
|
5d13463bdb | ||
|
|
c4cceceafc | ||
|
|
532a453f5a | ||
|
|
9355a3ae04 | ||
|
|
14456c2102 | ||
|
|
0c9264e8b3 | ||
|
|
0dd1032357 | ||
|
|
5011d12f16 | ||
|
|
a7e5ff2a9f | ||
|
|
ad1391f623 | ||
|
|
a2a5407b15 | ||
|
|
e9181f19d7 |
10
.editorconfig
Normal file
10
.editorconfig
Normal file
@@ -0,0 +1,10 @@
|
||||
# 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,3 +1,5 @@
|
||||
playwright.config.ts
|
||||
|
||||
**/node_modules/
|
||||
src/**/__mocks__/**/*
|
||||
dist/
|
||||
@@ -89,10 +91,7 @@ src/Explorer/Tables/TableEntityProcessor.ts
|
||||
src/Explorer/Tables/Utilities.ts
|
||||
src/Explorer/Tabs/ConflictsTab.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/MongoDocumentsTab.ts
|
||||
src/Explorer/Tabs/NotebookV2Tab.ts
|
||||
src/Explorer/Tabs/ScriptTabBase.ts
|
||||
src/Explorer/Tabs/TabComponents.ts
|
||||
@@ -128,7 +127,7 @@ src/Explorer/Controls/InputTypeahead/InputTypeaheadComponent.tsx
|
||||
src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx
|
||||
src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx
|
||||
src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx
|
||||
src/Explorer/Controls/TreeComponent/TreeComponent.tsx
|
||||
src/Explorer/Controls/TreeComponent/LegacyTreeComponent.tsx
|
||||
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx
|
||||
src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx
|
||||
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx
|
||||
@@ -145,4 +144,5 @@ src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx
|
||||
src/Explorer/Tree/ResourceTreeAdapter.tsx
|
||||
__mocks__/monaco-editor.ts
|
||||
src/Explorer/Tree/ResourceTree.tsx
|
||||
src/Utils/EndpointUtils.ts
|
||||
src/Utils/PriorityBasedExecutionUtils.ts
|
||||
136
.github/workflows/ci.yml
vendored
136
.github/workflows/ci.yml
vendored
@@ -8,6 +8,9 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
jobs:
|
||||
codemetrics:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -101,72 +104,6 @@ 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
|
||||
env:
|
||||
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@v4
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.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@v3
|
||||
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@v4
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.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@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: screenshots
|
||||
path: screenshots/
|
||||
nuget:
|
||||
name: Publish Nuget
|
||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||
@@ -216,3 +153,70 @@ jobs:
|
||||
name: packages
|
||||
with:
|
||||
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
|
||||
15
.github/workflows/cleanup.yml
vendored
15
.github/workflows/cleanup.yml
vendored
@@ -9,6 +9,10 @@ on:
|
||||
# Once every hour
|
||||
- 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
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
@@ -16,10 +20,17 @@ jobs:
|
||||
name: "Cleanup Test Database Accounts"
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
|
||||
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- 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
|
||||
with:
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -16,4 +16,8 @@ Contracts/*
|
||||
.env
|
||||
failure.png
|
||||
screenshots/*
|
||||
GettingStarted-ignore*.ipynb
|
||||
GettingStarted-ignore*.ipynb
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
5
.npmrc
5
.npmrc
@@ -1 +1,4 @@
|
||||
save-exact=true
|
||||
save-exact=true
|
||||
|
||||
# Ignore peer dependency conflicts
|
||||
force=true # TODO: Remove this when we update to React 17 or higher!
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -20,8 +20,8 @@
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true,
|
||||
"source.organizeImports": true
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "explicit"
|
||||
},
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# Why?
|
||||
|
||||
This adds a mock module for `canvas`. Nteract has a ignored require and undeclared dependency on this module. `cavnas` is a server side node module and is not used in browser side code for nteract.
|
||||
|
||||
Installing it locally (`npm install canvas`) will resolve the problem, but it is a native module so it is flaky depending on the system, node version, processor arch, etc. This module provides a simpler, more robust solution.
|
||||
|
||||
Remove this workaround if [this bug](https://github.com/nteract/any-vega/issues/2) ever gets resolved
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = {}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"name": "canvas",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com",
|
||||
"isTerminalEnabled" : true,
|
||||
"isPhoenixEnabled" : true
|
||||
}
|
||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
||||
"isTerminalEnabled": true,
|
||||
"isPhoenixEnabled": true
|
||||
}
|
||||
|
||||
9
images/EntraID.svg
Normal file
9
images/EntraID.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
3
images/Home_16.svg
Normal file
3
images/Home_16.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 968 B |
@@ -1,13 +0,0 @@
|
||||
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/"],
|
||||
|
||||
// A list of reporter names that Jest uses when writing coverage reports
|
||||
coverageReporters: ["json", "text", "cobertura"],
|
||||
coverageReporters: ["json", "text", "cobertura", "lcov"],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
coverageThreshold: {
|
||||
@@ -76,6 +76,11 @@ module.exports = {
|
||||
"^dnd-core$": "dnd-core/dist/cjs",
|
||||
"^react-dnd$": "react-dnd/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",
|
||||
uuid: require.resolve("uuid"), // Force module uuid to resolve with the CJS entry point, because Jest does not support package.json.exports. See https://github.com/uuidjs/uuid/issues/451
|
||||
},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
@@ -129,8 +134,7 @@ module.exports = {
|
||||
snapshotSerializers: ["enzyme-to-json/serializer"],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
// testEnvironment: "jest-environment-jsdom",
|
||||
|
||||
testEnvironment: "jsdom",
|
||||
modulePaths: ["node_modules", "<rootDir>/src"],
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
@@ -154,7 +158,7 @@ module.exports = {
|
||||
// testResultsProcessor: "./trxProcessor.js",
|
||||
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jasmine2",
|
||||
testRunner: "jest-circus/runner",
|
||||
|
||||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||
// testURL: "http://localhost",
|
||||
@@ -164,13 +168,13 @@ module.exports = {
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
transform: {
|
||||
"^.+\\.html?$": "html-loader-jest",
|
||||
"^.+\\.html?$": "jest-html-loader",
|
||||
"^.+\\.[t|j]sx?$": "babel-jest",
|
||||
"^.+\\.svg$": "<rootDir>/svgTransform.js",
|
||||
"^.+\\.svg$": "<rootDir>/jest/svgTransform.js",
|
||||
},
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
transformIgnorePatterns: ["/node_modules/", "/externals/"],
|
||||
transformIgnorePatterns: ["/node_modules/(?!@fluentui/react-icons)", "/externals/"],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
@@ -183,4 +187,7 @@ module.exports = {
|
||||
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
|
||||
// TODO: toMatchInlineSnapshot() does not work with prettier 3. Remove when fixed: https://github.com/jestjs/jest/issues/14305
|
||||
prettierPath: null,
|
||||
};
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
preset: "jest-playwright-preset",
|
||||
testMatch: ["<rootDir>/test/**/*.spec.[jt]s?(x)"],
|
||||
setupFiles: ["dotenv/config"],
|
||||
testEnvironment: "./test/playwrightEnv.js",
|
||||
setupFilesAfterEnv: ["expect-playwright"],
|
||||
};
|
||||
@@ -130,6 +130,7 @@
|
||||
@ActiveTabWidth: 141px;
|
||||
@TabsHeight: 30px;
|
||||
@TabsWidth: 140px;
|
||||
@ContentWrapper: 111px;
|
||||
@StatusIconContainerSize: 18px;
|
||||
@LoadingErrorIconSize: 14px;
|
||||
@ErrorIconContainer: 16px;
|
||||
@@ -147,6 +148,7 @@
|
||||
|
||||
// CommandBar
|
||||
@CommandBarButtonHeight: 40px;
|
||||
@FabricCommandBarButtonHeight: 34px;
|
||||
|
||||
/**********************************************************************************
|
||||
Portal Consts
|
||||
@@ -162,10 +164,11 @@
|
||||
/**********************************************************************************/
|
||||
|
||||
@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;
|
||||
@FabricBoxBorderShadow: 0 0 2px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.14);
|
||||
@FabricBoxMargin: 4px 3px 4px 3px;
|
||||
@FabricBoxBorderShadow: rgba(0, 0, 0, 0.133) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px 0px;
|
||||
@FabricBoxMargin: 4px 8px 4px 8px;
|
||||
|
||||
@FabricAccentMediumHigh: #0c695a;
|
||||
@FabricAccentMedium: #117865;
|
||||
@@ -333,4 +336,11 @@
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: @InfoPointerColor transparent;
|
||||
}
|
||||
/*********************************************************************************************************
|
||||
Screen Reader Only
|
||||
**********************************************************************************************************/
|
||||
.screenReaderOnly {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
}
|
||||
@@ -1906,8 +1906,14 @@ input::-webkit-calendar-picker-indicator::after {
|
||||
}
|
||||
|
||||
.nav-tabs-margin {
|
||||
padding-top: 8px;
|
||||
height: 32px;
|
||||
background-color: #f2f2f2;
|
||||
|
||||
.nav-tabs {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.navTabHeight {
|
||||
@@ -2074,14 +2080,6 @@ a:link {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.resourceTreeAndTabs {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
overflow-x: clip;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.collectiontitle {
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
@@ -2264,38 +2262,49 @@ a:link {
|
||||
width: 82px;
|
||||
}
|
||||
|
||||
.tabdocuments .scrollable {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
// .tabdocuments .scrollable {
|
||||
// height: 100%;
|
||||
// overflow-y: auto;
|
||||
// overflow-x: hidden;
|
||||
// padding-left: 5px;
|
||||
// padding-right: 5px;
|
||||
// width: 100%;
|
||||
// }
|
||||
|
||||
.tabdocuments > .tabdocumentsGridElement {
|
||||
width: 50%;
|
||||
}
|
||||
// .tabdocuments > .tabdocumentsGridElement {
|
||||
// width: 50%;
|
||||
// }
|
||||
|
||||
.tabdocuments > .evenlySpacedHeader {
|
||||
width: 30%;
|
||||
}
|
||||
// .tabdocuments > .evenlySpacedHeader {
|
||||
// width: 30%;
|
||||
// }
|
||||
|
||||
.tabdocuments.scrollable:focus,
|
||||
.tabdocuments.scrollable:active {
|
||||
outline: 1px dotted;
|
||||
}
|
||||
// .tabdocuments.scrollable:focus,
|
||||
// .tabdocuments.scrollable:active {
|
||||
// outline: 1px dotted;
|
||||
// }
|
||||
|
||||
.tabdocuments .scrollable table td {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
// .tabdocuments .scrollable table td {
|
||||
// white-space: nowrap;
|
||||
// overflow: hidden;
|
||||
// text-overflow: ellipsis;
|
||||
// }
|
||||
|
||||
.mongoDocumentEditor .monaco-editor.vs .redsquiggly {
|
||||
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 {
|
||||
color: #393939;
|
||||
}
|
||||
@@ -2305,21 +2314,15 @@ td a:hover {
|
||||
}
|
||||
|
||||
.loadMore {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding-left: 30%;
|
||||
padding-top: 2px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loadMore > a:focus {
|
||||
outline: 1px dotted;
|
||||
}
|
||||
|
||||
#content.active .tabdocuments .scrollable {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.table-fixed thead {
|
||||
width: 97%;
|
||||
padding-left: 18px;
|
||||
@@ -2355,10 +2358,9 @@ a:link {
|
||||
|
||||
.tabsManagerContainer {
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
min-height: 300px;
|
||||
overflow-y: scroll;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0; // This prevents it to grow past the parent's width if its content is too wide
|
||||
}
|
||||
|
||||
.tabs {
|
||||
@@ -2547,10 +2549,12 @@ a:link {
|
||||
}
|
||||
|
||||
.filterdivs {
|
||||
padding-top: 15px;
|
||||
height: 45px;
|
||||
margin-bottom: 8px;
|
||||
margin: 10px 0px;
|
||||
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 {
|
||||
@@ -2613,7 +2617,7 @@ a:link {
|
||||
|
||||
.tabPanesContainer {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -2647,7 +2651,7 @@ a:link {
|
||||
width: @ActiveTabWidth;
|
||||
}
|
||||
|
||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .tabNavText {
|
||||
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText {
|
||||
font-weight: bolder;
|
||||
border-bottom: 2px solid rgba(0, 120, 212, 1);
|
||||
}
|
||||
@@ -2683,67 +2687,71 @@ a:link {
|
||||
width: @TabsWidth;
|
||||
border-right: @ButtonBorderWidth solid @BaseMedium;
|
||||
white-space: nowrap;
|
||||
.contentWrapper {
|
||||
.flex-display();
|
||||
width: @ContentWrapper;
|
||||
|
||||
.statusIconContainer {
|
||||
width: @StatusIconContainerSize;
|
||||
height: @StatusIconContainerSize;
|
||||
margin-left: @SmallSpace;
|
||||
display: inline-flex;
|
||||
.statusIconContainer {
|
||||
width: @StatusIconContainerSize;
|
||||
height: @StatusIconContainerSize;
|
||||
margin-left: @SmallSpace;
|
||||
display: inline-flex;
|
||||
|
||||
.errorIconContainer {
|
||||
width: @ErrorIconContainer;
|
||||
height: @ErrorIconContainer;
|
||||
margin-top: 1px;
|
||||
.errorIconContainer {
|
||||
width: @ErrorIconContainer;
|
||||
height: @ErrorIconContainer;
|
||||
margin-top: 1px;
|
||||
|
||||
.errorIcon {
|
||||
width: @ErrorIconWidth;
|
||||
.errorIcon {
|
||||
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;
|
||||
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;
|
||||
margin: 0px 0px @SmallSpace @SmallSpace;
|
||||
}
|
||||
}
|
||||
|
||||
.errorIconContainer.actionsEnabled {
|
||||
&:hover {
|
||||
.hover();
|
||||
}
|
||||
|
||||
&:focus {
|
||||
.focus();
|
||||
}
|
||||
|
||||
&:active {
|
||||
.active();
|
||||
}
|
||||
.tabNavText {
|
||||
margin-left: @SmallSpace;
|
||||
margin-right: 2px;
|
||||
color: @BaseDark;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.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 {
|
||||
width: 30px;
|
||||
width: 29px;
|
||||
position: relative;
|
||||
padding-top: 2px;
|
||||
|
||||
@@ -2897,9 +2905,21 @@ a:link {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.settingsSectionInlineCheckbox {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.settingsSectionLabel {
|
||||
margin-bottom: @DefaultSpace;
|
||||
margin-right: 5px;
|
||||
|
||||
.panelInfoIcon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.pageOptionsPart {
|
||||
|
||||
@@ -25,33 +25,38 @@ a:focus {
|
||||
}
|
||||
|
||||
.resourceTreeAndTabs {
|
||||
border-radius: @FabricBoxBorderRadius;
|
||||
border-radius: 0px;
|
||||
box-shadow: @FabricBoxBorderShadow;
|
||||
margin: @FabricBoxMargin;
|
||||
margin-top: 4px;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.tabsManagerContainer {
|
||||
background-color: #fafafa
|
||||
background-color: #ffffff
|
||||
}
|
||||
|
||||
.nav-tabs-margin {
|
||||
padding-top: 8px;
|
||||
background-color: #fafafa
|
||||
padding-top: 5px;
|
||||
background-color: #ffffff
|
||||
}
|
||||
|
||||
.commandBarContainer {
|
||||
background-color: #ffffff;
|
||||
border-bottom: none;
|
||||
border-radius: @FabricBoxBorderRadius;
|
||||
border-radius: @FabricBoxBorderRadius @FabricBoxBorderRadius 0px 0px;
|
||||
box-shadow: @FabricBoxBorderShadow;
|
||||
margin: @FabricBoxMargin;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
padding-top: 2px;
|
||||
padding: 0px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.dividerContainer {
|
||||
padding: @SmallSpace 0px @SmallSpace 0px;
|
||||
height: @FabricCommandBarButtonHeight;
|
||||
.flex-display();
|
||||
|
||||
span {
|
||||
@@ -70,7 +75,7 @@ a:focus {
|
||||
border-bottom: 2px solid @FabricAccentMedium;
|
||||
}
|
||||
|
||||
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content>.tabNavText {
|
||||
.nav-tabs>li.active>.tabNavContentContainer>.tab_Content>.contentWrapper>.tabNavText {
|
||||
border-bottom: 0px none transparent;
|
||||
}
|
||||
|
||||
@@ -88,9 +93,11 @@ a:focus {
|
||||
width: calc(@TabsWidth - (@SmallSpace * 2));
|
||||
padding-bottom: @SmallSpace;
|
||||
|
||||
.statusIconContainer {
|
||||
margin-left: 0px;
|
||||
}
|
||||
.contentWrapper {
|
||||
.statusIconContainer {
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.tabIconSection {
|
||||
.cancelButton {
|
||||
@@ -158,9 +165,10 @@ a:focus {
|
||||
|
||||
|
||||
.dataExplorerErrorConsoleContainer {
|
||||
border-radius: @FabricBoxBorderRadius;
|
||||
border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius;
|
||||
box-shadow: @FabricBoxBorderShadow;
|
||||
margin: @FabricBoxMargin;
|
||||
margin-top: 0px;
|
||||
width: auto;
|
||||
align-self: auto;
|
||||
}
|
||||
|
||||
@@ -3,19 +3,6 @@
|
||||
.dataResourceTree {
|
||||
margin-left: @MediumSpace;
|
||||
overflow: auto;
|
||||
|
||||
.databaseHeader {
|
||||
padding: 1px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.collectionHeader {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.loadMoreHeader {
|
||||
color: RGB(5, 99, 193);
|
||||
}
|
||||
}
|
||||
|
||||
.notebookResourceTree {
|
||||
|
||||
23948
package-lock.json
generated
23948
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
100
package.json
100
package.json
@@ -5,21 +5,21 @@
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/cosmos": "4.0.0",
|
||||
"@azure/cosmos": "4.0.1-beta.3",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@azure/identity": "1.2.1",
|
||||
"@azure/ms-rest-nodeauth": "3.0.7",
|
||||
"@azure/identity": "1.5.2",
|
||||
"@azure/ms-rest-nodeauth": "3.1.1",
|
||||
"@azure/msal-browser": "2.14.2",
|
||||
"@babel/plugin-proposal-class-properties": "7.12.1",
|
||||
"@babel/plugin-proposal-decorators": "7.12.12",
|
||||
"@fluentui/react": "8.112.1",
|
||||
"@fluentui/react-components": "9.34.0",
|
||||
"@fluentui/react": "8.119.0",
|
||||
"@fluentui/react-components": "9.54.2",
|
||||
"@jupyterlab/services": "6.0.2",
|
||||
"@jupyterlab/terminal": "3.0.3",
|
||||
"@microsoft/applicationinsights-web": "2.6.1",
|
||||
"@nteract/commutable": "7.5.1",
|
||||
"@nteract/connected-components": "6.8.2",
|
||||
"@nteract/core": "15.1.0",
|
||||
"@nteract/core": "15.1.9",
|
||||
"@nteract/data-explorer": "8.0.3",
|
||||
"@nteract/directory-listing": "2.0.6",
|
||||
"@nteract/dropdown-menu": "1.0.1",
|
||||
@@ -42,21 +42,23 @@
|
||||
"@nteract/transform-vega": "7.0.6",
|
||||
"@octokit/rest": "17.9.2",
|
||||
"@phosphor/widgets": "1.9.3",
|
||||
"@testing-library/jest-dom": "5.11.9",
|
||||
"@testing-library/jest-dom": "6.4.6",
|
||||
"@types/lodash": "4.14.171",
|
||||
"@types/mkdirp": "1.0.1",
|
||||
"@types/node-fetch": "2.5.7",
|
||||
"@xmldom/xmldom": "0.7.13",
|
||||
"allotment": "1.20.2",
|
||||
"applicationinsights": "1.8.0",
|
||||
"bootstrap": "3.4.1",
|
||||
"canvas": "file:./canvas",
|
||||
"canvas": "2.11.2",
|
||||
"clean-webpack-plugin": "4.0.0",
|
||||
"clipboard-copy": "4.0.1",
|
||||
"copy-webpack-plugin": "11.0.0",
|
||||
"crossroads": "0.12.2",
|
||||
"css-element-queries": "1.1.1",
|
||||
"d3": "6.1.1",
|
||||
"datatables.net-colreorder-dt": "1.5.1",
|
||||
"datatables.net-dt": "1.10.19",
|
||||
"d3": "7.8.5",
|
||||
"datatables.net-colreorder-dt": "1.7.0",
|
||||
"datatables.net-dt": "1.13.8",
|
||||
"date-fns": "1.29.0",
|
||||
"dayjs": "1.8.19",
|
||||
"dom-to-image": "2.6.0",
|
||||
@@ -65,20 +67,23 @@
|
||||
"eslint-plugin-react": "7.33.2",
|
||||
"hasher": "1.2.0",
|
||||
"html2canvas": "1.0.0-rc.5",
|
||||
"i18next": "19.8.4",
|
||||
"i18next": "23.11.5",
|
||||
"i18next-browser-languagedetector": "6.0.1",
|
||||
"i18next-http-backend": "1.0.23",
|
||||
"iframe-resizer-react": "1.1.0",
|
||||
"immer": "9.0.6",
|
||||
"immutable": "4.0.0-rc.12",
|
||||
"is-ci": "2.0.0",
|
||||
"jquery": "3.5.1",
|
||||
"jquery-typeahead": "2.10.6",
|
||||
"jquery-ui-dist": "1.12.1",
|
||||
"jquery": "3.7.1",
|
||||
"jquery-typeahead": "2.11.1",
|
||||
"jquery-ui-dist": "1.13.2",
|
||||
"knockout": "3.5.1",
|
||||
"loader-utils": "2.0.3",
|
||||
"mkdirp": "1.0.4",
|
||||
"monaco-editor": "0.44.0",
|
||||
"ms": "2.1.3",
|
||||
"p-retry": "4.6.2",
|
||||
"patch-package": "8.0.0",
|
||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||
"post-robot": "10.0.42",
|
||||
"q": "1.5.1",
|
||||
@@ -88,45 +93,53 @@
|
||||
"react-dnd-html5-backend": "14.0.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-hotkeys": "2.0.0",
|
||||
"react-i18next": "11.8.5",
|
||||
"react-i18next": "14.1.2",
|
||||
"react-notification-system": "0.2.17",
|
||||
"react-redux": "7.1.3",
|
||||
"react-splitter-layout": "4.0.0",
|
||||
"react-string-format": "1.0.1",
|
||||
"react-window": "1.8.10",
|
||||
"react-youtube": "9.0.1",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rx-jupyter": "5.5.12",
|
||||
"sanitize-html": "2.3.3",
|
||||
"shell-quote": "1.7.3",
|
||||
"styled-components": "5.0.1",
|
||||
"swr": "0.4.0",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
"underscore": "1.9.1",
|
||||
"tinykeys": "2.1.0",
|
||||
"underscore": "1.12.1",
|
||||
"utility-types": "3.10.0",
|
||||
"zustand": "3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.9.0",
|
||||
"@babel/preset-env": "7.9.0",
|
||||
"@babel/preset-react": "7.9.4",
|
||||
"@babel/preset-typescript": "7.9.0",
|
||||
"@babel/core": "7.24.7",
|
||||
"@babel/preset-env": "7.24.7",
|
||||
"@babel/preset-react": "7.24.7",
|
||||
"@babel/preset-typescript": "7.24.7",
|
||||
"@playwright/test": "1.44.0",
|
||||
"@testing-library/react": "11.2.3",
|
||||
"@types/applicationinsights-js": "1.0.7",
|
||||
"@types/codemirror": "0.0.56",
|
||||
"@types/crossroads": "0.0.30",
|
||||
"@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/enzyme": "3.10.7",
|
||||
"@types/enzyme-adapter-react-16": "1.0.6",
|
||||
"@types/enzyme": "3.10.12",
|
||||
"@types/enzyme-adapter-react-16": "1.0.9",
|
||||
"@types/hasher": "0.0.31",
|
||||
"@types/jest": "26.0.20",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/jquery": "3.5.29",
|
||||
"@types/node": "12.11.1",
|
||||
"@types/post-robot": "10.0.1",
|
||||
"@types/q": "1.5.1",
|
||||
"@types/react": "17.0.3",
|
||||
"@types/react-dom": "17.0.3",
|
||||
"@types/react": "17.0.44",
|
||||
"@types/react-dom": "17.0.15",
|
||||
"@types/react-notification-system": "0.2.39",
|
||||
"@types/react-redux": "7.1.7",
|
||||
"@types/react-splitter-layout": "3.0.1",
|
||||
"@types/react-window": "1.8.8",
|
||||
"@types/sanitize-html": "1.27.2",
|
||||
"@types/sinon": "2.3.3",
|
||||
"@types/styled-components": "5.1.1",
|
||||
@@ -135,58 +148,58 @@
|
||||
"@typescript-eslint/eslint-plugin": "6.7.4",
|
||||
"@typescript-eslint/parser": "6.7.4",
|
||||
"@webpack-cli/serve": "2.0.5",
|
||||
"babel-jest": "24.9.0",
|
||||
"babel-jest": "29.7.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"buffer": "5.1.0",
|
||||
"case-sensitive-paths-webpack-plugin": "2.4.0",
|
||||
"create-file-webpack": "1.0.2",
|
||||
"css-loader": "6.8.1",
|
||||
"enzyme": "3.11.0",
|
||||
"enzyme-adapter-react-16": "1.15.5",
|
||||
"enzyme-to-json": "3.6.1",
|
||||
"enzyme-adapter-react-16": "1.15.8",
|
||||
"enzyme-to-json": "3.6.2",
|
||||
"eslint": "8.50.0",
|
||||
"eslint-cli": "1.1.1",
|
||||
"eslint-plugin-no-null": "1.0.2",
|
||||
"eslint-plugin-prefer-arrow": "1.2.3",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"expect-playwright": "0.3.3",
|
||||
"fast-glob": "3.2.5",
|
||||
"fs-extra": "7.0.0",
|
||||
"html-inline-css-webpack-plugin": "1.11.2",
|
||||
"html-loader": "0.5.5",
|
||||
"html-loader-jest": "0.2.1",
|
||||
"html-loader": "5.0.0",
|
||||
"html-webpack-plugin": "5.5.3",
|
||||
"jest": "26.6.3",
|
||||
"jest-canvas-mock": "2.3.1",
|
||||
"jest-playwright-preset": "1.5.1",
|
||||
"jest": "29.7.0",
|
||||
"jest-canvas-mock": "2.5.2",
|
||||
"jest-circus": "29.7.0",
|
||||
"jest-html-loader": "1.0.0",
|
||||
"jest-react-hooks-shallow": "1.5.1",
|
||||
"jest-trx-results-processor": "0.0.7",
|
||||
"jest-trx-results-processor": "3.0.2",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"less": "3.8.1",
|
||||
"less-loader": "11.1.3",
|
||||
"less-vars-loader": "1.1.0",
|
||||
"mini-css-extract-plugin": "2.1.0",
|
||||
"monaco-editor-webpack-plugin": "7.1.0",
|
||||
"node-fetch": "2.6.1",
|
||||
"playwright": "1.13.0",
|
||||
"node-fetch": "2.6.7",
|
||||
"prettier": "3.0.3",
|
||||
"process": "0.11.10",
|
||||
"querystring-es3": "0.2.1",
|
||||
"raw-loader": "0.5.1",
|
||||
"react-dev-utils": "11.0.4",
|
||||
"react-dev-utils": "12.0.1",
|
||||
"rimraf": "3.0.0",
|
||||
"sinon": "3.2.1",
|
||||
"style-loader": "0.23.0",
|
||||
"ts-loader": "9.2.4",
|
||||
"typedoc": "0.21.5",
|
||||
"typescript": "4.3.5",
|
||||
"typedoc": "0.26.2",
|
||||
"typescript": "4.9.5",
|
||||
"url-loader": "4.1.1",
|
||||
"wait-on": "4.0.2",
|
||||
"webpack": "5.88.2",
|
||||
"webpack-bundle-analyzer": "4.9.1",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "4.15.1"
|
||||
"webpack-dev-server": "4.15.2"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "patch-package",
|
||||
"start": "webpack serve --mode development",
|
||||
"dev": "echo \"WARNING: npm run dev has been deprecated\" && npm run build",
|
||||
"build:dataExplorer:ci": "npm run build:ci",
|
||||
@@ -198,6 +211,7 @@
|
||||
"test": "rimraf coverage && jest",
|
||||
"test:debug": "jest --runInBand",
|
||||
"test:e2e": "jest -c ./jest.config.playwright.js --detectOpenHandles",
|
||||
"test:file": "jest --coverage=false",
|
||||
"watch": "npm run start",
|
||||
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
|
||||
"build:ase": "gulp build:ase",
|
||||
@@ -233,4 +247,4 @@
|
||||
"printWidth": 120,
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
}
|
||||
}
|
||||
13
patches/@phosphor+virtualdom+1.2.0.patch
Normal file
13
patches/@phosphor+virtualdom+1.2.0.patch
Normal file
@@ -0,0 +1,13 @@
|
||||
diff --git a/node_modules/@phosphor/virtualdom/lib/index.d.ts b/node_modules/@phosphor/virtualdom/lib/index.d.ts
|
||||
index 95682b9..73e2daa 100644
|
||||
--- a/node_modules/@phosphor/virtualdom/lib/index.d.ts
|
||||
+++ b/node_modules/@phosphor/virtualdom/lib/index.d.ts
|
||||
@@ -58,7 +58,7 @@ export declare type ElementEventMap = {
|
||||
ondrop: DragEvent;
|
||||
ondurationchange: Event;
|
||||
onemptied: Event;
|
||||
- onended: MediaStreamErrorEvent;
|
||||
+ onended: ErrorEvent;
|
||||
onerror: ErrorEvent;
|
||||
onfocus: FocusEvent;
|
||||
oninput: Event;
|
||||
22
patches/datatables.net-colreorder+1.7.0.patch
Normal file
22
patches/datatables.net-colreorder+1.7.0.patch
Normal file
@@ -0,0 +1,22 @@
|
||||
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);
|
||||
|
||||
/**
|
||||
60
playwright.config.ts
Normal file
60
playwright.config.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
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: {
|
||||
trace: "off",
|
||||
video: "off",
|
||||
screenshot: "on",
|
||||
testIdAttribute: "data-test",
|
||||
contextOptions: {
|
||||
ignoreHTTPSErrors: true,
|
||||
},
|
||||
},
|
||||
|
||||
expect: {
|
||||
// Many of our expectations take a little longer than the default 5 seconds.
|
||||
timeout: 15 * 1000,
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
{
|
||||
name: "firefox",
|
||||
use: { ...devices["Desktop Firefox"] },
|
||||
},
|
||||
{
|
||||
name: "webkit",
|
||||
use: { ...devices["Desktop Safari"] },
|
||||
},
|
||||
/* Test against branded browsers. */
|
||||
{
|
||||
name: "Google Chrome",
|
||||
use: { ...devices["Desktop Chrome"], channel: "chrome" }, // or 'chrome-beta'
|
||||
},
|
||||
{
|
||||
name: "Microsoft Edge",
|
||||
use: { ...devices["Desktop Edge"], channel: "msedge" }, // or 'msedge-dev'
|
||||
},
|
||||
],
|
||||
|
||||
webServer: {
|
||||
command: "npm run start",
|
||||
url: "https://127.0.0.1:1234/_ready",
|
||||
timeout: 120 * 1000,
|
||||
ignoreHTTPSErrors: true,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
|
||||
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
||||
import { getApiShortDisplayName } from "../Utils/APITypeUtils";
|
||||
import { NormalizedEventKey } from "./Constants";
|
||||
|
||||
export interface CollapsedResourceTreeProps {
|
||||
toggleLeftPaneExpanded: () => void;
|
||||
isLeftPaneExpanded: boolean;
|
||||
}
|
||||
|
||||
export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps> = ({
|
||||
toggleLeftPaneExpanded,
|
||||
isLeftPaneExpanded,
|
||||
}: CollapsedResourceTreeProps): JSX.Element => {
|
||||
const focusButton = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
|
||||
|
||||
useEffect(() => {
|
||||
if (focusButton.current) {
|
||||
focusButton.current.focus();
|
||||
}
|
||||
});
|
||||
|
||||
const onKeyPressToggleLeftPaneExpanded = (event: React.KeyboardEvent) => {
|
||||
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
|
||||
toggleLeftPaneExpanded();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div id="mini" className={!isLeftPaneExpanded ? "mini toggle-mini" : "hiddenMain"}>
|
||||
<div className="main-nav nav">
|
||||
<ul className="nav">
|
||||
<li
|
||||
className="resourceTreeCollapse"
|
||||
id="collapseToggleLeftPaneButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={getApiShortDisplayName() + `Expand tree`}
|
||||
onClick={toggleLeftPaneExpanded}
|
||||
onKeyPress={onKeyPressToggleLeftPaneExpanded}
|
||||
ref={focusButton}
|
||||
>
|
||||
<span className="leftarrowCollapsed">
|
||||
<img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" />
|
||||
</span>
|
||||
<span className="collectionCollapsed">
|
||||
<span>{getApiShortDisplayName()}</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -88,6 +88,12 @@ export class CapabilityNames {
|
||||
public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics";
|
||||
public static readonly EnableMongo: string = "EnableMongo";
|
||||
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
|
||||
@@ -124,7 +130,39 @@ export enum MongoBackendEndpointType {
|
||||
remote,
|
||||
}
|
||||
|
||||
// TODO: 435619 Add default endpoints per cloud and use regional only when available
|
||||
export class BackendApi {
|
||||
public static readonly GenerateToken: string = "GenerateToken";
|
||||
public static readonly PortalSettings: string = "PortalSettings";
|
||||
public static readonly AccountRestrictions: string = "AccountRestrictions";
|
||||
public static readonly RuntimeProxy: string = "RuntimeProxy";
|
||||
public static readonly DisallowedLocations: string = "DisallowedLocations";
|
||||
}
|
||||
|
||||
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 {
|
||||
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
||||
public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete";
|
||||
@@ -136,6 +174,23 @@ export class CassandraBackend {
|
||||
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 AadEndpoints {
|
||||
public static readonly Prod: string = "https://login.microsoftonline.com/";
|
||||
public static readonly Fairfax: string = "https://login.microsoftonline.us/";
|
||||
public static readonly Mooncake: string = "https://login.partner.microsoftonline.cn/";
|
||||
}
|
||||
|
||||
export class Queries {
|
||||
public static CustomPageOption: string = "custom";
|
||||
public static UnlimitedPageOption: string = "unlimited";
|
||||
@@ -150,6 +205,12 @@ export class Queries {
|
||||
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 {
|
||||
public static readonly CollectionName: string = "___Query";
|
||||
public static readonly DatabaseName: string = "___Cosmos";
|
||||
@@ -209,6 +270,11 @@ export class HttpHeaders {
|
||||
public static partitionKey: string = "x-ms-documentdb-partitionkey";
|
||||
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 xAPIKey: string = "X-API-Key";
|
||||
}
|
||||
|
||||
export class ContentType {
|
||||
public static applicationJson: string = "application/json";
|
||||
}
|
||||
|
||||
export class ApiType {
|
||||
|
||||
@@ -1,47 +1,6 @@
|
||||
import { ResourceType } from "@azure/cosmos";
|
||||
import { Platform, resetConfigContext, updateConfigContext } from "../ConfigContext";
|
||||
import { updateUserContext } from "../UserContext";
|
||||
import { endpoint, getTokenFromAuthService, requestPlugin, tokenProvider } from "./CosmosClient";
|
||||
|
||||
describe("tokenProvider", () => {
|
||||
const options = {
|
||||
verb: "GET" as any,
|
||||
path: "/",
|
||||
resourceId: "",
|
||||
resourceType: "dbs" as ResourceType,
|
||||
headers: {},
|
||||
getAuthorizationTokenUsingMasterKey: () => "",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
});
|
||||
window.fetch = jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
json: () => "{}",
|
||||
headers: new Map(),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
import { endpoint, getTokenFromAuthService, requestPlugin } from "./CosmosClient";
|
||||
|
||||
describe("getTokenFromAuthService", () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import * as Cosmos from "@azure/cosmos";
|
||||
import { sendCachedDataMessage } from "Common/MessageHandler";
|
||||
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
|
||||
import { AuthorizationToken, MessageTypes } from "Contracts/MessageTypes";
|
||||
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { PriorityLevel } from "../Common/Constants";
|
||||
import { BackendApi, PriorityLevel } from "../Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { Platform, configContext } from "../ConfigContext";
|
||||
import { userContext } from "../UserContext";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
@@ -18,7 +19,18 @@ const _global = typeof self === "undefined" ? window : self;
|
||||
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
const { verb, resourceId, resourceType, headers } = requestInfo;
|
||||
|
||||
if (userContext.features.enableAadDataPlane && userContext.aadToken) {
|
||||
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && userContext.apiType === "SQL";
|
||||
if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) {
|
||||
Logger.logInfo(
|
||||
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
|
||||
"Explorer/tokenProvider",
|
||||
);
|
||||
if (!userContext.aadToken) {
|
||||
logConsoleError(
|
||||
`AAD token does not exist. Please click on "Login for Entra ID" button prior to performing Entra ID RBAC operations`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const AUTH_PREFIX = `type=aad&ver=1.0&sig=`;
|
||||
const authorizationToken = `${AUTH_PREFIX}${userContext.aadToken}`;
|
||||
return authorizationToken;
|
||||
@@ -40,9 +52,10 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
case Cosmos.ResourceType.item:
|
||||
case Cosmos.ResourceType.pkranges:
|
||||
// User resource tokens
|
||||
// TODO userContext.fabricContext.databaseConnectionInfo can be undefined
|
||||
headers[HttpHeaders.msDate] = new Date().toUTCString();
|
||||
const resourceTokens = userContext.fabricDatabaseConnectionInfo.resourceTokens;
|
||||
checkDatabaseResourceTokensValidity(userContext.fabricDatabaseConnectionInfo.resourceTokensTimestamp);
|
||||
const resourceTokens = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
||||
checkDatabaseResourceTokensValidity(userContext.fabricContext.databaseConnectionInfo.resourceTokensTimestamp);
|
||||
return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
|
||||
|
||||
case Cosmos.ResourceType.none:
|
||||
@@ -50,19 +63,36 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
case Cosmos.ResourceType.offer:
|
||||
case Cosmos.ResourceType.user:
|
||||
case Cosmos.ResourceType.permission:
|
||||
// User master tokens
|
||||
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(MessageTypes.GetAuthorizationToken, [
|
||||
requestInfo,
|
||||
]);
|
||||
console.log("Response from Fabric: ", authorizationToken);
|
||||
headers[HttpHeaders.msDate] = authorizationToken.XDate;
|
||||
return decodeURIComponent(authorizationToken.PrimaryReadWriteToken);
|
||||
// For now, these operations aren't used, so fetching the authorization token is commented out.
|
||||
// This provider must return a real token to pass validation by the client, so we return the cached resource token
|
||||
// (which is a valid token, but won't work for these operations).
|
||||
const resourceTokens2 = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
||||
return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId);
|
||||
|
||||
/* ************** TODO: Uncomment this code if we need to support these operations **************
|
||||
// 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) {
|
||||
Logger.logInfo(`Master Key exists`, "Explorer/tokenProvider");
|
||||
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
|
||||
await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
|
||||
await Cosmos.setAuthorizationTokenHeaderUsingMasterKey(
|
||||
verb,
|
||||
resourceId,
|
||||
resourceType,
|
||||
headers,
|
||||
userContext.masterKey,
|
||||
);
|
||||
return decodeURIComponent(headers.authorization);
|
||||
}
|
||||
|
||||
@@ -94,6 +124,37 @@ export async function getTokenFromAuthService(
|
||||
verb: string,
|
||||
resourceType: string,
|
||||
resourceId?: string,
|
||||
): Promise<AuthorizationToken> {
|
||||
if (!useNewPortalBackendEndpoint(BackendApi.RuntimeProxy)) {
|
||||
return getTokenFromAuthService_ToBeDeprecated(verb, resourceType, resourceId);
|
||||
}
|
||||
|
||||
try {
|
||||
const host: string = configContext.PORTAL_BACKEND_ENDPOINT;
|
||||
const response: Response = await _global.fetch(host + "/api/connectionstring/runtimeproxy/authorizationtokens", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-ms-encrypted-auth-token": userContext.accessToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
verb,
|
||||
resourceType,
|
||||
resourceId,
|
||||
}),
|
||||
});
|
||||
const result: AuthorizationToken = await response.json();
|
||||
return result;
|
||||
} catch (error) {
|
||||
logConsoleError(`Failed to get authorization headers for ${resourceType}: ${getErrorMessage(error)}`);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTokenFromAuthService_ToBeDeprecated(
|
||||
verb: string,
|
||||
resourceType: string,
|
||||
resourceId?: string,
|
||||
): Promise<AuthorizationToken> {
|
||||
try {
|
||||
const host = configContext.BACKEND_ENDPOINT;
|
||||
@@ -127,8 +188,11 @@ enum SDKSupportedCapabilities {
|
||||
let _client: Cosmos.CosmosClient;
|
||||
|
||||
export function client(): Cosmos.CosmosClient {
|
||||
if (_client) return _client;
|
||||
|
||||
if (_client) {
|
||||
if (!userContext.hasDataPlaneRbacSettingChanged) {
|
||||
return _client;
|
||||
}
|
||||
}
|
||||
let _defaultHeaders: Cosmos.CosmosHeaders = {};
|
||||
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
|
||||
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
||||
@@ -147,7 +211,7 @@ export function client(): Cosmos.CosmosClient {
|
||||
|
||||
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
|
||||
key: userContext.masterKey,
|
||||
key: userContext.dataPlaneRbacEnabled ? "" : userContext.masterKey,
|
||||
tokenProvider,
|
||||
userAgentSuffix: "Azure Portal",
|
||||
defaultHeaders: _defaultHeaders,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { userContext } from "../UserContext";
|
||||
|
||||
export const getEntityName = (): string => {
|
||||
export const getEntityName = (multiple?: boolean): string => {
|
||||
if (userContext.apiType === "Mongo") {
|
||||
return "document";
|
||||
return multiple ? "documents" : "document";
|
||||
}
|
||||
|
||||
return "item";
|
||||
return multiple ? "items" : "item";
|
||||
};
|
||||
|
||||
@@ -53,7 +53,8 @@ const replaceKnownError = (errorMessage: string): string => {
|
||||
return "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character.";
|
||||
} else if (
|
||||
errorMessage?.indexOf("The user aborted a request") >= 0 ||
|
||||
errorMessage?.indexOf("The operation was aborted") >= 0
|
||||
errorMessage?.indexOf("The operation was aborted") >= 0 ||
|
||||
errorMessage === "signal is aborted without reason"
|
||||
) {
|
||||
return "User aborted query.";
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { QueryOperationOptions } from "@azure/cosmos";
|
||||
import { QueryResults } from "../Contracts/ViewModels";
|
||||
|
||||
interface QueryResponse {
|
||||
@@ -10,13 +11,17 @@ interface QueryResponse {
|
||||
}
|
||||
|
||||
export interface MinimalQueryIterator {
|
||||
fetchNext: () => Promise<QueryResponse>;
|
||||
fetchNext: (queryOperationOptions?: QueryOperationOptions) => Promise<QueryResponse>;
|
||||
}
|
||||
|
||||
// Pick<QueryIterator<any>, "fetchNext">;
|
||||
|
||||
export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> {
|
||||
return documentsIterator.fetchNext().then((response) => {
|
||||
export function nextPage(
|
||||
documentsIterator: MinimalQueryIterator,
|
||||
firstItemIndex: number,
|
||||
queryOperationOptions?: QueryOperationOptions,
|
||||
): Promise<QueryResults> {
|
||||
return documentsIterator.fetchNext(queryOperationOptions).then((response) => {
|
||||
const documents = response.resources;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const headers = (response as any).headers || {}; // TODO this is a private key. Remove any
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
|
||||
import Q from "q";
|
||||
import * as _ from "underscore";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { getDataExplorerWindow } from "../Utils/WindowUtils";
|
||||
import * as Constants from "./Constants";
|
||||
@@ -27,15 +29,24 @@ export function handleCachedDataMessage(message: any): void {
|
||||
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>(
|
||||
messageType: MessageTypes,
|
||||
messageType: MessageTypes | FabricMessageTypes,
|
||||
params: Object[],
|
||||
scope?: string,
|
||||
timeoutInMs?: number,
|
||||
): Q.Promise<TResponseDataModel> {
|
||||
let cachedDataPromise: CachedDataPromise<TResponseDataModel> = {
|
||||
deferred: Q.defer<TResponseDataModel>(),
|
||||
startTime: new Date(),
|
||||
id: _.uniqueId(),
|
||||
id: _.uniqueId(scope),
|
||||
};
|
||||
RequestMap[cachedDataPromise.id] = cachedDataPromise;
|
||||
sendMessage({ type: messageType, params: params, id: cachedDataPromise.id });
|
||||
@@ -47,6 +58,10 @@ export function sendCachedDataMessage<TResponseDataModel>(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param data Overwrite the data property of the message
|
||||
*/
|
||||
export function sendMessage(data: any): void {
|
||||
_sendMessage({
|
||||
signature: "pcIframe",
|
||||
@@ -82,10 +97,18 @@ const _sendMessage = (message: any): void => {
|
||||
const portalChildWindow = getDataExplorerWindow(window) || window;
|
||||
if (portalChildWindow === window) {
|
||||
// Current window is a child of portal, send message to portal window
|
||||
portalChildWindow.parent.postMessage(message, portalChildWindow.document.referrer || "*");
|
||||
if (portalChildWindow.document.referrer) {
|
||||
portalChildWindow.parent.postMessage(message, portalChildWindow.document.referrer);
|
||||
} else {
|
||||
Logger.logError("Iframe failed to send message to portal", "MessageHandler");
|
||||
}
|
||||
} else {
|
||||
// Current window is not a child of portal, send message to the child window instead (which is data explorer)
|
||||
portalChildWindow.postMessage(message, portalChildWindow.location.origin || "*");
|
||||
if (portalChildWindow.location.origin) {
|
||||
portalChildWindow.postMessage(message, portalChildWindow.location.origin);
|
||||
} else {
|
||||
Logger.logError("Iframe failed to send message to data explorer", "MessageHandler");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||
import {
|
||||
allowedMongoProxyEndpoints,
|
||||
allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||
validateEndpoint,
|
||||
} from "Utils/EndpointUtils";
|
||||
import queryString from "querystring";
|
||||
import { allowedMongoProxyEndpoints, validateEndpoint } from "Utils/EndpointValidation";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
@@ -10,7 +14,7 @@ import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { hasFlag } from "../Platform/Hosted/extractFeatures";
|
||||
import { userContext } from "../UserContext";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
||||
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyEndpoints } from "./Constants";
|
||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||
import { sendMessage } from "./MessageHandler";
|
||||
|
||||
@@ -62,6 +66,73 @@ export function queryDocuments(
|
||||
isResourceList: boolean,
|
||||
query: 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> {
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
@@ -122,6 +193,54 @@ export function readDocument(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
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> {
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
@@ -169,6 +288,51 @@ export function createDocument(
|
||||
collection: Collection,
|
||||
partitionKeyProperty: string,
|
||||
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> {
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
@@ -208,6 +372,56 @@ export function updateDocument(
|
||||
collection: Collection,
|
||||
documentId: DocumentId,
|
||||
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> {
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
@@ -237,7 +451,7 @@ export function updateDocument(
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
...authHeaders(),
|
||||
[HttpHeaders.contentType]: "application/json",
|
||||
[HttpHeaders.contentType]: ContentType.applicationJson,
|
||||
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
||||
},
|
||||
})
|
||||
@@ -250,6 +464,53 @@ export function updateDocument(
|
||||
}
|
||||
|
||||
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 resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
const idComponents = documentId.self.split("/");
|
||||
@@ -277,7 +538,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
...authHeaders(),
|
||||
[HttpHeaders.contentType]: "application/json",
|
||||
[HttpHeaders.contentType]: ContentType.applicationJson,
|
||||
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
||||
},
|
||||
})
|
||||
@@ -289,8 +550,97 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteDocuments(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
documentIds: DocumentId[],
|
||||
): Promise<{
|
||||
deletedCount: number;
|
||||
isAcknowledged: boolean;
|
||||
}> {
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
|
||||
const rids = documentIds.map((documentId) => documentId.id());
|
||||
|
||||
const params = {
|
||||
databaseID: databaseId,
|
||||
collectionID: collection.id(),
|
||||
resourceUrl: `${resourceEndpoint}`,
|
||||
resourceIDs: rids,
|
||||
subscriptionID: userContext.subscriptionId,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
databaseAccountName: databaseAccount.name,
|
||||
};
|
||||
const endpoint = getFeatureEndpointOrDefault("bulkdelete");
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/bulkdelete`, {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify(params),
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
...authHeaders(),
|
||||
[HttpHeaders.contentType]: ContentType.applicationJson,
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
return result;
|
||||
}
|
||||
return await errorHandling(response, "deleting documents", params);
|
||||
});
|
||||
}
|
||||
|
||||
export function createMongoCollectionWithProxy(
|
||||
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> {
|
||||
const { databaseAccount } = userContext;
|
||||
const shardKey: string = params.partitionKey?.paths[0];
|
||||
@@ -334,13 +684,20 @@ export function createMongoCollectionWithProxy(
|
||||
return await errorHandling(response, "creating collection", mongoParams);
|
||||
});
|
||||
}
|
||||
|
||||
export function getFeatureEndpointOrDefault(feature: string): string {
|
||||
const endpoint =
|
||||
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
||||
validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints)
|
||||
? userContext.features.mongoProxyEndpoint
|
||||
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||
let endpoint;
|
||||
if (useMongoProxyEndpoint(feature)) {
|
||||
endpoint = configContext.MONGO_PROXY_ENDPOINT;
|
||||
} else {
|
||||
endpoint =
|
||||
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
||||
validateEndpoint(userContext.features.mongoProxyEndpoint, [
|
||||
...allowedMongoProxyEndpoints,
|
||||
...allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||
])
|
||||
? userContext.features.mongoProxyEndpoint
|
||||
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
return getEndpoint(endpoint);
|
||||
}
|
||||
@@ -349,11 +706,38 @@ export function getEndpoint(endpoint: string): string {
|
||||
let url = endpoint + "/api/mongo/explorer";
|
||||
|
||||
if (userContext.authType === AuthType.EncryptedToken) {
|
||||
url = url.replace("api/mongo", "api/guest/mongo");
|
||||
if (endpoint === configContext.MONGO_PROXY_ENDPOINT) {
|
||||
url = url.replace("api/mongo", "api/connectionstring/mongo");
|
||||
} else {
|
||||
url = url.replace("api/mongo", "api/guest/mongo");
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
export function useMongoProxyEndpoint(api: string): boolean {
|
||||
const activeMongoProxyEndpoints: string[] = [
|
||||
MongoProxyEndpoints.Local,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
];
|
||||
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
|
||||
// It causes problems for TypeScript understanding the types
|
||||
async function errorHandling(response: Response, action: string, params: unknown): Promise<void> {
|
||||
|
||||
@@ -3,8 +3,7 @@ import * as _ from "underscore";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import DocumentId, { IDocumentIdContainer } from "../Explorer/Tree/DocumentId";
|
||||
import { useDatabases } from "../Explorer/useDatabases";
|
||||
import { userContext } from "../UserContext";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
@@ -162,10 +161,10 @@ export class QueriesClient {
|
||||
{
|
||||
partitionKey: QueriesClient.PartitionKey,
|
||||
partitionKeyProperties: ["id"],
|
||||
} as DocumentsTab,
|
||||
} as IDocumentIdContainer,
|
||||
query,
|
||||
[query.queryName],
|
||||
); // TODO: Remove DocumentId's dependency on DocumentsTab
|
||||
);
|
||||
const options: any = { partitionKey: query.resourceId };
|
||||
return deleteDocument(queriesCollection, documentId)
|
||||
.then(
|
||||
|
||||
212
src/Common/QueryError.ts
Normal file
212
src/Common/QueryError.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { getErrorMessage } from "Common/ErrorHandlingUtils";
|
||||
import { monaco } from "Explorer/LazyMonaco";
|
||||
|
||||
export enum QueryErrorSeverity {
|
||||
Error = "Error",
|
||||
Warning = "Warning",
|
||||
}
|
||||
|
||||
export class QueryErrorLocation {
|
||||
constructor(
|
||||
public start: ErrorPosition,
|
||||
public end: ErrorPosition,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class ErrorPosition {
|
||||
constructor(
|
||||
public offset: number,
|
||||
public lineNumber?: number,
|
||||
public column?: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
// Maps severities to numbers for sorting.
|
||||
const severityMap: Record<QueryErrorSeverity, number> = {
|
||||
Error: 1,
|
||||
Warning: 0,
|
||||
};
|
||||
|
||||
export function compareSeverity(left: QueryErrorSeverity, right: QueryErrorSeverity): number {
|
||||
return severityMap[left] - severityMap[right];
|
||||
}
|
||||
|
||||
export function createMonacoErrorLocationResolver(
|
||||
editor: monaco.editor.IStandaloneCodeEditor,
|
||||
selection?: monaco.Selection,
|
||||
): (location: { start: number; end: number }) => QueryErrorLocation {
|
||||
return ({ start, end }) => {
|
||||
// Start and end are absolute offsets (character index) in the document.
|
||||
// But we need line numbers and columns for the monaco editor.
|
||||
// To get those, we use the editor's model to convert the offsets to positions.
|
||||
const model = editor.getModel();
|
||||
if (!model) {
|
||||
return new QueryErrorLocation(new ErrorPosition(start), new ErrorPosition(end));
|
||||
}
|
||||
|
||||
// If the error was found in a selection, adjust the start and end positions to be relative to the document.
|
||||
if (selection) {
|
||||
// Get the character index of the start of the selection.
|
||||
const selectionStartOffset = model.getOffsetAt(selection.getStartPosition());
|
||||
|
||||
// Adjust the start and end positions to be relative to the document.
|
||||
start = selectionStartOffset + start;
|
||||
end = selectionStartOffset + end;
|
||||
|
||||
// Now, when we resolve the positions, they will be relative to the document and appear in the correct location.
|
||||
}
|
||||
|
||||
const startPos = model.getPositionAt(start);
|
||||
const endPos = model.getPositionAt(end);
|
||||
return new QueryErrorLocation(
|
||||
new ErrorPosition(start, startPos.lineNumber, startPos.column),
|
||||
new ErrorPosition(end, endPos.lineNumber, endPos.column),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const createMonacoMarkersForQueryErrors = (errors: QueryError[]) => {
|
||||
if (!errors) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return errors
|
||||
.map((error): monaco.editor.IMarkerData => {
|
||||
// Validate that we have what we need to make a marker
|
||||
if (
|
||||
error.location === undefined ||
|
||||
error.location.start === undefined ||
|
||||
error.location.end === undefined ||
|
||||
error.location.start.lineNumber === undefined ||
|
||||
error.location.end.lineNumber === undefined ||
|
||||
error.location.start.column === undefined ||
|
||||
error.location.end.column === undefined
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
message: error.message,
|
||||
severity: error.getMonacoSeverity(),
|
||||
startLineNumber: error.location.start.lineNumber,
|
||||
startColumn: error.location.start.column,
|
||||
endLineNumber: error.location.end.lineNumber,
|
||||
endColumn: error.location.end.column,
|
||||
};
|
||||
})
|
||||
.filter((marker) => !!marker);
|
||||
};
|
||||
|
||||
export default class QueryError {
|
||||
constructor(
|
||||
public message: string,
|
||||
public severity: QueryErrorSeverity,
|
||||
public code?: string,
|
||||
public location?: QueryErrorLocation,
|
||||
) {}
|
||||
|
||||
getMonacoSeverity(): monaco.MarkerSeverity {
|
||||
// It's very difficult to use the monaco.MarkerSeverity enum from here, so we'll just use the numbers directly.
|
||||
// See: https://microsoft.github.io/monaco-editor/typedoc/enums/MarkerSeverity.html
|
||||
switch (this.severity) {
|
||||
case QueryErrorSeverity.Error:
|
||||
return 8;
|
||||
case QueryErrorSeverity.Warning:
|
||||
return 4;
|
||||
default:
|
||||
return 2; // Info
|
||||
}
|
||||
}
|
||||
|
||||
/** Attempts to parse a query error from a string or object.
|
||||
*
|
||||
* @param error The error to parse.
|
||||
* @returns An array of query errors if the error could be parsed, or null otherwise.
|
||||
*/
|
||||
static tryParse(
|
||||
error: unknown,
|
||||
locationResolver?: (location: { start: number; end: number }) => QueryErrorLocation,
|
||||
): QueryError[] {
|
||||
locationResolver =
|
||||
locationResolver ||
|
||||
(({ start, end }) => new QueryErrorLocation(new ErrorPosition(start), new ErrorPosition(end)));
|
||||
const errors = QueryError.tryParseObject(error, locationResolver);
|
||||
if (errors !== null) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
const errorMessage = getErrorMessage(error as string | Error);
|
||||
|
||||
// Map some well known messages to richer errors
|
||||
const knownError = knownErrors[errorMessage];
|
||||
if (knownError) {
|
||||
return [knownError];
|
||||
} else {
|
||||
return [new QueryError(errorMessage, QueryErrorSeverity.Error)];
|
||||
}
|
||||
}
|
||||
|
||||
static read(
|
||||
error: unknown,
|
||||
locationResolver: (location: { start: number; end: number }) => QueryErrorLocation,
|
||||
): QueryError | null {
|
||||
if (typeof error !== "object" || error === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const message = "message" in error && typeof error.message === "string" ? error.message : undefined;
|
||||
if (!message) {
|
||||
return null; // Invalid error (no message).
|
||||
}
|
||||
|
||||
const severity =
|
||||
"severity" in error && typeof error.severity === "string" ? (error.severity as QueryErrorSeverity) : undefined;
|
||||
const location =
|
||||
"location" in error && typeof error.location === "object"
|
||||
? locationResolver(error.location as { start: number; end: number })
|
||||
: undefined;
|
||||
const code = "code" in error && typeof error.code === "string" ? error.code : undefined;
|
||||
return new QueryError(message, severity, code, location);
|
||||
}
|
||||
|
||||
private static tryParseObject(
|
||||
error: unknown,
|
||||
locationResolver: (location: { start: number; end: number }) => QueryErrorLocation,
|
||||
): QueryError[] | null {
|
||||
if (typeof error === "object" && "message" in error) {
|
||||
error = error.message;
|
||||
}
|
||||
|
||||
if (typeof error !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Assign to a new variable because of a TypeScript flow typing quirk, see below.
|
||||
let message = error;
|
||||
if (message.startsWith("Message: ")) {
|
||||
// Reassigning this to 'error' restores the original type of 'error', which is 'unknown'.
|
||||
// So we use a separate variable to avoid this.
|
||||
message = message.substring("Message: ".length);
|
||||
}
|
||||
|
||||
const lines = message.split("\n");
|
||||
message = lines[0].trim();
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(message);
|
||||
} catch (e) {
|
||||
// Not a query error.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof parsed === "object" && "errors" in parsed && Array.isArray(parsed.errors)) {
|
||||
return parsed.errors.map((e) => QueryError.read(e, locationResolver)).filter((e) => e !== null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const knownErrors: Record<string, QueryError> = {
|
||||
"User aborted query.": new QueryError("User aborted query.", QueryErrorSeverity.Warning),
|
||||
};
|
||||
@@ -1,90 +0,0 @@
|
||||
import { ResourceTree } from "Explorer/Tree/ResourceTree";
|
||||
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
|
||||
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
||||
import refreshImg from "../../images/refresh-cosmos.svg";
|
||||
import { AuthType } from "../AuthType";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree";
|
||||
import { ResourceTree2 } from "../Explorer/Tree2/ResourceTree";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getApiShortDisplayName } from "../Utils/APITypeUtils";
|
||||
import { Platform, configContext } from "./../ConfigContext";
|
||||
import { NormalizedEventKey } from "./Constants";
|
||||
|
||||
export interface ResourceTreeContainerProps {
|
||||
toggleLeftPaneExpanded: () => void;
|
||||
isLeftPaneExpanded: boolean;
|
||||
container: Explorer;
|
||||
}
|
||||
|
||||
export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps> = ({
|
||||
toggleLeftPaneExpanded,
|
||||
isLeftPaneExpanded,
|
||||
container,
|
||||
}: ResourceTreeContainerProps): JSX.Element => {
|
||||
const focusButton = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
|
||||
|
||||
useEffect(() => {
|
||||
if (isLeftPaneExpanded) {
|
||||
if (focusButton.current) {
|
||||
focusButton.current.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const onKeyPressToggleLeftPaneExpanded = (event: React.KeyboardEvent) => {
|
||||
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
|
||||
toggleLeftPaneExpanded();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div id="main" className={isLeftPaneExpanded ? "main" : "hiddenMain"}>
|
||||
{/* Collections Window - - Start */}
|
||||
<div id="mainslide" className="flexContainer">
|
||||
{/* Collections Window Title/Command Bar - Start */}
|
||||
<div className="collectiontitle">
|
||||
<div className="coltitle">
|
||||
<span className="titlepadcol">{getApiShortDisplayName()}</span>
|
||||
<div className="float-right">
|
||||
<span
|
||||
className="padimgcolrefresh"
|
||||
data-test="refreshTree"
|
||||
role="button"
|
||||
data-bind="click: onRefreshResourcesClick, clickBubble: false, event: { keypress: onRefreshDatabasesKeyPress }"
|
||||
tabIndex={0}
|
||||
aria-label={getApiShortDisplayName() + `Refresh tree`}
|
||||
title="Refresh tree"
|
||||
>
|
||||
<img className="refreshcol" src={refreshImg} alt="Refresh Tree" />
|
||||
</span>
|
||||
<span
|
||||
className="padimgcolrefresh1"
|
||||
id="expandToggleLeftPaneButton"
|
||||
role="button"
|
||||
onClick={toggleLeftPaneExpanded}
|
||||
onKeyPress={onKeyPressToggleLeftPaneExpanded}
|
||||
tabIndex={0}
|
||||
aria-label={getApiShortDisplayName() + `Collapse Tree`}
|
||||
title="Collapse Tree"
|
||||
ref={focusButton}
|
||||
>
|
||||
<img className="refreshcol1" src={arrowLeftImg} alt="Hide" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{userContext.authType === AuthType.ResourceToken ? (
|
||||
<ResourceTokenTree />
|
||||
) : userContext.features.enableKoResourceTree ? (
|
||||
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
|
||||
) : configContext.platform === Platform.Fabric ? (
|
||||
<ResourceTree2 container={container} />
|
||||
) : (
|
||||
<ResourceTree container={container} />
|
||||
)}
|
||||
</div>
|
||||
{/* Collections Window - End */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -142,7 +142,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
||||
<Image
|
||||
{...imageProps}
|
||||
src={EditIcon}
|
||||
alt="editEntity"
|
||||
alt={`Edit ${entityProperty} entity`}
|
||||
onClick={onEditEntity}
|
||||
tabIndex={0}
|
||||
onKeyPress={handleKeyPress}
|
||||
@@ -156,7 +156,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
||||
<Image
|
||||
{...imageProps}
|
||||
src={DeleteIcon}
|
||||
alt="delete entity"
|
||||
alt={`Delete ${entityProperty} entity`}
|
||||
id="deleteEntity"
|
||||
onClick={onDeleteEntity}
|
||||
tabIndex={0}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`requestPlugin Emulator builds a url for emulator proxy via webpack 1`] = `
|
||||
Object {
|
||||
{
|
||||
"endpoint": "http://localhost/proxy",
|
||||
"headers": Object {
|
||||
"headers": {
|
||||
"x-ms-proxy-target": "http://localhost",
|
||||
},
|
||||
"path": "/dbs/foo",
|
||||
@@ -11,9 +11,9 @@ Object {
|
||||
`;
|
||||
|
||||
exports[`requestPlugin Hosted builds a proxy URL in development 1`] = `
|
||||
Object {
|
||||
{
|
||||
"endpoint": "http://localhost/proxy",
|
||||
"headers": Object {
|
||||
"headers": {
|
||||
"x-ms-proxy-target": "baz",
|
||||
},
|
||||
"path": "/dbs/foo",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`nextPage returns results for the next page 1`] = `
|
||||
Object {
|
||||
{
|
||||
"activityId": "foo",
|
||||
"documents": Array [],
|
||||
"documents": [],
|
||||
"firstItemIndex": 11,
|
||||
"hasMoreResults": false,
|
||||
"headers": Object {},
|
||||
"headers": {},
|
||||
"itemCount": 0,
|
||||
"lastItemIndex": 10,
|
||||
"requestCharge": 1,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`getCommonQueryOptions builds the correct default options objects 1`] = `
|
||||
Object {
|
||||
{
|
||||
"disableNonStreamingOrderByQuery": true,
|
||||
"enableScanInQuery": true,
|
||||
"forceQueryPlan": true,
|
||||
"maxDegreeOfParallelism": 0,
|
||||
@@ -11,7 +12,8 @@ Object {
|
||||
`;
|
||||
|
||||
exports[`getCommonQueryOptions reads from localStorage 1`] = `
|
||||
Object {
|
||||
{
|
||||
"disableNonStreamingOrderByQuery": true,
|
||||
"enableScanInQuery": true,
|
||||
"forceQueryPlan": true,
|
||||
"maxDegreeOfParallelism": 17,
|
||||
|
||||
@@ -6,13 +6,13 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { getCollectionName } from "../../Utils/APITypeUtils";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { createUpdateCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||
import { createUpdateGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||
import { createUpdateMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||
import { createUpdateSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { createUpdateTable } from "../../Utils/arm/generatedClients/cosmos/tableResources";
|
||||
import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { createMongoCollectionWithProxy } from "../MongoProxyClient";
|
||||
@@ -96,6 +96,9 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
|
||||
if (params.uniqueKeyPolicy) {
|
||||
resource.uniqueKeyPolicy = params.uniqueKeyPolicy;
|
||||
}
|
||||
if (params.vectorEmbeddingPolicy) {
|
||||
resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy;
|
||||
}
|
||||
|
||||
const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = {
|
||||
properties: {
|
||||
@@ -266,6 +269,7 @@ const createCollectionWithSDK = async (params: DataModels.CreateCollectionParams
|
||||
indexingPolicy: params.indexingPolicy || undefined,
|
||||
uniqueKeyPolicy: params.uniqueKeyPolicy || undefined,
|
||||
analyticalStorageTtl: params.analyticalStorageTtl,
|
||||
vectorEmbeddingPolicy: params.vectorEmbeddingPolicy,
|
||||
} as ContainerRequest; // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed
|
||||
const collectionOptions: RequestOptions = {};
|
||||
const createDatabaseBody: DatabaseRequest = { id: params.databaseId };
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as DataModels from "../../Contracts/DataModels";
|
||||
import { useDatabases } from "../../Explorer/useDatabases";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { getDatabaseName } from "../../Utils/APITypeUtils";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { createUpdateCassandraKeyspace } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||
import { createUpdateGremlinDatabase } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||
import { createUpdateMongoDBDatabase } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
MongoDBDatabaseCreateUpdateParameters,
|
||||
SqlDatabaseCreateUpdateParameters,
|
||||
} from "../../Utils/arm/generatedClients/cosmos/types";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
@@ -152,8 +152,18 @@ async function createDatabaseWithSDK(params: DataModels.CreateDatabaseParams): P
|
||||
createBody.throughput = params.offerThroughput;
|
||||
}
|
||||
}
|
||||
|
||||
const response: DatabaseResponse = await client().databases.create(createBody);
|
||||
let response: DatabaseResponse;
|
||||
try {
|
||||
response = await client().databases.create(createBody);
|
||||
} catch (error) {
|
||||
if (error.message.includes("Shared throughput database creation is not supported for serverless accounts")) {
|
||||
createBody.maxThroughput = undefined;
|
||||
createBody.throughput = undefined;
|
||||
response = await client().databases.create(createBody);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return response.resource;
|
||||
}
|
||||
|
||||
|
||||
195
src/Common/dataAccess/dataTransfers.ts
Normal file
195
src/Common/dataAccess/dataTransfers.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
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,3 +1,4 @@
|
||||
import { BulkOperationType, OperationInput } from "@azure/cosmos";
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
@@ -24,3 +25,58 @@ export const deleteDocument = async (collection: CollectionBase, documentId: Doc
|
||||
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,4 +1,5 @@
|
||||
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||
import { Queries } from "../Constants";
|
||||
import { client } from "../CosmosClient";
|
||||
@@ -26,5 +27,6 @@ export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {
|
||||
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
|
||||
Queries.itemsPerPage;
|
||||
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
|
||||
options.disableNonStreamingOrderByQuery = !isVectorSearchEnabled();
|
||||
return options;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { QueryOperationOptions } from "@azure/cosmos";
|
||||
import { QueryResults } from "../../Contracts/ViewModels";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
@@ -8,12 +9,13 @@ export const queryDocumentsPage = async (
|
||||
resourceName: string,
|
||||
documentsIterator: MinimalQueryIterator,
|
||||
firstItemIndex: number,
|
||||
queryOperationOptions?: QueryOperationOptions,
|
||||
): Promise<QueryResults> => {
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
|
||||
|
||||
try {
|
||||
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex);
|
||||
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex, queryOperationOptions);
|
||||
const itemCount = (result.documents && result.documents.length) || 0;
|
||||
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
|
||||
return result;
|
||||
|
||||
@@ -2,7 +2,6 @@ import { CosmosClient } from "@azure/cosmos";
|
||||
import { sampleDataClient } from "Common/SampleDataClient";
|
||||
import { userContext } from "UserContext";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
@@ -31,7 +30,6 @@ export async function readCollectionInternal(
|
||||
collectionId: string,
|
||||
): Promise<DataModels.Collection> {
|
||||
let collection: DataModels.Collection;
|
||||
const clearMessage = logConsoleProgress(`Querying container ${collectionId}`);
|
||||
try {
|
||||
const response = await cosmosClient.database(databaseId).container(collectionId).read();
|
||||
collection = response.resource as DataModels.Collection;
|
||||
@@ -39,6 +37,5 @@ export async function readCollectionInternal(
|
||||
handleError(error, "ReadCollection", `Error while querying container ${collectionId}`);
|
||||
throw error;
|
||||
}
|
||||
clearMessage();
|
||||
return collection;
|
||||
}
|
||||
|
||||
@@ -18,13 +18,13 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
|
||||
|
||||
if (
|
||||
configContext.platform === Platform.Fabric &&
|
||||
userContext.fabricDatabaseConnectionInfo &&
|
||||
userContext.fabricDatabaseConnectionInfo.databaseId === databaseId
|
||||
userContext.fabricContext &&
|
||||
userContext.fabricContext.databaseConnectionInfo.databaseId === databaseId
|
||||
) {
|
||||
const collections: DataModels.Collection[] = [];
|
||||
const promises: Promise<ContainerResponse>[] = [];
|
||||
|
||||
for (const collectionResourceId in userContext.fabricDatabaseConnectionInfo.resourceTokens) {
|
||||
for (const collectionResourceId in userContext.fabricContext.databaseConnectionInfo.resourceTokens) {
|
||||
// Dictionary key looks like this: dbs/SampleDB/colls/Container
|
||||
const resourceIdObj = collectionResourceId.split("/");
|
||||
const tokenDatabaseId = resourceIdObj[1];
|
||||
|
||||
@@ -14,8 +14,8 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
||||
let databases: DataModels.Database[];
|
||||
const clearMessage = logConsoleProgress(`Querying databases`);
|
||||
|
||||
if (configContext.platform === Platform.Fabric && userContext.fabricDatabaseConnectionInfo?.resourceTokens) {
|
||||
const tokensData = userContext.fabricDatabaseConnectionInfo;
|
||||
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.databaseConnectionInfo.resourceTokens) {
|
||||
const tokensData = userContext.fabricContext.databaseConnectionInfo;
|
||||
|
||||
const databaseIdsSet = new Set<string>(); // databaseId
|
||||
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import { JunoEndpoints } from "Common/Constants";
|
||||
import {
|
||||
BackendApi,
|
||||
CassandraProxyEndpoints,
|
||||
JunoEndpoints,
|
||||
MongoProxyEndpoints,
|
||||
PortalBackendEndpoints,
|
||||
} from "Common/Constants";
|
||||
import {
|
||||
allowedAadEndpoints,
|
||||
allowedArcadiaEndpoints,
|
||||
allowedCassandraProxyEndpoints,
|
||||
allowedEmulatorEndpoints,
|
||||
allowedGraphEndpoints,
|
||||
allowedHostedExplorerEndpoints,
|
||||
allowedJunoOrigins,
|
||||
allowedMongoBackendEndpoints,
|
||||
allowedMongoProxyEndpoints,
|
||||
allowedMsalRedirectEndpoints,
|
||||
defaultAllowedArmEndpoints,
|
||||
defaultAllowedBackendEndpoints,
|
||||
validateEndpoint,
|
||||
} from "Utils/EndpointValidation";
|
||||
} from "Utils/EndpointUtils";
|
||||
|
||||
export enum Platform {
|
||||
Portal = "Portal",
|
||||
@@ -34,10 +42,22 @@ export interface ConfigContext {
|
||||
ARM_API_VERSION: string;
|
||||
GRAPH_ENDPOINT: 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_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||
BACKEND_ENDPOINT?: string;
|
||||
PORTAL_BACKEND_ENDPOINT?: string;
|
||||
NEW_BACKEND_APIS?: BackendApi[];
|
||||
MONGO_BACKEND_ENDPOINT?: string;
|
||||
MONGO_PROXY_ENDPOINT?: string;
|
||||
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED?: boolean;
|
||||
NEW_MONGO_APIS?: string[];
|
||||
CASSANDRA_PROXY_ENDPOINT?: string;
|
||||
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: boolean;
|
||||
NEW_CASSANDRA_APIS?: string[];
|
||||
PROXY_PATH?: string;
|
||||
JUNO_ENDPOINT: string;
|
||||
GITHUB_CLIENT_ID: string;
|
||||
@@ -67,6 +87,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
||||
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
||||
`^https:\\/\\/.*\\.azure-test\\.net$`,
|
||||
`^https:\\/\\/cosmos-explorer-preview\\.azurewebsites\\.net$`,
|
||||
], // Webpack injects this at build time
|
||||
gitSha: process.env.GIT_SHA,
|
||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||
@@ -76,12 +97,32 @@ let configContext: Readonly<ConfigContext> = {
|
||||
ARM_API_VERSION: "2016-06-01",
|
||||
GRAPH_ENDPOINT: "https://graph.microsoft.com",
|
||||
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_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
|
||||
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
|
||||
JUNO_ENDPOINT: JunoEndpoints.Prod,
|
||||
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",
|
||||
"bulkdelete",
|
||||
],
|
||||
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,
|
||||
isPhoenixEnabled: false,
|
||||
};
|
||||
@@ -127,10 +168,18 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
delete newContext.BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.MONGO_PROXY_ENDPOINT, allowedMongoProxyEndpoints)) {
|
||||
delete newContext.MONGO_PROXY_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) {
|
||||
delete newContext.MONGO_BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.CASSANDRA_PROXY_ENDPOINT, allowedCassandraProxyEndpoints)) {
|
||||
delete newContext.CASSANDRA_PROXY_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.JUNO_ENDPOINT, allowedJunoOrigins)) {
|
||||
delete newContext.JUNO_ENDPOINT;
|
||||
}
|
||||
@@ -148,12 +197,12 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
|
||||
// Injected for local development. These will be removed in the production bundle by webpack
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
const port: string = process.env.PORT || "1234";
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: "https://localhost:" + port,
|
||||
MONGO_BACKEND_ENDPOINT: "https://localhost:" + port,
|
||||
PROXY_PATH: "/proxy",
|
||||
EMULATOR_ENDPOINT: "https://localhost:8081",
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac,
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Mpac,
|
||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Mpac,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
export interface QueryRequestOptions {
|
||||
$skipToken?: string;
|
||||
$top?: number;
|
||||
subscriptions: string[];
|
||||
$allowPartialScopes: boolean;
|
||||
subscriptions?: string[];
|
||||
}
|
||||
|
||||
export interface QueryResponse {
|
||||
|
||||
27
src/Contracts/DataExplorerMessagesContract.ts
Normal file
27
src/Contracts/DataExplorerMessagesContract.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
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 { ConnectionStatusType, ContainerStatusType } from "../Common/Constants";
|
||||
import { CapacityMode, ConnectionStatusType, ContainerStatusType } from "../Common/Constants";
|
||||
|
||||
export interface ArmEntity {
|
||||
id: string;
|
||||
@@ -35,6 +35,7 @@ export interface DatabaseAccountExtendedProperties {
|
||||
ipRules?: IpRule[];
|
||||
privateEndpointConnections?: unknown[];
|
||||
capacity?: { totalThroughputLimit: number };
|
||||
capacityMode?: CapacityMode;
|
||||
locations?: DatabaseAccountResponseLocation[];
|
||||
postgresqlEndpoint?: string;
|
||||
publicNetworkAccess?: string;
|
||||
@@ -157,8 +158,10 @@ export interface Collection extends Resource {
|
||||
changeFeedPolicy?: ChangeFeedPolicy;
|
||||
analyticalStorageTtl?: number;
|
||||
geospatialConfig?: GeospatialConfig;
|
||||
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
|
||||
schema?: ISchema;
|
||||
requestSchema?: () => void;
|
||||
computedProperties?: ComputedProperties;
|
||||
}
|
||||
|
||||
export interface CollectionsWithPagination {
|
||||
@@ -193,10 +196,23 @@ export interface IndexingPolicy {
|
||||
indexingMode: "consistent" | "lazy" | "none";
|
||||
includedPaths: any;
|
||||
excludedPaths: any;
|
||||
compositeIndexes?: any;
|
||||
spatialIndexes?: any;
|
||||
compositeIndexes?: 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 {
|
||||
paths: string[];
|
||||
kind: "Hash" | "Range" | "MultiHash";
|
||||
@@ -325,6 +341,18 @@ export interface CreateCollectionParams {
|
||||
partitionKey?: PartitionKey;
|
||||
uniqueKeyPolicy?: UniqueKeyPolicy;
|
||||
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 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MessageTypes } from "Contracts/MessageTypes";
|
||||
import * as ActionContracts from "./ActionContracts";
|
||||
import * as Diagnostics from "./Diagnostics";
|
||||
import { MessageTypes } from "./MessageTypes";
|
||||
import * as Versions from "./Versions";
|
||||
|
||||
export { ActionContracts, Diagnostics, MessageTypes, Versions };
|
||||
|
||||
13
src/Contracts/FabricMessageTypes.ts
Normal file
13
src/Contracts/FabricMessageTypes.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Data Explorer -> Fabric communication.
|
||||
*/
|
||||
export enum FabricMessageTypes {
|
||||
GetAuthorizationToken = "GetAuthorizationToken",
|
||||
GetAllResourceTokens = "GetAllResourceTokens",
|
||||
Ready = "Ready",
|
||||
}
|
||||
|
||||
export interface AuthorizationToken {
|
||||
XDate: string;
|
||||
PrimaryReadWriteToken: string;
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
import { AuthorizationToken, MessageTypes } from "./MessageTypes";
|
||||
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||
|
||||
export type FabricMessage =
|
||||
// This is the version of these messages
|
||||
export const FABRIC_RPC_VERSION = "2";
|
||||
|
||||
// Fabric to Data Explorer
|
||||
|
||||
// TODO Deprecated. Remove this section once DE is updated
|
||||
export type FabricMessageV1 =
|
||||
| {
|
||||
type: "newContainer";
|
||||
databaseName: string;
|
||||
@@ -26,38 +32,53 @@ export type FabricMessage =
|
||||
| {
|
||||
type: "allResourceTokens";
|
||||
message: {
|
||||
id: string;
|
||||
error: string | undefined;
|
||||
endpoint: string | undefined;
|
||||
databaseId: string | undefined;
|
||||
resourceTokens: unknown | undefined;
|
||||
resourceTokensTimestamp: number | undefined;
|
||||
};
|
||||
};
|
||||
// -----------------------------
|
||||
|
||||
export type DataExploreMessage =
|
||||
| "ready"
|
||||
export type FabricMessageV2 =
|
||||
| {
|
||||
type: MessageTypes.TelemetryInfo;
|
||||
data: {
|
||||
action: "LoadDatabases";
|
||||
actionModifier: "success" | "start";
|
||||
defaultExperience: "SQL";
|
||||
type: "newContainer";
|
||||
databaseName: string;
|
||||
}
|
||||
| {
|
||||
type: "initialize";
|
||||
version: string;
|
||||
id: string;
|
||||
message: {
|
||||
connectionId: string;
|
||||
isVisible: boolean;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: MessageTypes.GetAuthorizationToken;
|
||||
id: string;
|
||||
params: GetCosmosTokenMessageOptions[];
|
||||
type: "authorizationToken";
|
||||
message: {
|
||||
id: string;
|
||||
error: string | undefined;
|
||||
data: AuthorizationToken | undefined;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: MessageTypes.GetAllResourceTokens;
|
||||
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 = {
|
||||
token: string;
|
||||
date: string;
|
||||
@@ -66,12 +87,9 @@ export type CosmosDBTokenResponse = {
|
||||
export type CosmosDBConnectionInfoResponse = {
|
||||
endpoint: string;
|
||||
databaseId: string;
|
||||
resourceTokens: unknown;
|
||||
resourceTokens: { [resourceId: string]: string };
|
||||
};
|
||||
|
||||
export interface FabricDatabaseConnectionInfo {
|
||||
endpoint: string;
|
||||
databaseId: string;
|
||||
resourceTokens: { [resourceId: string]: string };
|
||||
export interface FabricDatabaseConnectionInfo extends CosmosDBConnectionInfoResponse {
|
||||
resourceTokensTimestamp: number;
|
||||
}
|
||||
@@ -1,6 +1,13 @@
|
||||
/**
|
||||
* Messaging types used with Data Explorer <-> Portal communication,
|
||||
* Hosted <-> Explorer communication and Data Explorer -> Fabric communication.
|
||||
* Hosted <-> Explorer 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 {
|
||||
TelemetryInfo,
|
||||
@@ -37,13 +44,9 @@ export enum MessageTypes {
|
||||
DisplayNPSSurvey,
|
||||
OpenVCoreMongoNetworkingBlade,
|
||||
OpenVCoreMongoConnectionStringsBlade,
|
||||
|
||||
// Data Explorer -> Fabric communication
|
||||
GetAuthorizationToken,
|
||||
GetAllResourceTokens,
|
||||
}
|
||||
|
||||
export interface AuthorizationToken {
|
||||
XDate: string;
|
||||
PrimaryReadWriteToken: string;
|
||||
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.
|
||||
Ready, // unused. Can be removed if the portal uses the same list of enums.
|
||||
OpenCESCVAFeedbackBlade,
|
||||
ActivateTab,
|
||||
}
|
||||
|
||||
@@ -135,6 +135,7 @@ export interface Collection extends CollectionBase {
|
||||
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
||||
geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
|
||||
documentIds: ko.ObservableArray<DocumentId>;
|
||||
computedProperties: ko.Observable<DataModels.ComputedProperties>;
|
||||
|
||||
cassandraKeys: CassandraTableKeys;
|
||||
cassandraSchema: CassandraTableKey[];
|
||||
@@ -175,6 +176,11 @@ export interface Collection extends CollectionBase {
|
||||
loadTriggers(): Promise<any>;
|
||||
loadOffer(): Promise<void>;
|
||||
|
||||
showStoredProcedures: ko.Observable<boolean>;
|
||||
showTriggers: ko.Observable<boolean>;
|
||||
showUserDefinedFunctions: ko.Observable<boolean>;
|
||||
showConflicts: ko.Observable<boolean>;
|
||||
|
||||
createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure;
|
||||
createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction;
|
||||
createTriggerNode(data: TriggerDefinition | SqlTriggerResource): Trigger;
|
||||
@@ -323,9 +329,9 @@ export enum DocumentExplorerState {
|
||||
noDocumentSelected,
|
||||
newDocumentValid,
|
||||
newDocumentInvalid,
|
||||
exisitingDocumentNoEdits,
|
||||
exisitingDocumentDirtyValid,
|
||||
exisitingDocumentDirtyInvalid,
|
||||
existingDocumentNoEdits,
|
||||
existingDocumentDirtyValid,
|
||||
existingDocumentDirtyInvalid,
|
||||
}
|
||||
|
||||
export enum IndexingPolicyEditorState {
|
||||
@@ -338,9 +344,9 @@ export enum IndexingPolicyEditorState {
|
||||
export enum ScriptEditorState {
|
||||
newInvalid,
|
||||
newValid,
|
||||
exisitingNoEdits,
|
||||
exisitingDirtyValid,
|
||||
exisitingDirtyInvalid,
|
||||
existingNoEdits,
|
||||
existingDirtyValid,
|
||||
existingDirtyInvalid,
|
||||
}
|
||||
|
||||
export enum CollectionTabKind {
|
||||
@@ -386,9 +392,11 @@ export interface DataExplorerInputsFrame {
|
||||
dnsSuffix?: string;
|
||||
serverId?: string;
|
||||
extensionEndpoint?: string;
|
||||
portalBackendEndpoint?: string;
|
||||
mongoProxyEndpoint?: string;
|
||||
cassandraProxyEndpoint?: string;
|
||||
subscriptionType?: SubscriptionType;
|
||||
quotaId?: string;
|
||||
addCollectionDefaultFlight?: string;
|
||||
isTryCosmosDBSubscription?: boolean;
|
||||
loadDatabaseAccountTimestamp?: number;
|
||||
sharedThroughputMinimum?: number;
|
||||
@@ -406,6 +414,7 @@ export interface DataExplorerInputsFrame {
|
||||
features?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
feedbackPolicies?: any;
|
||||
}
|
||||
|
||||
export interface SelfServeFrameInputs {
|
||||
@@ -416,6 +425,7 @@ export interface SelfServeFrameInputs {
|
||||
authorizationToken: string;
|
||||
csmEndpoint: string;
|
||||
flights?: readonly string[];
|
||||
catalogAPIKey: string;
|
||||
}
|
||||
|
||||
export class MonacoEditorSettings {
|
||||
|
||||
@@ -36,21 +36,21 @@ describe("The Heatmap Control", () => {
|
||||
});
|
||||
|
||||
it("should call _getChartSettings when drawHeatmap is invoked", () => {
|
||||
const _getChartSettings = spyOn<any>(heatmap, "_getChartSettings");
|
||||
const _getChartSettings = jest.spyOn(heatmap, "_getChartSettings");
|
||||
heatmap.drawHeatmap();
|
||||
expect(_getChartSettings.calls.any()).toBe(true);
|
||||
expect(_getChartSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call _getLayoutSettings when drawHeatmap is invoked", () => {
|
||||
const _getLayoutSettings = spyOn<any>(heatmap, "_getLayoutSettings");
|
||||
const _getLayoutSettings = jest.spyOn(heatmap, "_getLayoutSettings");
|
||||
heatmap.drawHeatmap();
|
||||
expect(_getLayoutSettings.calls.any()).toBe(true);
|
||||
expect(_getLayoutSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call _getChartDisplaySettings when drawHeatmap is invoked", () => {
|
||||
const _getChartDisplaySettings = spyOn<any>(heatmap, "_getChartDisplaySettings");
|
||||
const _getChartDisplaySettings = jest.spyOn(heatmap, "_getChartDisplaySettings");
|
||||
heatmap.drawHeatmap();
|
||||
expect(_getChartDisplaySettings.calls.any()).toBe(true);
|
||||
expect(_getChartDisplaySettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("drawHeatmap should render a Heatmap inside the div element", () => {
|
||||
|
||||
@@ -96,7 +96,8 @@ export class Heatmap {
|
||||
return output;
|
||||
}
|
||||
|
||||
private _getChartSettings(): ChartSettings[] {
|
||||
// public for testing purposes
|
||||
public _getChartSettings(): ChartSettings[] {
|
||||
return [
|
||||
{
|
||||
z: this._chartData.dataPoints,
|
||||
@@ -131,7 +132,8 @@ export class Heatmap {
|
||||
];
|
||||
}
|
||||
|
||||
private _getLayoutSettings(): LayoutSettings {
|
||||
// public for testing purposes
|
||||
public _getLayoutSettings(): LayoutSettings {
|
||||
return {
|
||||
margin: {
|
||||
l: 40,
|
||||
@@ -177,7 +179,8 @@ export class Heatmap {
|
||||
};
|
||||
}
|
||||
|
||||
private _getChartDisplaySettings(): DisplaySettings {
|
||||
// public for testing purposes
|
||||
public _getChartDisplaySettings(): DisplaySettings {
|
||||
return {
|
||||
/* heatmap can be fully responsive however the min-height needed in that case is greater than the iframe portal height, hence explicit width + height have been set in _getLayoutSettings
|
||||
responsive: true,*/
|
||||
|
||||
1954
src/Definitions/datatables.d.ts
vendored
1954
src/Definitions/datatables.d.ts
vendored
File diff suppressed because it is too large
Load Diff
2
src/Definitions/jquery-typescript.d.ts
vendored
2
src/Definitions/jquery-typescript.d.ts
vendored
@@ -5,7 +5,7 @@
|
||||
* https://github.com/running-coder/jquery-typeahead/issues/156
|
||||
* TODO: Replace this minimum definition by the official one when it comes out.
|
||||
*/
|
||||
/// <reference path="jquery.d.ts" />
|
||||
/// <reference types="jquery" />
|
||||
|
||||
interface JQueryTypeaheadParam {
|
||||
input: string;
|
||||
|
||||
2
src/Definitions/jquery-ui.d.ts
vendored
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: https://github.com/borisyankov/DefinitelyTyped
|
||||
|
||||
/// <reference path="jquery.d.ts"/>
|
||||
/// <reference types="jquery"/>
|
||||
|
||||
declare namespace JQueryUI {
|
||||
// Accordion //////////////////////////////////////////////////
|
||||
|
||||
1890
src/Definitions/jquery.d.ts
vendored
1890
src/Definitions/jquery.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||
@@ -19,7 +20,6 @@ import { userContext } from "../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { Platform, configContext } from "./../ConfigContext";
|
||||
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
|
||||
import Explorer from "./Explorer";
|
||||
import { useNotebook } from "./Notebook/useNotebook";
|
||||
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
|
||||
@@ -41,6 +41,10 @@ export interface DatabaseContextMenuButtonParams {
|
||||
* New resource tree (in ReactJS)
|
||||
*/
|
||||
export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => {
|
||||
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const items: TreeNodeMenuItem[] = [
|
||||
{
|
||||
iconSrc: AddCollectionIcon,
|
||||
@@ -100,6 +104,16 @@ export const createCollectionContextMenuButton = (
|
||||
});
|
||||
}
|
||||
|
||||
if (useNotebook.getState().isShellEnabled && userContext.apiType === "Cassandra") {
|
||||
items.push({
|
||||
iconSrc: HostedTerminalIcon,
|
||||
onClick: () => {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
||||
},
|
||||
label: "Open Cassandra Shell",
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
configContext.platform !== Platform.Fabric &&
|
||||
(userContext.apiType === "SQL" || userContext.apiType === "Gremlin")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Icon, Label, Stack } from "@fluentui/react";
|
||||
import { DirectionalHint, Icon, Label, Stack, TooltipHost } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { NormalizedEventKey } from "../../../Common/Constants";
|
||||
import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
|
||||
@@ -8,6 +8,7 @@ export interface CollapsibleSectionProps {
|
||||
isExpandedByDefault: boolean;
|
||||
onExpand?: () => void;
|
||||
children: JSX.Element;
|
||||
tooltipContent?: string | JSX.Element | JSX.Element[];
|
||||
}
|
||||
|
||||
export interface CollapsibleSectionState {
|
||||
@@ -26,8 +27,8 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
||||
this.setState({ isExpanded: !this.state.isExpanded });
|
||||
};
|
||||
|
||||
public componentDidUpdate(): void {
|
||||
if (this.state.isExpanded && this.props.onExpand) {
|
||||
public componentDidUpdate(_prevProps: CollapsibleSectionProps, prevState: CollapsibleSectionState): void {
|
||||
if (!prevState.isExpanded && this.state.isExpanded && this.props.onExpand) {
|
||||
this.props.onExpand();
|
||||
}
|
||||
}
|
||||
@@ -43,7 +44,7 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
className="collapsibleSection"
|
||||
className={"collapsibleSection"}
|
||||
horizontal
|
||||
verticalAlign="center"
|
||||
tokens={accordionStackTokens}
|
||||
@@ -55,6 +56,19 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
||||
>
|
||||
<Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} />
|
||||
<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>
|
||||
{this.state.isExpanded && this.props.children}
|
||||
</>
|
||||
|
||||
@@ -11,7 +11,7 @@ exports[`CollapsibleSectionComponent renders 1`] = `
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* React component for Command button component.
|
||||
*/
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import * as React from "react";
|
||||
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
|
||||
import { KeyCodes } from "../../../Common/Constants";
|
||||
@@ -30,7 +31,7 @@ export interface CommandButtonComponentProps {
|
||||
/**
|
||||
* Click handler for command button click
|
||||
*/
|
||||
onCommandClick: (e: React.SyntheticEvent) => void;
|
||||
onCommandClick?: (e: React.SyntheticEvent | KeyboardEvent) => void;
|
||||
|
||||
/**
|
||||
* Label for the button
|
||||
@@ -107,10 +108,17 @@ export interface CommandButtonComponentProps {
|
||||
* Vertical bar to divide buttons
|
||||
*/
|
||||
isDivider?: boolean;
|
||||
|
||||
/**
|
||||
* Aria-label for the button
|
||||
*/
|
||||
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> {
|
||||
|
||||
@@ -3,6 +3,37 @@ import * as React from "react";
|
||||
import { loadMonaco, monaco } from "../../LazyMonaco";
|
||||
// import "./EditorReact.less";
|
||||
|
||||
// In development, add a function to window to allow us to get the editor instance for a given element
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const win = window as any;
|
||||
win._monaco_getEditorForElement =
|
||||
win._monaco_getEditorForElement ||
|
||||
((element: HTMLElement) => {
|
||||
const editorId = element.dataset["monacoEditorId"];
|
||||
if (!editorId || !win.__monaco_editors || typeof win.__monaco_editors !== "object") {
|
||||
return null;
|
||||
}
|
||||
return win.__monaco_editors[editorId];
|
||||
});
|
||||
|
||||
win._monaco_getEditorContentForElement =
|
||||
win._monaco_getEditorContentForElement ||
|
||||
((element: HTMLElement) => {
|
||||
const editor = win._monaco_getEditorForElement(element);
|
||||
return editor ? editor.getValue() : null;
|
||||
});
|
||||
|
||||
win._monaco_setEditorContentForElement =
|
||||
win._monaco_setEditorContentForElement ||
|
||||
((element: HTMLElement, text: string) => {
|
||||
const editor = win._monaco_getEditorForElement(element);
|
||||
if (editor) {
|
||||
editor.setValue(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
interface EditorReactStates {
|
||||
showEditor: boolean;
|
||||
}
|
||||
@@ -11,7 +42,7 @@ export interface EditorReactProps {
|
||||
content: string;
|
||||
isReadOnly: boolean;
|
||||
ariaLabel: string; // Sets what will be read to the user to define the control
|
||||
onContentSelected?: (selectedContent: string) => void; // Called when text is selected
|
||||
onContentSelected?: (selectedContent: string, selection: monaco.Selection) => void; // Called when text is selected
|
||||
onContentChanged?: (newContent: string) => void; // Called when text is changed
|
||||
theme?: string; // Monaco editor theme
|
||||
wordWrap?: monaco.editor.IEditorOptions["wordWrap"];
|
||||
@@ -20,13 +51,38 @@ export interface EditorReactProps {
|
||||
lineDecorationsWidth?: monaco.editor.IEditorOptions["lineDecorationsWidth"];
|
||||
minimap?: monaco.editor.IEditorOptions["minimap"];
|
||||
scrollBeyondLastLine?: monaco.editor.IEditorOptions["scrollBeyondLastLine"];
|
||||
fontSize?: monaco.editor.IEditorOptions["fontSize"];
|
||||
monacoContainerStyles?: React.CSSProperties;
|
||||
className?: string;
|
||||
spinnerClassName?: string;
|
||||
|
||||
modelMarkers?: monaco.editor.IMarkerData[];
|
||||
enableWordWrapContextMenuItem?: boolean; // Enable/Disable "Word Wrap" context menu item
|
||||
onWordWrapChanged?: (wordWrap: "on" | "off") => void; // Called when word wrap is changed
|
||||
}
|
||||
|
||||
export class EditorReact extends React.Component<EditorReactProps, EditorReactStates> {
|
||||
private static readonly VIEWING_OPTIONS_GROUP_ID = "viewingoptions"; // Group ID for the context menu group
|
||||
private rootNode: HTMLElement;
|
||||
private editor: monaco.editor.IStandaloneCodeEditor;
|
||||
public editor: monaco.editor.IStandaloneCodeEditor;
|
||||
private selectionListener: monaco.IDisposable;
|
||||
monacoApi: {
|
||||
default: typeof monaco;
|
||||
Emitter: typeof monaco.Emitter;
|
||||
MarkerTag: typeof monaco.MarkerTag;
|
||||
MarkerSeverity: typeof monaco.MarkerSeverity;
|
||||
CancellationTokenSource: typeof monaco.CancellationTokenSource;
|
||||
Uri: typeof monaco.Uri;
|
||||
KeyCode: typeof monaco.KeyCode;
|
||||
KeyMod: typeof monaco.KeyMod;
|
||||
Position: typeof monaco.Position;
|
||||
Range: typeof monaco.Range;
|
||||
Selection: typeof monaco.Selection;
|
||||
SelectionDirection: typeof monaco.SelectionDirection;
|
||||
Token: typeof monaco.Token;
|
||||
editor: typeof monaco.editor;
|
||||
languages: typeof monaco.languages;
|
||||
};
|
||||
|
||||
public constructor(props: EditorReactProps) {
|
||||
super(props);
|
||||
@@ -46,10 +102,28 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||
}, 100);
|
||||
}
|
||||
|
||||
public componentDidUpdate(previous: EditorReactProps) {
|
||||
if (this.props.content !== previous.content) {
|
||||
this.editor?.setValue(this.props.content);
|
||||
public componentDidUpdate() {
|
||||
if (!this.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingContent = this.editor.getModel().getValue();
|
||||
|
||||
if (this.props.content !== existingContent) {
|
||||
if (this.props.isReadOnly) {
|
||||
this.editor.setValue(this.props.content || ""); // Monaco throws an error if you set the value to undefined.
|
||||
} else {
|
||||
this.editor.pushUndoStop();
|
||||
this.editor.executeEdits("", [
|
||||
{
|
||||
range: this.editor.getModel().getFullModelRange(),
|
||||
text: this.props.content,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
this.monacoApi.editor.setModelMarkers(this.editor.getModel(), "owner", this.props.modelMarkers || []);
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
@@ -59,9 +133,12 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{!this.state.showEditor && <Spinner size={SpinnerSize.large} className="spinner" />}
|
||||
{!this.state.showEditor && (
|
||||
<Spinner size={SpinnerSize.large} className={this.props.spinnerClassName || "spinner"} />
|
||||
)}
|
||||
<div
|
||||
className="jsonEditor"
|
||||
data-test="EditorReact/Host/Unloaded"
|
||||
className={this.props.className || "jsonEditor"}
|
||||
style={this.props.monacoContainerStyles}
|
||||
ref={(elt: HTMLElement) => this.setRef(elt)}
|
||||
/>
|
||||
@@ -71,9 +148,26 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||
|
||||
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
|
||||
this.editor = editor;
|
||||
const queryEditorModel = this.editor.getModel();
|
||||
this.rootNode.dataset["test"] = "EditorReact/Host/Loaded";
|
||||
|
||||
// In development, we want to be able to access the editor instance from the console
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
this.rootNode.dataset["monacoEditorId"] = this.editor.getId();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const win = window as any;
|
||||
|
||||
win["__monaco_editors"] = win["__monaco_editors"] || {};
|
||||
win["__monaco_editors"][this.editor.getId()] = this.editor;
|
||||
}
|
||||
|
||||
if (!this.props.isReadOnly && this.props.onContentChanged) {
|
||||
queryEditorModel.onDidChangeContent(() => {
|
||||
// Hooking the model's onDidChangeContent event because of some event ordering issues.
|
||||
// If a single user input causes BOTH the editor content to change AND the cursor selection to change (which is likely),
|
||||
// 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();
|
||||
this.props.onContentChanged(queryEditorModel.getValue());
|
||||
});
|
||||
@@ -83,10 +177,27 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||
this.selectionListener = this.editor.onDidChangeCursorSelection(
|
||||
(event: monaco.editor.ICursorSelectionChangedEvent) => {
|
||||
const selectedContent: string = this.editor.getModel().getValueInRange(event.selection);
|
||||
this.props.onContentSelected(selectedContent);
|
||||
this.props.onContentSelected(selectedContent, event.selection);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.enableWordWrapContextMenuItem) {
|
||||
editor.addAction({
|
||||
// An unique identifier of the contributed action.
|
||||
id: "wordwrap",
|
||||
label: "Toggle Word Wrap",
|
||||
contextMenuGroupId: EditorReact.VIEWING_OPTIONS_GROUP_ID,
|
||||
contextMenuOrder: 1,
|
||||
// Method that will be executed when the action is triggered.
|
||||
// @param editor The editor instance is passed in as a convenience
|
||||
run: (ed) => {
|
||||
const newOption = ed.getOption(this.monacoApi.editor.EditorOption.wordWrap) === "on" ? "off" : "on";
|
||||
ed.updateOptions({ wordWrap: newOption });
|
||||
this.props.onWordWrapChanged(newOption);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,7 +209,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||
value: this.props.content,
|
||||
readOnly: this.props.isReadOnly,
|
||||
ariaLabel: this.props.ariaLabel,
|
||||
fontSize: 12,
|
||||
fontSize: this.props.fontSize || 12,
|
||||
automaticLayout: true,
|
||||
theme: this.props.theme,
|
||||
wordWrap: this.props.wordWrap || "off",
|
||||
@@ -107,11 +218,19 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
|
||||
lineDecorationsWidth: this.props.lineDecorationsWidth,
|
||||
minimap: this.props.minimap,
|
||||
scrollBeyondLastLine: this.props.scrollBeyondLastLine,
|
||||
fixedOverflowWidgets: true,
|
||||
};
|
||||
|
||||
this.rootNode.innerHTML = "";
|
||||
const monaco = await loadMonaco();
|
||||
createCallback(monaco?.editor?.create(this.rootNode, options));
|
||||
this.monacoApi = await loadMonaco();
|
||||
|
||||
try {
|
||||
createCallback(this.monacoApi.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) {
|
||||
this.setState({
|
||||
|
||||
@@ -18,7 +18,7 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
<Stack
|
||||
className="options"
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
horizontal={true}
|
||||
horizontalAlign="space-between"
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
horizontal={true}
|
||||
horizontalAlign="start"
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
@@ -61,16 +61,16 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
label="Base Url"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
[
|
||||
{
|
||||
"key": "https://localhost:1234/explorer.html",
|
||||
"text": "localhost:1234",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"key": "https://cosmos.azure.com/explorer.html",
|
||||
"text": "cosmos.azure.com",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"key": "https://portal.azure.com",
|
||||
"text": "portal",
|
||||
},
|
||||
@@ -78,8 +78,8 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
}
|
||||
selectedKey="https://localhost:1234/explorer.html"
|
||||
styles={
|
||||
Object {
|
||||
"dropdown": Object {
|
||||
{
|
||||
"dropdown": {
|
||||
"width": 200,
|
||||
},
|
||||
}
|
||||
@@ -89,20 +89,20 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
label="Platform"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
[
|
||||
{
|
||||
"key": "Hosted",
|
||||
"text": "Hosted",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"key": "Portal",
|
||||
"text": "Portal",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"key": "Emulator",
|
||||
"text": "Emulator",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"key": "",
|
||||
"text": "None",
|
||||
},
|
||||
@@ -110,8 +110,8 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
}
|
||||
selectedKey="Hosted"
|
||||
styles={
|
||||
Object {
|
||||
"dropdown": Object {
|
||||
{
|
||||
"dropdown": {
|
||||
"width": 200,
|
||||
},
|
||||
}
|
||||
@@ -208,7 +208,7 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
@@ -222,8 +222,8 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
onChange={[Function]}
|
||||
placeholder="https://notebookserver"
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
{
|
||||
"fieldGroup": {
|
||||
"width": 300,
|
||||
},
|
||||
}
|
||||
@@ -235,8 +235,8 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
onChange={[Function]}
|
||||
placeholder=""
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
{
|
||||
"fieldGroup": {
|
||||
"width": 300,
|
||||
},
|
||||
}
|
||||
@@ -248,8 +248,8 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
onChange={[Function]}
|
||||
placeholder=""
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
{
|
||||
"fieldGroup": {
|
||||
"width": 300,
|
||||
},
|
||||
}
|
||||
@@ -265,8 +265,8 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
onChange={[Function]}
|
||||
placeholder=""
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
{
|
||||
"fieldGroup": {
|
||||
"width": 300,
|
||||
},
|
||||
}
|
||||
@@ -279,8 +279,8 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
onChange={[Function]}
|
||||
placeholder="https://localhost:1234/explorer.html"
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
{
|
||||
"fieldGroup": {
|
||||
"width": 300,
|
||||
},
|
||||
}
|
||||
@@ -292,8 +292,8 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
onChange={[Function]}
|
||||
placeholder=""
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
{
|
||||
"fieldGroup": {
|
||||
"width": 300,
|
||||
},
|
||||
}
|
||||
|
||||
37
src/Explorer/Controls/IndeterminateProgressBar.tsx
Normal file
37
src/Explorer/Controls/IndeterminateProgressBar.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ProgressBar, makeStyles } from "@fluentui/react-components";
|
||||
import React from "react";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
indeterminateProgressBarRoot: {
|
||||
"@media screen and (prefers-reduced-motion: reduce)": {
|
||||
animationIterationCount: "infinite",
|
||||
animationDuration: "3s",
|
||||
animationName: {
|
||||
"0%": {
|
||||
opacity: ".2", // matches indeterminate bar width
|
||||
},
|
||||
"50%": {
|
||||
opacity: "1",
|
||||
},
|
||||
"100%": {
|
||||
opacity: ".2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
indeterminateProgressBarBar: {
|
||||
"@media screen and (prefers-reduced-motion: reduce)": {
|
||||
maxWidth: "100%",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const IndeterminateProgressBar: React.FC = () => {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<ProgressBar
|
||||
bar={{ className: styles.indeterminateProgressBarBar }}
|
||||
className={styles.indeterminateProgressBarRoot}
|
||||
/>
|
||||
);
|
||||
};
|
||||
68
src/Explorer/Controls/MessageBanner.tsx
Normal file
68
src/Explorer/Controls/MessageBanner.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { Button, MessageBar, MessageBarActions, MessageBarBody } from "@fluentui/react-components";
|
||||
import { DismissRegular } from "@fluentui/react-icons";
|
||||
import React, { useState } from "react";
|
||||
|
||||
export enum MessageBannerState {
|
||||
/** The banner should be visible if the triggering conditions are met. */
|
||||
Allowed = "allowed",
|
||||
|
||||
/** The banner has been dismissed by the user and will not be shown until the component is recreated, even if the visibility condition is true. */
|
||||
Dismissed = "dismissed",
|
||||
|
||||
/** The banner has been supressed by the user and will not be shown at all, even if the visibility condition is true. */
|
||||
Suppressed = "suppressed",
|
||||
}
|
||||
|
||||
export type MessageBannerProps = {
|
||||
/** A CSS class for the root MessageBar component */
|
||||
className: string;
|
||||
|
||||
/** A unique ID for the message that will be used to store it's dismiss/suppress state across sessions. */
|
||||
messageId: string;
|
||||
|
||||
/** The current visibility state for the banner IGNORING the user's dimiss/suppress preference
|
||||
*
|
||||
* If this value is true but the user has dismissed the banner, the banner will NOT be shown.
|
||||
*/
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
/** A component that shows a message banner which can be dismissed by the user.
|
||||
*
|
||||
* In the future, this can also support persisting the dismissed state in local storage without requiring changes to all the components that use it.
|
||||
*
|
||||
* A message banner can be in three "states":
|
||||
* - Allowed: The banner should be visible if the triggering conditions are met.
|
||||
* - Dismissed: The banner has been dismissed by the user and will not be shown until the component is recreated, even if the visibility condition is true.
|
||||
* - Suppressed: The banner has been supressed by the user and will not be shown at all, even if the visibility condition is true.
|
||||
*
|
||||
* The "Dismissed" state represents the user clicking the "x" in the banner to dismiss it.
|
||||
* The "Suppressed" state represents the user clicking "Don't show this again".
|
||||
*/
|
||||
export const MessageBanner: React.FC<MessageBannerProps> = ({ visible, className, children }) => {
|
||||
const [state, setState] = useState<MessageBannerState>(MessageBannerState.Allowed);
|
||||
|
||||
if (state !== MessageBannerState.Allowed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageBar className={className}>
|
||||
<MessageBarBody>{children}</MessageBarBody>
|
||||
<MessageBarActions
|
||||
containerAction={
|
||||
<Button
|
||||
aria-label="dismiss"
|
||||
appearance="transparent"
|
||||
icon={<DismissRegular />}
|
||||
onClick={() => setState(MessageBannerState.Dismissed)}
|
||||
/>
|
||||
}
|
||||
></MessageBarActions>
|
||||
</MessageBar>
|
||||
);
|
||||
};
|
||||
@@ -4,8 +4,8 @@ exports[`GalleryCardComponent renders 1`] = `
|
||||
<StyledDocumentCardBase
|
||||
aria-label="name"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
{
|
||||
"root": {
|
||||
"display": "inline-block",
|
||||
"marginRight": 20,
|
||||
"width": 256,
|
||||
@@ -16,8 +16,8 @@ exports[`GalleryCardComponent renders 1`] = `
|
||||
<StyledDocumentCardActivityBase
|
||||
activity="Invalid Date"
|
||||
people={
|
||||
Array [
|
||||
Object {
|
||||
[
|
||||
{
|
||||
"name": "author",
|
||||
"profileImageSrc": false,
|
||||
},
|
||||
@@ -26,8 +26,8 @@ exports[`GalleryCardComponent renders 1`] = `
|
||||
/>
|
||||
<StyledDocumentCardPreviewBase
|
||||
previewImages={
|
||||
Array [
|
||||
Object {
|
||||
[
|
||||
{
|
||||
"height": 144,
|
||||
"imageFit": 2,
|
||||
"previewImageSrc": "thumbnailUrl",
|
||||
@@ -40,8 +40,8 @@ exports[`GalleryCardComponent renders 1`] = `
|
||||
<Text
|
||||
nowrap={true}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
{
|
||||
"root": {
|
||||
"height": 18,
|
||||
"padding": "2px 16px",
|
||||
},
|
||||
@@ -69,15 +69,15 @@ exports[`GalleryCardComponent renders 1`] = `
|
||||
/>
|
||||
<span
|
||||
style={
|
||||
Object {
|
||||
{
|
||||
"padding": "8px 16px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
{
|
||||
"root": {
|
||||
"color": "#605E5C",
|
||||
"paddingRight": 8,
|
||||
},
|
||||
@@ -88,8 +88,8 @@ exports[`GalleryCardComponent renders 1`] = `
|
||||
<Icon
|
||||
iconName="RedEye"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
{
|
||||
"root": {
|
||||
"verticalAlign": "middle",
|
||||
},
|
||||
}
|
||||
@@ -100,8 +100,8 @@ exports[`GalleryCardComponent renders 1`] = `
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
{
|
||||
"root": {
|
||||
"color": "#605E5C",
|
||||
"paddingRight": 8,
|
||||
},
|
||||
@@ -112,8 +112,8 @@ exports[`GalleryCardComponent renders 1`] = `
|
||||
<Icon
|
||||
iconName="Download"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
{
|
||||
"root": {
|
||||
"verticalAlign": "middle",
|
||||
},
|
||||
}
|
||||
@@ -124,8 +124,8 @@ exports[`GalleryCardComponent renders 1`] = `
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
{
|
||||
"root": {
|
||||
"color": "#605E5C",
|
||||
"paddingRight": 8,
|
||||
},
|
||||
@@ -136,8 +136,8 @@ exports[`GalleryCardComponent renders 1`] = `
|
||||
<Icon
|
||||
iconName="Heart"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
{
|
||||
"root": {
|
||||
"verticalAlign": "middle",
|
||||
},
|
||||
}
|
||||
@@ -151,8 +151,8 @@ exports[`GalleryCardComponent renders 1`] = `
|
||||
<StyledDocumentCardDetailsBase>
|
||||
<Separator
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
{
|
||||
"root": {
|
||||
"height": 1,
|
||||
"padding": 0,
|
||||
},
|
||||
@@ -161,22 +161,22 @@ exports[`GalleryCardComponent renders 1`] = `
|
||||
/>
|
||||
<span
|
||||
style={
|
||||
Object {
|
||||
{
|
||||
"padding": "0px 16px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledTooltipHostBase
|
||||
calloutProps={
|
||||
Object {
|
||||
{
|
||||
"gapSpace": 0,
|
||||
}
|
||||
}
|
||||
content="Favorite"
|
||||
id="TooltipHost-IconButton-Heart"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
{
|
||||
"root": {
|
||||
"display": "inline-block",
|
||||
"float": "left",
|
||||
},
|
||||
@@ -186,7 +186,7 @@ exports[`GalleryCardComponent renders 1`] = `
|
||||
<CustomizedIconButton
|
||||
ariaLabel="Favorite"
|
||||
iconProps={
|
||||
Object {
|
||||
{
|
||||
"iconName": "Heart",
|
||||
}
|
||||
}
|
||||
@@ -196,15 +196,15 @@ exports[`GalleryCardComponent renders 1`] = `
|
||||
</StyledTooltipHostBase>
|
||||
<StyledTooltipHostBase
|
||||
calloutProps={
|
||||
Object {
|
||||
{
|
||||
"gapSpace": 0,
|
||||
}
|
||||
}
|
||||
content="Download"
|
||||
id="TooltipHost-IconButton-Download"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
{
|
||||
"root": {
|
||||
"display": "inline-block",
|
||||
"float": "left",
|
||||
},
|
||||
@@ -214,7 +214,7 @@ exports[`GalleryCardComponent renders 1`] = `
|
||||
<CustomizedIconButton
|
||||
ariaLabel="Download"
|
||||
iconProps={
|
||||
Object {
|
||||
{
|
||||
"iconName": "Download",
|
||||
}
|
||||
}
|
||||
@@ -224,15 +224,15 @@ exports[`GalleryCardComponent renders 1`] = `
|
||||
</StyledTooltipHostBase>
|
||||
<StyledTooltipHostBase
|
||||
calloutProps={
|
||||
Object {
|
||||
{
|
||||
"gapSpace": 0,
|
||||
}
|
||||
}
|
||||
content="Remove"
|
||||
id="TooltipHost-IconButton-Delete"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
{
|
||||
"root": {
|
||||
"display": "inline-block",
|
||||
"float": "right",
|
||||
},
|
||||
@@ -242,7 +242,7 @@ exports[`GalleryCardComponent renders 1`] = `
|
||||
<CustomizedIconButton
|
||||
ariaLabel="Remove"
|
||||
iconProps={
|
||||
Object {
|
||||
{
|
||||
"iconName": "Delete",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
exports[`CodeOfConduct renders 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 20,
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ exports[`CodeOfConduct renders 1`] = `
|
||||
<StackItem>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
{
|
||||
"fontSize": "20px",
|
||||
"fontWeight": 500,
|
||||
}
|
||||
@@ -41,12 +41,12 @@ exports[`CodeOfConduct renders 1`] = `
|
||||
label="I have read and accept the code of conduct."
|
||||
onChange={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"label": Object {
|
||||
{
|
||||
"label": {
|
||||
"margin": 0,
|
||||
"padding": "2 0 2 0",
|
||||
},
|
||||
"text": Object {
|
||||
"text": {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ exports[`InfoComponent renders 1`] = `
|
||||
<StyledHoverCardBase
|
||||
instantOpenOnClick={true}
|
||||
plainCardProps={
|
||||
Object {
|
||||
{
|
||||
"onRenderPlainCard": [Function],
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,8 @@ exports[`InfoComponent renders 1`] = `
|
||||
className="infoIconMain"
|
||||
iconName="Help"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
{
|
||||
"root": {
|
||||
"verticalAlign": "middle",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -13,14 +13,14 @@ exports[`GalleryViewerComponent renders 1`] = `
|
||||
itemKey="OfficialSamples"
|
||||
key="OfficialSamples"
|
||||
style={
|
||||
Object {
|
||||
{
|
||||
"marginTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ exports[`GalleryViewerComponent renders 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 20,
|
||||
"padding": 10,
|
||||
}
|
||||
@@ -50,8 +50,8 @@ exports[`GalleryViewerComponent renders 1`] = `
|
||||
</StackItem>
|
||||
<StackItem
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
{
|
||||
"root": {
|
||||
"minWidth": 200,
|
||||
},
|
||||
}
|
||||
@@ -60,20 +60,20 @@ exports[`GalleryViewerComponent renders 1`] = `
|
||||
<Dropdown
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
[
|
||||
{
|
||||
"key": 0,
|
||||
"text": "Most viewed",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"key": 1,
|
||||
"text": "Most downloaded",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"key": 3,
|
||||
"text": "Most recent",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"key": 2,
|
||||
"text": "Most favorited",
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
exports[`NotebookMetadataComponent renders liked notebook 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ exports[`NotebookMetadataComponent renders liked notebook 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 30,
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ exports[`NotebookMetadataComponent renders liked notebook 1`] = `
|
||||
<Text>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
{
|
||||
"iconName": "HeartFill",
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ exports[`NotebookMetadataComponent renders liked notebook 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
@@ -96,8 +96,8 @@ exports[`NotebookMetadataComponent renders liked notebook 1`] = `
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
{
|
||||
"root": {
|
||||
"fontWeight": 600,
|
||||
},
|
||||
}
|
||||
@@ -115,7 +115,7 @@ exports[`NotebookMetadataComponent renders liked notebook 1`] = `
|
||||
exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
@@ -123,7 +123,7 @@ exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 30,
|
||||
}
|
||||
}
|
||||
@@ -141,7 +141,7 @@ exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
|
||||
<Text>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
{
|
||||
"iconName": "Heart",
|
||||
}
|
||||
}
|
||||
@@ -165,7 +165,7 @@ exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
@@ -208,8 +208,8 @@ exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
{
|
||||
"root": {
|
||||
"fontWeight": 600,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
}
|
||||
|
||||
.settingsV2ToolTip {
|
||||
padding: 10px;
|
||||
font: 12px @DataExplorerFont;
|
||||
max-width: 300px;
|
||||
padding: 10px;
|
||||
font: 12px @DataExplorerFont;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.autoPilotSelector span {
|
||||
height: 25px;
|
||||
font: 14px @DataExplorerFont;
|
||||
height: 25px;
|
||||
font: 14px @DataExplorerFont;
|
||||
}
|
||||
|
||||
.settingsV2TabsContainer {
|
||||
@@ -25,7 +25,14 @@
|
||||
font-family: @DataExplorerFont;
|
||||
}
|
||||
|
||||
.settingsV2IndexingPolicyEditor {
|
||||
.settingsV2Editor {
|
||||
width: 100%;
|
||||
height: 60vh;
|
||||
}
|
||||
}
|
||||
|
||||
.settingsV2EditorSpinner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
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 { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
||||
import * as React from "react";
|
||||
import DiscardIcon from "../../../../images/discard.svg";
|
||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||
@@ -18,6 +28,10 @@ import { userContext } from "../../../UserContext";
|
||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import {
|
||||
PartitionKeyComponent,
|
||||
PartitionKeyComponentProps,
|
||||
} from "../../Controls/Settings/SettingsSubComponents/PartitionKeyComponent";
|
||||
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
||||
import "./SettingsComponent.less";
|
||||
@@ -103,6 +117,11 @@ export interface SettingsComponentState {
|
||||
indexesToAdd: AddMongoIndexProps[];
|
||||
indexTransformationProgress: number;
|
||||
|
||||
computedPropertiesContent: DataModels.ComputedProperties;
|
||||
computedPropertiesContentBaseline: DataModels.ComputedProperties;
|
||||
shouldDiscardComputedProperties: boolean;
|
||||
isComputedPropertiesDirty: boolean;
|
||||
|
||||
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode;
|
||||
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode;
|
||||
conflictResolutionPolicyPath: string;
|
||||
@@ -127,7 +146,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private offer: DataModels.Offer;
|
||||
private changeFeedPolicyVisible: boolean;
|
||||
private isFixedContainer: boolean;
|
||||
private shouldShowComputedPropertiesEditor: boolean;
|
||||
private shouldShowIndexingPolicyEditor: boolean;
|
||||
private shouldShowPartitionKeyEditor: boolean;
|
||||
private isVectorSearchEnabled: boolean;
|
||||
private totalThroughputUsed: number;
|
||||
public mongoDBCollectionResource: MongoDBCollectionResource;
|
||||
|
||||
@@ -139,7 +161,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.collection = this.props.settingsTab.collection as ViewModels.Collection;
|
||||
this.offer = this.collection?.offer();
|
||||
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
|
||||
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
|
||||
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;
|
||||
|
||||
@@ -191,6 +216,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
isMongoIndexingPolicyDiscardable: false,
|
||||
indexTransformationProgress: undefined,
|
||||
|
||||
computedPropertiesContent: undefined,
|
||||
computedPropertiesContentBaseline: undefined,
|
||||
shouldDiscardComputedProperties: false,
|
||||
isComputedPropertiesDirty: false,
|
||||
|
||||
conflictResolutionPolicyMode: undefined,
|
||||
conflictResolutionPolicyModeBaseline: undefined,
|
||||
conflictResolutionPolicyPath: undefined,
|
||||
@@ -281,6 +311,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.state.isSubSettingsSaveable ||
|
||||
this.state.isIndexingPolicyDirty ||
|
||||
this.state.isConflictResolutionDirty ||
|
||||
this.state.isComputedPropertiesDirty ||
|
||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable)
|
||||
);
|
||||
};
|
||||
@@ -291,6 +322,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.state.isSubSettingsDiscardable ||
|
||||
this.state.isIndexingPolicyDirty ||
|
||||
this.state.isConflictResolutionDirty ||
|
||||
this.state.isComputedPropertiesDirty ||
|
||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable)
|
||||
);
|
||||
};
|
||||
@@ -395,6 +427,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
isMongoIndexingPolicySaveable: false,
|
||||
isMongoIndexingPolicyDiscardable: false,
|
||||
isConflictResolutionDirty: false,
|
||||
computedPropertiesContent: this.state.computedPropertiesContentBaseline,
|
||||
shouldDiscardComputedProperties: true,
|
||||
isComputedPropertiesDirty: false,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -514,6 +549,31 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private onMongoIndexingPolicyDiscardableChange = (isMongoIndexingPolicyDiscardable: boolean): void =>
|
||||
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 => {
|
||||
this.totalThroughputUsed = 0;
|
||||
(useDatabases.getState().databases || []).forEach(async (database) => {
|
||||
@@ -636,7 +696,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
const indexingPolicyContent = this.collection.indexingPolicy();
|
||||
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
|
||||
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
|
||||
|
||||
const conflictResolutionPolicyMode = parseConflictResolutionMode(conflictResolutionPolicy?.mode);
|
||||
const conflictResolutionPolicyPath = conflictResolutionPolicy?.conflictResolutionPath;
|
||||
const conflictResolutionPolicyProcedure = parseConflictResolutionProcedure(
|
||||
@@ -645,6 +704,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
const geospatialConfigTypeString: string =
|
||||
(this.collection.geospatialConfig && this.collection.geospatialConfig()?.type) || GeospatialConfigType.Geometry;
|
||||
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 {
|
||||
throughput: offerThroughput,
|
||||
@@ -671,6 +736,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
conflictResolutionPolicyProcedureBaseline: conflictResolutionPolicyProcedure,
|
||||
geospatialConfigType: geoSpatialConfigType,
|
||||
geospatialConfigTypeBaseline: geoSpatialConfigType,
|
||||
computedPropertiesContent: computedPropertiesContent,
|
||||
computedPropertiesContentBaseline: computedPropertiesContent,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -787,7 +854,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private saveCollectionSettings = async (startKey: number): Promise<void> => {
|
||||
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
|
||||
|
||||
if (this.state.isSubSettingsSaveable || this.state.isIndexingPolicyDirty || this.state.isConflictResolutionDirty) {
|
||||
if (
|
||||
this.state.isSubSettingsSaveable ||
|
||||
this.state.isIndexingPolicyDirty ||
|
||||
this.state.isConflictResolutionDirty ||
|
||||
this.state.isComputedPropertiesDirty
|
||||
) {
|
||||
let defaultTtl: number;
|
||||
switch (this.state.timeToLive) {
|
||||
case TtlType.On:
|
||||
@@ -825,6 +897,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
newCollection.conflictResolutionPolicy = conflictResolutionChanges;
|
||||
}
|
||||
|
||||
if (this.state.isComputedPropertiesDirty) {
|
||||
newCollection.computedProperties = this.state.computedPropertiesContent;
|
||||
}
|
||||
|
||||
const updatedCollection: DataModels.Collection = await updateCollection(
|
||||
this.collection.databaseId,
|
||||
this.collection.id(),
|
||||
@@ -838,6 +914,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.collection.conflictResolutionPolicy(updatedCollection.conflictResolutionPolicy);
|
||||
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
|
||||
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
|
||||
this.collection.computedProperties(updatedCollection.computedProperties);
|
||||
|
||||
if (wasIndexingPolicyModified) {
|
||||
await this.refreshIndexTransformationProgress();
|
||||
@@ -848,6 +925,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
isSubSettingsDiscardable: false,
|
||||
isIndexingPolicyDirty: false,
|
||||
isConflictResolutionDirty: false,
|
||||
isComputedPropertiesDirty: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1026,6 +1104,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
indexTransformationProgress: this.state.indexTransformationProgress,
|
||||
refreshIndexTransformationProgress: this.refreshIndexTransformationProgress,
|
||||
onIndexingPolicyDirtyChange: this.onIndexingPolicyDirtyChange,
|
||||
isVectorSearchEnabled: this.isVectorSearchEnabled,
|
||||
};
|
||||
|
||||
const mongoIndexingPolicyComponentProps: MongoIndexingPolicyComponentProps = {
|
||||
@@ -1042,6 +1121,16 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
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 = {
|
||||
collection: this.collection,
|
||||
conflictResolutionPolicyMode: this.state.conflictResolutionPolicyMode,
|
||||
@@ -1056,6 +1145,16 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
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[] = [];
|
||||
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
||||
tabs.push({
|
||||
@@ -1069,6 +1168,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
content: <SubSettingsComponent {...subSettingsComponentProps} />,
|
||||
});
|
||||
|
||||
if (this.isVectorSearchEnabled) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.ContainerVectorPolicyTab,
|
||||
content: <ContainerVectorPolicyComponent {...containerVectorPolicyProps} />,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.shouldShowIndexingPolicyEditor) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||
@@ -1091,6 +1197,20 @@ 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 = {
|
||||
onLinkClick: this.onPivotChange,
|
||||
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
getThroughputApplyLongDelayMessage,
|
||||
getThroughputApplyShortDelayMessage,
|
||||
getToolTipContainer,
|
||||
indexingPolicynUnsavedWarningMessage,
|
||||
manualToAutoscaleDisclaimerElement,
|
||||
mongoIndexTransformationRefreshingMessage,
|
||||
mongoIndexingPolicyAADError,
|
||||
@@ -39,7 +38,6 @@ class SettingsRenderUtilsTestComponent extends React.Component {
|
||||
|
||||
{manualToAutoscaleDisclaimerElement}
|
||||
{ttlWarning}
|
||||
{indexingPolicynUnsavedWarningMessage}
|
||||
{updateThroughputDelayedApplyWarningMessage}
|
||||
|
||||
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||
|
||||
@@ -61,6 +61,8 @@ export interface PriceBreakdown {
|
||||
currencySign: string;
|
||||
}
|
||||
|
||||
export type editorType = "indexPolicy" | "computedProperties";
|
||||
|
||||
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "windowtext" } };
|
||||
|
||||
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
||||
@@ -254,9 +256,10 @@ export const ttlWarning: JSX.Element = (
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const indexingPolicynUnsavedWarningMessage: JSX.Element = (
|
||||
export const unsavedEditorWarningMessage = (editor: editorType): JSX.Element => (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes.
|
||||
You have not saved the latest changes made to your{" "}
|
||||
{editor === "indexPolicy" ? "indexing policy" : "computed properties"}. Please click save to confirm the changes.
|
||||
</Text>
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Stack } from "@fluentui/react";
|
||||
import { VectorEmbeddingPolicy } from "Contracts/DataModels";
|
||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||
import { titleAndInputStackProps } from "Explorer/Controls/Settings/SettingsRenderUtils";
|
||||
import React from "react";
|
||||
|
||||
export interface ContainerVectorPolicyComponentProps {
|
||||
vectorEmbeddingPolicy: VectorEmbeddingPolicy;
|
||||
}
|
||||
|
||||
export const ContainerVectorPolicyComponent: React.FC<ContainerVectorPolicyComponentProps> = ({
|
||||
vectorEmbeddingPolicy,
|
||||
}) => {
|
||||
return (
|
||||
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative" } }}>
|
||||
<EditorReact
|
||||
language={"json"}
|
||||
content={JSON.stringify(vectorEmbeddingPolicy || {}, null, 4)}
|
||||
isReadOnly={true}
|
||||
wordWrap={"on"}
|
||||
ariaLabel={"Container vector policy"}
|
||||
lineNumbers={"on"}
|
||||
scrollBeyondLastLine={false}
|
||||
className={"settingsV2Editor"}
|
||||
spinnerClassName={"settingsV2EditorSpinner"}
|
||||
fontSize={14}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import * as monaco from "monaco-editor";
|
||||
import * as React from "react";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import { loadMonaco } from "../../../LazyMonaco";
|
||||
import { indexingPolicynUnsavedWarningMessage, titleAndInputStackProps } from "../SettingsRenderUtils";
|
||||
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils";
|
||||
import { isDirty, isIndexTransforming } from "../SettingsUtils";
|
||||
import { IndexingPolicyRefreshComponent } from "./IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface IndexingPolicyComponentProps {
|
||||
logIndexingPolicySuccessMessage: () => void;
|
||||
indexTransformationProgress: number;
|
||||
refreshIndexTransformationProgress: () => Promise<void>;
|
||||
isVectorSearchEnabled?: boolean;
|
||||
onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void;
|
||||
}
|
||||
|
||||
@@ -119,10 +120,15 @@ export class IndexingPolicyComponent extends React.Component<
|
||||
indexTransformationProgress={this.props.indexTransformationProgress}
|
||||
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
|
||||
/>
|
||||
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
|
||||
<MessageBar messageBarType={MessageBarType.warning}>{indexingPolicynUnsavedWarningMessage}</MessageBar>
|
||||
{this.props.isVectorSearchEnabled && (
|
||||
<MessageBar messageBarType={MessageBarType.severeWarning}>
|
||||
Container vector policies and vector indexes are not modifiable after container creation
|
||||
</MessageBar>
|
||||
)}
|
||||
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
|
||||
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
|
||||
<MessageBar messageBarType={MessageBarType.warning}>{unsavedEditorWarningMessage("indexPolicy")}</MessageBar>
|
||||
)}
|
||||
<div className="settingsV2Editor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ exports[`IndexingPolicyRefreshComponent renders 1`] = `
|
||||
>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
{
|
||||
"root": {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
addMongoIndexStackProps,
|
||||
createAndAddMongoIndexStackProps,
|
||||
customDetailsListStyles,
|
||||
indexingPolicynUnsavedWarningMessage,
|
||||
infoAndToolTipTextStyle,
|
||||
mediumWidthStackStyles,
|
||||
mongoCompoundIndexNotSupportedMessage,
|
||||
@@ -27,15 +26,16 @@ import {
|
||||
onRenderRow,
|
||||
separatorStyles,
|
||||
subComponentStackProps,
|
||||
unsavedEditorWarningMessage,
|
||||
} from "../../SettingsRenderUtils";
|
||||
import {
|
||||
AddMongoIndexProps,
|
||||
getMongoIndexType,
|
||||
getMongoIndexTypeText,
|
||||
isIndexTransforming,
|
||||
MongoIndexIdField,
|
||||
MongoIndexTypes,
|
||||
MongoNotificationType,
|
||||
getMongoIndexType,
|
||||
getMongoIndexTypeText,
|
||||
isIndexTransforming,
|
||||
} from "../../SettingsUtils";
|
||||
import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
||||
import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
|
||||
@@ -297,7 +297,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
if (this.getMongoWarningNotificationMessage()) {
|
||||
warningMessage = this.getMongoWarningNotificationMessage();
|
||||
} else if (this.isMongoIndexingPolicySaveable()) {
|
||||
warningMessage = indexingPolicynUnsavedWarningMessage;
|
||||
warningMessage = unsavedEditorWarningMessage("indexPolicy");
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
exports[`AddMongoIndexComponent renders 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 5,
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ exports[`AddMongoIndexComponent renders 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 20,
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,8 @@ exports[`AddMongoIndexComponent renders 1`] = `
|
||||
componentRef={[Function]}
|
||||
onChange={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
{
|
||||
"root": {
|
||||
"paddingLeft": 10,
|
||||
"width": 210,
|
||||
},
|
||||
@@ -34,12 +34,12 @@ exports[`AddMongoIndexComponent renders 1`] = `
|
||||
ariaLabel="Index Type 1"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
[
|
||||
{
|
||||
"key": "Single",
|
||||
"text": "Single Field",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"key": "Wildcard",
|
||||
"text": "Wildcard",
|
||||
},
|
||||
@@ -48,8 +48,8 @@ exports[`AddMongoIndexComponent renders 1`] = `
|
||||
placeholder="Select an index type"
|
||||
selectedKey="Single"
|
||||
styles={
|
||||
Object {
|
||||
"dropdown": Object {
|
||||
{
|
||||
"dropdown": {
|
||||
"paddingleft": 10,
|
||||
"width": 202,
|
||||
},
|
||||
@@ -60,7 +60,7 @@ exports[`AddMongoIndexComponent renders 1`] = `
|
||||
ariaLabel="Undo Button 1"
|
||||
disabled={false}
|
||||
iconProps={
|
||||
Object {
|
||||
{
|
||||
"iconName": "Undo",
|
||||
}
|
||||
}
|
||||
@@ -70,8 +70,8 @@ exports[`AddMongoIndexComponent renders 1`] = `
|
||||
<StyledMessageBar
|
||||
messageBarType={1}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
{
|
||||
"root": {
|
||||
"marginLeft": 10,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ exports[`MongoIndexingPolicyComponent error shown for collection with compound i
|
||||
exports[`MongoIndexingPolicyComponent renders 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 20,
|
||||
}
|
||||
}
|
||||
@@ -29,14 +29,14 @@ exports[`MongoIndexingPolicyComponent renders 1`] = `
|
||||
</Text>
|
||||
<Stack
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
{
|
||||
"root": {
|
||||
"width": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 5,
|
||||
}
|
||||
}
|
||||
@@ -47,8 +47,8 @@ exports[`MongoIndexingPolicyComponent renders 1`] = `
|
||||
>
|
||||
<StyledWithViewportComponent
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
[
|
||||
{
|
||||
"fieldName": "definition",
|
||||
"isResizable": true,
|
||||
"key": "definition",
|
||||
@@ -56,7 +56,7 @@ exports[`MongoIndexingPolicyComponent renders 1`] = `
|
||||
"minWidth": 100,
|
||||
"name": "Definition",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"fieldName": "type",
|
||||
"isResizable": true,
|
||||
"key": "type",
|
||||
@@ -64,7 +64,7 @@ exports[`MongoIndexingPolicyComponent renders 1`] = `
|
||||
"minWidth": 100,
|
||||
"name": "Type",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"fieldName": "actionButton",
|
||||
"isResizable": true,
|
||||
"key": "actionButton",
|
||||
@@ -75,15 +75,15 @@ exports[`MongoIndexingPolicyComponent renders 1`] = `
|
||||
]
|
||||
}
|
||||
disableSelectionZone={true}
|
||||
items={Array []}
|
||||
items={[]}
|
||||
layoutMode={1}
|
||||
onRenderRow={[Function]}
|
||||
selectionMode={0}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"selectors": Object {
|
||||
".ms-FocusZone": Object {
|
||||
{
|
||||
"root": {
|
||||
"selectors": {
|
||||
".ms-FocusZone": {
|
||||
"paddingTop": 0,
|
||||
},
|
||||
},
|
||||
@@ -93,14 +93,14 @@ exports[`MongoIndexingPolicyComponent renders 1`] = `
|
||||
/>
|
||||
<Stack
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
{
|
||||
"root": {
|
||||
"width": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
@@ -117,11 +117,11 @@ exports[`MongoIndexingPolicyComponent renders 1`] = `
|
||||
</Stack>
|
||||
<Separator
|
||||
styles={
|
||||
Object {
|
||||
"root": Array [
|
||||
Object {
|
||||
"selectors": Object {
|
||||
"::before": Object {
|
||||
{
|
||||
"root": [
|
||||
{
|
||||
"selectors": {
|
||||
"::before": {
|
||||
"background": undefined,
|
||||
},
|
||||
},
|
||||
@@ -132,8 +132,8 @@ exports[`MongoIndexingPolicyComponent renders 1`] = `
|
||||
/>
|
||||
<Stack
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
{
|
||||
"root": {
|
||||
"width": 600,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -47,7 +47,7 @@ describe("ScaleComponent", () => {
|
||||
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
|
||||
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(true);
|
||||
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(false);
|
||||
expect(wrapper.find("#throughputApplyLongDelayMessage").html()).toContain(targetThroughput);
|
||||
expect(wrapper.find("#throughputApplyLongDelayMessage").html()).toContain(`${targetThroughput}`);
|
||||
|
||||
const newCollection = { ...collection };
|
||||
const maxThroughput = 5000;
|
||||
@@ -66,7 +66,7 @@ describe("ScaleComponent", () => {
|
||||
wrapper = shallow(<ScaleComponent {...newProps} />);
|
||||
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
|
||||
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false);
|
||||
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(maxThroughput);
|
||||
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(`${maxThroughput}`);
|
||||
});
|
||||
|
||||
it("autoScale disabled", () => {
|
||||
|
||||
@@ -306,7 +306,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
};
|
||||
|
||||
const costElement = (): JSX.Element => {
|
||||
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, true);
|
||||
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, false);
|
||||
return (
|
||||
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
|
||||
{newThroughput && newThroughputCostElement()}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,36 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ComputedPropertiesComponent renders 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
{
|
||||
"childrenGap": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
"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>
|
||||
`;
|
||||
@@ -3,7 +3,7 @@
|
||||
exports[`ConflictResolutionComponent Path text field displayed 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 20,
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,12 @@ exports[`ConflictResolutionComponent Path text field displayed 1`] = `
|
||||
label="Mode"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
[
|
||||
{
|
||||
"key": "LastWriterWins",
|
||||
"text": "Last Write Wins (default)",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"key": "Custom",
|
||||
"text": "Merge Procedure (custom)",
|
||||
},
|
||||
@@ -25,19 +25,19 @@ exports[`ConflictResolutionComponent Path text field displayed 1`] = `
|
||||
}
|
||||
selectedKey="LastWriterWins"
|
||||
styles={
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
{
|
||||
"flexContainer": [
|
||||
{
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"selectors": {
|
||||
".ms-ChoiceField-field.is-checked::after": {
|
||||
"borderColor": undefined,
|
||||
},
|
||||
".ms-ChoiceField-field.is-checked::before": Object {
|
||||
".ms-ChoiceField-field.is-checked::before": {
|
||||
"borderColor": undefined,
|
||||
},
|
||||
".ms-ChoiceField-wrapper label": Object {
|
||||
".ms-ChoiceField-wrapper label": {
|
||||
"fontFamily": undefined,
|
||||
"fontSize": 14,
|
||||
"padding": "2px 5px",
|
||||
@@ -55,12 +55,12 @@ exports[`ConflictResolutionComponent Path text field displayed 1`] = `
|
||||
onChange={[Function]}
|
||||
onRenderLabel={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
{
|
||||
"fieldGroup": {
|
||||
"borderColor": "",
|
||||
"height": 25,
|
||||
"selectors": Object {
|
||||
":disabled": Object {
|
||||
"selectors": {
|
||||
":disabled": {
|
||||
"backgroundColor": undefined,
|
||||
"borderColor": undefined,
|
||||
},
|
||||
@@ -77,7 +77,7 @@ exports[`ConflictResolutionComponent Path text field displayed 1`] = `
|
||||
exports[`ConflictResolutionComponent Sproc text field displayed 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 20,
|
||||
}
|
||||
}
|
||||
@@ -86,12 +86,12 @@ exports[`ConflictResolutionComponent Sproc text field displayed 1`] = `
|
||||
label="Mode"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
[
|
||||
{
|
||||
"key": "LastWriterWins",
|
||||
"text": "Last Write Wins (default)",
|
||||
},
|
||||
Object {
|
||||
{
|
||||
"key": "Custom",
|
||||
"text": "Merge Procedure (custom)",
|
||||
},
|
||||
@@ -99,19 +99,19 @@ exports[`ConflictResolutionComponent Sproc text field displayed 1`] = `
|
||||
}
|
||||
selectedKey="Custom"
|
||||
styles={
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
{
|
||||
"flexContainer": [
|
||||
{
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"selectors": {
|
||||
".ms-ChoiceField-field.is-checked::after": {
|
||||
"borderColor": "",
|
||||
},
|
||||
".ms-ChoiceField-field.is-checked::before": Object {
|
||||
".ms-ChoiceField-field.is-checked::before": {
|
||||
"borderColor": "",
|
||||
},
|
||||
".ms-ChoiceField-wrapper label": Object {
|
||||
".ms-ChoiceField-wrapper label": {
|
||||
"fontFamily": undefined,
|
||||
"fontSize": 14,
|
||||
"padding": "2px 5px",
|
||||
@@ -129,12 +129,12 @@ exports[`ConflictResolutionComponent Sproc text field displayed 1`] = `
|
||||
onChange={[Function]}
|
||||
onRenderLabel={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
{
|
||||
"fieldGroup": {
|
||||
"borderColor": "",
|
||||
"height": 25,
|
||||
"selectors": Object {
|
||||
":disabled": Object {
|
||||
"selectors": {
|
||||
":disabled": {
|
||||
"backgroundColor": undefined,
|
||||
"borderColor": undefined,
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
exports[`IndexingPolicyComponent renders 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 5,
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ exports[`IndexingPolicyComponent renders 1`] = `
|
||||
refreshIndexTransformationProgress={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="settingsV2IndexingPolicyEditor"
|
||||
className="settingsV2Editor"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 20,
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,8 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||
<Text
|
||||
id="throughputApplyLongDelayMessage"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
{
|
||||
"root": {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
@@ -30,7 +30,7 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||
</StyledMessageBar>
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
{
|
||||
"childrenGap": 20,
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user