Compare commits

..

1 Commits

Author SHA1 Message Date
Steve Faulkner
a266fdc89c Create codeql-analysis.yml 2020-10-14 14:58:44 -05:00
330 changed files with 18923 additions and 15521 deletions

View File

@@ -1,14 +1,7 @@
# These options are only needed when if running end to end tests locally
PORTAL_RUNNER_USERNAME= PORTAL_RUNNER_USERNAME=
PORTAL_RUNNER_PASSWORD= PORTAL_RUNNER_PASSWORD=
PORTAL_RUNNER_SUBSCRIPTION= PORTAL_RUNNER_SUBSCRIPTION=
PORTAL_RUNNER_RESOURCE_GROUP= PORTAL_RUNNER_RESOURCE_GROUP=
PORTAL_RUNNER_DATABASE_ACCOUNT= PORTAL_RUNNER_DATABASE_ACCOUNT=
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY= PORTAL_RUNNER_CONNECTION_STRING=
PORTAL_RUNNER_CONNECTION_STRING=
NOTEBOOKS_TEST_RUNNER_TENANT_ID=
NOTEBOOKS_TEST_RUNNER_CLIENT_ID=
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET=
CASSANDRA_CONNECTION_STRING=
MONGO_CONNECTION_STRING=
TABLES_CONNECTION_STRING=
DATA_EXPLORER_ENDPOINT=https://localhost:1234/hostedExplorer.html

View File

@@ -14,6 +14,9 @@ src/Common/DataAccessUtilityBase.ts
src/Common/DeleteFeedback.ts src/Common/DeleteFeedback.ts
src/Common/DocumentClientUtilityBase.ts src/Common/DocumentClientUtilityBase.ts
src/Common/EditableUtility.ts src/Common/EditableUtility.ts
src/Common/EnvironmentUtility.ts
src/Common/ErrorParserUtility.test.ts
src/Common/ErrorParserUtility.ts
src/Common/HashMap.test.ts src/Common/HashMap.test.ts
src/Common/HashMap.ts src/Common/HashMap.ts
src/Common/HeadersUtility.test.ts src/Common/HeadersUtility.test.ts
@@ -201,6 +204,8 @@ src/Explorer/Tabs/QueryTab.test.ts
src/Explorer/Tabs/QueryTab.ts src/Explorer/Tabs/QueryTab.ts
src/Explorer/Tabs/QueryTablesTab.ts src/Explorer/Tabs/QueryTablesTab.ts
src/Explorer/Tabs/ScriptTabBase.ts src/Explorer/Tabs/ScriptTabBase.ts
src/Explorer/Tabs/SettingsTab.test.ts
src/Explorer/Tabs/SettingsTab.ts
src/Explorer/Tabs/SparkMasterTab.ts src/Explorer/Tabs/SparkMasterTab.ts
src/Explorer/Tabs/StoredProcedureTab.ts src/Explorer/Tabs/StoredProcedureTab.ts
src/Explorer/Tabs/TabComponents.ts src/Explorer/Tabs/TabComponents.ts
@@ -287,6 +292,8 @@ src/Utils/DatabaseAccountUtils.ts
src/Utils/JunoUtils.ts src/Utils/JunoUtils.ts
src/Utils/MessageValidation.ts src/Utils/MessageValidation.ts
src/Utils/NotebookConfigurationUtils.ts src/Utils/NotebookConfigurationUtils.ts
src/Utils/OfferUtils.test.ts
src/Utils/OfferUtils.ts
src/Utils/PricingUtils.test.ts src/Utils/PricingUtils.test.ts
src/Utils/QueryUtils.test.ts src/Utils/QueryUtils.test.ts
src/Utils/QueryUtils.ts src/Utils/QueryUtils.ts
@@ -391,5 +398,19 @@ src/Explorer/Tree/ResourceTreeAdapterForResourceToken.tsx
src/GalleryViewer/Cards/GalleryCardComponent.tsx src/GalleryViewer/Cards/GalleryCardComponent.tsx
src/GalleryViewer/GalleryViewer.tsx src/GalleryViewer/GalleryViewer.tsx
src/GalleryViewer/GalleryViewerComponent.tsx src/GalleryViewer/GalleryViewerComponent.tsx
cypress/integration/dataexplorer/CASSANDRA/addCollection.spec.ts
cypress/integration/dataexplorer/GRAPH/addCollection.spec.ts
cypress/integration/dataexplorer/ci-tests/addCollectionPane.spec.ts
cypress/integration/dataexplorer/ci-tests/createDatabase.spec.ts
cypress/integration/dataexplorer/ci-tests/deleteCollection.spec.ts
cypress/integration/dataexplorer/ci-tests/deleteDatabase.spec.ts
cypress/integration/dataexplorer/MONGO/addCollection.spec.ts
cypress/integration/dataexplorer/MONGO/addCollectionAutopilot.spec.ts
cypress/integration/dataexplorer/MONGO/addCollectionExistingDatabase.spec.ts
cypress/integration/dataexplorer/MONGO/provisionDatabaseThroughput.spec.ts
cypress/integration/dataexplorer/SQL/addCollection.spec.ts
cypress/integration/dataexplorer/TABLE/addCollection.spec.ts
cypress/integration/notebook/newNotebook.spec.ts
cypress/integration/notebook/resourceTree.spec.ts
__mocks__/monaco-editor.ts __mocks__/monaco-editor.ts
src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx

View File

@@ -42,13 +42,6 @@ module.exports = {
"no-null/no-null": "error", "no-null/no-null": "error",
"@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-explicit-any": "error",
"prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }], "prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }],
eqeqeq: "error", eqeqeq: "error"
"no-restricted-syntax": [
"error",
{
selector: "CallExpression[callee.object.name='JSON'][callee.property.name='stringify'] Identifier[name=/$err/]",
message: "Do not use JSON.stringify(error). It will print '{}'"
}
]
} }
}; };

View File

@@ -79,32 +79,32 @@ jobs:
name: dist name: dist
path: dist/ path: dist/
endtoendemulator: endtoendemulator:
name: "End To End Emulator Tests" name: "End To End Tests | Emulator | SQL"
needs: [lint, format, compile, unittest] needs: [lint, format, compile, unittest]
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: southpolesteve/cosmos-emulator-github-action@v1
- name: Use Node.js 12.x - name: Use Node.js 12.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 12.x
- uses: southpolesteve/cosmos-emulator-github-action@v1 - name: Restore Cypress Binary Cache
uses: actions/cache@v2
with:
path: ~/.cache/Cypress
key: ${{ runner.os }}-cypress-binary-cache
- name: End to End Tests - name: End to End Tests
run: | run: |
npm ci npm ci
npm start & npm start &
npm run wait-for-server npm ci --prefix ./cypress
npx jest -c ./jest.config.e2e.js --detectOpenHandles sql npm run test:ci --prefix ./cypress -- --spec ./integration/dataexplorer/ci-tests/createDatabase.spec.ts
shell: bash shell: bash
env: env:
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/explorer.html?platform=Emulator" EMULATOR_ENDPOINT: https://0.0.0.0:8081/
PLATFORM: "Emulator"
NODE_TLS_REJECT_UNAUTHORIZED: 0 NODE_TLS_REJECT_UNAUTHORIZED: 0
- uses: actions/upload-artifact@v2 CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
if: failure()
with:
name: screenshots
path: failed-*
accessibility: accessibility:
name: "Accessibility | Hosted" name: "Accessibility | Hosted"
needs: [lint, format, compile, unittest] needs: [lint, format, compile, unittest]
@@ -123,13 +123,13 @@ jobs:
sudo sysctl -p sudo sysctl -p
npm ci npm ci
npm start & npm start &
npx wait-on -i 5000 https-get://0.0.0.0:1234/ npx wait-on -i 5000 https-get://0.0.0.0:1234/
node utils/accesibilityCheck.js node utils/accesibilityCheck.js
shell: bash shell: bash
env: env:
NODE_TLS_REJECT_UNAUTHORIZED: 0 NODE_TLS_REJECT_UNAUTHORIZED: 0
endtoendhosted: endtoendpuppeteer:
name: "End to End Hosted Tests" name: "End to end puppeteer tests"
needs: [lint, format, compile, unittest] needs: [lint, format, compile, unittest]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -138,7 +138,7 @@ jobs:
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12.x node-version: 12.x
- name: End to End Hosted Tests - name: End to End Puppeteer Tests
run: | run: |
npm ci npm ci
npm start & npm start &
@@ -147,27 +147,13 @@ jobs:
shell: bash shell: bash
env: env:
NODE_TLS_REJECT_UNAUTHORIZED: 0 NODE_TLS_REJECT_UNAUTHORIZED: 0
PORTAL_RUNNER_SUBSCRIPTION: ${{ secrets.PORTAL_RUNNER_SUBSCRIPTION }}
PORTAL_RUNNER_RESOURCE_GROUP: ${{ secrets.PORTAL_RUNNER_RESOURCE_GROUP }}
PORTAL_RUNNER_DATABASE_ACCOUNT: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT }}
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY }}
NOTEBOOKS_TEST_RUNNER_TENANT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_TENANT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }} PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }} MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }} CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }}
TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }}
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html"
- uses: actions/upload-artifact@v2
if: failure()
with:
name: screenshots
path: failed-*
nuget: nuget:
name: Publish Nuget name: Publish Nuget
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted, accessibility] needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendpuppeteer]
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
@@ -191,7 +177,7 @@ jobs:
nugetmpac: nugetmpac:
name: Publish Nuget MPAC name: Publish Nuget MPAC
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted, accessibility] needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendpuppeteer]
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
@@ -213,28 +199,3 @@ jobs:
name: packages name: packages
with: with:
path: "*.nupkg" path: "*.nupkg"
nugetie:
name: Publish Nuget IE
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted, accessibility]
runs-on: ubuntu-latest
env:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
steps:
- uses: nuget/setup-nuget@v1
with:
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
- name: Download Dist Folder
uses: actions/download-artifact@v2
with:
name: dist
- run: cp ./configs/prod.json config.json
- run: sed -i 's/Azure.Cosmos.DB.Data.Explorer/Azure.Cosmos.DB.Data.Explorer.IE/g' DataExplorer.nuspec
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "GitHub" -Password "$AZURE_DEVOPS_PAT"
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
- run: nuget push -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
- uses: actions/upload-artifact@v2
name: packages
with:
path: "*.nupkg"

71
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
name: "CodeQL"
on:
push:
branches: [master]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: '0 5 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['javascript']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

25
.github/workflows/runners.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Runners
on:
schedule:
- cron: "0 * 1 * *"
jobs:
sqlcreatecollection:
runs-on: ubuntu-latest
name: "SQL | Create Collection"
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- run: npm ci
- run: npm run test:e2e
env:
PORTAL_RUNNER_APP_INSIGHTS_KEY: ${{ secrets.PORTAL_RUNNER_APP_INSIGHTS_KEY }}
PORTAL_RUNNER_USERNAME: ${{ secrets.PORTAL_RUNNER_USERNAME }}
PORTAL_RUNNER_PASSWORD: ${{ secrets.PORTAL_RUNNER_PASSWORD }}
PORTAL_RUNNER_SUBSCRIPTION: 69e02f2d-f059-4409-9eac-97e8a276ae2c
PORTAL_RUNNER_RESOURCE_GROUP: runners
PORTAL_RUNNER_DATABASE_ACCOUNT: portal-sql-runner
- uses: actions/upload-artifact@v2
if: failure()
with:
name: screenshots
path: failure.png

3
.gitignore vendored
View File

@@ -9,6 +9,9 @@ pkg/DataExplorer/*
test/out/* test/out/*
workers/**/*.js workers/**/*.js
*.trx *.trx
cypress/videos
cypress/screenshots
cypress/fixtures
notebookapp/* notebookapp/*
Contracts/* Contracts/*
.DS_Store .DS_Store

Binary file not shown.

View File

@@ -13,18 +13,29 @@ UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), ht
### Watch mode ### Watch mode
Run `npm start` to start the development server and automatically rebuild on changes Run `npm run watch` to start the development server and automatically rebuild on changes
### Hosted Development (https://cosmos.azure.com) ### Specifying Development Platform
- Visit: `https://localhost:1234/hostedExplorer.html` Setting the environment variable `PLATFORM` during the build process will force the explorer to load the specified platform. By default in development it will run in `Hosted` mode. Valid options:
- Local sign in via AAD will NOT work. Connection string only in dev mode. Use the Portal if you need AAD auth.
- The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine. - Hosted
- Emulator
- Portal
`PLATFORM=Emulator npm run watch`
### Hosted Development
The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine.
To run pure hosted mode, in `webpack.config.js` change index HtmlWebpackPlugin to use hostedExplorer.html and change entry for index to use HostedExplorer.ts.
### Emulator Development ### Emulator Development
- Start the Cosmos Emulator In a window environment, running `npm run build` will automatically copy the built files from `/dist` over to the default emulator install paths. In a non-windows enironment you can specify an alternate endpoint using `EMULATOR_ENDPOINT` and webpack dev server will proxy requests for you.
- Visit: https://localhost:1234/index.html
`PLATFORM=Emulator EMULATOR_ENDPOINT=https://my-vm.azure.com:8081 npm run watch`
#### Setting up a Remote Emulator #### Setting up a Remote Emulator
@@ -44,8 +55,16 @@ The Cosmos emulator currently only runs in Windows environments. You can still d
### Portal Development ### Portal Development
- Visit: https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Flocalhost%3A1234%2Fexplorer.html The Cosmos Portal that consumes this repo is not currently open source. If you have access to this project, `npm run build` will copy the built files over to the portal where they will be loaded by the portal development environment
- You may have to manually visit https://localhost:1234/explorer.html first and click through any SSL certificate warnings
You can however load a local running instance of data explorer in the production portal.
1. Turn off browser SSL validation for localhost: chrome://flags/#allow-insecure-localhost OR Install valid SSL certs for localhost (on IE, follow these [instructions](https://www.technipages.com/ie-bypass-problem-with-this-websites-security-certificate) to install the localhost certificate in the right place)
2. Whitelist `https://localhost:1234` domain for CORS in the Azure Cosmos DB portal
3. Start the project in portal mode: `PLATFORM=Portal npm run watch`
4. Load the portal using the following link: https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Flocalhost%3A1234%2Fexplorer.html
Live reload will occur, but data explorer will not properly integrate again with the parent iframe. You will have to manually reload the page.
### Testing ### Testing
@@ -57,21 +76,24 @@ Unit tests are located adjacent to the code under test and run with [Jest](https
#### End to End CI Tests #### End to End CI Tests
Jest and Puppeteer are used for end to end browser based tests and are contained in `test/`. To run these tests locally: [Cypress](https://www.cypress.io/) is used for end to end tests and are contained in `cypress/`. Currently, it operates as sub project with its own typescript config and dependencies. It also only operates against the emulator. To run cypress tests:
1. Copy .env.example to .env 1. Ensure the emulator is running
2. Update the values in .env including your local data explorer endpoint (ask a teammate/codeowner for help with .env values) 2. Start cosmos explorer in emulator mode: `PLATFORM=Emulator npm run watch`
3. Make sure all packages are installed `npm install` 3. Move into `cypress/` folder: `cd cypress`
4. Run the server `npm run start` and wait for it to start 4. Install dependencies: `npm install`
5. Run `npm run test:e2e` 5. Run cypress headless(`npm run test`) or in interactive mode(`npm run test:debug`)
#### End to End Production Runners
Jest and Puppeteer are used for end to end production runners and are contained in `test/`. To run these tests locally:
1. Copy .env.example to .env and fill in all variables
2. Run `npm run test:e2e`
### Releasing ### Releasing
We generally adhere to the release strategy [documented by the Azure SDK Guidelines](https://azure.github.io/azure-sdk/policies_repobranching.html#release-branches). Most releases should happen from the master branch. If master contains commits that cannot be released, you may create a release from a `release/` or `hotfix/` branch. See linked documentation for more details. We generally adhear to the release strategy [documented by the Azure SDK Guidelines](https://azure.github.io/azure-sdk/policies_repobranching.html#release-branches). Most releases should happen from the master branch. If master contains commits that cannot be released, you may create a release from a `release/` or `hotfix/` branch. See linked documentation for more details.
### Architechture
[![](https://mermaid.ink/img/eyJjb2RlIjoiZ3JhcGggTFJcbiAgaG9zdGVkKGh0dHBzOi8vY29zbW9zLmF6dXJlLmNvbSlcbiAgcG9ydGFsKFBvcnRhbClcbiAgZW11bGF0b3IoRW11bGF0b3IpXG4gIGFhZFtBQURdXG4gIHJlc291cmNlVG9rZW5bUmVzb3VyY2UgVG9rZW5dXG4gIGNvbm5lY3Rpb25TdHJpbmdbQ29ubmVjdGlvbiBTdHJpbmddXG4gIHBvcnRhbFRva2VuW0VuY3J5cHRlZCBQb3J0YWwgVG9rZW5dXG4gIG1hc3RlcktleVtNYXN0ZXIgS2V5XVxuICBhcm1bQVJNIFJlc291cmNlIFByb3ZpZGVyXVxuICBkYXRhcGxhbmVbRGF0YSBQbGFuZV1cbiAgcHJveHlbUG9ydGFsIEFQSSBQcm94eV1cbiAgc3FsW1NRTF1cbiAgbW9uZ29bTW9uZ29dXG4gIHRhYmxlc1tUYWJsZXNdXG4gIGNhc3NhbmRyYVtDYXNzYW5kcmFdXG4gIGdyYWZbR3JhcGhdXG5cblxuICBlbXVsYXRvciAtLT4gbWFzdGVyS2V5IC0tLS0-IGRhdGFwbGFuZVxuICBwb3J0YWwgLS0-IGFhZFxuICBob3N0ZWQgLS0-IHBvcnRhbFRva2VuICYgcmVzb3VyY2VUb2tlbiAmIGNvbm5lY3Rpb25TdHJpbmcgJiBhYWRcbiAgYWFkIC0tLT4gYXJtXG4gIGFhZCAtLS0-IGRhdGFwbGFuZVxuICBhYWQgLS0tPiBwcm94eVxuICByZXNvdXJjZVRva2VuIC0tLT4gc3FsIC0tPiBkYXRhcGxhbmVcbiAgcG9ydGFsVG9rZW4gLS0tPiBwcm94eVxuICBwcm94eSAtLT4gZGF0YXBsYW5lXG4gIGNvbm5lY3Rpb25TdHJpbmcgLS0-IHNxbCAmIG1vbmdvICYgY2Fzc2FuZHJhICYgZ3JhZiAmIHRhYmxlc1xuICBzcWwgLS0-IGRhdGFwbGFuZVxuICB0YWJsZXMgLS0-IGRhdGFwbGFuZVxuICBtb25nbyAtLT4gcHJveHlcbiAgY2Fzc2FuZHJhIC0tPiBwcm94eVxuICBncmFmIC0tPiBwcm94eVxuXG5cdFx0IiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQifSwidXBkYXRlRWRpdG9yIjpmYWxzZX0)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ3JhcGggTFJcbiAgaG9zdGVkKGh0dHBzOi8vY29zbW9zLmF6dXJlLmNvbSlcbiAgcG9ydGFsKFBvcnRhbClcbiAgZW11bGF0b3IoRW11bGF0b3IpXG4gIGFhZFtBQURdXG4gIHJlc291cmNlVG9rZW5bUmVzb3VyY2UgVG9rZW5dXG4gIGNvbm5lY3Rpb25TdHJpbmdbQ29ubmVjdGlvbiBTdHJpbmddXG4gIHBvcnRhbFRva2VuW0VuY3J5cHRlZCBQb3J0YWwgVG9rZW5dXG4gIG1hc3RlcktleVtNYXN0ZXIgS2V5XVxuICBhcm1bQVJNIFJlc291cmNlIFByb3ZpZGVyXVxuICBkYXRhcGxhbmVbRGF0YSBQbGFuZV1cbiAgcHJveHlbUG9ydGFsIEFQSSBQcm94eV1cbiAgc3FsW1NRTF1cbiAgbW9uZ29bTW9uZ29dXG4gIHRhYmxlc1tUYWJsZXNdXG4gIGNhc3NhbmRyYVtDYXNzYW5kcmFdXG4gIGdyYWZbR3JhcGhdXG5cblxuICBlbXVsYXRvciAtLT4gbWFzdGVyS2V5IC0tLS0-IGRhdGFwbGFuZVxuICBwb3J0YWwgLS0-IGFhZFxuICBob3N0ZWQgLS0-IHBvcnRhbFRva2VuICYgcmVzb3VyY2VUb2tlbiAmIGNvbm5lY3Rpb25TdHJpbmcgJiBhYWRcbiAgYWFkIC0tLT4gYXJtXG4gIGFhZCAtLS0-IGRhdGFwbGFuZVxuICBhYWQgLS0tPiBwcm94eVxuICByZXNvdXJjZVRva2VuIC0tLT4gc3FsIC0tPiBkYXRhcGxhbmVcbiAgcG9ydGFsVG9rZW4gLS0tPiBwcm94eVxuICBwcm94eSAtLT4gZGF0YXBsYW5lXG4gIGNvbm5lY3Rpb25TdHJpbmcgLS0-IHNxbCAmIG1vbmdvICYgY2Fzc2FuZHJhICYgZ3JhZiAmIHRhYmxlc1xuICBzcWwgLS0-IGRhdGFwbGFuZVxuICB0YWJsZXMgLS0-IGRhdGFwbGFuZVxuICBtb25nbyAtLT4gcHJveHlcbiAgY2Fzc2FuZHJhIC0tPiBwcm94eVxuICBncmFmIC0tPiBwcm94eVxuXG5cdFx0IiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQifSwidXBkYXRlRWRpdG9yIjpmYWxzZX0)
# Contributing # Contributing

View File

@@ -3,9 +3,6 @@
# Add steps that build, run tests, deploy, and more: # Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml # https://aka.ms/yaml
trigger:
- master
pool: pool:
vmImage: "ubuntu-latest" vmImage: "ubuntu-latest"

View File

@@ -1,4 +1,3 @@
module.exports = { module.exports = {
presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-react", "@babel/preset-typescript"], presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-react", "@babel/preset-typescript"]
plugins: [["@babel/plugin-proposal-decorators", { legacy: true }]]
}; };

View File

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

View File

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

View File

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

4
cypress/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
cypress.env.json
cypress/report
cypress/screenshots
cypress/videos

51
cypress/cleanup.js Normal file
View File

@@ -0,0 +1,51 @@
// Cleans up old databases from previous test runs
const { CosmosClient } = require("@azure/cosmos");
// TODO: Add support for other API connection strings
const mongoRegex = RegExp("mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com");
async function cleanup() {
const connectionString = process.env.CYPRESS_CONNECTION_STRING;
if (!connectionString) {
throw new Error("Connection string not provided");
}
let client;
switch (true) {
case connectionString.includes("mongodb://"): {
const [, key, accountName] = connectionString.match(mongoRegex);
client = new CosmosClient({
key,
endpoint: `https://${accountName}.documents.azure.com:443/`
});
break;
}
// TODO: Add support for other API connection strings
default:
client = new CosmosClient(connectionString);
break;
}
const response = await client.databases.readAll().fetchAll();
return Promise.all(
response.resources.map(async db => {
const dbTimestamp = new Date(db._ts * 1000);
const twentyMinutesAgo = new Date(Date.now() - 1000 * 60 * 20);
if (dbTimestamp < twentyMinutesAgo) {
await client.database(db.id).delete();
console.log(`DELETED: ${db.id} | Timestamp: ${dbTimestamp}`);
} else {
console.log(`SKIPPED: ${db.id} | Timestamp: ${dbTimestamp}`);
}
})
);
}
cleanup()
.then(() => {
process.exit(0);
})
.catch(error => {
console.error(error);
process.exit(1);
});

15
cypress/cypress.json Normal file
View File

@@ -0,0 +1,15 @@
{
"integrationFolder": "./integration",
"pluginsFile": false,
"fixturesFolder": false,
"supportFile": "./support/index.js",
"defaultCommandTimeout": 90000,
"chromeWebSecurity": false,
"reporter": "mochawesome",
"reporterOptions": {
"reportDir": "cypress/report",
"json": true,
"overwrite": false,
"html": false
}
}

View File

@@ -0,0 +1,66 @@
// 1. Click on "New Container" on the command bar.
// 2. Pane with the title "Add Container" should appear on the right side of the screen
// 3. It includes an input box for the database Id.
// 4. It includes a checkbox called "Create now".
// 5. When the checkbox is marked, enter new database id.
// 3. Create a database WITH "Provision throughput" checked.
// 4. Enter minimum throughput value of 400.
// 5. Enter container id to the container id text box.
// 6. Enter partition key to the partition key text box.
// 7. Click "OK" to create a new container.
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
const connectionString = require("../../../utilities/connectionString");
let crypt = require("crypto");
context("Cassandra API Test - createDatabase", () => {
beforeEach(() => {
connectionString.loginUsingConnectionString(connectionString.constants.cassandra);
});
it("Create a new table in Cassandra API", () => {
const keyspaceId = `KeyspaceId${crypt.randomBytes(8).toString("hex")}`;
const tableId = `TableId112`;
cy.get("iframe").then($element => {
const $body = $element.contents().find("body");
cy.wrap($body)
.find('div[class="commandBarContainer"]')
.should("be.visible")
.find('button[data-test="New Table"]')
.should("be.visible")
.click();
cy.wrap($body)
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body)
.find('input[id="keyspace-id"]')
.should("be.visible")
.type(keyspaceId);
cy.wrap($body)
.find('input[class="textfontclr"]')
.type(tableId);
cy.wrap($body)
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body)
.find('data-test="addCollection-createCollection"')
.click();
cy.wait(10000);
cy.wrap($body)
.find('div[data-test="resourceTreeId"]')
.should("exist")
.find('div[class="treeComponent dataResourceTree"]')
.should("contain", tableId);
});
});
});

View File

@@ -0,0 +1,81 @@
// 1. Click on "New Graph" on the command bar.
// 2. Pane with the title "Add Container" should appear on the right side of the screen
// 3. It includes an input box for the database Id.
// 4. It includes a checkbox called "Create now".
// 5. When the checkbox is marked, enter new database id.
// 3. Create a database WITH "Provision throughput" checked.
// 4. Enter minimum throughput value of 400.
// 5. Enter container id to the container id text box.
// 6. Enter partition key to the partition key text box.
// 7. Click "OK" to create a new container.
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
const connectionString = require("../../../utilities/connectionString");
let crypt = require("crypto");
context("Graph API Test", () => {
beforeEach(() => {
connectionString.loginUsingConnectionString(connectionString.constants.graph);
});
it("Create a new graph in Graph API", () => {
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
const graphId = `TestGraph${crypt.randomBytes(8).toString("hex")}`;
const partitionKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then($element => {
const $body = $element.contents().find("body");
cy.wrap($body)
.find('div[class="commandBarContainer"]')
.should("be.visible")
.find('button[data-test="New Graph"]')
.should("be.visible")
.click();
cy.wrap($body)
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body)
.find('input[data-test="addCollection-createNewDatabase"]')
.check();
cy.wrap($body)
.find('input[data-test="addCollection-newDatabaseId"]')
.should("be.visible")
.type(dbId);
cy.wrap($body)
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
.check();
cy.wrap($body)
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body)
.find('input[data-test="addCollection-collectionId"]')
.type(graphId);
cy.wrap($body)
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(partitionKey);
cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000);
cy.wrap($body)
.find('div[data-test="resourceTreeId"]')
.should("exist")
.find('div[class="treeComponent dataResourceTree"]')
.should("contain", dbId)
.click()
.should("contain", graphId);
});
});
});

View File

@@ -0,0 +1,80 @@
// 1. Click on "New Container" on the command bar.
// 2. Pane with the title "Add Container" should appear on the right side of the screen
// 3. It includes an input box for the database Id.
// 4. It includes a checkbox called "Create now".
// 5. When the checkbox is marked, enter new database id.
// 3. Create a database WITH "Provision throughput" checked.
// 4. Enter minimum throughput value of 400.
// 5. Enter container id to the container id text box.
// 6. Enter partition key to the partition key text box.
// 7. Click "OK" to create a new container.
// // 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
const connectionString = require("../../../utilities/connectionString");
let crypt = require("crypto");
context("Mongo API Test - createDatabase", () => {
beforeEach(() => {
connectionString.loginUsingConnectionString();
});
it("Create a new collection in Mongo API", () => {
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then($element => {
const $body = $element.contents().find("body");
cy.wrap($body)
.find('div[class="commandBarContainer"]')
.should("be.visible")
.find('button[data-test="New Collection"]')
.should("be.visible")
.click();
cy.wrap($body)
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body)
.find('input[data-test="addCollection-createNewDatabase"]')
.check();
cy.wrap($body)
.find('input[data-test="addCollection-newDatabaseId"]')
.type(dbId);
cy.wrap($body)
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
.check();
cy.wrap($body)
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body)
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body)
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(sharedKey);
cy.wrap($body)
.find("#submitBtnAddCollection")
.click();
cy.wait(10000);
cy.wrap($body)
.find('div[data-test="resourceTreeId"]')
.should("exist")
.find('div[class="treeComponent dataResourceTree"]')
.should("contain", dbId)
.click()
.should("contain", collectionId);
});
});
});

View File

@@ -0,0 +1,96 @@
// 1. Click on "New Container" on the command bar.
// 2. Pane with the title "Add Container" should appear on the right side of the screen
// 3. It includes an input box for the database Id.
// 4. It includes a checkbox called "Create now".
// 5. When the checkbox is marked, enter new database id.
// 3. Create a database WITH "Provision throughput" checked.
// 4. Enter minimum throughput value of 400.
// 5. Enter container id to the container id text box.
// 6. Enter partition key to the partition key text box.
// 7. Click "OK" to create a new container.
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
const connectionString = require("../../../utilities/connectionString");
let crypt = require("crypto");
context("Mongo API Test", () => {
beforeEach(() => {
connectionString.loginUsingConnectionString();
});
it.skip("Create a new collection in Mongo API - Autopilot", () => {
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then($element => {
const $body = $element.contents().find("body");
cy.wrap($body)
.find('div[class="commandBarContainer"]')
.should("be.visible")
.find('button[data-test="New Collection"]')
.should("be.visible")
.click();
cy.wrap($body)
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body)
.find('input[data-test="addCollection-createNewDatabase"]')
.check();
cy.wrap($body)
.find('input[data-test="addCollection-newDatabaseId"]')
.type(dbId);
cy.wrap($body)
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
.check();
cy.wrap($body)
.find('div[class="throughputModeContainer"]')
.should("be.visible")
.and(input => {
expect(input.get(0).textContent, "first item").contains("Autopilot (preview)");
expect(input.get(1).textContent, "second item").contains("Manual");
});
cy.wrap($body)
.find('input[id="newContainer-databaseThroughput-autoPilotRadio"]')
.check();
cy.wrap($body)
.find('select[name="autoPilotTiers"]')
// .eq(1).should('contain', '4,000 RU/s');
// // .select('4,000 RU/s').should('have.value', '1');
.find('option[value="2"]')
.then($element => $element.get(1).setAttribute("selected", "selected"));
cy.wrap($body)
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body)
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(sharedKey);
cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000);
cy.wrap($body)
.find('div[data-test="resourceTreeId"]')
.should("exist")
.find('div[class="treeComponent dataResourceTree"]')
.should("contain", dbId)
.click()
.should("contain", collectionId);
});
});
});

View File

@@ -0,0 +1,67 @@
const connectionString = require("../../../utilities/connectionString");
let crypt = require("crypto");
context("Mongo API Test", () => {
beforeEach(() => {
connectionString.loginUsingConnectionString();
});
it.skip("Create a new collection in existing database in Mongo API", () => {
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then($element => {
const $body = $element.contents().find("body");
cy.wrap($body)
.find('span[class="nodeLabel"]')
.should("be.visible")
.then($span => {
const dbId1 = $span.text();
cy.log("DBBB", dbId1);
cy.wrap($body)
.find('div[class="commandBarContainer"]')
.should("be.visible")
.find('button[data-test="New Collection"]')
.should("be.visible")
.click();
cy.wrap($body)
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body)
.find('input[data-test="addCollection-existingDatabase"]')
.check();
cy.wrap($body)
.find('input[data-test="addCollection-existingDatabase"]')
.type(dbId1);
cy.wrap($body)
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body)
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(sharedKey);
cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000);
cy.wrap($body)
.find('div[data-test="resourceTreeId"]')
.should("exist")
.find('div[class="treeComponent dataResourceTree"]')
.click()
.should("contain", collectionId);
});
});
});
});

View File

@@ -0,0 +1,203 @@
const connectionString = require("../../../utilities/connectionString");
let crypt = require("crypto");
context.skip("Mongo API Test", () => {
beforeEach(() => {
connectionString.loginUsingConnectionString();
});
it("Create a new collection in Mongo API - Provision database throughput", () => {
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then($element => {
const $body = $element.contents().find("body");
cy.wrap($body)
.find('div[class="commandBarContainer"]')
.should("be.visible")
.find('button[data-test="New Collection"]')
.should("be.visible")
.click();
cy.wrap($body)
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body)
.find(".createNewDatabaseOrUseExisting")
.should("have.length", 2)
.and(input => {
expect(input.get(0).textContent, "first item").contains("Create new");
expect(input.get(1).textContent, "second item").contains("Use existing");
});
cy.wrap($body)
.find('input[data-test="addCollection-createNewDatabase"]')
.check();
cy.wrap($body)
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
.check();
cy.wrap($body)
.find('input[data-test="addCollection-newDatabaseId"]')
.type(dbId);
cy.wrap($body)
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
.check();
cy.wrap($body)
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body)
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body)
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(sharedKey);
cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000);
cy.wrap($body)
.find('div[data-test="resourceTreeId"]')
.should("exist")
.find('div[class="treeComponent dataResourceTree"]')
.should("contain", dbId)
.click()
.should("contain", collectionId);
});
});
it("Create a new collection - without provision database throughput", () => {
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const collectionIdTitle = `Add Collection`;
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then($element => {
const $body = $element.contents().find("body");
cy.wrap($body)
.find('div[class="commandBarContainer"]')
.should("be.visible")
.find('button[data-test="New Collection"]')
.should("be.visible")
.click();
cy.wrap($body)
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body)
.find('input[data-test="addCollection-createNewDatabase"]')
.check();
cy.wrap($body)
.find('input[data-test="addCollection-newDatabaseId"]')
.type(dbId);
cy.wrap($body)
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
.uncheck();
cy.wrap($body)
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body)
.find('input[id="tab2"]')
.check({ force: true });
cy.wrap($body)
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(sharedKey);
cy.wrap($body)
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000);
cy.wrap($body)
.find('div[data-test="resourceTreeId"]')
.should("exist")
.find('div[class="treeComponent dataResourceTree"]')
.should("contain", dbId)
.click()
.should("contain", collectionId);
});
});
it("Create a new collection - without provision database throughput Fixed Storage Capacity", () => {
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then($element => {
const $body = $element.contents().find("body");
cy.wrap($body)
.find('div[class="commandBarContainer"]')
.should("be.visible")
.find('button[data-test="New Collection"]')
.should("be.visible")
.click();
cy.wrap($body)
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body)
.find('input[data-test="addCollection-createNewDatabase"]')
.check();
cy.wrap($body)
.find('input[data-test="addCollection-newDatabaseId"]')
.type(dbId);
cy.wrap($body)
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
.uncheck();
cy.wrap($body)
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body)
.find('input[id="tab1"]')
.check({ force: true });
cy.wrap($body)
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000);
cy.wrap($body)
.find('div[data-test="resourceTreeId"]')
.should("exist")
.find('div[class="treeComponent dataResourceTree"]')
.should("contain", dbId)
.click()
.should("contain", collectionId);
});
});
});

View File

@@ -0,0 +1,79 @@
// 1. Click on "New Container" on the command bar.
// 2. Pane with the title "Add Container" should appear on the right side of the screen
// 3. It includes an input box for the database Id.
// 4. It includes a checkbox called "Create now".
// 5. When the checkbox is marked, enter new database id.
// 3. Create a database WITH "Provision throughput" checked.
// 4. Enter minimum throughput value of 400.
// 5. Enter container id to the container id text box.
// 6. Enter partition key to the partition key text box.
// 7. Click "OK" to create a new container.
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
const connectionString = require("../../../utilities/connectionString");
let crypt = require("crypto");
context("SQL API Test", () => {
beforeEach(() => {
connectionString.loginUsingConnectionString();
});
it("Create a new container in SQL API", () => {
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const sharedKey = `SharedKey${crypt.randomBytes(8).toString("hex")}`;
connectionString.loginUsingConnectionString();
cy.get("iframe").then($element => {
const $body = $element.contents().find("body");
cy.wrap($body)
.find('div[class="commandBarContainer"]')
.should("be.visible")
.find('button[data-test="New Container"]')
.should("be.visible")
.click();
cy.wrap($body)
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body)
.find('input[data-test="addCollection-createNewDatabase"]')
.check();
cy.wrap($body)
.find('input[data-test="addCollectionPane-databaseSharedThroughput"]')
.check();
cy.wrap($body)
.find('input[data-test="addCollection-newDatabaseId"]')
.type(dbId);
cy.wrap($body)
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body)
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body)
.find('input[data-test="addCollection-partitionKeyValue"]')
.type(sharedKey);
cy.wrap($body)
.find("#submitBtnAddCollection")
.click();
cy.wait(10000);
cy.wrap($body)
.find('div[data-test="resourceTreeId"]')
.should("exist")
.find('div[class="treeComponent dataResourceTree"]')
.should("contain", dbId);
});
});
});

View File

@@ -0,0 +1,60 @@
// 1. Click on "New Container" on the command bar.
// 2. Pane with the title "Add Container" should appear on the right side of the screen
// 3. It includes an input box for the database Id.
// 4. It includes a checkbox called "Create now".
// 5. When the checkbox is marked, enter new database id.
// 3. Create a database WITH "Provision throughput" checked.
// 4. Enter minimum throughput value of 400.
// 5. Enter container id to the container id text box.
// 6. Enter partition key to the partition key text box.
// 7. Click "OK" to create a new container.
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
const connectionString = require("../../../utilities/connectionString");
let crypt = require("crypto");
context("Table API Test", () => {
beforeEach(() => {
connectionString.loginUsingConnectionString(connectionString.constants.table);
});
it("Create a new table in Table API", () => {
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
cy.get("iframe").then($element => {
const $body = $element.contents().find("body");
cy.wrap($body)
.find('div[class="commandBarContainer"]')
.should("be.visible")
.find('button[data-test="New Table"]')
.should("be.visible")
.click();
cy.wrap($body)
.find('div[class="contextual-pane-in"]')
.should("be.visible")
.find('span[id="containerTitle"]');
cy.wrap($body)
.find('input[data-test="addCollection-collectionId"]')
.type(collectionId);
cy.wrap($body)
.find('input[data-test="databaseThroughputValue"]')
.should("have.value", "400");
cy.wrap($body)
.find('input[data-test="addCollection-createCollection"]')
.click();
cy.wait(10000);
cy.wrap($body)
.find('div[data-test="resourceTreeId"]')
.should("exist")
.find('div[class="treeComponent dataResourceTree"]')
.should("contain", collectionId);
});
});
});

View File

@@ -0,0 +1,55 @@
// 1. Click on "New Container" on the command bar.
// 2. Pane with the title "Add Container" should appear on the right side of the screen
// 3. It includes an input box for the database Id.
// 4. It includes a checkbox called "Create now".
// 5. When the checkbox is marked, enter new database id.
// 3. Create a database WITH "Provision throughput" checked.
// 4. Enter minimum throughput value of 400.
// 5. Enter container id to the container id text box.
// 6. Enter partition key to the partition key text box.
// 7. Click "OK" to create a new container.
// 8. Verify the new container is created along with the database id and should appead in the Data Explorer list in the left side of the screen.
let crypt = require("crypto");
context("Emulator - createDatabase", () => {
beforeEach(() => {
cy.visit("http://localhost:1234/explorer.html");
});
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
const collectionIdTitle = `Add Collection`;
const partitionKey = `PartitionKey${crypt.randomBytes(8).toString("hex")}`;
it("Create a new collection", () => {
cy.contains("New Container").click();
// cy.contains(collectionIdTitle);
cy.get(".createNewDatabaseOrUseExisting")
.should("have.length", 2)
.and(input => {
expect(input.get(0).textContent, "first item").contains("Create new");
expect(input.get(1).textContent, "second item").contains("Use existing");
});
cy.get('input[data-test="addCollection-createNewDatabase"]').check();
cy.get('input[data-test="addCollection-newDatabaseId"]').type(dbId);
cy.get('input[data-test="addCollection-collectionId"]').type(collectionId);
cy.get('input[data-test="databaseThroughputValue"]').should("have.value", "400");
cy.get('input[data-test="addCollection-partitionKeyValue"]').type(partitionKey);
cy.get('input[data-test="addCollection-createCollection"]').click();
cy.get('div[data-test="resourceTreeId"]').should("exist");
cy.get('div[data-test="resourceTree-collectionsTree"]').should("contain", dbId);
cy.get('div[data-test="databaseList"]').should("contain", collectionId);
});
});

View File

@@ -0,0 +1,65 @@
// 1. Click on "New Database" on the command bar
// 2. a Pane with the title "Add Database" should appear on the right side of the screen
// i. It includes an input box for the database Id.
// ii. It includes a checkbox called "Provision throughput".
// iii. Whe the checkbox is marked, a new input with a throughput control let's you customize RU at the database level
// 3. Create a database WITHOUT "Provision throughput" checked.
// 4. It should appear in the Data Explorer list.
// 5. Repeat steps 1-3 but create a database WITH "Provision throughput" with the default RU value.
// 6. It should appear in the Data Explorer list.
// 7. If expanded, it should have the list item called "Scale", that once clicked, it should show the "Scale" tab.
// 8. Inside that tab, a throughput control will let you change the RU value within the permited range.
// 9. If you change the value, it should enable the "Save" button.
// 10. Click "Save" and verify that the process completes without error.
// 11. Close the tab and reopen it and verify that the input contains the last saved value.%
const crypto = require("crypto");
const client = require("../../../utilities/cosmosClient");
const randomString = crypto.randomBytes(2).toString("hex");
const databaseId = `TestDB-${randomString}`;
const collectionId = `TestColl-${randomString}`;
context("Emulator - Create database -> container -> item", () => {
beforeEach(async () => {
const { resources } = await client.databases.readAll().fetchAll();
for (const database of resources) {
await client.database(database.id).delete();
}
});
it("creates a new database", () => {
cy.visit("https://0.0.0.0:1234/explorer.html?platform=Emulator");
cy.contains("New Container").click();
cy.get("[data-test=addCollection-newDatabaseId]").click();
cy.get("[data-test=addCollection-newDatabaseId]").type(databaseId);
cy.get("[data-test=addCollection-collectionId]").click();
cy.get("[data-test=addCollection-collectionId]").type(collectionId);
cy.get("[data-test=addCollection-partitionKeyValue]").click();
cy.get("[data-test=addCollection-partitionKeyValue]").type("/pk");
cy.get('input[name="createCollection"]').click();
cy.get(".dataResourceTree").should("contain", databaseId);
cy.get(".dataResourceTree")
.contains(databaseId)
.click();
cy.get(".dataResourceTree").should("contain", collectionId);
cy.get(".dataResourceTree")
.contains(collectionId)
.click();
cy.get(".dataResourceTree")
.contains("Items")
.click();
cy.get(".dataResourceTree")
.contains("Items")
.click();
cy.wait(1000); // React rendering inside KO causes some weird async rendering that makes this test flaky without waiting
cy.get(".commandBarContainer")
.contains("New Item")
.click();
cy.wait(1000); // React rendering inside KO causes some weird async rendering that makes this test flaky without waiting
cy.get(".commandBarContainer")
.contains("Save")
.click();
cy.wait(1000); // React rendering inside KO causes some weird async rendering that makes this test flaky without waiting
cy.get(".documentsGridHeaderContainer").should("contain", "replace_with_new_document_id");
});
});

View File

@@ -0,0 +1,46 @@
// 1. Click last database in the resource tree
// 2. Click the last collection within the database
// 3. Select the context menu within the collection
// 4. Select "Delete Container" option in the dropdown
// 5. On Selection, Delete Container pane opens on the right side
// 6. Enter the same collection id that is to be deleted and click ok
// 7. Now, the resource tree refreshes, the deleted collection should not appear under the database
let crypt = require("crypto");
context("Emulator - deleteCollection", () => {
beforeEach(() => {
cy.visit("http://localhost:1234/explorer.html");
});
it("Delete a collection", () => {
cy.get(".databaseId")
.last()
.click();
cy.get(".collectionList")
.last()
.then($id => {
const collectionId = $id.text();
cy.get('span[data-test="collectionEllipsisMenu"]').should("exist");
cy.get('span[data-test="collectionEllipsisMenu"]')
.invoke("show")
.last()
.click();
cy.get('div[data-test="collectionContextMenu"]')
.contains("Delete Container")
.click({ force: true });
cy.get('input[data-test="confirmCollectionId"]').type(collectionId.trim());
cy.get('input[data-test="deleteCollection"]').click();
cy.get('div[data-test="databaseList"]').should("not.contain", collectionId);
cy.get('div[data-test="databaseMenu"]').should("not.contain", collectionId);
});
});
});

View File

@@ -0,0 +1,83 @@
// 1. Click last database in the resource tree
// 2. Select the context menu within the database
// 4. Select "Delete Database" option in the dropdown
// 5. On Selection, Delete Database pane opens on the right side
// 6. Enter the same database id that is to be deleted and click ok
// 7. Now, the resource tree refreshes, the deleted database should not appear in the resource tree
let crypt = require("crypto");
context("Emulator - deleteDatabase", () => {
beforeEach(() => {
const dbId = `TestDatabase${crypt.randomBytes(8).toString("hex")}`;
const collectionId = `TestCollection${crypt.randomBytes(8).toString("hex")}`;
let db_rid = "";
const date = new Date().toUTCString();
let authToken = "";
cy.visit("http://localhost:1234/explorer.html");
// Creating auth token for collection creation
cy.request({
method: "GET",
url: "https://localhost:8081/_explorer/authorization/post/dbs/",
headers: {
"x-ms-date": date,
authorization: "-"
}
})
.then(response => {
authToken = response.body.Token; // Getting auth token for collection creation
return new Cypress.Promise((resolve, reject) => {
return resolve();
});
})
.then(() => {
cy.request({
method: "POST",
url: "https://localhost:8081/dbs",
headers: {
"x-ms-date": date,
authorization: authToken,
"x-ms-version": "2018-12-31"
},
body: {
id: dbId
}
}).then(response => {
cy.log("Response", response);
db_rid = response.body._rid;
return new Cypress.Promise((resolve, reject) => {
cy.log("Rid", db_rid);
return resolve();
});
});
});
});
it("Delete a database", () => {
cy.get('span[data-test="refreshTree"]').click();
cy.get(".databaseId")
.last()
.then($id => {
const dbId = $id.text();
cy.get('span[data-test="databaseEllipsisMenu"]').should("exist");
cy.get('span[data-test="databaseEllipsisMenu"]')
.invoke("show")
.last()
.click();
cy.get('div[data-test="databaseContextMenu"]')
.contains("Delete Database")
.click({ force: true });
cy.get('input[data-test="confirmDatabaseId"]').type(dbId.trim());
cy.get('input[data-test="deleteDatabase"]').click();
cy.get('div[data-test="databaseList"]').should("not.contain", dbId);
});
});
});

View File

@@ -0,0 +1,35 @@
# Notebook end-to-end tests
This describes how to run the tests locally
## Stand up a local notebook container instance:
Instructions on how to build and run the container [here](https://microsoft.sharepoint.com/teams/DocDB/_layouts/OneNote.aspx?id=%2Fteams%2FDocDB%2FSiteAssets%2FDocDB%20Team%20Notebook&wd=target%28Tools%20_%20SDK%2FPortal%2FDevelopment.one%7CF800BE8E-1E31-48FE-90D7-EF698EF88112%2FHow%20to%20build%20notebook%20service%7C4BAA153B-422C-41E2-B997-F3FCE02CD743%2F%29)
## Run a local data explorer
Instructions are in [`DataExplorer/README.md`](https://msdata.visualstudio.com/CosmosDB/_git/cosmosdb-dataexplorer?path=%2FProduct%2FPortal%2FDataExplorer%2FREADME.md&_a=preview).
Make sure you can run Data Explorer locally from the web browser.
## Run cypress tests
1. Edit the URL for your DataExplorer in the `.spec.ts` file
2. Run the test:
```bash
cd DataExplorer/cypress
npm i
npm t -- --spec 'integration/notebook/newNotebook.spec.ts'
```
To run in Debug mode:
```
npm run test:debug
```
This opens Cypress UI
## Troubleshooting
* The tests are recorded in the `videos` folder.
* Cypress does not support hover: workarounds [here](https://docs.cypress.io/api/commands/hover.html#Workarounds).
## References
* [Cypress API](https://docs.cypress.io/api/api/table-of-contents.html)
* [Cypress cookbook](https://docs.cypress.io/faq/questions/using-cypress-faq.html#How-do-I-get-an-element%E2%80%99s-text-contents)
* [Cypress best practices](https://docs.cypress.io/guides/references/best-practices.html#Selecting-Elements)

View File

@@ -0,0 +1,93 @@
// THIS ADDS A NEW NOTEBOOK TO YOUR NOTEBOOKS
context("New Notebook smoke test", () => {
const timeout = 15000; // in ms
const explorerUrl =
"https://localhost:1234/explorer.html?feature.notebookserverurl=https%3A%2F%2Flocalhost%3A10001%2F12345%2Fnotebook&feature.notebookServerToken=token&feature.enablenotebooks=true";
/**
* Wait for UI to be ready
*/
const waitForReady = () => {
cy.get(".splashScreenContainer", { timeout }).should("be.visible");
};
beforeEach(() => {
cy.visit(explorerUrl);
waitForReady();
});
it("Create a new notebook and run some code", () => {
// Create new notebook
cy.contains("New Notebook").click();
// Check tab name
cy.get("li.tabList .tabNavText").should($span => {
const text = $span.text();
expect(text).to.match(/^Untitled.*\.ipynb$/);
});
// Wait for python3 | idle status
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
const text = $p.text();
expect(text).to.match(/^python3.*idle$/);
});
// Click on a cell
cy.get(".cell-container")
.as("cellContainer")
.click();
// Type in some code
cy.get("@cellContainer").type("2+4");
// Execute
cy.get('[data-test="Run"]')
.first()
.click();
// Verify results
cy.get("@cellContainer").within(() => {
cy.get("pre code span").should("contain", "6");
});
// Restart kernel
cy.get('[data-test="Run"] button')
.eq(-1)
.click();
cy.get("li")
.contains("Restart Kernel")
.click();
// Wait for python3 | restarting status
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
const text = $p.text();
expect(text).to.match(/^python3.*restarting$/);
});
// Wait for python3 | idle status
cy.get('[data-test="notebookStatusBar"] [data-test="kernelStatus"]', { timeout }).should($p => {
const text = $p.text();
expect(text).to.match(/^python3.*idle$/);
});
// Click on a cell
cy.get(".cell-container")
.as("cellContainer")
.find(".input")
.as("codeInput")
.click();
// Type in some code
cy.get("@codeInput").type("{backspace}{backspace}{backspace}4+5");
// Execute
cy.get('[data-test="Run"]')
.first()
.click();
// Verify results
cy.get("@cellContainer").within(() => {
cy.get("pre code span").should("contain", "9");
});
});
});

View File

@@ -0,0 +1,172 @@
context("Resource tree notebook file manipulation", () => {
const timeout = 15000; // in ms
const explorerUrl =
"https://localhost:1234/explorer.html?feature.notebookserverurl=https%3A%2F%2Flocalhost%3A10001%2F12345%2Fnotebook&feature.notebookServerToken=token&feature.enablenotebooks=true";
/**
* Wait for UI to be ready
*/
const waitForReady = () => {
cy.get(".splashScreenContainer", { timeout }).should("be.visible");
};
const clickContextMenuAndSelectOption = (nodeLabel, option) => {
cy.get(`.treeNodeHeader[data-test="${nodeLabel}"]`)
.find("button.treeMenuEllipsis")
.click();
cy.get('[data-test="treeComponentMenuItemContainer"]')
.contains(option)
.click();
};
const createFolder = folder => {
clickContextMenuAndSelectOption("My Notebooks/", "New Directory");
cy.get("#stringInputPane").within(() => {
cy.get('input[name="collectionIdConfirmation"]').type(folder);
cy.get("form").submit();
});
};
const deleteItem = nodeName => {
clickContextMenuAndSelectOption(`${nodeName}`, "Delete");
cy.get(".ms-Dialog-main")
.contains("Delete")
.click();
};
beforeEach(() => {
cy.visit(explorerUrl);
waitForReady();
});
it("Create and remove a directory", () => {
const folder = "e2etest_folder1";
createFolder(folder);
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).should("exist");
deleteItem(`${folder}/`);
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).should("not.exist");
});
it("Create and rename a directory", () => {
const folder = "e2etest_folder2";
const renamedFolder = "e2etest_folder2_renamed";
createFolder(folder);
// Rename
clickContextMenuAndSelectOption(`${folder}/`, "Rename");
cy.get("#stringInputPane").within(() => {
cy.get('input[name="collectionIdConfirmation"]')
.clear()
.type(renamedFolder);
cy.get("form").submit();
});
cy.get(`.treeNodeHeader[data-test="${renamedFolder}/"]`).should("exist");
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).should("not.exist");
deleteItem(`${renamedFolder}/`);
cy.get(`.treeNodeHeader[data-test="${renamedFolder}/"]`).should("not.exist");
});
it("Create a notebook inside a directory", () => {
const folder = "e2etest_folder3";
const newNotebookName = "Untitled.ipynb";
createFolder(folder);
clickContextMenuAndSelectOption(`${folder}/`, "New Notebook");
// Verify tab is open
cy.get(".tabList")
.contains(newNotebookName)
.should("exist");
// Close tab
cy.get(`.tabList[title="notebooks/${folder}/${newNotebookName}"]`)
.find(".cancelButton")
.click();
// When running from command line, closing the tab is too fast
cy.get("body").then($body => {
if ($body.find(".ms-Dialog-main").length) {
// For some reason, this does not work
// cy.get(".ms-Dialog-main").contains("Close").click();
cy.get(".ms-Dialog-main .ms-Button--primary").click();
}
});
// Expand folder node
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).click();
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("exist");
// Delete notebook
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`)
.find("button.treeMenuEllipsis")
.click();
cy.get('[data-test="treeComponentMenuItemContainer"]')
.contains("Delete")
.click();
// Confirm
cy.get(".ms-Dialog-main")
.contains("Delete")
.click();
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("not.exist");
deleteItem(`${folder}/`);
});
it("Create and rename a notebook inside a directory", () => {
const folder = "e2etest_folder4";
const newNotebookName = "Untitled.ipynb";
const renamedNotebookName = "mynotebook.ipynb";
createFolder(folder);
clickContextMenuAndSelectOption(`${folder}/`, "New Notebook");
// Close tab
cy.get(`.tabList[title="notebooks/${folder}/${newNotebookName}"]`)
.find(".cancelButton")
.click();
cy.get("body").then($body => {
if ($body.find(".ms-Dialog-main").length) {
// For some reason, this does not work
// cy.get(".ms-Dialog-main").contains("Close").click();
cy.get(".ms-Dialog-main .ms-Button--primary").click();
}
});
// Expand folder node
cy.get(`.treeNodeHeader[data-test="${folder}/"]`).click();
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("exist");
// Rename notebook
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`)
.find("button.treeMenuEllipsis")
.click();
cy.get('[data-test="treeComponentMenuItemContainer"]')
.contains("Rename")
.click();
cy.get("#stringInputPane").within(() => {
cy.get('input[name="collectionIdConfirmation"]')
.clear()
.type(renamedNotebookName);
cy.get("form").submit();
});
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${newNotebookName}"]`).should("not.exist");
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${renamedNotebookName}"]`).should("exist");
// Delete notebook
cy.get(`.nodeChildren[data-test="${folder}/"] .treeNodeHeader[data-test="${renamedNotebookName}"]`)
.find("button.treeMenuEllipsis")
.click();
cy.get('[data-test="treeComponentMenuItemContainer"]')
.contains("Delete")
.click();
// Confirm
cy.get(".ms-Dialog-main")
.contains("Delete")
.click();
// Give it time to settle
cy.wait(1000);
deleteItem(`${folder}/`);
});
});

3066
cypress/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
cypress/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "cosmos-explorer-cypress",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "cypress run",
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
"test:sql": "cypress run --browser chrome --spec \"./integration/dataexplorer/SQL/*\"",
"test:ci": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/ https-get://0.0.0.0:8081/_explorer/index.html && cypress run --browser edge --headless",
"test:debug": "cypress open"
},
"devDependencies": {
"cypress": "^4.8.0",
"mocha": "^7.0.1",
"mochawesome": "^4.1.0",
"mochawesome-merge": "^4.0.1",
"mochawesome-report-generator": "^4.1.0",
"typescript": "3.4.3",
"wait-on": "^4.0.2"
},
"dependencies": {
"@microsoft/applicationinsights-web": "^2.5.2"
}
}

23
cypress/support/index.js Normal file
View File

@@ -0,0 +1,23 @@
let appInsightsLib = require("@microsoft/applicationinsights-web");
const appInsights = new appInsightsLib.ApplicationInsights({
config: {
instrumentationKey: "fe61c39f-7d32-4488-a191-b13621965315"
/* ...Other Configuration Options... */
}
});
appInsights.loadAppInsights();
Cypress.on("fail", (error, runnable) => {
// App Insights will record the fail tests for Create Collection
let message = JSON.stringify(runnable.title);
appInsights.trackTrace({
message: `${message}`,
properties: {
passed: false,
error: error
}
});
throw error; // throw error to have test still fail
});

11
cypress/tsconfig.json Normal file
View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"strict": true,
"noEmit": true,
"module": "commonjs",
"target": "es5",
"lib": ["es5", "dom", "es6"],
"types": ["cypress", "node"]
},
"include": ["**/*.ts", "**/*.spec.ts"]
}

View File

@@ -0,0 +1,41 @@
module.exports = {
loginUsingConnectionString: function() {
const prodUrl = Cypress.env("TEST_ENDPOINT") || "https://localhost:1234/hostedExplorer.html";
const timeout = 15000;
cy.visit(prodUrl);
cy.get('iframe[id="explorerMenu"]').should("be.visible");
cy.get("iframe").then($element => {
const $body = $element.contents().find("body");
cy.wrap($body)
.find("#connectExplorer")
.should("exist")
.find("div[class='connectExplorer']")
.should("exist")
.find("p[class='welcomeText']")
.should("exist");
cy.wrap($body.find("div > p.switchConnectTypeText"))
.should("exist")
.last()
.click({ force: true });
const secret = Cypress.env("CONNECTION_STRING");
cy.wrap($body)
.find("input[class='inputToken']")
.should("exist")
.type(secret, {
force: true
});
cy.wrap($body.find("input[value='Connect']"), { timeout })
.first()
.click({ force: true });
cy.wait(15000);
});
}
};

View File

@@ -0,0 +1,6 @@
const { CosmosClient } = require("@azure/cosmos");
module.exports = new CosmosClient({
endpoint: "https://0.0.0.0:8081",
key: "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
});

View File

@@ -3,8 +3,8 @@
/******************************************************************************/ /******************************************************************************/
@font-face { @font-face {
font-family: wf_segoe-ui_normal; font-family: wf_segoe-ui_normal;
src: url("../../fonts/segoe-ui/west-european/normal/latest.woff"); src: url('../../fonts/segoe-ui/west-european/normal/latest.woff');
} }
@DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; @DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
@@ -20,26 +20,26 @@
COLORS COLORS
/******************************************************************************/ /******************************************************************************/
@AccentMediumHigh: #0058ad; @AccentMediumHigh: #0058AD;
@AccentMedium: #004e87; @AccentMedium: #004E87;
@AccentHigh: #1ebaed; @AccentHigh: #1EBAED;
@AccentExtraHigh: #55b3ff; @AccentExtraHigh: #55B3FF;
@AccentLow: #edf6ff; @AccentLow: #EDF6FF;
@AccentMediumLow: #ddeefe; @AccentMediumLow: #DDEEFE;
@AccentLight: #eef7ff; @AccentLight: #EEF7FF;
@AccentExtra: #ddf0ff; @AccentExtra: #DDF0FF;
@SelectionHigh: #b91f26; @SelectionHigh: #B91F26;
@BaseLight: #ffffff; @BaseLight: #FFFFFF;
@BaseDark: #000000; @BaseDark: #000000;
@NotificationLow: #fff4ce; @NotificationLow: #FFF4CE;
@NotificationHigh: #f9e9b0; @NotificationHigh: #F9E9B0;
@Purple1: #8a2da5; @Purple1: #8A2DA5;
@Dirty: #9b4f96; @Dirty: #9b4f96;
@BaseLow: #f2f2f2; @BaseLow: #F2F2F2;
@BaseMediumLow: #e6e6e6; @BaseMediumLow: #E6E6E6;
@BaseMedium: #cccccc; @BaseMedium: #CCCCCC;
@BaseMediumHigh: #767676; @BaseMediumHigh: #767676;
@BaseHigh: #393939; @BaseHigh: #393939;
@@ -53,7 +53,7 @@
@ErrorColor: @SelectionHigh; @ErrorColor: @SelectionHigh;
@SelectionColor: #3074b0; @SelectionColor: #3074B0;
@FocusColor: #605e5c; @FocusColor: #605e5c;
@@ -80,7 +80,7 @@
@ImgWidth: 14px; @ImgWidth: 14px;
@ImgHeight: 14px; @ImgHeight: 14px;
@toggleFontWeight: 700; @toggleFontWeight:700;
//Resource Tree //Resource Tree
@TreeLineHeight: 17px; @TreeLineHeight: 17px;
@@ -144,16 +144,16 @@
/**********************************************************************************/ /**********************************************************************************/
.flex-display(@display: flex) { .flex-display(@display: flex) {
display: ~"-webkit-@{display}"; display: ~"-webkit-@{display}";
display: ~"-ms-@{display}box"; // IE10 uses -ms-flexbox display: ~"-ms-@{display}box"; // IE10 uses -ms-flexbox
display: ~"-ms-@{display}"; // IE11 display: ~"-ms-@{display}"; // IE11
display: @display; display: @display;
} }
.flex-direction(@direction: column) { .flex-direction(@direction: column) {
-webkit-flex-direction: @direction; -webkit-flex-direction: @direction;
-ms-flex-direction: @direction; -ms-flex-direction: @direction;
flex-direction: @direction; flex-direction: @direction;
} }
/************************************************************************************* /*************************************************************************************
@@ -161,31 +161,32 @@
**************************************************************************************/ **************************************************************************************/
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
.selectedRadio, .selectedRadio,
.selectedRadio:hover, .selectedRadio:hover,
.selectedRadio:active, .selectedRadio:active,
.selectedRadio.dirty, .selectedRadio.dirty,
.tab [type="radio"]:checked ~ label, .tab [type=radio]:checked ~ label,
.tab [type="radio"]:checked ~ label:hover { .tab [type=radio]:checked ~ label:hover {
-ms-high-contrast-adjust: none; -ms-high-contrast-adjust: none;
-webkit-text-fill-color: HighlightText; -webkit-text-fill-color: HighlightText;
color: HighlightText; color: HighlightText;
border-color: HighlightText; border-color: HighlightText;
background-color: Highlight; background-color: Highlight;
} }
.queryMetricsSummaryTuple { .queryMetricsSummaryTuple {
th,
td { th, td {
&:nth-child(2) {
width: @IETableDataWidth; &:nth-child(2) {
} width: @IETableDataWidth;
}
&:nth-child(3) {
width: 50%; &:nth-child(3) {
} width: 50%;
}
}
} }
}
} }
/******************************************************************************************** /********************************************************************************************
@@ -193,15 +194,15 @@
*********************************************************************************************/ *********************************************************************************************/
.hover() { .hover() {
background-color: @AccentLight; background-color: @AccentLight;
} }
.active() { .active() {
background-color: @AccentExtra; background-color: @AccentExtra;
} }
.focus() { .focus() {
outline: 1px dashed @FocusColor; outline: 1px dashed @FocusColor;
} }
/************************************************************************************************ /************************************************************************************************
@@ -211,87 +212,63 @@
@ToggleWidth: 180px; @ToggleWidth: 180px;
.toggleSwitch() { .toggleSwitch() {
max-width: 100%; max-width: 100%;
margin-bottom: @SmallSpace; margin-bottom: @SmallSpace;
padding: @SmallSpace; padding: @SmallSpace;
cursor: pointer; cursor: pointer;
color: @BaseHigh; color: @BaseHigh;
font-weight: 400; font-weight: 400;
font-size: @mediumFontSize; font-size: @mediumFontSize;
font-family: @DataExplorerFont; font-family: @DataExplorerFont;
} }
.selectedToggle() { .selectedToggle() {
border-bottom: 2px solid @BaseHigh; border-bottom: 2px solid @BaseHigh;
} }
.unselectedToggle() { .unselectedToggle() {
color: @AccentMediumHigh; color: @AccentMediumHigh;
} }
/******************************************************************************************************** /********************************************************************************************************
Common Data Explorer Icons Common Data Explorer Icons
*********************************************************************************************************/ *********************************************************************************************************/
.dataExplorerIcons() { .dataExplorerIcons() {
cursor: pointer; cursor: pointer;
width: @ImgWidth; width: @ImgWidth;
height: @ImgHeight; height: @ImgHeight;
} }
/********************************************************************************************************* /*********************************************************************************************************
Info Tooltip Info Tooltip
**********************************************************************************************************/ **********************************************************************************************************/
.infoTooltip() { .infoTooltip() {
position: relative; position: relative;
display: inline-block; display: inline-block;
} }
.tooltipText(@textColor: @BaseLight, @backgroundColor: @BaseHigh) { .tooltipText(@textColor: @BaseLight, @backgroundColor: @BaseHigh) {
visibility: hidden; visibility: hidden;
background-color: @backgroundColor; background-color: @backgroundColor;
color: @textColor; color: @textColor;
position: absolute; position: absolute;
z-index: 1; z-index: 1;
left: @MediumSpace; left: @MediumSpace;
padding: @MediumSpace; padding: @MediumSpace;
} }
.tooltipTextAfter(@color: @BaseDark) { .tooltipTextAfter(@color: @BaseDark) {
content: ""; content: "";
position: absolute; position: absolute;
right: 100%; right: 100%;
border-style: solid; border-style: solid;
border-color: transparent @color transparent transparent; border-color: transparent @color transparent transparent;
left: 0px; left: 0px;
width: 0; width: 0;
height: 0; height: 0;
border-color: @InfoPointerColor transparent; border-color: @InfoPointerColor transparent;
} }
.tooltipVisible() { .tooltipVisible() {
visibility: visible; visibility: visible;
}
.inputTooltip() {
position: relative;
}
.inputTooltipText(@textColor: @BaseLight, @backgroundColor: @BaseHigh) {
background-color: @backgroundColor;
color: @textColor;
position: absolute;
z-index: 1;
padding: @MediumSpace;
}
.inputTooltipTextAfter(@color: @BaseDark) {
content: "";
position: absolute;
right: 100%;
border-style: solid;
border-color: transparent @color transparent transparent;
left: 10px;
width: 0;
height: 0;
border-color: @InfoPointerColor transparent;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -13,11 +13,6 @@
@NavMediumSpace: 10px; @NavMediumSpace: 10px;
@NavLargeSpace: 15px; @NavLargeSpace: 15px;
.skip-link {
position: fixed;
top: -200px;
}
html { html {
font-family: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; font-family: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
padding: 0px; padding: 0px;

View File

@@ -1,12 +1,20 @@
@import "./Common/Constants"; @import "./Common/Constants";
.main {
width: 100%;
float: left;
transition: all .0s ease-in-out;
-ms-transition: all 0s ease-in-out;
-webkit-transition: all 0s ease-in-out;
-moz-transition: all .0s ease-in-out;
height: 100%;
background-color: white;
border-left: 0px solid white;
}
.resourceTree { .resourceTree {
height: 100%; height: 100%;
flex: 0 0 auto; flex: 0 0 auto;
.main {
height: 100%;
}
} }
.resourceTreeScroll { .resourceTreeScroll {

3945
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,12 +4,8 @@
"description": "Cosmos Explorer", "description": "Cosmos Explorer",
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "3.9.0", "@azure/cosmos": "3.9.0",
"@azure/cosmos-language-service": "0.0.5", "@azure/cosmos-language-service": "0.0.4",
"@azure/identity": "1.1.0",
"@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-decorators": "7.12.12",
"@jupyterlab/services": "6.0.0-rc.2", "@jupyterlab/services": "6.0.0-rc.2",
"@jupyterlab/terminal": "3.0.0-rc.2", "@jupyterlab/terminal": "3.0.0-rc.2",
"@microsoft/applicationinsights-web": "2.5.9", "@microsoft/applicationinsights-web": "2.5.9",
@@ -25,7 +21,7 @@
"@nteract/jupyter-widgets": "2.0.0", "@nteract/jupyter-widgets": "2.0.0",
"@nteract/logos": "1.0.0", "@nteract/logos": "1.0.0",
"@nteract/markdown": "4.4.0", "@nteract/markdown": "4.4.0",
"@nteract/monaco-editor": "3.2.2", "@nteract/monaco-editor": "3.2.0",
"@nteract/octicons": "2.0.0", "@nteract/octicons": "2.0.0",
"@nteract/outputs": "3.0.9", "@nteract/outputs": "3.0.9",
"@nteract/presentational-components": "3.0.7", "@nteract/presentational-components": "3.0.7",
@@ -46,7 +42,7 @@
"applicationinsights": "1.8.0", "applicationinsights": "1.8.0",
"babel-polyfill": "6.26.0", "babel-polyfill": "6.26.0",
"bootstrap": "3.4.1", "bootstrap": "3.4.1",
"canvas": "file:./canvas", "canvas": "2.6.1",
"clean-webpack-plugin": "0.1.19", "clean-webpack-plugin": "0.1.19",
"copy-webpack-plugin": "6.0.2", "copy-webpack-plugin": "6.0.2",
"crossroads": "0.12.2", "crossroads": "0.12.2",
@@ -70,7 +66,7 @@
"jquery-ui-dist": "1.12.1", "jquery-ui-dist": "1.12.1",
"knockout": "3.5.1", "knockout": "3.5.1",
"mkdirp": "1.0.4", "mkdirp": "1.0.4",
"monaco-editor": "0.18.1", "monaco-editor": "0.15.6",
"object.entries": "1.1.0", "object.entries": "1.1.0",
"office-ui-fabric-react": "7.134.1", "office-ui-fabric-react": "7.134.1",
"p-retry": "4.2.0", "p-retry": "4.2.0",
@@ -87,7 +83,6 @@
"react-notification-system": "0.2.17", "react-notification-system": "0.2.17",
"react-redux": "7.1.3", "react-redux": "7.1.3",
"redux": "4.0.4", "redux": "4.0.4",
"reflect-metadata": "0.1.13",
"rx-jupyter": "5.5.12", "rx-jupyter": "5.5.12",
"rxjs": "6.6.3", "rxjs": "6.6.3",
"styled-components": "4.3.2", "styled-components": "4.3.2",
@@ -120,7 +115,7 @@
"@types/prop-types": "15.5.8", "@types/prop-types": "15.5.8",
"@types/puppeteer": "3.0.1", "@types/puppeteer": "3.0.1",
"@types/q": "1.5.1", "@types/q": "1.5.1",
"@types/react": "16.9.56", "@types/react": "16.9.49",
"@types/react-dom": "16.0.7", "@types/react-dom": "16.0.7",
"@types/react-notification-system": "0.2.39", "@types/react-notification-system": "0.2.39",
"@types/react-redux": "7.1.7", "@types/react-redux": "7.1.7",
@@ -199,8 +194,8 @@
"compile": "tsc", "compile": "tsc",
"compile:contracts": "tsc -p ./tsconfig.contracts.json", "compile:contracts": "tsc -p ./tsconfig.contracts.json",
"compile:strict": "tsc -p ./tsconfig.strict.json", "compile:strict": "tsc -p ./tsconfig.strict.json",
"format": "prettier --write \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"", "format": "prettier --write \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
"format:check": "prettier --check \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"", "format:check": "prettier --check \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
"lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"", "lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"",
"build:contracts": "npm run compile:contracts", "build:contracts": "npm run compile:contracts",
"strictEligibleFiles": "node ./strict-migration-tools/index.js", "strictEligibleFiles": "node ./strict-migration-tools/index.js",

View File

@@ -3,6 +3,7 @@
"offerThroughput": 400, "offerThroughput": 400,
"databaseLevelThroughput": false, "databaseLevelThroughput": false,
"collectionId": "Persons", "collectionId": "Persons",
"rupmEnabled": false,
"partitionKey": { "kind": "Hash", "paths": ["/name"] }, "partitionKey": { "kind": "Hash", "paths": ["/name"] },
"data": [ "data": [
"g.addV('person').property(id, '1').property('name', 'Eva').property('age', 44)", "g.addV('person').property(id, '1').property('name', 'Eva').property('age', 44)",
@@ -12,4 +13,4 @@
"g.V('1').addE('knows').to(g.V('2')).outV().addE('knows').to(g.V('3'))", "g.V('1').addE('knows').to(g.V('2')).outV().addE('knows').to(g.V('3'))",
"g.V('3').addE('knows').to(g.V('4'))" "g.V('3').addE('knows').to(g.V('4'))"
] ]
} }

View File

@@ -1,437 +1,489 @@
import { HashMap } from "./HashMap"; import { AutopilotTier } from "../Contracts/DataModels";
import { HashMap } from "./HashMap";
export class AuthorizationEndpoints {
public static arm: string = "https://management.core.windows.net/"; export class AuthorizationEndpoints {
public static common: string = "https://login.windows.net/"; public static arm: string = "https://management.core.windows.net/";
} public static common: string = "https://login.windows.net/";
}
export class CodeOfConductEndpoints {
public static privacyStatement: string = "https://aka.ms/ms-privacy-policy"; export class CodeOfConductEndpoints {
public static codeOfConduct: string = "https://aka.ms/cosmos-code-of-conduct"; public static privacyStatement: string = "https://aka.ms/ms-privacy-policy";
public static termsOfUse: string = "https://aka.ms/ms-terms-of-use"; public static codeOfConduct: string = "https://aka.ms/cosmos-code-of-conduct";
} public static termsOfUse: string = "https://aka.ms/ms-terms-of-use";
}
export class EndpointsRegex {
public static readonly cassandra = [ export class EndpointsRegex {
"AccountEndpoint=(.*).cassandra.cosmosdb.azure.com", public static readonly cassandra = [
"HostName=(.*).cassandra.cosmos.azure.com" "AccountEndpoint=(.*).cassandra.cosmosdb.azure.com",
]; "HostName=(.*).cassandra.cosmos.azure.com"
public static readonly mongo = "mongodb://.*:(.*)@(.*).documents.azure.com"; ];
public static readonly mongoCompute = "mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com"; public static readonly mongo = "mongodb://.*:(.*)@(.*).documents.azure.com";
public static readonly sql = "AccountEndpoint=https://(.*).documents.azure.com"; public static readonly mongoCompute = "mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com";
public static readonly table = "TableEndpoint=https://(.*).table.cosmosdb.azure.com"; public static readonly sql = "AccountEndpoint=https://(.*).documents.azure.com";
} public static readonly table = "TableEndpoint=https://(.*).table.cosmosdb.azure.com";
}
export class ApiEndpoints {
public static runtimeProxy: string = "/api/RuntimeProxy"; export class ApiEndpoints {
public static guestRuntimeProxy: string = "/api/guest/RuntimeProxy"; public static runtimeProxy: string = "/api/RuntimeProxy";
} public static guestRuntimeProxy: string = "/api/guest/RuntimeProxy";
}
export class ServerIds {
public static localhost: string = "localhost"; export class ServerIds {
public static blackforest: string = "blackforest"; public static localhost: string = "localhost";
public static fairfax: string = "fairfax"; public static blackforest: string = "blackforest";
public static mooncake: string = "mooncake"; public static fairfax: string = "fairfax";
public static productionPortal: string = "prod"; public static mooncake: string = "mooncake";
public static dev: string = "dev"; public static productionPortal: string = "prod";
} public static dev: string = "dev";
}
export class ArmApiVersions {
public static readonly documentDB: string = "2015-11-06"; export class ArmApiVersions {
public static readonly arcadia: string = "2019-06-01-preview"; public static readonly documentDB: string = "2015-11-06";
public static readonly arcadiaLivy: string = "2019-11-01-preview"; public static readonly arcadia: string = "2019-06-01-preview";
public static readonly arm: string = "2015-11-01"; public static readonly arcadiaLivy: string = "2019-11-01-preview";
public static readonly armFeatures: string = "2014-08-01-preview"; public static readonly arm: string = "2015-11-01";
public static readonly publicVersion = "2020-04-01"; public static readonly armFeatures: string = "2014-08-01-preview";
} public static readonly publicVersion = "2020-04-01";
}
export class ArmResourceTypes {
public static readonly notebookWorkspaces = "Microsoft.DocumentDB/databaseAccounts/notebookWorkspaces"; export class ArmResourceTypes {
public static readonly synapseWorkspaces = "Microsoft.Synapse/workspaces"; public static readonly notebookWorkspaces = "Microsoft.DocumentDB/databaseAccounts/notebookWorkspaces";
} public static readonly synapseWorkspaces = "Microsoft.Synapse/workspaces";
}
export class BackendDefaults {
public static partitionKeyKind: string = "Hash"; export class BackendDefaults {
public static singlePartitionStorageInGb: string = "10"; public static partitionKeyKind: string = "Hash";
public static multiPartitionStorageInGb: string = "100"; public static singlePartitionStorageInGb: string = "10";
public static maxChangeFeedRetentionDuration: number = 10; public static multiPartitionStorageInGb: string = "100";
public static partitionKeyVersion = 2; public static maxChangeFeedRetentionDuration: number = 10;
} public static partitionKeyVersion = 2;
}
export class ClientDefaults {
public static requestTimeoutMs: number = 60000; export class ClientDefaults {
public static portalCacheTimeoutMs: number = 10000; public static requestTimeoutMs: number = 60000;
public static errorNotificationTimeoutMs: number = 5000; public static portalCacheTimeoutMs: number = 10000;
public static copyHelperTimeoutMs: number = 2000; public static errorNotificationTimeoutMs: number = 5000;
public static waitForDOMElementMs: number = 500; public static copyHelperTimeoutMs: number = 2000;
public static cacheBustingTimeoutMs: number = public static waitForDOMElementMs: number = 500;
10 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/; public static cacheBustingTimeoutMs: number =
public static databaseThroughputIncreaseFactor: number = 100; 10 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/;
public static readonly arcadiaTokenRefreshInterval: number = public static databaseThroughputIncreaseFactor: number = 100;
20 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/; public static readonly arcadiaTokenRefreshInterval: number =
public static readonly arcadiaTokenRefreshIntervalPaddingMs: number = 2000; 20 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/;
} public static readonly arcadiaTokenRefreshIntervalPaddingMs: number = 2000;
}
export class AccountKind {
public static DocumentDB: string = "DocumentDB"; export class AccountKind {
public static MongoDB: string = "MongoDB"; public static DocumentDB: string = "DocumentDB";
public static Parse: string = "Parse"; public static MongoDB: string = "MongoDB";
public static GlobalDocumentDB: string = "GlobalDocumentDB"; public static Parse: string = "Parse";
public static Default: string = AccountKind.DocumentDB; public static GlobalDocumentDB: string = "GlobalDocumentDB";
} public static Default: string = AccountKind.DocumentDB;
}
export class CorrelationBackend {
public static Url: string = "https://aka.ms/cosmosdbanalytics"; export class CorrelationBackend {
} public static Url: string = "https://aka.ms/cosmosdbanalytics";
}
export class DefaultAccountExperience {
public static DocumentDB: string = "DocumentDB"; export class DefaultAccountExperience {
public static Graph: string = "Graph"; public static DocumentDB: string = "DocumentDB";
public static MongoDB: string = "MongoDB"; public static Graph: string = "Graph";
public static ApiForMongoDB: string = "Azure Cosmos DB for MongoDB API"; public static MongoDB: string = "MongoDB";
public static Table: string = "Table"; public static ApiForMongoDB: string = "Azure Cosmos DB for MongoDB API";
public static Cassandra: string = "Cassandra"; public static Table: string = "Table";
public static Default: string = DefaultAccountExperience.DocumentDB; public static Cassandra: string = "Cassandra";
} public static Default: string = DefaultAccountExperience.DocumentDB;
}
export class CapabilityNames {
public static EnableTable: string = "EnableTable"; export class CapabilityNames {
public static EnableGremlin: string = "EnableGremlin"; public static EnableTable: string = "EnableTable";
public static EnableCassandra: string = "EnableCassandra"; public static EnableGremlin: string = "EnableGremlin";
public static EnableAutoScale: string = "EnableAutoScale"; public static EnableCassandra: string = "EnableCassandra";
public static readonly EnableNotebooks: string = "EnableNotebooks"; public static EnableAutoScale: string = "EnableAutoScale";
public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics"; public static readonly EnableNotebooks: string = "EnableNotebooks";
public static readonly EnableMongo: string = "EnableMongo"; public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics";
public static readonly EnableServerless: string = "EnableServerless"; public static readonly EnableMongo: string = "EnableMongo";
} public static readonly EnableServerless: string = "EnableServerless";
}
export class Features {
public static readonly cosmosdb = "cosmosdb"; export class Features {
public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy"; public static readonly cosmosdb = "cosmosdb";
public static readonly executeSproc = "dataexplorerexecutesproc"; public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy";
public static readonly hostedDataExplorer = "hosteddataexplorerenabled"; public static readonly enableRupm = "enablerupm";
public static readonly enableTtl = "enablettl"; public static readonly executeSproc = "dataexplorerexecutesproc";
public static readonly enableNotebooks = "enablenotebooks"; public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
public static readonly enableGalleryPublish = "enablegallerypublish"; public static readonly enableTtl = "enablettl";
public static readonly enableLinkInjection = "enablelinkinjection"; public static readonly enableNotebooks = "enablenotebooks";
public static readonly enableSpark = "enablespark"; public static readonly enableGalleryPublish = "enablegallerypublish";
public static readonly livyEndpoint = "livyendpoint"; public static readonly enableCodeOfConduct = "enablecodeofconduct";
public static readonly notebookServerUrl = "notebookserverurl"; public static readonly enableLinkInjection = "enablelinkinjection";
public static readonly notebookServerToken = "notebookservertoken"; public static readonly enableSpark = "enablespark";
public static readonly notebookBasePath = "notebookbasepath"; public static readonly livyEndpoint = "livyendpoint";
public static readonly canExceedMaximumValue = "canexceedmaximumvalue"; public static readonly notebookServerUrl = "notebookserverurl";
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput"; public static readonly notebookServerToken = "notebookservertoken";
public static readonly ttl90Days = "ttl90days"; public static readonly notebookBasePath = "notebookbasepath";
public static readonly enableRightPanelV2 = "enablerightpanelv2"; public static readonly canExceedMaximumValue = "canexceedmaximumvalue";
public static readonly enableSchema = "enableschema"; public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
public static readonly enableSDKoperations = "enablesdkoperations"; public static readonly enableAutoPilotV2 = "enableautopilotv2";
public static readonly showMinRUSurvey = "showminrusurvey"; public static readonly ttl90Days = "ttl90days";
public static readonly selfServeType = "selfservetype"; public static readonly enableRightPanelV2 = "enablerightpanelv2";
} public static readonly enableSDKoperations = "enablesdkoperations";
}
// flight names returned from the portal are always lowercase
export class Flights { // flight names returned from the portal are always lowercase
public static readonly SettingsV2 = "settingsv2"; export class Flights {
public static readonly MongoIndexEditor = "mongoindexeditor"; public static readonly SettingsV2 = "settingsv2";
public static readonly AutoscaleTest = "autoscaletest"; }
public static readonly MongoIndexing = "mongoindexing";
} export class AfecFeatures {
public static readonly Spark = "spark-public-preview";
export class AfecFeatures { public static readonly Notebooks = "sparknotebooks-public-preview";
public static readonly Spark = "spark-public-preview"; public static readonly StorageAnalytics = "storageanalytics-public-preview";
public static readonly Notebooks = "sparknotebooks-public-preview"; }
public static readonly StorageAnalytics = "storageanalytics-public-preview";
} export class Spark {
public static readonly MaxWorkerCount = 10;
export class Spark { public static readonly SKUs: HashMap<string> = new HashMap({
public static readonly MaxWorkerCount = 10; "Cosmos.Spark.D1s": "D1s / 1 core / 4GB RAM",
public static readonly SKUs: HashMap<string> = new HashMap({ "Cosmos.Spark.D2s": "D2s / 2 cores / 8GB RAM",
"Cosmos.Spark.D1s": "D1s / 1 core / 4GB RAM", "Cosmos.Spark.D4s": "D4s / 4 cores / 16GB RAM",
"Cosmos.Spark.D2s": "D2s / 2 cores / 8GB RAM", "Cosmos.Spark.D8s": "D8s / 8 cores / 32GB RAM",
"Cosmos.Spark.D4s": "D4s / 4 cores / 16GB RAM", "Cosmos.Spark.D16s": "D16s / 16 cores / 64GB RAM",
"Cosmos.Spark.D8s": "D8s / 8 cores / 32GB RAM", "Cosmos.Spark.D32s": "D32s / 32 cores / 128GB RAM",
"Cosmos.Spark.D16s": "D16s / 16 cores / 64GB RAM", "Cosmos.Spark.D64s": "D64s / 64 cores / 256GB RAM"
"Cosmos.Spark.D32s": "D32s / 32 cores / 128GB RAM", });
"Cosmos.Spark.D64s": "D64s / 64 cores / 256GB RAM" }
});
} export class TagNames {
public static defaultExperience: string = "defaultExperience";
export class TagNames { }
public static defaultExperience: string = "defaultExperience";
} export class MongoDBAccounts {
public static protocol: string = "https";
export class MongoDBAccounts { public static defaultPort: string = "10255";
public static protocol: string = "https"; }
public static defaultPort: string = "10255";
} export enum MongoBackendEndpointType {
local,
export enum MongoBackendEndpointType { remote
local, }
remote
} // TODO: 435619 Add default endpoints per cloud and use regional only when available
export class CassandraBackend {
// TODO: 435619 Add default endpoints per cloud and use regional only when available public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
export class CassandraBackend { public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete";
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete"; public static readonly queryApi: string = "api/cassandra";
public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete"; public static readonly guestQueryApi: string = "api/guest/cassandra";
public static readonly queryApi: string = "api/cassandra"; public static readonly keysApi: string = "api/cassandra/keys";
public static readonly guestQueryApi: string = "api/guest/cassandra"; public static readonly guestKeysApi: string = "api/guest/cassandra/keys";
public static readonly keysApi: string = "api/cassandra/keys"; public static readonly schemaApi: string = "api/cassandra/schema";
public static readonly guestKeysApi: string = "api/guest/cassandra/keys"; public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
public static readonly schemaApi: string = "api/cassandra/schema"; }
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
} export class RUPMStates {
public static on: string = "on";
export class Queries { public static off: string = "off";
public static CustomPageOption: string = "custom"; }
public static UnlimitedPageOption: string = "unlimited";
public static itemsPerPage: number = 100; export class Queries {
public static unlimitedItemsPerPage: number = 100; // TODO: Figure out appropriate value so it works for accounts with a large number of partitions public static CustomPageOption: string = "custom";
public static UnlimitedPageOption: string = "unlimited";
public static QueryEditorMinHeightRatio: number = 0.1; public static itemsPerPage: number = 100;
public static QueryEditorMaxHeightRatio: number = 0.4; public static unlimitedItemsPerPage: number = 100; // TODO: Figure out appropriate value so it works for accounts with a large number of partitions
public static readonly DefaultMaxDegreeOfParallelism = 6;
} public static QueryEditorMinHeightRatio: number = 0.1;
public static QueryEditorMaxHeightRatio: number = 0.4;
export class SavedQueries { public static readonly DefaultMaxDegreeOfParallelism = 6;
public static readonly CollectionName: string = "___Query"; }
public static readonly DatabaseName: string = "___Cosmos";
public static readonly OfferThroughput: number = 400; export class SavedQueries {
public static readonly PartitionKeyProperty: string = "id"; public static readonly CollectionName: string = "___Query";
} public static readonly DatabaseName: string = "___Cosmos";
public static readonly OfferThroughput: number = 400;
export class DocumentsGridMetrics { public static readonly PartitionKeyProperty: string = "id";
public static DocumentsPerPage: number = 100; }
public static IndividualRowHeight: number = 34;
public static BufferHeight: number = 28; export class DocumentsGridMetrics {
public static SplitterMinWidth: number = 200; public static DocumentsPerPage: number = 100;
public static SplitterMaxWidth: number = 360; public static IndividualRowHeight: number = 34;
public static BufferHeight: number = 28;
public static DocumentEditorMinWidthRatio: number = 0.2; public static SplitterMinWidth: number = 200;
public static DocumentEditorMaxWidthRatio: number = 0.4; public static SplitterMaxWidth: number = 360;
}
public static DocumentEditorMinWidthRatio: number = 0.2;
export class ExplorerMetrics { public static DocumentEditorMaxWidthRatio: number = 0.4;
public static SplitterMinWidth: number = 240; }
public static SplitterMaxWidth: number = 400;
public static CollapsedResourceTreeWidth: number = 36; export class ExplorerMetrics {
} public static SplitterMinWidth: number = 240;
public static SplitterMaxWidth: number = 400;
export class SplitterMetrics { public static CollapsedResourceTreeWidth: number = 36;
public static CollapsedPositionLeft: number = ExplorerMetrics.CollapsedResourceTreeWidth; }
}
export class SplitterMetrics {
export class Areas { public static CollapsedPositionLeft: number = ExplorerMetrics.CollapsedResourceTreeWidth;
public static ResourceTree: string = "Resource Tree"; }
public static ContextualPane: string = "Contextual Pane";
public static Tab: string = "Tab"; export class Areas {
public static ShareDialog: string = "Share Access Dialog"; public static ResourceTree: string = "Resource Tree";
public static Notebook: string = "Notebook"; public static ContextualPane: string = "Contextual Pane";
} public static Tab: string = "Tab";
public static ShareDialog: string = "Share Access Dialog";
export class HttpHeaders { public static Notebook: string = "Notebook";
public static activityId: string = "x-ms-activity-id"; }
public static apiType: string = "x-ms-cosmos-apitype";
public static authorization: string = "authorization"; export class HttpHeaders {
public static collectionIndexTransformationProgress: string = public static activityId: string = "x-ms-activity-id";
"x-ms-documentdb-collection-index-transformation-progress"; public static apiType: string = "x-ms-cosmos-apitype";
public static continuation: string = "x-ms-continuation"; public static authorization: string = "authorization";
public static correlationRequestId: string = "x-ms-correlation-request-id"; public static collectionIndexTransformationProgress: string =
public static enableScriptLogging: string = "x-ms-documentdb-script-enable-logging"; "x-ms-documentdb-collection-index-transformation-progress";
public static guestAccessToken: string = "x-ms-encrypted-auth-token"; public static continuation: string = "x-ms-continuation";
public static getReadOnlyKey: string = "x-ms-get-read-only-key"; public static correlationRequestId: string = "x-ms-correlation-request-id";
public static connectionString: string = "x-ms-connection-string"; public static enableScriptLogging: string = "x-ms-documentdb-script-enable-logging";
public static msDate: string = "x-ms-date"; public static guestAccessToken: string = "x-ms-encrypted-auth-token";
public static location: string = "Location"; public static getReadOnlyKey: string = "x-ms-get-read-only-key";
public static contentType: string = "Content-Type"; public static connectionString: string = "x-ms-connection-string";
public static offerReplacePending: string = "x-ms-offer-replace-pending"; public static msDate: string = "x-ms-date";
public static user: string = "x-ms-user"; public static location: string = "Location";
public static populatePartitionStatistics: string = "x-ms-documentdb-populatepartitionstatistics"; public static contentType: string = "Content-Type";
public static queryMetrics: string = "x-ms-documentdb-query-metrics"; public static offerReplacePending: string = "x-ms-offer-replace-pending";
public static requestCharge: string = "x-ms-request-charge"; public static user: string = "x-ms-user";
public static resourceQuota: string = "x-ms-resource-quota"; public static populatePartitionStatistics: string = "x-ms-documentdb-populatepartitionstatistics";
public static resourceUsage: string = "x-ms-resource-usage"; public static queryMetrics: string = "x-ms-documentdb-query-metrics";
public static retryAfterMs: string = "x-ms-retry-after-ms"; public static requestCharge: string = "x-ms-request-charge";
public static scriptLogResults: string = "x-ms-documentdb-script-log-results"; public static resourceQuota: string = "x-ms-resource-quota";
public static populateCollectionThroughputInfo = "x-ms-documentdb-populatecollectionthroughputinfo"; public static resourceUsage: string = "x-ms-resource-usage";
public static supportSpatialLegacyCoordinates = "x-ms-documentdb-supportspatiallegacycoordinates"; public static retryAfterMs: string = "x-ms-retry-after-ms";
public static usePolygonsSmallerThanAHemisphere = "x-ms-documentdb-usepolygonssmallerthanahemisphere"; public static scriptLogResults: string = "x-ms-documentdb-script-log-results";
public static autoPilotThroughput = "autoscaleSettings"; public static populateCollectionThroughputInfo = "x-ms-documentdb-populatecollectionthroughputinfo";
public static autoPilotThroughputSDK = "x-ms-cosmos-offer-autopilot-settings"; public static supportSpatialLegacyCoordinates = "x-ms-documentdb-supportspatiallegacycoordinates";
public static partitionKey: string = "x-ms-documentdb-partitionkey"; public static usePolygonsSmallerThanAHemisphere = "x-ms-documentdb-usepolygonssmallerthanahemisphere";
public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput"; public static autoPilotThroughput = "autoscaleSettings";
public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot"; public static autoPilotThroughputSDK = "x-ms-cosmos-offer-autopilot-settings";
} public static autoPilotTier = "x-ms-cosmos-offer-autopilot-tier";
public static partitionKey: string = "x-ms-documentdb-partitionkey";
export class ApiType { public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput";
// Mapped to hexadecimal values in the backend public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot";
public static readonly MongoDB: number = 1; }
public static readonly Gremlin: number = 2;
public static readonly Cassandra: number = 4; export class ApiType {
public static readonly Table: number = 8; // Mapped to hexadecimal values in the backend
public static readonly SQL: number = 16; public static readonly MongoDB: number = 1;
} public static readonly Gremlin: number = 2;
public static readonly Cassandra: number = 4;
export class HttpStatusCodes { public static readonly Table: number = 8;
public static readonly OK: number = 200; public static readonly SQL: number = 16;
public static readonly Created: number = 201; }
public static readonly Accepted: number = 202;
public static readonly NoContent: number = 204; export class HttpStatusCodes {
public static readonly NotModified: number = 304; public static readonly OK: number = 200;
public static readonly Unauthorized: number = 401; public static readonly Created: number = 201;
public static readonly Forbidden: number = 403; public static readonly Accepted: number = 202;
public static readonly NotFound: number = 404; public static readonly NoContent: number = 204;
public static readonly TooManyRequests: number = 429; public static readonly NotModified: number = 304;
public static readonly Conflict: number = 409; public static readonly Unauthorized: number = 401;
public static readonly Forbidden: number = 403;
public static readonly InternalServerError: number = 500; public static readonly NotFound: number = 404;
public static readonly BadGateway: number = 502; public static readonly TooManyRequests: number = 429;
public static readonly ServiceUnavailable: number = 503; public static readonly Conflict: number = 409;
public static readonly GatewayTimeout: number = 504;
public static readonly InternalServerError: number = 500;
public static readonly RetryableStatusCodes: number[] = [ public static readonly BadGateway: number = 502;
HttpStatusCodes.TooManyRequests, public static readonly ServiceUnavailable: number = 503;
HttpStatusCodes.InternalServerError, // TODO: Handle all 500s on Portal backend and remove from retries list public static readonly GatewayTimeout: number = 504;
HttpStatusCodes.BadGateway,
HttpStatusCodes.ServiceUnavailable, public static readonly RetryableStatusCodes: number[] = [
HttpStatusCodes.GatewayTimeout HttpStatusCodes.TooManyRequests,
]; HttpStatusCodes.InternalServerError, // TODO: Handle all 500s on Portal backend and remove from retries list
} HttpStatusCodes.BadGateway,
HttpStatusCodes.ServiceUnavailable,
export class Urls { HttpStatusCodes.GatewayTimeout
public static feedbackEmail = "https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Data%20Explorer%20Feedback"; ];
public static autoscaleMigration = "https://aka.ms/cosmos-autoscale-migration"; }
public static freeTierInformation = "https://aka.ms/cosmos-free-tier";
public static cosmosPricing = "https://aka.ms/azure-cosmos-db-pricing"; export class Urls {
} public static feedbackEmail = "https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Data%20Explorer%20Feedback";
public static autoscaleMigration = "https://aka.ms/cosmos-autoscale-migration";
export class HashRoutePrefixes { public static freeTierInformation = "https://aka.ms/cosmos-free-tier";
public static databases: string = "/dbs/{db_id}"; public static cosmosPricing = "https://aka.ms/azure-cosmos-db-pricing";
public static collections: string = "/dbs/{db_id}/colls/{coll_id}"; }
public static sprocHash: string = "/sprocs/";
public static sprocs: string = HashRoutePrefixes.collections + HashRoutePrefixes.sprocHash + "{sproc_id}"; export class HashRoutePrefixes {
public static docs: string = HashRoutePrefixes.collections + "/docs/{doc_id}/"; public static databases: string = "/dbs/{db_id}";
public static conflicts: string = HashRoutePrefixes.collections + "/conflicts"; public static collections: string = "/dbs/{db_id}/colls/{coll_id}";
public static sprocHash: string = "/sprocs/";
public static databasesWithId(databaseId: string): string { public static sprocs: string = HashRoutePrefixes.collections + HashRoutePrefixes.sprocHash + "{sproc_id}";
return this.databases.replace("{db_id}", databaseId).replace("/", ""); // strip the first slash since hasher adds it public static docs: string = HashRoutePrefixes.collections + "/docs/{doc_id}/";
} public static conflicts: string = HashRoutePrefixes.collections + "/conflicts";
public static collectionsWithIds(databaseId: string, collectionId: string): string { public static databasesWithId(databaseId: string): string {
const transformedDatabasePrefix: string = this.collections.replace("{db_id}", databaseId); return this.databases.replace("{db_id}", databaseId).replace("/", ""); // strip the first slash since hasher adds it
}
return transformedDatabasePrefix.replace("{coll_id}", collectionId).replace("/", ""); // strip the first slash since hasher adds it
} public static collectionsWithIds(databaseId: string, collectionId: string): string {
const transformedDatabasePrefix: string = this.collections.replace("{db_id}", databaseId);
public static sprocWithIds(
databaseId: string, return transformedDatabasePrefix.replace("{coll_id}", collectionId).replace("/", ""); // strip the first slash since hasher adds it
collectionId: string, }
sprocId: string,
stripFirstSlash: boolean = true public static sprocWithIds(
): string { databaseId: string,
const transformedDatabasePrefix: string = this.sprocs.replace("{db_id}", databaseId); collectionId: string,
sprocId: string,
const transformedSprocRoute: string = transformedDatabasePrefix stripFirstSlash: boolean = true
.replace("{coll_id}", collectionId) ): string {
.replace("{sproc_id}", sprocId); const transformedDatabasePrefix: string = this.sprocs.replace("{db_id}", databaseId);
if (!!stripFirstSlash) {
return transformedSprocRoute.replace("/", ""); // strip the first slash since hasher adds it const transformedSprocRoute: string = transformedDatabasePrefix
} .replace("{coll_id}", collectionId)
.replace("{sproc_id}", sprocId);
return transformedSprocRoute; if (!!stripFirstSlash) {
} return transformedSprocRoute.replace("/", ""); // strip the first slash since hasher adds it
}
public static conflictsWithIds(databaseId: string, collectionId: string) {
const transformedDatabasePrefix: string = this.conflicts.replace("{db_id}", databaseId); return transformedSprocRoute;
}
return transformedDatabasePrefix.replace("{coll_id}", collectionId).replace("/", ""); // strip the first slash since hasher adds it;
} public static conflictsWithIds(databaseId: string, collectionId: string) {
const transformedDatabasePrefix: string = this.conflicts.replace("{db_id}", databaseId);
public static docsWithIds(databaseId: string, collectionId: string, docId: string) {
const transformedDatabasePrefix: string = this.docs.replace("{db_id}", databaseId); return transformedDatabasePrefix.replace("{coll_id}", collectionId).replace("/", ""); // strip the first slash since hasher adds it;
}
return transformedDatabasePrefix
.replace("{coll_id}", collectionId) public static docsWithIds(databaseId: string, collectionId: string, docId: string) {
.replace("{doc_id}", docId) const transformedDatabasePrefix: string = this.docs.replace("{db_id}", databaseId);
.replace("/", ""); // strip the first slash since hasher adds it
} return transformedDatabasePrefix
} .replace("{coll_id}", collectionId)
.replace("{doc_id}", docId)
export class ConfigurationOverridesValues { .replace("/", ""); // strip the first slash since hasher adds it
public static IsBsonSchemaV2: string = "true"; }
} }
export class KeyCodes { export class ConfigurationOverridesValues {
public static Space: number = 32; public static IsBsonSchemaV2: string = "true";
public static Enter: number = 13; }
public static Escape: number = 27;
public static UpArrow: number = 38; export class KeyCodes {
public static DownArrow: number = 40; public static Space: number = 32;
public static LeftArrow: number = 37; public static Enter: number = 13;
public static RightArrow: number = 39; public static Escape: number = 27;
public static Tab: number = 9; public static UpArrow: number = 38;
} public static DownArrow: number = 40;
public static LeftArrow: number = 37;
// Normalized per: https://www.w3.org/TR/uievents-key/#named-key-attribute-values public static RightArrow: number = 39;
export class NormalizedEventKey { public static Tab: number = 9;
public static readonly Space = " "; }
public static readonly Enter = "Enter";
public static readonly Escape = "Escape"; // Normalized per: https://www.w3.org/TR/uievents-key/#named-key-attribute-values
public static readonly UpArrow = "ArrowUp"; export class NormalizedEventKey {
public static readonly DownArrow = "ArrowDown"; public static readonly Space = " ";
public static readonly LeftArrow = "ArrowLeft"; public static readonly Enter = "Enter";
public static readonly RightArrow = "ArrowRight"; public static readonly Escape = "Escape";
} public static readonly UpArrow = "ArrowUp";
public static readonly DownArrow = "ArrowDown";
export class TryCosmosExperience { public static readonly LeftArrow = "ArrowLeft";
public static extendUrl: string = "https://trycosmosdb.azure.com/api/resource/extendportal?userId={0}"; public static readonly RightArrow = "ArrowRight";
public static deleteUrl: string = "https://trycosmosdb.azure.com/api/resource/deleteportal?userId={0}"; }
public static collectionsPerAccount: number = 3;
public static maxRU: number = 5000; export class TryCosmosExperience {
public static defaultRU: number = 3000; public static extendUrl: string = "https://trycosmosdb.azure.com/api/resource/extendportal?userId={0}";
} public static deleteUrl: string = "https://trycosmosdb.azure.com/api/resource/deleteportal?userId={0}";
public static collectionsPerAccount: number = 3;
export class OfferVersions { public static maxRU: number = 5000;
public static V1: string = "V1"; public static defaultRU: number = 3000;
public static V2: string = "V2"; }
}
export class OfferVersions {
export enum ConflictOperationType { public static V1: string = "V1";
Replace = "replace", public static V2: string = "V2";
Create = "create", }
Delete = "delete"
} export enum ConflictOperationType {
Replace = "replace",
export const EmulatorMasterKey = Create = "create",
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")] Delete = "delete"
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; }
// A variable @MyVariable defined in Constants.less is accessible as StyleConstants.MyVariable export class AutoPilot {
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less"); public static tier1Text: string = "4,000 RU/s";
public static tier2Text: string = "20,000 RU/s";
export class Notebook { public static tier3Text: string = "100,000 RU/s";
public static readonly defaultBasePath = "./notebooks"; public static tier4Text: string = "500,000 RU/s";
public static readonly heartbeatDelayMs = 5000;
public static readonly kernelRestartInitialDelayMs = 1000; public static tierText = {
public static readonly kernelRestartMaxDelayMs = 20000; [AutopilotTier.Tier1]: "Tier 1",
public static readonly autoSaveIntervalMs = 120000; [AutopilotTier.Tier2]: "Tier 2",
} [AutopilotTier.Tier3]: "Tier 3",
[AutopilotTier.Tier4]: "Tier 4"
export class SparkLibrary { };
public static readonly nameMinLength = 3;
public static readonly nameMaxLength = 63; public static tierMaxRus = {
} [AutopilotTier.Tier1]: 2000,
[AutopilotTier.Tier2]: 20000,
export class AnalyticalStorageTtl { [AutopilotTier.Tier3]: 100000,
public static readonly Days90: number = 7776000; [AutopilotTier.Tier4]: 500000
public static readonly Infinite: number = -1; };
public static readonly Disabled: number = 0;
} public static tierMinRus = {
[AutopilotTier.Tier1]: 0,
export class TerminalQueryParams { [AutopilotTier.Tier2]: 0,
public static readonly Terminal = "terminal"; [AutopilotTier.Tier3]: 0,
public static readonly Server = "server"; [AutopilotTier.Tier4]: 0
public static readonly Token = "token"; };
public static readonly SubscriptionId = "subscriptionId";
public static readonly TerminalEndpoint = "terminalEndpoint"; public static tierStorageInGB = {
} [AutopilotTier.Tier1]: 50,
[AutopilotTier.Tier2]: 200,
[AutopilotTier.Tier3]: 1000,
[AutopilotTier.Tier4]: 5000
};
}
export class DataExplorerVersions {
public static readonly v_1_0_0: string = "1.0.0";
public static readonly v_1_0_1: string = "1.0.1";
}
export class DataExplorerFeatures {
public static offerCache: string = "OfferCache";
}
export const DataExplorerFeaturesVersions: any = {
OfferCache: DataExplorerVersions.v_1_0_1
};
export const EmulatorMasterKey =
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
// A variable @MyVariable defined in Constants.less is accessible as StyleConstants.MyVariable
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
export class Notebook {
public static readonly defaultBasePath = "./notebooks";
public static readonly heartbeatDelayMs = 5000;
public static readonly kernelRestartInitialDelayMs = 1000;
public static readonly kernelRestartMaxDelayMs = 20000;
public static readonly autoSaveIntervalMs = 120000;
}
export class SparkLibrary {
public static readonly nameMinLength = 3;
public static readonly nameMaxLength = 63;
}
export class AnalyticalStorageTtl {
public static readonly Days90: number = 7776000;
public static readonly Infinite: number = -1;
public static readonly Disabled: number = 0;
}
export class TerminalQueryParams {
public static readonly Terminal = "terminal";
public static readonly Server = "server";
public static readonly Token = "token";
public static readonly SubscriptionId = "subscriptionId";
public static readonly TerminalEndpoint = "terminalEndpoint";
}

View File

@@ -112,6 +112,7 @@ describe("endpoint", () => {
describe("requestPlugin", () => { describe("requestPlugin", () => {
beforeEach(() => { beforeEach(() => {
delete window.dataExplorerPlatform;
resetConfigContext(); resetConfigContext();
}); });

View File

@@ -1,7 +1,6 @@
import * as Cosmos from "@azure/cosmos"; import * as Cosmos from "@azure/cosmos";
import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos"; import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos";
import { configContext, Platform } from "../ConfigContext"; import { configContext, Platform } from "../ConfigContext";
import { getErrorMessage } from "./ErrorHandlingUtils";
import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { EmulatorMasterKey, HttpHeaders } from "./Constants"; import { EmulatorMasterKey, HttpHeaders } from "./Constants";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
@@ -70,7 +69,7 @@ export async function getTokenFromAuthService(verb: string, resourceType: string
const result = JSON.parse(await response.json()); const result = JSON.parse(await response.json());
return result; return result;
} catch (error) { } catch (error) {
logConsoleError(`Failed to get authorization headers for ${resourceType}: ${getErrorMessage(error)}`); logConsoleError(`Failed to get authorization headers for ${resourceType}: ${JSON.stringify(error)}`);
return Promise.reject(error); return Promise.reject(error);
} }
} }

View File

@@ -1,13 +1,13 @@
import { getCommonQueryOptions } from "./queryDocuments"; import { getCommonQueryOptions } from "./DataAccessUtilityBase";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
describe("getCommonQueryOptions", () => { describe("getCommonQueryOptions", () => {
it("builds the correct default options objects", () => { it("builds the correct default options objects", () => {
expect(getCommonQueryOptions({})).toMatchSnapshot(); expect(getCommonQueryOptions({})).toMatchSnapshot();
}); });
it("reads from localStorage", () => { it("reads from localStorage", () => {
LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, 37); LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, 37);
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, 17); LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, 17);
expect(getCommonQueryOptions({})).toMatchSnapshot(); expect(getCommonQueryOptions({})).toMatchSnapshot();
}); });
}); });

View File

@@ -0,0 +1,198 @@
import {
ConflictDefinition,
FeedOptions,
ItemDefinition,
OfferDefinition,
QueryIterator,
Resource
} from "@azure/cosmos";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import Q from "q";
import { configContext, Platform } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import * as ViewModels from "../Contracts/ViewModels";
import ConflictId from "../Explorer/Tree/ConflictId";
import DocumentId from "../Explorer/Tree/DocumentId";
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
import { OfferUtils } from "../Utils/OfferUtils";
import * as Constants from "./Constants";
import { client } from "./CosmosClient";
import * as HeadersUtility from "./HeadersUtility";
import { sendCachedDataMessage } from "./MessageHandler";
export function getCommonQueryOptions(options: FeedOptions): any {
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);
options = options || {};
options.populateQueryMetrics = true;
options.enableScanInQuery = options.enableScanInQuery || true;
if (!options.partitionKey) {
options.forceQueryPlan = true;
}
options.maxItemCount =
options.maxItemCount ||
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
Constants.Queries.itemsPerPage;
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
return options;
}
export function queryDocuments(
databaseId: string,
containerId: string,
query: string,
options: any
): Q.Promise<QueryIterator<ItemDefinition & Resource>> {
options = getCommonQueryOptions(options);
const documentsIterator = client()
.database(databaseId)
.container(containerId)
.items.query(query, options);
return Q(documentsIterator);
}
export function getPartitionKeyHeaderForConflict(conflictId: ConflictId): Object {
const partitionKeyDefinition: DataModels.PartitionKey = conflictId.partitionKey;
const partitionKeyValue: any = conflictId.partitionKeyValue;
return getPartitionKeyHeader(partitionKeyDefinition, partitionKeyValue);
}
export function getPartitionKeyHeader(partitionKeyDefinition: DataModels.PartitionKey, partitionKeyValue: any): Object {
if (!partitionKeyDefinition) {
return undefined;
}
if (partitionKeyValue === undefined) {
return [{}];
}
return [partitionKeyValue];
}
export function updateDocument(
collection: ViewModels.CollectionBase,
documentId: DocumentId,
newDocument: any
): Q.Promise<any> {
const partitionKey = documentId.partitionKeyValue;
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), partitionKey)
.replace(newDocument)
.then(response => response.resource)
);
}
export function executeStoredProcedure(
collection: ViewModels.Collection,
storedProcedure: StoredProcedure,
partitionKeyValue: any,
params: any[]
): Q.Promise<any> {
// TODO remove this deferred. Kept it because of timeout code at bottom of function
const deferred = Q.defer<any>();
client()
.database(collection.databaseId)
.container(collection.id())
.scripts.storedProcedure(storedProcedure.id())
.execute(partitionKeyValue, params, { enableScriptLogging: true })
.then(response =>
deferred.resolve({
result: response.resource,
scriptLogs: response.headers[Constants.HttpHeaders.scriptLogResults]
})
)
.catch(error => deferred.reject(error));
return deferred.promise.timeout(
Constants.ClientDefaults.requestTimeoutMs,
`Request timed out while executing stored procedure ${storedProcedure.id()}`
);
}
export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.items.create(newDocument)
.then(response => response.resource)
);
}
export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
const partitionKey = documentId.partitionKeyValue;
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), partitionKey)
.read()
.then(response => response.resource)
);
}
export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
const partitionKey = documentId.partitionKeyValue;
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), partitionKey)
.delete()
);
}
export function deleteConflict(
collection: ViewModels.CollectionBase,
conflictId: ConflictId,
options: any = {}
): Q.Promise<any> {
options.partitionKey = options.partitionKey || getPartitionKeyHeaderForConflict(conflictId);
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.conflict(conflictId.id())
.delete(options)
);
}
export function refreshCachedOffers(): Q.Promise<void> {
if (configContext.platform === Platform.Portal) {
return sendCachedDataMessage(MessageTypes.RefreshOffers, []);
} else {
return Q();
}
}
export function refreshCachedResources(options?: any): Q.Promise<void> {
if (configContext.platform === Platform.Portal) {
return sendCachedDataMessage(MessageTypes.RefreshResources, []);
} else {
return Q();
}
}
export function queryConflicts(
databaseId: string,
containerId: string,
query: string,
options: any
): Q.Promise<QueryIterator<ConflictDefinition & Resource>> {
const documentsIterator = client()
.database(databaseId)
.container(containerId)
.conflicts.query(query, options);
return Q(documentsIterator);
}

View File

@@ -0,0 +1,244 @@
import { ConflictDefinition, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import Q from "q";
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import ConflictId from "../Explorer/Tree/ConflictId";
import DocumentId from "../Explorer/Tree/DocumentId";
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
import * as Constants from "./Constants";
import { sendNotificationForError } from "./dataAccess/sendNotificationForError";
import * as DataAccessUtilityBase from "./DataAccessUtilityBase";
import { MinimalQueryIterator, nextPage } from "./IteratorUtilities";
import * as Logger from "./Logger";
// TODO: Log all promise resolutions and errors with verbosity levels
export function queryDocuments(
databaseId: string,
containerId: string,
query: string,
options: any
): Q.Promise<QueryIterator<ItemDefinition & Resource>> {
return DataAccessUtilityBase.queryDocuments(databaseId, containerId, query, options);
}
export function queryConflicts(
databaseId: string,
containerId: string,
query: string,
options: any
): Q.Promise<QueryIterator<ConflictDefinition & Resource>> {
return DataAccessUtilityBase.queryConflicts(databaseId, containerId, query, options);
}
export function getEntityName() {
const defaultExperience =
window.dataExplorer && window.dataExplorer.defaultExperience && window.dataExplorer.defaultExperience();
if (defaultExperience === Constants.DefaultAccountExperience.MongoDB) {
return "document";
}
return "item";
}
export function executeStoredProcedure(
collection: ViewModels.Collection,
storedProcedure: StoredProcedure,
partitionKeyValue: any,
params: any[]
): Q.Promise<any> {
var deferred = Q.defer<any>();
const clearMessage = logConsoleProgress(`Executing stored procedure ${storedProcedure.id()}`);
DataAccessUtilityBase.executeStoredProcedure(collection, storedProcedure, partitionKeyValue, params)
.then(
(response: any) => {
deferred.resolve(response);
logConsoleInfo(
`Finished executing stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
);
},
(error: any) => {
logConsoleError(
`Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}: ${JSON.stringify(
error
)}`
);
Logger.logError(JSON.stringify(error), "ExecuteStoredProcedure", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}
export function queryDocumentsPage(
resourceName: string,
documentsIterator: MinimalQueryIterator,
firstItemIndex: number,
options: any
): Q.Promise<ViewModels.QueryResults> {
var deferred = Q.defer<ViewModels.QueryResults>();
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
Q(nextPage(documentsIterator, firstItemIndex))
.then(
(result: ViewModels.QueryResults) => {
const itemCount = (result.documents && result.documents.length) || 0;
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
deferred.resolve(result);
},
(error: any) => {
logConsoleError(`Failed to query ${entityName} for container ${resourceName}: ${JSON.stringify(error)}`);
Logger.logError(JSON.stringify(error), "QueryDocumentsPage", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}
export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
var deferred = Q.defer<any>();
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Reading ${entityName} ${documentId.id()}`);
DataAccessUtilityBase.readDocument(collection, documentId)
.then(
(document: any) => {
deferred.resolve(document);
},
(error: any) => {
logConsoleError(`Failed to read ${entityName} ${documentId.id()}: ${JSON.stringify(error)}`);
Logger.logError(JSON.stringify(error), "ReadDocument", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}
export function updateDocument(
collection: ViewModels.CollectionBase,
documentId: DocumentId,
newDocument: any
): Q.Promise<any> {
var deferred = Q.defer<any>();
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Updating ${entityName} ${documentId.id()}`);
DataAccessUtilityBase.updateDocument(collection, documentId, newDocument)
.then(
(updatedDocument: any) => {
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);
deferred.resolve(updatedDocument);
},
(error: any) => {
logConsoleError(`Failed to update ${entityName} ${documentId.id()}: ${JSON.stringify(error)}`);
Logger.logError(JSON.stringify(error), "UpdateDocument", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}
export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
var deferred = Q.defer<any>();
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Creating new ${entityName} for container ${collection.id()}`);
DataAccessUtilityBase.createDocument(collection, newDocument)
.then(
(savedDocument: any) => {
logConsoleInfo(`Successfully created new ${entityName} for container ${collection.id()}`);
deferred.resolve(savedDocument);
},
(error: any) => {
logConsoleError(
`Error while creating new ${entityName} for container ${collection.id()}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "CreateDocument", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}
export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
var deferred = Q.defer<any>();
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Deleting ${entityName} ${documentId.id()}`);
DataAccessUtilityBase.deleteDocument(collection, documentId)
.then(
(response: any) => {
logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`);
deferred.resolve(response);
},
(error: any) => {
logConsoleError(`Error while deleting ${entityName} ${documentId.id()}:\n ${JSON.stringify(error)}`);
Logger.logError(JSON.stringify(error), "DeleteDocument", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}
export function deleteConflict(
collection: ViewModels.CollectionBase,
conflictId: ConflictId,
options?: any
): Q.Promise<any> {
var deferred = Q.defer<any>();
const clearMessage = logConsoleProgress(`Deleting conflict ${conflictId.id()}`);
DataAccessUtilityBase.deleteConflict(collection, conflictId, options)
.then(
(response: any) => {
logConsoleInfo(`Successfully deleted conflict ${conflictId.id()}`);
deferred.resolve(response);
},
(error: any) => {
logConsoleError(`Error while deleting conflict ${conflictId.id()}:\n ${JSON.stringify(error)}`);
Logger.logError(JSON.stringify(error), "DeleteConflict", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}
export function refreshCachedResources(options: any = {}): Q.Promise<void> {
return DataAccessUtilityBase.refreshCachedResources(options);
}
export function refreshCachedOffers(): Q.Promise<void> {
return DataAccessUtilityBase.refreshCachedOffers();
}

View File

@@ -1,10 +0,0 @@
import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType";
import { userContext } from "../UserContext";
export const getEntityName = (): string => {
if (userContext.defaultExperience === DefaultAccountExperienceType.MongoDB) {
return "document";
}
return "item";
};

View File

@@ -1,6 +1,8 @@
export function normalizeArmEndpoint(uri: string): string { export default class EnvironmentUtility {
if (uri && uri.slice(-1) !== "/") { public static normalizeArmEndpointUri(uri: string): string {
return `${uri}/`; if (uri && uri.slice(-1) !== "/") {
return `${uri}/`;
}
return uri;
} }
return uri;
} }

View File

@@ -1,56 +0,0 @@
import { ARMError } from "../Utils/arm/request";
import { HttpStatusCodes } from "./Constants";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { SubscriptionType } from "../Contracts/SubscriptionType";
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { logError } from "./Logger";
import { sendMessage } from "./MessageHandler";
export const handleError = (error: string | ARMError | Error, area: string, consoleErrorPrefix?: string): void => {
const errorMessage = getErrorMessage(error);
const errorCode = error instanceof ARMError ? error.code : undefined;
// logs error to data explorer console
const consoleErrorMessage = consoleErrorPrefix ? `${consoleErrorPrefix}:\n ${errorMessage}` : errorMessage;
logConsoleError(consoleErrorMessage);
// logs error to both app insight and kusto
logError(errorMessage, area, errorCode);
// checks for errors caused by firewall and sends them to portal to handle
sendNotificationForError(errorMessage, errorCode);
};
export const getErrorMessage = (error: string | Error = ""): string => {
const errorMessage = typeof error === "string" ? error : error.message;
return replaceKnownError(errorMessage);
};
export const getErrorStack = (error: string | Error): string => {
return typeof error === "string" ? undefined : error.stack;
};
const sendNotificationForError = (errorMessage: string, errorCode: number | string): void => {
if (errorCode === HttpStatusCodes.Forbidden) {
if (errorMessage?.toLowerCase().indexOf("sharedoffer is disabled for your account") > 0) {
return;
}
sendMessage({
type: MessageTypes.ForbiddenError,
reason: errorMessage
});
}
};
const replaceKnownError = (errorMessage: string): string => {
if (
window.dataExplorer?.subscriptionType() === SubscriptionType.Internal &&
errorMessage?.indexOf("SharedOffer is Disabled for your account") >= 0
) {
return "Database throughput is not supported for internal subscriptions.";
} else if (errorMessage?.indexOf("Partition key paths must contain only valid") >= 0) {
return "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character.";
}
return errorMessage;
};

View File

@@ -0,0 +1,24 @@
import * as ErrorParserUtility from "./ErrorParserUtility";
describe("Error Parser Utility", () => {
describe("shouldEnableCrossPartitionKeyForResourceWithPartitionKey()", () => {
it("should parse a backend error correctly", () => {
// A fake error matching what is thrown by the SDK on a bad collection create request
const innerMessage =
"The partition key component definition path '/asdwqr31 @#$#$WRadf' could not be accepted, failed near position '10'. Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character.";
const message = `Message: {\"Errors\":[\"${innerMessage}\"]}\r\nActivityId: 97b2e684-7505-4921-85f6-2513b9b28220, Request URI: /apps/89fdcf25-2a0b-4d2a-aab6-e161e565b26f/services/54911149-7bb1-4e7d-a1fa-22c8b36a4bb9/partitions/cc2a7a04-5f5a-4709-bcf7-8509b264963f/replicas/132304018743619218p, RequestStats: , SDK: Microsoft.Azure.Documents.Common/2.10.0`;
const err = new Error(message) as any;
err.code = 400;
err.body = {
code: "BadRequest",
message
};
err.headers = {};
err.activityId = "97b2e684-7505-4921-85f6-2513b9b28220";
const parsedError = ErrorParserUtility.parse(err);
expect(parsedError.length).toBe(1);
expect(parsedError[0].message).toBe(innerMessage);
});
});
});

View File

@@ -0,0 +1,69 @@
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
export function replaceKnownError(err: string): string {
if (
window.dataExplorer.subscriptionType() === ViewModels.SubscriptionType.Internal &&
err.indexOf("SharedOffer is Disabled for your account") >= 0
) {
return "Database throughput is not supported for internal subscriptions.";
} else if (err.indexOf("Partition key paths must contain only valid") >= 0) {
return "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character.";
}
return err;
}
export function parse(err: any): DataModels.ErrorDataModel[] {
try {
return _parse(err);
} catch (e) {
return [<DataModels.ErrorDataModel>{ message: JSON.stringify(err) }];
}
}
function _parse(err: any): DataModels.ErrorDataModel[] {
var normalizedErrors: DataModels.ErrorDataModel[] = [];
if (err.message && !err.code) {
normalizedErrors.push(err);
} else {
const innerErrors: any[] = _getInnerErrors(err.message);
normalizedErrors = innerErrors.map(innerError =>
typeof innerError === "string" ? { message: innerError } : innerError
);
}
return normalizedErrors;
}
function _getInnerErrors(message: string): any[] {
/*
The backend error message has an inner-message which is a stringified object.
For SQL errors, the "errors" property is an array of SqlErrorDataModel.
Example:
"Message: {"Errors":["Resource with specified id or name already exists"]}\r\nActivityId: 80005000008d40b6a, Request URI: /apps/19000c000c0a0005/services/mctestdocdbprod-MasterService-0-00066ab9937/partitions/900005f9000e676fb8/replicas/13000000000955p"
For non-SQL errors the "Errors" propery is an array of string.
Example:
"Message: {"errors":[{"severity":"Error","location":{"start":7,"end":8},"code":"SC1001","message":"Syntax error, incorrect syntax near '.'."}]}\r\nActivityId: d3300016d4084e310a, Request URI: /apps/12401f9e1df77/services/dc100232b1f44545/partitions/f86f3bc0001a2f78/replicas/13085003638s"
*/
let innerMessage: any = null;
const singleLineMessage = message.replace(/[\r\n]|\r|\n/g, "");
try {
// Multi-Partition error flavor
const regExp = /^(.*)ActivityId: (.*)/g;
const regString = regExp.exec(singleLineMessage);
const innerMessageString = regString[1];
innerMessage = JSON.parse(innerMessageString);
} catch (e) {
// Single-partition error flavor
const regExp = /^Message: (.*)ActivityId: (.*), Request URI: (.*)/g;
const regString = regExp.exec(singleLineMessage);
const innerMessageString = regString[1];
innerMessage = JSON.parse(innerMessageString);
}
return innerMessage.errors ? innerMessage.errors : innerMessage.Errors;
}

View File

@@ -21,8 +21,14 @@ export function logWarning(message: string, area: string, code?: number): void {
return _logEntry(entry); return _logEntry(entry);
} }
export function logError(errorMessage: string, area: string, code?: number | string): void { export function logError(message: string | Error, area: string, code?: number): void {
const entry: Diagnostics.LogEntry = _generateLogEntry(Diagnostics.LogEntryLevel.Error, errorMessage, area, code); let logMessage: string;
if (typeof message === "string") {
logMessage = message;
} else {
logMessage = JSON.stringify(message, Object.getOwnPropertyNames(message));
}
const entry: Diagnostics.LogEntry = _generateLogEntry(Diagnostics.LogEntryLevel.Error, logMessage, area, code);
return _logEntry(entry); return _logEntry(entry);
} }
@@ -53,7 +59,7 @@ function _generateLogEntry(
level: Diagnostics.LogEntryLevel, level: Diagnostics.LogEntryLevel,
message: string, message: string,
area: string, area: string,
code?: number | string code?: number
): Diagnostics.LogEntry { ): Diagnostics.LogEntry {
return { return {
timestamp: new Date().getUTCSeconds(), timestamp: new Date().getUTCSeconds(),

View File

@@ -1,64 +0,0 @@
import * as OfferUtility from "./OfferUtility";
import { SDKOfferDefinition, Offer } from "../Contracts/DataModels";
import { OfferResponse } from "@azure/cosmos";
describe("parseSDKOfferResponse", () => {
it("manual throughput", () => {
const mockOfferDefinition = {
content: {
offerThroughput: 500,
collectionThroughputInfo: {
minimumRUForCollection: 400,
numPhysicalPartitions: 1
}
},
id: "test"
} as SDKOfferDefinition;
const mockResponse = {
resource: mockOfferDefinition
} as OfferResponse;
const expectedResult: Offer = {
manualThroughput: 500,
autoscaleMaxThroughput: undefined,
minimumThroughput: 400,
id: "test",
offerDefinition: mockOfferDefinition,
offerReplacePending: false
};
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
});
it("autoscale throughput", () => {
const mockOfferDefinition = {
content: {
offerThroughput: 400,
collectionThroughputInfo: {
minimumRUForCollection: 400,
numPhysicalPartitions: 1
},
offerAutopilotSettings: {
maxThroughput: 5000
}
},
id: "test"
} as SDKOfferDefinition;
const mockResponse = {
resource: mockOfferDefinition
} as OfferResponse;
const expectedResult: Offer = {
manualThroughput: undefined,
autoscaleMaxThroughput: 5000,
minimumThroughput: 400,
id: "test",
offerDefinition: mockOfferDefinition,
offerReplacePending: false
};
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
});
});

View File

@@ -1,37 +0,0 @@
import { Offer, SDKOfferDefinition } from "../Contracts/DataModels";
import { OfferResponse } from "@azure/cosmos";
import { HttpHeaders } from "./Constants";
export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer | undefined => {
const offerDefinition: SDKOfferDefinition | undefined = offerResponse?.resource;
if (!offerDefinition) {
return undefined;
}
const offerContent = offerDefinition.content;
if (!offerContent) {
return undefined;
}
const minimumThroughput = offerContent.collectionThroughputInfo?.minimumRUForCollection;
const autopilotSettings = offerContent.offerAutopilotSettings;
if (autopilotSettings && autopilotSettings.maxThroughput && minimumThroughput) {
return {
id: offerDefinition.id,
autoscaleMaxThroughput: autopilotSettings.maxThroughput,
manualThroughput: undefined,
minimumThroughput,
offerDefinition,
offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true"
};
}
return {
id: offerDefinition.id,
autoscaleMaxThroughput: undefined,
manualThroughput: offerContent.offerThroughput,
minimumThroughput,
offerDefinition,
offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true"
};
};

View File

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

View File

@@ -3,18 +3,17 @@ import * as _ from "underscore";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import Explorer from "../Explorer/Explorer"; import Explorer from "../Explorer/Explorer";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import DocumentsTab from "../Explorer/Tabs/DocumentsTab"; import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { QueryUtils } from "../Utils/QueryUtils"; import { QueryUtils } from "../Utils/QueryUtils";
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants"; import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage"; import { createDocument, deleteDocument, queryDocuments, queryDocumentsPage } from "./DocumentClientUtilityBase";
import { createCollection } from "./dataAccess/createCollection"; import { createCollection } from "./dataAccess/createCollection";
import { handleError } from "./ErrorHandlingUtils"; import * as ErrorParserUtility from "./ErrorParserUtility";
import { createDocument } from "./dataAccess/createDocument"; import * as Logger from "./Logger";
import { deleteDocument } from "./dataAccess/deleteDocument";
import { queryDocuments } from "./dataAccess/queryDocuments";
export class QueriesClient { export class QueriesClient {
private static readonly PartitionKey: DataModels.PartitionKey = { private static readonly PartitionKey: DataModels.PartitionKey = {
@@ -33,7 +32,10 @@ export class QueriesClient {
return Promise.resolve(queriesCollection.rawDataModel); return Promise.resolve(queriesCollection.rawDataModel);
} }
const clearMessage = NotificationConsoleUtils.logConsoleProgress("Setting up account for saving queries"); const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
"Setting up account for saving queries"
);
return createCollection({ return createCollection({
collectionId: SavedQueries.CollectionName, collectionId: SavedQueries.CollectionName,
createNewDatabase: true, createNewDatabase: true,
@@ -44,22 +46,33 @@ export class QueriesClient {
}) })
.then( .then(
(collection: DataModels.Collection) => { (collection: DataModels.Collection) => {
NotificationConsoleUtils.logConsoleInfo("Successfully set up account for saving queries"); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
"Successfully set up account for saving queries"
);
return Promise.resolve(collection); return Promise.resolve(collection);
}, },
(error: any) => { (error: any) => {
handleError(error, "setupQueriesCollection", "Failed to set up account for saving queries"); const stringifiedError: string = JSON.stringify(error);
return Promise.reject(error); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to set up account for saving queries: ${stringifiedError}`
);
Logger.logError(stringifiedError, "setupQueriesCollection");
return Promise.reject(stringifiedError);
} }
) )
.finally(() => clearMessage()); .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
} }
public async saveQuery(query: DataModels.Query): Promise<void> { public async saveQuery(query: DataModels.Query): Promise<void> {
const queriesCollection = this.findQueriesCollection(); const queriesCollection = this.findQueriesCollection();
if (!queriesCollection) { if (!queriesCollection) {
const errorMessage: string = "Account not set up to perform saved query operations"; const errorMessage: string = "Account not set up to perform saved query operations";
NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to save query ${query.queryName}: ${errorMessage}`
);
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
@@ -67,85 +80,121 @@ export class QueriesClient {
this.validateQuery(query); this.validateQuery(query);
} catch (error) { } catch (error) {
const errorMessage: string = "Invalid query specified"; const errorMessage: string = "Invalid query specified";
NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to save query ${query.queryName}: ${errorMessage}`
);
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Saving query ${query.queryName}`); const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Saving query ${query.queryName}`
);
query.id = query.queryName; query.id = query.queryName;
return createDocument(queriesCollection, query) return createDocument(queriesCollection, query)
.then( .then(
(savedQuery: DataModels.Query) => { (savedQuery: DataModels.Query) => {
NotificationConsoleUtils.logConsoleInfo(`Successfully saved query ${query.queryName}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully saved query ${query.queryName}`
);
return Promise.resolve(); return Promise.resolve();
}, },
(error: any) => { (error: any) => {
if (error.code === HttpStatusCodes.Conflict.toString()) { let errorMessage: string;
error = `Query ${query.queryName} already exists`; const parsedError: DataModels.ErrorDataModel = ErrorParserUtility.parse(error)[0];
if (parsedError.code === HttpStatusCodes.Conflict.toString()) {
errorMessage = `Query ${query.queryName} already exists`;
} else {
errorMessage = parsedError.message;
} }
handleError(error, "saveQuery", `Failed to save query ${query.queryName}`); NotificationConsoleUtils.logConsoleMessage(
return Promise.reject(error); ConsoleDataType.Error,
`Failed to save query ${query.queryName}: ${errorMessage}`
);
Logger.logError(JSON.stringify(parsedError), "saveQuery");
return Promise.reject(errorMessage);
} }
) )
.finally(() => clearMessage()); .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
} }
public async getQueries(): Promise<DataModels.Query[]> { public async getQueries(): Promise<DataModels.Query[]> {
const queriesCollection = this.findQueriesCollection(); const queriesCollection = this.findQueriesCollection();
if (!queriesCollection) { if (!queriesCollection) {
const errorMessage: string = "Account not set up to perform saved query operations"; const errorMessage: string = "Account not set up to perform saved query operations";
NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to fetch saved queries: ${errorMessage}`
);
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
const options: any = { enableCrossPartitionQuery: true }; const options: any = { enableCrossPartitionQuery: true };
const clearMessage = NotificationConsoleUtils.logConsoleProgress("Fetching saved queries"); const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Fetching saved queries");
const queryIterator: QueryIterator<ItemDefinition & Resource> = queryDocuments( return queryDocuments(SavedQueries.DatabaseName, SavedQueries.CollectionName, this.fetchQueriesQuery(), options)
SavedQueries.DatabaseName,
SavedQueries.CollectionName,
this.fetchQueriesQuery(),
options
);
const fetchQueries = async (firstItemIndex: number): Promise<ViewModels.QueryResults> =>
await queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex);
return QueryUtils.queryAllPages(fetchQueries)
.then( .then(
(results: ViewModels.QueryResults) => { (queryIterator: QueryIterator<ItemDefinition & Resource>) => {
let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => { const fetchQueries = (firstItemIndex: number): Q.Promise<ViewModels.QueryResults> =>
if (!document) { queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex, options);
return undefined; return QueryUtils.queryAllPages(fetchQueries).then(
(results: ViewModels.QueryResults) => {
let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => {
if (!document) {
return undefined;
}
const { id, resourceId, query, queryName } = document;
const parsedQuery: DataModels.Query = {
resourceId: resourceId,
queryName: queryName,
query: query,
id: id
};
try {
this.validateQuery(parsedQuery);
return parsedQuery;
} catch (error) {
return undefined;
}
});
queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully fetched saved queries");
return Promise.resolve(queries);
},
(error: any) => {
const stringifiedError: string = JSON.stringify(error);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to fetch saved queries: ${stringifiedError}`
);
Logger.logError(stringifiedError, "getSavedQueries");
return Promise.reject(stringifiedError);
} }
const { id, resourceId, query, queryName } = document; );
const parsedQuery: DataModels.Query = {
resourceId: resourceId,
queryName: queryName,
query: query,
id: id
};
try {
this.validateQuery(parsedQuery);
return parsedQuery;
} catch (error) {
return undefined;
}
});
queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery);
NotificationConsoleUtils.logConsoleInfo("Successfully fetched saved queries");
return Promise.resolve(queries);
}, },
(error: any) => { (error: any) => {
handleError(error, "getSavedQueries", "Failed to fetch saved queries"); // should never get into this state but we handle this regardless
return Promise.reject(error); const stringifiedError: string = JSON.stringify(error);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to fetch saved queries: ${stringifiedError}`
);
Logger.logError(stringifiedError, "getSavedQueries");
return Promise.reject(stringifiedError);
} }
) )
.finally(() => clearMessage()); .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
} }
public async deleteQuery(query: DataModels.Query): Promise<void> { public async deleteQuery(query: DataModels.Query): Promise<void> {
const queriesCollection = this.findQueriesCollection(); const queriesCollection = this.findQueriesCollection();
if (!queriesCollection) { if (!queriesCollection) {
const errorMessage: string = "Account not set up to perform saved query operations"; const errorMessage: string = "Account not set up to perform saved query operations";
NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to fetch saved queries: ${errorMessage}`
);
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
@@ -153,10 +202,16 @@ export class QueriesClient {
this.validateQuery(query); this.validateQuery(query);
} catch (error) { } catch (error) {
const errorMessage: string = "Invalid query specified"; const errorMessage: string = "Invalid query specified";
NotificationConsoleUtils.logConsoleError(`Failed to delete query ${query.queryName}: ${errorMessage}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to delete query ${query.queryName}: ${errorMessage}`
);
} }
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting query ${query.queryName}`); const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Deleting query ${query.queryName}`
);
query.id = query.queryName; query.id = query.queryName;
const documentId = new DocumentId( const documentId = new DocumentId(
{ {
@@ -170,15 +225,23 @@ export class QueriesClient {
return deleteDocument(queriesCollection, documentId) return deleteDocument(queriesCollection, documentId)
.then( .then(
() => { () => {
NotificationConsoleUtils.logConsoleInfo(`Successfully deleted query ${query.queryName}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully deleted query ${query.queryName}`
);
return Promise.resolve(); return Promise.resolve();
}, },
(error: any) => { (error: any) => {
handleError(error, "deleteQuery", `Failed to delete query ${query.queryName}`); const stringifiedError: string = JSON.stringify(error);
return Promise.reject(error); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to delete query ${query.queryName}: ${stringifiedError}`
);
Logger.logError(stringifiedError, "deleteQuery");
return Promise.reject(stringifiedError);
} }
) )
.finally(() => clearMessage()); .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
} }
public getResourceId(): string { public getResourceId(): string {

View File

@@ -23,10 +23,10 @@ export class Splitter {
public splitterId: string; public splitterId: string;
public leftSideId: string; public leftSideId: string;
public splitter!: HTMLElement; public splitter: HTMLElement;
public leftSide!: HTMLElement; public leftSide: HTMLElement;
public lastX!: number; public lastX: number;
public lastWidth!: number; public lastWidth: number;
private isCollapsed: ko.Observable<boolean>; private isCollapsed: ko.Observable<boolean>;
private bounds: SplitterBounds; private bounds: SplitterBounds;
@@ -42,10 +42,9 @@ export class Splitter {
} }
public initialize() { public initialize() {
if (document.getElementById(this.splitterId) !== null && document.getElementById(this.leftSideId) != null) { this.splitter = document.getElementById(this.splitterId);
this.splitter = <HTMLElement>document.getElementById(this.splitterId); this.leftSide = document.getElementById(this.leftSideId);
this.leftSide = <HTMLElement>document.getElementById(this.leftSideId);
}
const isVerticalSplitter: boolean = this.direction === SplitterDirection.Vertical; const isVerticalSplitter: boolean = this.direction === SplitterDirection.Vertical;
const splitterOptions: JQueryUI.ResizableOptions = { const splitterOptions: JQueryUI.ResizableOptions = {
animate: true, animate: true,

View File

@@ -1,5 +1,6 @@
jest.mock("../../Utils/arm/request"); jest.mock("../../Utils/arm/request");
jest.mock("../CosmosClient"); jest.mock("../CosmosClient");
jest.mock("../DataAccessUtilityBase");
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels"; import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";

View File

@@ -1,4 +1,5 @@
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ErrorParserUtility from "../ErrorParserUtility";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { ContainerResponse, DatabaseResponse } from "@azure/cosmos"; import { ContainerResponse, DatabaseResponse } from "@azure/cosmos";
import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/ContainerRequest"; import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/ContainerRequest";
@@ -22,19 +23,21 @@ import {
getGremlinGraph getGremlinGraph
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources"; import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { logConsoleProgress, logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { refreshCachedResources } from "../DataAccessUtilityBase";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { createDatabase } from "./createDatabase"; import { createDatabase } from "./createDatabase";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { handleError } from "../ErrorHandlingUtils";
export const createCollection = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => { export const createCollection = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
let collection: DataModels.Collection;
const clearMessage = logConsoleProgress( const clearMessage = logConsoleProgress(
`Creating a new container ${params.collectionId} for database ${params.databaseId}` `Creating a new container ${params.collectionId} for database ${params.databaseId}`
); );
try { try {
let collection: DataModels.Collection;
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) { if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
if (params.createNewDatabase) { if (params.createNewDatabase) {
const createDatabaseParams: DataModels.CreateDatabaseParams = { const createDatabaseParams: DataModels.CreateDatabaseParams = {
@@ -51,15 +54,18 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
} else { } else {
collection = await createCollectionWithSDK(params); collection = await createCollectionWithSDK(params);
} }
logConsoleInfo(`Successfully created container ${params.collectionId}`);
return collection;
} catch (error) { } catch (error) {
handleError(error, "CreateCollection", `Error while creating container ${params.collectionId}`); const sanitizedError = ErrorParserUtility.replaceKnownError(JSON.stringify(error));
throw error; logConsoleError(`Error while creating container ${params.collectionId}:\n ${sanitizedError}`);
} finally { logError(JSON.stringify(error), "CreateCollection", error.code);
sendNotificationForError(error);
clearMessage(); clearMessage();
throw error;
} }
logConsoleInfo(`Successfully created container ${params.collectionId}`);
await refreshCachedResources();
clearMessage();
return collection;
}; };
const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => { const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {

View File

@@ -24,28 +24,36 @@ import {
createUpdateGremlinDatabase, createUpdateGremlinDatabase,
getGremlinDatabase getGremlinDatabase
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { handleError } from "../ErrorHandlingUtils"; import { logConsoleProgress, logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { logError } from "../Logger";
import { refreshCachedOffers, refreshCachedResources } from "../DataAccessUtilityBase";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
export async function createDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> { export async function createDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
let database: DataModels.Database;
const clearMessage = logConsoleProgress(`Creating a new database ${params.databaseId}`); const clearMessage = logConsoleProgress(`Creating a new database ${params.databaseId}`);
try { try {
if (userContext.defaultExperience === DefaultAccountExperienceType.Table) { if (userContext.defaultExperience === DefaultAccountExperienceType.Table) {
throw new Error("Creating database resources is not allowed for tables accounts"); throw new Error("Creating database resources is not allowed for tables accounts");
} }
const database: DataModels.Database = await (window.authType === AuthType.AAD && !userContext.useSDKOperations if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
? createDatabaseWithARM(params) database = await createDatabaseWithARM(params);
: createDatabaseWithSDK(params)); } else {
database = await createDatabaseWithSDK(params);
logConsoleInfo(`Successfully created database ${params.databaseId}`); }
return database;
} catch (error) { } catch (error) {
handleError(error, "CreateDatabase", `Error while creating database ${params.databaseId}`); logConsoleError(`Error while creating database ${params.databaseId}:\n ${error.message}`);
throw error; logError(JSON.stringify(error), "CreateDatabase", error.code);
} finally { sendNotificationForError(error);
clearMessage(); clearMessage();
throw error;
} }
logConsoleInfo(`Successfully created database ${params.databaseId}`);
await refreshCachedResources();
await refreshCachedOffers();
clearMessage();
return database;
} }
async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> { async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {

View File

@@ -1,25 +0,0 @@
import { CollectionBase } from "../../Contracts/ViewModels";
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
export const createDocument = async (collection: CollectionBase, newDocument: unknown): Promise<unknown> => {
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Creating new ${entityName} for container ${collection.id()}`);
try {
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.items.create(newDocument);
logConsoleInfo(`Successfully created new ${entityName} for container ${collection.id()}`);
return response?.resource;
} catch (error) {
handleError(error, "CreateDocument", `Error while creating new ${entityName} for container ${collection.id()}`);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -1,78 +1,28 @@
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { Resource, StoredProcedureDefinition } from "@azure/cosmos"; import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import { import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
SqlStoredProcedureCreateUpdateParameters,
SqlStoredProcedureResource
} from "../../Utils/arm/generatedClients/2020-04-01/types";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { import { logError } from "../Logger";
createUpdateSqlStoredProcedure, import { sendNotificationForError } from "./sendNotificationForError";
getSqlStoredProcedure
} from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { userContext } from "../../UserContext";
export async function createStoredProcedure( export async function createStoredProcedure(
databaseId: string, databaseId: string,
collectionId: string, collectionId: string,
storedProcedure: StoredProcedureDefinition storedProcedure: StoredProcedureDefinition
): Promise<StoredProcedureDefinition & Resource> { ): Promise<StoredProcedureDefinition & Resource> {
let createdStoredProcedure: StoredProcedureDefinition & Resource;
const clearMessage = logConsoleProgress(`Creating stored procedure ${storedProcedure.id}`); const clearMessage = logConsoleProgress(`Creating stored procedure ${storedProcedure.id}`);
try { try {
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB
) {
try {
const getResponse = await getSqlStoredProcedure(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
storedProcedure.id
);
if (getResponse?.properties?.resource) {
throw new Error(
`Create stored procedure failed: stored procedure with id ${storedProcedure.id} already exists`
);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const createSprocParams: SqlStoredProcedureCreateUpdateParameters = {
properties: {
resource: storedProcedure as SqlStoredProcedureResource,
options: {}
}
};
const rpResponse = await createUpdateSqlStoredProcedure(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
storedProcedure.id,
createSprocParams
);
return rpResponse && (rpResponse.properties?.resource as StoredProcedureDefinition & Resource);
}
const response = await client() const response = await client()
.database(databaseId) .database(databaseId)
.container(collectionId) .container(collectionId)
.scripts.storedProcedures.create(storedProcedure); .scripts.storedProcedures.create(storedProcedure);
return response?.resource; createdStoredProcedure = response.resource;
} catch (error) { } catch (error) {
handleError(error, "CreateStoredProcedure", `Error while creating stored procedure ${storedProcedure.id}`); logConsoleError(`Error while creating stored procedure ${storedProcedure.id}:\n ${JSON.stringify(error)}`);
throw error; logError(JSON.stringify(error), "CreateStoredProcedure", error.code);
} finally { sendNotificationForError(error);
clearMessage();
} }
clearMessage();
return createdStoredProcedure;
} }

View File

@@ -1,73 +1,28 @@
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { Resource, TriggerDefinition } from "@azure/cosmos"; import { Resource, TriggerDefinition } from "@azure/cosmos";
import { import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
SqlTriggerCreateUpdateParameters,
SqlTriggerResource
} from "../../Utils/arm/generatedClients/2020-04-01/types";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; import { logError } from "../Logger";
import { handleError } from "../ErrorHandlingUtils"; import { sendNotificationForError } from "./sendNotificationForError";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { userContext } from "../../UserContext";
export async function createTrigger( export async function createTrigger(
databaseId: string, databaseId: string,
collectionId: string, collectionId: string,
trigger: TriggerDefinition trigger: TriggerDefinition
): Promise<TriggerDefinition & Resource> { ): Promise<TriggerDefinition & Resource> {
let createdTrigger: TriggerDefinition & Resource;
const clearMessage = logConsoleProgress(`Creating trigger ${trigger.id}`); const clearMessage = logConsoleProgress(`Creating trigger ${trigger.id}`);
try { try {
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB
) {
try {
const getResponse = await getSqlTrigger(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
trigger.id
);
if (getResponse?.properties?.resource) {
throw new Error(`Create trigger failed: ${trigger.id} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const createTriggerParams: SqlTriggerCreateUpdateParameters = {
properties: {
resource: trigger as SqlTriggerResource,
options: {}
}
};
const rpResponse = await createUpdateSqlTrigger(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
trigger.id,
createTriggerParams
);
return rpResponse && (rpResponse.properties?.resource as TriggerDefinition & Resource);
}
const response = await client() const response = await client()
.database(databaseId) .database(databaseId)
.container(collectionId) .container(collectionId)
.scripts.triggers.create(trigger); .scripts.triggers.create(trigger);
return response.resource; createdTrigger = response.resource;
} catch (error) { } catch (error) {
handleError(error, "CreateTrigger", `Error while creating trigger ${trigger.id}`); logConsoleError(`Error while creating trigger ${trigger.id}:\n ${JSON.stringify(error)}`);
throw error; logError(JSON.stringify(error), "CreateTrigger", error.code);
} finally { sendNotificationForError(error);
clearMessage();
} }
clearMessage();
return createdTrigger;
} }

View File

@@ -1,82 +1,28 @@
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos"; import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
import { import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
SqlUserDefinedFunctionCreateUpdateParameters,
SqlUserDefinedFunctionResource
} from "../../Utils/arm/generatedClients/2020-04-01/types";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { import { logError } from "../Logger";
createUpdateSqlUserDefinedFunction, import { sendNotificationForError } from "./sendNotificationForError";
getSqlUserDefinedFunction
} from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { userContext } from "../../UserContext";
export async function createUserDefinedFunction( export async function createUserDefinedFunction(
databaseId: string, databaseId: string,
collectionId: string, collectionId: string,
userDefinedFunction: UserDefinedFunctionDefinition userDefinedFunction: UserDefinedFunctionDefinition
): Promise<UserDefinedFunctionDefinition & Resource> { ): Promise<UserDefinedFunctionDefinition & Resource> {
let createdUserDefinedFunction: UserDefinedFunctionDefinition & Resource;
const clearMessage = logConsoleProgress(`Creating user defined function ${userDefinedFunction.id}`); const clearMessage = logConsoleProgress(`Creating user defined function ${userDefinedFunction.id}`);
try { try {
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB
) {
try {
const getResponse = await getSqlUserDefinedFunction(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
userDefinedFunction.id
);
if (getResponse?.properties?.resource) {
throw new Error(
`Create user defined function failed: user defined function with id ${userDefinedFunction.id} already exists`
);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const createUDFParams: SqlUserDefinedFunctionCreateUpdateParameters = {
properties: {
resource: userDefinedFunction as SqlUserDefinedFunctionResource,
options: {}
}
};
const rpResponse = await createUpdateSqlUserDefinedFunction(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
userDefinedFunction.id,
createUDFParams
);
return rpResponse && (rpResponse.properties?.resource as UserDefinedFunctionDefinition & Resource);
}
const response = await client() const response = await client()
.database(databaseId) .database(databaseId)
.container(collectionId) .container(collectionId)
.scripts.userDefinedFunctions.create(userDefinedFunction); .scripts.userDefinedFunctions.create(userDefinedFunction);
return response?.resource; createdUserDefinedFunction = response.resource;
} catch (error) { } catch (error) {
handleError( logConsoleError(`Error while creating user defined function ${userDefinedFunction.id}:\n ${JSON.stringify(error)}`);
error, logError(JSON.stringify(error), "CreateUserupdateUserDefinedFunction", error.code);
"CreateUserupdateUserDefinedFunction", sendNotificationForError(error);
`Error while creating user defined function ${userDefinedFunction.id}`
);
throw error;
} finally {
clearMessage();
} }
clearMessage();
return createdUserDefinedFunction;
} }

View File

@@ -7,6 +7,7 @@ import { AuthType } from "../../AuthType";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { updateUserContext } from "../../UserContext"; import { updateUserContext } from "../../UserContext";
import { DatabaseAccount } from "../../Contracts/DataModels"; import { DatabaseAccount } from "../../Contracts/DataModels";
import { sendCachedDataMessage } from "../MessageHandler";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
describe("deleteCollection", () => { describe("deleteCollection", () => {
@@ -17,6 +18,7 @@ describe("deleteCollection", () => {
} as DatabaseAccount, } as DatabaseAccount,
defaultExperience: DefaultAccountExperienceType.DocumentDB defaultExperience: DefaultAccountExperienceType.DocumentDB
}); });
(sendCachedDataMessage as jest.Mock).mockResolvedValue(undefined);
}); });
it("should call ARM if logged in with AAD", async () => { it("should call ARM if logged in with AAD", async () => {

View File

@@ -5,10 +5,12 @@ import { deleteCassandraTable } from "../../Utils/arm/generatedClients/2020-04-0
import { deleteMongoDBCollection } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; import { deleteMongoDBCollection } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { deleteGremlinGraph } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; import { deleteGremlinGraph } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { deleteTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources"; import { deleteTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { handleError } from "../ErrorHandlingUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { refreshCachedResources } from "../DataAccessUtilityBase";
export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> { export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`); const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
@@ -21,13 +23,15 @@ export async function deleteCollection(databaseId: string, collectionId: string)
.container(collectionId) .container(collectionId)
.delete(); .delete();
} }
logConsoleInfo(`Successfully deleted container ${collectionId}`);
} catch (error) { } catch (error) {
handleError(error, "DeleteCollection", `Error while deleting container ${collectionId}`); logConsoleError(`Error while deleting container ${collectionId}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "DeleteCollection", error.code);
sendNotificationForError(error);
throw error; throw error;
} finally {
clearMessage();
} }
logConsoleInfo(`Successfully deleted container ${collectionId}`);
clearMessage();
await refreshCachedResources();
} }
function deleteCollectionWithARM(databaseId: string, collectionId: string): Promise<void> { function deleteCollectionWithARM(databaseId: string, collectionId: string): Promise<void> {

View File

@@ -1,36 +0,0 @@
import ConflictId from "../../Explorer/Tree/ConflictId";
import { CollectionBase } from "../../Contracts/ViewModels";
import { RequestOptions } from "@azure/cosmos";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
export const deleteConflict = async (collection: CollectionBase, conflictId: ConflictId): Promise<void> => {
const clearMessage = logConsoleProgress(`Deleting conflict ${conflictId.id()}`);
try {
const options = {
partitionKey: getPartitionKeyHeaderForConflict(conflictId)
};
await client()
.database(collection.databaseId)
.container(collection.id())
.conflict(conflictId.id())
.delete(options as RequestOptions);
logConsoleInfo(`Successfully deleted conflict ${conflictId.id()}`);
} catch (error) {
handleError(error, "DeleteConflict", `Error while deleting conflict ${conflictId.id()}`);
throw error;
} finally {
clearMessage();
}
};
const getPartitionKeyHeaderForConflict = (conflictId: ConflictId): unknown => {
if (!conflictId.partitionKey) {
return undefined;
}
return conflictId.partitionKeyValue === undefined ? [{}] : [conflictId.partitionKeyValue];
};

View File

@@ -7,6 +7,7 @@ import { AuthType } from "../../AuthType";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { updateUserContext } from "../../UserContext"; import { updateUserContext } from "../../UserContext";
import { DatabaseAccount } from "../../Contracts/DataModels"; import { DatabaseAccount } from "../../Contracts/DataModels";
import { sendCachedDataMessage } from "../MessageHandler";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
describe("deleteDatabase", () => { describe("deleteDatabase", () => {
@@ -17,6 +18,7 @@ describe("deleteDatabase", () => {
} as DatabaseAccount, } as DatabaseAccount,
defaultExperience: DefaultAccountExperienceType.DocumentDB defaultExperience: DefaultAccountExperienceType.DocumentDB
}); });
(sendCachedDataMessage as jest.Mock).mockResolvedValue(undefined);
}); });
it("should call ARM if logged in with AAD", async () => { it("should call ARM if logged in with AAD", async () => {

View File

@@ -4,10 +4,12 @@ import { deleteSqlDatabase } from "../../Utils/arm/generatedClients/2020-04-01/s
import { deleteCassandraKeyspace } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; import { deleteCassandraKeyspace } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { deleteMongoDBDatabase } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; import { deleteMongoDBDatabase } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { deleteGremlinDatabase } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; import { deleteGremlinDatabase } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { handleError } from "../ErrorHandlingUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { refreshCachedResources } from "../DataAccessUtilityBase";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
export async function deleteDatabase(databaseId: string): Promise<void> { export async function deleteDatabase(databaseId: string): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting database ${databaseId}`); const clearMessage = logConsoleProgress(`Deleting database ${databaseId}`);
@@ -23,13 +25,15 @@ export async function deleteDatabase(databaseId: string): Promise<void> {
.database(databaseId) .database(databaseId)
.delete(); .delete();
} }
logConsoleInfo(`Successfully deleted database ${databaseId}`);
} catch (error) { } catch (error) {
handleError(error, "DeleteDatabase", `Error while deleting database ${databaseId}`); logConsoleError(`Error while deleting database ${databaseId}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "DeleteDatabase", error.code);
sendNotificationForError(error);
throw error; throw error;
} finally {
clearMessage();
} }
logConsoleInfo(`Successfully deleted database ${databaseId}`);
clearMessage();
await refreshCachedResources();
} }
function deleteDatabaseWithARM(databaseId: string): Promise<void> { function deleteDatabaseWithARM(databaseId: string): Promise<void> {

View File

@@ -1,25 +0,0 @@
import { CollectionBase } from "../../Contracts/ViewModels";
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import DocumentId from "../../Explorer/Tree/DocumentId";
export const deleteDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<void> => {
const entityName: string = getEntityName();
const clearMessage = logConsoleProgress(`Deleting ${entityName} ${documentId.id()}`);
try {
await client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), documentId.partitionKeyValue)
.delete();
logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`);
} catch (error) {
handleError(error, "DeleteDocument", `Error while deleting ${entityName} ${documentId.id()}`);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -1,10 +1,7 @@
import { AuthType } from "../../AuthType"; import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { deleteSqlStoredProcedure } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; import { logError } from "../Logger";
import { handleError } from "../ErrorHandlingUtils"; import { sendNotificationForError } from "./sendNotificationForError";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { userContext } from "../../UserContext";
export async function deleteStoredProcedure( export async function deleteStoredProcedure(
databaseId: string, databaseId: string,
@@ -13,30 +10,17 @@ export async function deleteStoredProcedure(
): Promise<void> { ): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting stored procedure ${storedProcedureId}`); const clearMessage = logConsoleProgress(`Deleting stored procedure ${storedProcedureId}`);
try { try {
if ( await client()
window.authType === AuthType.AAD && .database(databaseId)
!userContext.useSDKOperations && .container(collectionId)
userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB .scripts.storedProcedure(storedProcedureId)
) { .delete();
await deleteSqlStoredProcedure(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
storedProcedureId
);
} else {
await client()
.database(databaseId)
.container(collectionId)
.scripts.storedProcedure(storedProcedureId)
.delete();
}
} catch (error) { } catch (error) {
handleError(error, "DeleteStoredProcedure", `Error while deleting stored procedure ${storedProcedureId}`); logConsoleError(`Error while deleting stored procedure ${storedProcedureId}:\n ${JSON.stringify(error)}`);
throw error; logError(JSON.stringify(error), "DeleteStoredProcedure", error.code);
} finally { sendNotificationForError(error);
clearMessage();
} }
clearMessage();
return undefined;
} }

View File

@@ -1,38 +1,22 @@
import { AuthType } from "../../AuthType"; import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { deleteSqlTrigger } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; import { logError } from "../Logger";
import { handleError } from "../ErrorHandlingUtils"; import { sendNotificationForError } from "./sendNotificationForError";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { userContext } from "../../UserContext";
export async function deleteTrigger(databaseId: string, collectionId: string, triggerId: string): Promise<void> { export async function deleteTrigger(databaseId: string, collectionId: string, triggerId: string): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting trigger ${triggerId}`); const clearMessage = logConsoleProgress(`Deleting trigger ${triggerId}`);
try { try {
if ( await client()
window.authType === AuthType.AAD && .database(databaseId)
!userContext.useSDKOperations && .container(collectionId)
userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB .scripts.trigger(triggerId)
) { .delete();
await deleteSqlTrigger(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
triggerId
);
} else {
await client()
.database(databaseId)
.container(collectionId)
.scripts.trigger(triggerId)
.delete();
}
} catch (error) { } catch (error) {
handleError(error, "DeleteTrigger", `Error while deleting trigger ${triggerId}`); logConsoleError(`Error while deleting trigger ${triggerId}:\n ${JSON.stringify(error)}`);
throw error; logError(JSON.stringify(error), "DeleteTrigger", error.code);
} finally { sendNotificationForError(error);
clearMessage();
} }
clearMessage();
return undefined;
} }

View File

@@ -1,38 +1,22 @@
import { AuthType } from "../../AuthType"; import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { deleteSqlUserDefinedFunction } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; import { logError } from "../Logger";
import { handleError } from "../ErrorHandlingUtils"; import { sendNotificationForError } from "./sendNotificationForError";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { userContext } from "../../UserContext";
export async function deleteUserDefinedFunction(databaseId: string, collectionId: string, id: string): Promise<void> { export async function deleteUserDefinedFunction(databaseId: string, collectionId: string, id: string): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting user defined function ${id}`); const clearMessage = logConsoleProgress(`Deleting user defined function ${id}`);
try { try {
if ( await client()
window.authType === AuthType.AAD && .database(databaseId)
!userContext.useSDKOperations && .container(collectionId)
userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB .scripts.userDefinedFunction(id)
) { .delete();
await deleteSqlUserDefinedFunction(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
id
);
} else {
await client()
.database(databaseId)
.container(collectionId)
.scripts.userDefinedFunction(id)
.delete();
}
} catch (error) { } catch (error) {
handleError(error, "DeleteUserDefinedFunction", `Error while deleting user defined function ${id}`); logConsoleError(`Error while deleting user defined function ${id}:\n ${JSON.stringify(error)}`);
throw error; logError(JSON.stringify(error), "DeleteUserDefinedFunction", error.code);
} finally { sendNotificationForError(error);
clearMessage();
} }
clearMessage();
return undefined;
} }

View File

@@ -1,48 +0,0 @@
import { Collection } from "../../Contracts/ViewModels";
import { ClientDefaults, HttpHeaders } from "../Constants";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import StoredProcedure from "../../Explorer/Tree/StoredProcedure";
export interface ExecuteSprocResult {
result: StoredProcedure;
scriptLogs: string;
}
export const executeStoredProcedure = async (
collection: Collection,
storedProcedure: StoredProcedure,
partitionKeyValue: string,
params: string[]
): Promise<ExecuteSprocResult> => {
const clearMessage = logConsoleProgress(`Executing stored procedure ${storedProcedure.id()}`);
const timeout = setTimeout(() => {
throw Error(`Request timed out while executing stored procedure ${storedProcedure.id()}`);
}, ClientDefaults.requestTimeoutMs);
try {
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.scripts.storedProcedure(storedProcedure.id())
.execute(partitionKeyValue, params, { enableScriptLogging: true });
clearTimeout(timeout);
logConsoleInfo(
`Finished executing stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
);
return {
result: response.resource,
scriptLogs: response.headers[HttpHeaders.scriptLogResults] as string
};
} catch (error) {
handleError(
error,
"ExecuteStoredProcedure",
`Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -1,92 +0,0 @@
import { AuthType } from "../../AuthType";
import { armRequest } from "../../Utils/arm/request";
import { configContext } from "../../ConfigContext";
import { handleError } from "../ErrorHandlingUtils";
import { userContext } from "../../UserContext";
interface TimeSeriesData {
data: {
timeStamp: string;
total: number;
}[];
metadatavalues: {
name: {
localizedValue: string;
value: string;
};
value: string;
};
}
interface MetricsData {
displayDescription: string;
errorCode: string;
id: string;
name: {
value: string;
localizedValue: string;
};
timeseries: TimeSeriesData[];
type: string;
unit: string;
}
interface MetricsResponse {
cost: number;
interval: string;
namespace: string;
resourceregion: string;
timespan: string;
value: MetricsData[];
}
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
if (window.authType !== AuthType.AAD) {
return undefined;
}
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const filter = `DatabaseName eq '${databaseName}' and CollectionName eq '${containerName}'`;
const metricNames = "DataUsage,IndexUsage";
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/providers/microsoft.insights/metrics`;
try {
const metricsResponse: MetricsResponse = await armRequest({
host: configContext.ARM_ENDPOINT,
path,
method: "GET",
apiVersion: "2018-01-01",
queryParams: {
filter,
metricNames
}
});
if (metricsResponse?.value?.length !== 2) {
return undefined;
}
const dataUsageData: MetricsData = metricsResponse.value[0];
const indexUsagedata: MetricsData = metricsResponse.value[1];
const dataUsageSizeInKb: number = getUsageSizeInKb(dataUsageData);
const indexUsageSizeInKb: number = getUsageSizeInKb(indexUsagedata);
return dataUsageSizeInKb + indexUsageSizeInKb;
} catch (error) {
handleError(error, "getCollectionUsageSize");
throw error;
}
};
const getUsageSizeInKb = (metricsData: MetricsData): number => {
if (metricsData?.errorCode !== "Success") {
throw Error(`Get collection usage size failed: ${metricsData.errorCode}`);
}
const timeSeriesData: TimeSeriesData = metricsData?.timeseries?.[0];
const usageSizeInBytes: number = timeSeriesData?.data?.[0]?.total;
return usageSizeInBytes ? usageSizeInBytes / 1024 : 0;
};

View File

@@ -1,28 +0,0 @@
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import * as Constants from "../Constants";
import { AuthType } from "../../AuthType";
export async function getIndexTransformationProgress(databaseId: string, collectionId: string): Promise<number> {
if (window.authType !== AuthType.AAD) {
return undefined;
}
let indexTransformationPercentage: number;
const clearMessage = logConsoleProgress(`Reading container ${collectionId}`);
try {
const response = await client()
.database(databaseId)
.container(collectionId)
.read({ populateQuotaInfo: true });
indexTransformationPercentage = parseInt(
response.headers[Constants.HttpHeaders.collectionIndexTransformationProgress] as string
);
} catch (error) {
handleError(error, "ReadMongoDBCollection", `Error while reading container ${collectionId}`);
throw error;
}
clearMessage();
return indexTransformationPercentage;
}

View File

@@ -1,14 +0,0 @@
import { ConflictDefinition, FeedOptions, QueryIterator, Resource } from "@azure/cosmos";
import { client } from "../CosmosClient";
export const queryConflicts = (
databaseId: string,
containerId: string,
query: string,
options: FeedOptions
): QueryIterator<ConflictDefinition & Resource> => {
return client()
.database(databaseId)
.container(containerId)
.conflicts.query(query, options);
};

View File

@@ -1,34 +0,0 @@
import { Queries } from "../Constants";
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { client } from "../CosmosClient";
export const queryDocuments = (
databaseId: string,
containerId: string,
query: string,
options: FeedOptions
): QueryIterator<ItemDefinition & Resource> => {
options = getCommonQueryOptions(options);
return client()
.database(databaseId)
.container(containerId)
.items.query(query, options);
};
export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);
options = options || {};
options.populateQueryMetrics = true;
options.enableScanInQuery = options.enableScanInQuery || true;
if (!options.partitionKey) {
options.forceQueryPlan = true;
}
options.maxItemCount =
options.maxItemCount ||
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
Queries.itemsPerPage;
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
return options;
};

View File

@@ -1,26 +0,0 @@
import { QueryResults } from "../../Contracts/ViewModels";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { MinimalQueryIterator, nextPage } from "../IteratorUtilities";
import { handleError } from "../ErrorHandlingUtils";
import { getEntityName } from "../DocumentUtility";
export const queryDocumentsPage = async (
resourceName: string,
documentsIterator: MinimalQueryIterator,
firstItemIndex: number
): Promise<QueryResults> => {
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
try {
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex);
const itemCount = (result.documents && result.documents.length) || 0;
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
return result;
} catch (error) {
handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -1,7 +1,8 @@
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils"; import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
export async function readCollection(databaseId: string, collectionId: string): Promise<DataModels.Collection> { export async function readCollection(databaseId: string, collectionId: string): Promise<DataModels.Collection> {
let collection: DataModels.Collection; let collection: DataModels.Collection;
@@ -13,7 +14,9 @@ export async function readCollection(databaseId: string, collectionId: string):
.read(); .read();
collection = response.resource as DataModels.Collection; collection = response.resource as DataModels.Collection;
} catch (error) { } catch (error) {
handleError(error, "ReadCollection", `Error while querying container ${collectionId}`); logConsoleError(`Error while querying container ${collectionId}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "ReadCollection", error.code);
sendNotificationForError(error);
throw error; throw error;
} }
clearMessage(); clearMessage();

View File

@@ -1,123 +1,126 @@
import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels"; import { HttpHeaders } from "../Constants";
import { handleError } from "../ErrorHandlingUtils"; import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { getSqlContainerThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; import { getSqlContainerThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { getMongoDBCollectionThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; import { getMongoDBCollectionThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { getCassandraTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; import { getCassandraTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { getGremlinGraphThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; import { getGremlinGraphThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { getTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/tableResources"; import { getTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { readOfferWithSDK } from "./readOfferWithSDK"; import { logError } from "../Logger";
import { readOffers } from "./readOffers";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise<Offer> => { export const readCollectionOffer = async (
params: DataModels.ReadCollectionOfferParams
): Promise<DataModels.OfferWithHeaders> => {
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`); const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
let offerId = params.offerId;
if (!offerId) {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
try {
offerId = await getCollectionOfferIdWithARM(params.databaseId, params.collectionId);
} catch (error) {
clearMessage();
if (error.code !== "NotFound") {
throw error;
}
return undefined;
}
} else {
offerId = await getCollectionOfferIdWithSDK(params.collectionResourceId);
if (!offerId) {
clearMessage();
return undefined;
}
}
}
const options: RequestOptions = {
initialHeaders: {
[HttpHeaders.populateCollectionThroughputInfo]: true
}
};
try { try {
if ( const response = await client()
window.authType === AuthType.AAD && .offer(offerId)
!userContext.useSDKOperations && .read(options);
userContext.defaultExperience !== DefaultAccountExperienceType.Table return (
) { response && {
return await readCollectionOfferWithARM(params.databaseId, params.collectionId); ...response.resource,
} headers: response.headers
}
return await readOfferWithSDK(params.offerId, params.collectionResourceId); );
} catch (error) { } catch (error) {
handleError(error, "ReadCollectionOffer", `Error while querying offer for collection ${params.collectionId}`); logConsoleError(`Error while querying offer for collection ${params.collectionId}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "ReadCollectionOffer", error.code);
sendNotificationForError(error);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();
} }
}; };
const readCollectionOfferWithARM = async (databaseId: string, collectionId: string): Promise<Offer> => { const getCollectionOfferIdWithARM = async (databaseId: string, collectionId: string): Promise<string> => {
let rpResponse;
const subscriptionId = userContext.subscriptionId; const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup; const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name; const accountName = userContext.databaseAccount.name;
const defaultExperience = userContext.defaultExperience; const defaultExperience = userContext.defaultExperience;
switch (defaultExperience) {
let rpResponse; case DefaultAccountExperienceType.DocumentDB:
try { rpResponse = await getSqlContainerThroughput(
switch (defaultExperience) { subscriptionId,
case DefaultAccountExperienceType.DocumentDB: resourceGroup,
rpResponse = await getSqlContainerThroughput( accountName,
subscriptionId, databaseId,
resourceGroup, collectionId
accountName, );
databaseId, break;
collectionId case DefaultAccountExperienceType.MongoDB:
); rpResponse = await getMongoDBCollectionThroughput(
break; subscriptionId,
case DefaultAccountExperienceType.MongoDB: resourceGroup,
rpResponse = await getMongoDBCollectionThroughput( accountName,
subscriptionId, databaseId,
resourceGroup, collectionId
accountName, );
databaseId, break;
collectionId case DefaultAccountExperienceType.Cassandra:
); rpResponse = await getCassandraTableThroughput(
break; subscriptionId,
case DefaultAccountExperienceType.Cassandra: resourceGroup,
rpResponse = await getCassandraTableThroughput( accountName,
subscriptionId, databaseId,
resourceGroup, collectionId
accountName, );
databaseId, break;
collectionId case DefaultAccountExperienceType.Graph:
); rpResponse = await getGremlinGraphThroughput(
break; subscriptionId,
case DefaultAccountExperienceType.Graph: resourceGroup,
rpResponse = await getGremlinGraphThroughput( accountName,
subscriptionId, databaseId,
resourceGroup, collectionId
accountName, );
databaseId, break;
collectionId case DefaultAccountExperienceType.Table:
); rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId);
break; break;
case DefaultAccountExperienceType.Table: default:
rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId); throw new Error(`Unsupported default experience type: ${defaultExperience}`);
break;
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
return undefined;
} }
const resource = rpResponse?.properties?.resource; return rpResponse?.name;
if (resource) { };
const offerId: string = rpResponse.name;
const minimumThroughput: number = const getCollectionOfferIdWithSDK = async (collectionResourceId: string): Promise<string> => {
typeof resource.minimumThroughput === "string" const offers = await readOffers();
? parseInt(resource.minimumThroughput) const offer = offers.find(offer => offer.resource === collectionResourceId);
: resource.minimumThroughput; return offer?.id;
const autoscaleSettings = resource.autoscaleSettings;
if (autoscaleSettings) {
return {
id: offerId,
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
manualThroughput: undefined,
minimumThroughput,
offerReplacePending: resource.offerReplacePending === "true"
};
}
return {
id: offerId,
autoscaleMaxThroughput: undefined,
manualThroughput: resource.throughput,
minimumThroughput,
offerReplacePending: resource.offerReplacePending === "true"
};
}
return undefined;
}; };

View File

@@ -0,0 +1,48 @@
import * as DataModels from "../../Contracts/DataModels";
import * as HeadersUtility from "../HeadersUtility";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContainerDefinition, Resource } from "@azure/cosmos";
import { HttpHeaders } from "../Constants";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
interface ResourceWithStatistics {
statistics: DataModels.Statistic[];
}
export const readCollectionQuotaInfo = async (
collection: ViewModels.Collection
): Promise<DataModels.CollectionQuotaInfo> => {
const clearMessage = logConsoleProgress(`Querying containers for database ${collection.id}`);
const options: RequestOptions = {};
options.populateQuotaInfo = true;
options.initialHeaders = options.initialHeaders || {};
options.initialHeaders[HttpHeaders.populatePartitionStatistics] = true;
try {
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.read(options);
const quota: DataModels.CollectionQuotaInfo = HeadersUtility.getQuota(response.headers);
const resource = response.resource as ContainerDefinition & Resource & ResourceWithStatistics;
quota["usageSizeInKB"] = resource.statistics.reduce(
(previousValue: number, currentValue: DataModels.Statistic) => previousValue + currentValue.sizeInKB,
0
);
quota["numPartitions"] = resource.statistics.length;
quota["uniqueKeyPolicy"] = collection.uniqueKeyPolicy; // TODO: Remove after refactoring (#119617)
return quota;
} catch (error) {
logConsoleError(`Error while querying quota info for container ${collection.id}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "ReadCollectionQuotaInfo", error.code);
sendNotificationForError(error);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -2,16 +2,18 @@ import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { listSqlContainers } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; import { listSqlContainers } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { listCassandraTables } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; import { listCassandraTables } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { listMongoDBCollections } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; import { listMongoDBCollections } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; import { listGremlinGraphs } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { listTables } from "../../Utils/arm/generatedClients/2020-04-01/tableResources"; import { listTables } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> { export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
let collections: DataModels.Collection[];
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`); const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
try { try {
if ( if (
@@ -20,20 +22,22 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB && userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table userContext.defaultExperience !== DefaultAccountExperienceType.Table
) { ) {
return await readCollectionsWithARM(databaseId); collections = await readCollectionsWithARM(databaseId);
} else {
const sdkResponse = await client()
.database(databaseId)
.containers.readAll()
.fetchAll();
collections = sdkResponse.resources as DataModels.Collection[];
} }
const sdkResponse = await client()
.database(databaseId)
.containers.readAll()
.fetchAll();
return sdkResponse.resources as DataModels.Collection[];
} catch (error) { } catch (error) {
handleError(error, "ReadCollections", `Error while querying containers for database ${databaseId}`); logConsoleError(`Error while querying containers for database ${databaseId}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "ReadCollections", error.code);
sendNotificationForError(error);
throw error; throw error;
} finally {
clearMessage();
} }
clearMessage();
return collections;
} }
async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Collection[]> { async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Collection[]> {

View File

@@ -1,43 +1,69 @@
import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels"; import { HttpHeaders } from "../Constants";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { handleError } from "../ErrorHandlingUtils"; import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logError } from "../Logger";
import { readOfferWithSDK } from "./readOfferWithSDK"; import { readOffers } from "./readOffers";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => { export const readDatabaseOffer = async (
params: DataModels.ReadDatabaseOfferParams
): Promise<DataModels.OfferWithHeaders> => {
const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`); const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`);
let offerId = params.offerId;
if (!offerId) {
offerId = await (window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
? getDatabaseOfferIdWithARM(params.databaseId)
: getDatabaseOfferIdWithSDK(params.databaseResourceId));
if (!offerId) {
clearMessage();
return undefined;
}
}
const options: RequestOptions = {
initialHeaders: {
[HttpHeaders.populateCollectionThroughputInfo]: true
}
};
try { try {
if ( const response = await client()
window.authType === AuthType.AAD && .offer(offerId)
!userContext.useSDKOperations && .read(options);
userContext.defaultExperience !== DefaultAccountExperienceType.Table return (
) { response && {
return await readDatabaseOfferWithARM(params.databaseId); ...response.resource,
} headers: response.headers
}
return await readOfferWithSDK(params.offerId, params.databaseResourceId); );
} catch (error) { } catch (error) {
handleError(error, "ReadDatabaseOffer", `Error while querying offer for database ${params.databaseId}`); logConsoleError(`Error while querying offer for database ${params.databaseId}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "ReadDatabaseOffer", error.code);
sendNotificationForError(error);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();
} }
}; };
const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => { const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> => {
let rpResponse;
const subscriptionId = userContext.subscriptionId; const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup; const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name; const accountName = userContext.databaseAccount.name;
const defaultExperience = userContext.defaultExperience; const defaultExperience = userContext.defaultExperience;
let rpResponse;
try { try {
switch (defaultExperience) { switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB: case DefaultAccountExperienceType.DocumentDB:
@@ -55,41 +81,18 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
default: default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`); throw new Error(`Unsupported default experience type: ${defaultExperience}`);
} }
return rpResponse?.name;
} catch (error) { } catch (error) {
if (error.code !== "NotFound") { if (error.code !== "NotFound") {
throw error; throw error;
} }
return undefined; return undefined;
} }
};
const resource = rpResponse?.properties?.resource;
if (resource) { const getDatabaseOfferIdWithSDK = async (databaseResourceId: string): Promise<string> => {
const offerId: string = rpResponse.name; const offers = await readOffers();
const minimumThroughput: number = const offer = offers.find(offer => offer.resource === databaseResourceId);
typeof resource.minimumThroughput === "string" return offer?.id;
? parseInt(resource.minimumThroughput)
: resource.minimumThroughput;
const autoscaleSettings = resource.autoscaleSettings;
if (autoscaleSettings) {
return {
id: offerId,
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
manualThroughput: undefined,
minimumThroughput,
offerReplacePending: resource.offerReplacePending === "true"
};
}
return {
id: offerId,
autoscaleMaxThroughput: undefined,
manualThroughput: resource.throughput,
minimumThroughput,
offerReplacePending: resource.offerReplacePending === "true"
};
}
return undefined;
}; };

View File

@@ -2,23 +2,24 @@ import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { listSqlDatabases } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; import { listSqlDatabases } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources"; import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { listMongoDBDatabases } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; import { listMongoDBDatabases } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; import { listGremlinDatabases } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
export async function readDatabases(): Promise<DataModels.Database[]> { export async function readDatabases(): Promise<DataModels.Database[]> {
if (userContext.defaultExperience === DefaultAccountExperienceType.Table) {
return [{ id: "TablesDB" } as DataModels.Database];
}
let databases: DataModels.Database[]; let databases: DataModels.Database[];
const clearMessage = logConsoleProgress(`Querying databases`); const clearMessage = logConsoleProgress(`Querying databases`);
try { try {
if ( if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
) {
databases = await readDatabasesWithARM(); databases = await readDatabasesWithARM();
} else { } else {
const sdkResponse = await client() const sdkResponse = await client()
@@ -27,7 +28,9 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
databases = sdkResponse.resources as DataModels.Database[]; databases = sdkResponse.resources as DataModels.Database[];
} }
} catch (error) { } catch (error) {
handleError(error, "ReadDatabases", `Error while querying databases`); logConsoleError(`Error while querying databases:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "ReadDatabases", error.code);
sendNotificationForError(error);
throw error; throw error;
} }
clearMessage(); clearMessage();

View File

@@ -1,27 +0,0 @@
import { Item } from "@azure/cosmos";
import { CollectionBase } from "../../Contracts/ViewModels";
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import DocumentId from "../../Explorer/Tree/DocumentId";
export const readDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<Item> => {
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Reading ${entityName} ${documentId.id()}`);
try {
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), documentId.partitionKeyValue)
.read();
return response?.resource;
} catch (error) {
handleError(error, "ReadDocument", `Failed to read ${entityName} ${documentId.id()}`);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -1,30 +0,0 @@
import { userContext } from "../../UserContext";
import { getMongoDBCollection } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { MongoDBCollectionResource } from "../../Utils/arm/generatedClients/2020-04-01/types";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { handleError } from "../ErrorHandlingUtils";
import { AuthType } from "../../AuthType";
export async function readMongoDBCollectionThroughRP(
databaseId: string,
collectionId: string
): Promise<MongoDBCollectionResource> {
if (window.authType !== AuthType.AAD) {
return undefined;
}
let collection: MongoDBCollectionResource;
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const clearMessage = logConsoleProgress(`Reading container ${collectionId}`);
try {
const response = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
collection = response.properties.resource;
} catch (error) {
handleError(error, "ReadMongoDBCollection", `Error while reading container ${collectionId}`);
throw error;
}
clearMessage();
return collection;
}

View File

@@ -1,29 +0,0 @@
import { HttpHeaders } from "../Constants";
import { Offer } from "../../Contracts/DataModels";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { parseSDKOfferResponse } from "../OfferUtility";
import { readOffers } from "./readOffers";
export const readOfferWithSDK = async (offerId: string, resourceId: string): Promise<Offer> => {
if (!offerId) {
const offers = await readOffers();
const offer = offers.find(offer => offer.resource === resourceId);
if (!offer) {
return undefined;
}
offerId = offer.id;
}
const options: RequestOptions = {
initialHeaders: {
[HttpHeaders.populateCollectionThroughputInfo]: true
}
};
const response = await client()
.offer(offerId)
.read(options);
return parseSDKOfferResponse(response);
};

View File

@@ -1,10 +1,26 @@
import { SDKOfferDefinition } from "../../Contracts/DataModels"; import { Offer } from "../../Contracts/DataModels";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { ClientDefaults } from "../Constants";
import { MessageTypes } from "../../Contracts/ExplorerContracts";
import { Platform, configContext } from "../../ConfigContext";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { handleError, getErrorMessage } from "../ErrorHandlingUtils"; import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { sendCachedDataMessage } from "../MessageHandler";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
export const readOffers = async (): Promise<SDKOfferDefinition[]> => { export const readOffers = async (): Promise<Offer[]> => {
const clearMessage = logConsoleProgress(`Querying offers`); const clearMessage = logConsoleProgress(`Querying offers`);
try {
if (configContext.platform === Platform.Portal) {
return sendCachedDataMessage<Offer[]>(MessageTypes.AllOffers, [
userContext.databaseAccount.id,
ClientDefaults.portalCacheTimeoutMs
]);
}
} catch (error) {
// If error getting cached Offers, continue on and read via SDK
}
try { try {
const response = await client() const response = await client()
@@ -13,11 +29,13 @@ export const readOffers = async (): Promise<SDKOfferDefinition[]> => {
return response?.resources; return response?.resources;
} catch (error) { } catch (error) {
// This should be removed when we can correctly identify if an account is serverless when connected using connection string too. // This should be removed when we can correctly identify if an account is serverless when connected using connection string too.
if (getErrorMessage(error)?.includes("Reading or replacing offers is not supported for serverless accounts")) { if (error.message.includes("Reading or replacing offers is not supported for serverless accounts")) {
return []; return [];
} }
handleError(error, "ReadOffers", `Error while querying offers`); logConsoleError(`Error while querying offers:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "ReadOffers", error.code);
sendNotificationForError(error);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();

View File

@@ -1,43 +1,27 @@
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { Resource, StoredProcedureDefinition } from "@azure/cosmos"; import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils"; import { logError } from "../Logger";
import { listSqlStoredProcedures } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; import { sendNotificationForError } from "./sendNotificationForError";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { userContext } from "../../UserContext";
export async function readStoredProcedures( export async function readStoredProcedures(
databaseId: string, databaseId: string,
collectionId: string collectionId: string
): Promise<(StoredProcedureDefinition & Resource)[]> { ): Promise<(StoredProcedureDefinition & Resource)[]> {
let sprocs: (StoredProcedureDefinition & Resource)[];
const clearMessage = logConsoleProgress(`Querying stored procedures for container ${collectionId}`); const clearMessage = logConsoleProgress(`Querying stored procedures for container ${collectionId}`);
try { try {
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB
) {
const rpResponse = await listSqlStoredProcedures(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId
);
return rpResponse?.value?.map(sproc => sproc.properties?.resource as StoredProcedureDefinition & Resource);
}
const response = await client() const response = await client()
.database(databaseId) .database(databaseId)
.container(collectionId) .container(collectionId)
.scripts.storedProcedures.readAll() .scripts.storedProcedures.readAll()
.fetchAll(); .fetchAll();
return response?.resources; sprocs = response.resources;
} catch (error) { } catch (error) {
handleError(error, "ReadStoredProcedures", `Failed to query stored procedures for container ${collectionId}`); logConsoleError(`Failed to query stored procedures for container ${collectionId}: ${JSON.stringify(error)}`);
throw error; logError(JSON.stringify(error), "ReadStoredProcedures", error.code);
} finally { sendNotificationForError(error);
clearMessage();
} }
clearMessage();
return sprocs;
} }

View File

@@ -1,43 +1,27 @@
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { Resource, TriggerDefinition } from "@azure/cosmos"; import { Resource, TriggerDefinition } from "@azure/cosmos";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { listSqlTriggers } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; import { logError } from "../Logger";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
import { handleError } from "../ErrorHandlingUtils";
export async function readTriggers( export async function readTriggers(
databaseId: string, databaseId: string,
collectionId: string collectionId: string
): Promise<(TriggerDefinition & Resource)[]> { ): Promise<(TriggerDefinition & Resource)[]> {
let triggers: (TriggerDefinition & Resource)[];
const clearMessage = logConsoleProgress(`Querying triggers for container ${collectionId}`); const clearMessage = logConsoleProgress(`Querying triggers for container ${collectionId}`);
try { try {
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB
) {
const rpResponse = await listSqlTriggers(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId
);
return rpResponse?.value?.map(trigger => trigger.properties?.resource as TriggerDefinition & Resource);
}
const response = await client() const response = await client()
.database(databaseId) .database(databaseId)
.container(collectionId) .container(collectionId)
.scripts.triggers.readAll() .scripts.triggers.readAll()
.fetchAll(); .fetchAll();
return response?.resources; triggers = response.resources;
} catch (error) { } catch (error) {
handleError(error, "ReadTriggers", `Failed to query triggers for container ${collectionId}`); logConsoleError(`Failed to query triggers for container ${collectionId}: ${JSON.stringify(error)}`);
throw error; logError(JSON.stringify(error), "ReadTriggers", error.code);
} finally { sendNotificationForError(error);
clearMessage();
} }
clearMessage();
return triggers;
} }

View File

@@ -1,47 +1,27 @@
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos"; import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils"; import { logError } from "../Logger";
import { listSqlUserDefinedFunctions } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; import { sendNotificationForError } from "./sendNotificationForError";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { userContext } from "../../UserContext";
export async function readUserDefinedFunctions( export async function readUserDefinedFunctions(
databaseId: string, databaseId: string,
collectionId: string collectionId: string
): Promise<(UserDefinedFunctionDefinition & Resource)[]> { ): Promise<(UserDefinedFunctionDefinition & Resource)[]> {
let udfs: (UserDefinedFunctionDefinition & Resource)[];
const clearMessage = logConsoleProgress(`Querying user defined functions for container ${collectionId}`); const clearMessage = logConsoleProgress(`Querying user defined functions for container ${collectionId}`);
try { try {
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB
) {
const rpResponse = await listSqlUserDefinedFunctions(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId
);
return rpResponse?.value?.map(udf => udf.properties?.resource as UserDefinedFunctionDefinition & Resource);
}
const response = await client() const response = await client()
.database(databaseId) .database(databaseId)
.container(collectionId) .container(collectionId)
.scripts.userDefinedFunctions.readAll() .scripts.userDefinedFunctions.readAll()
.fetchAll(); .fetchAll();
return response?.resources; udfs = response.resources;
} catch (error) { } catch (error) {
handleError( logConsoleError(`Failed to query user defined functions for container ${collectionId}: ${JSON.stringify(error)}`);
error, logError(JSON.stringify(error), "ReadUserDefinedFunctions", error.code);
"ReadUserDefinedFunctions", sendNotificationForError(error);
`Failed to query user defined functions for container ${collectionId}`
);
throw error;
} finally {
clearMessage();
} }
clearMessage();
return udfs;
} }

View File

@@ -0,0 +1,20 @@
import * as Constants from "../Constants";
import { sendMessage } from "../MessageHandler";
import { MessageTypes } from "../../Contracts/ExplorerContracts";
interface CosmosError {
code: number;
message?: string;
}
export function sendNotificationForError(error: CosmosError): void {
if (error && error.code === Constants.HttpStatusCodes.Forbidden) {
if (error.message && error.message.toLowerCase().indexOf("sharedoffer is disabled for your account") > 0) {
return;
}
sendMessage({
type: MessageTypes.ForbiddenError,
reason: error && error.message ? error.message : error
});
}
}

View File

@@ -3,10 +3,7 @@ import { Collection } from "../../Contracts/DataModels";
import { ContainerDefinition } from "@azure/cosmos"; import { ContainerDefinition } from "@azure/cosmos";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { import {
CreateUpdateOptions,
ExtendedResourceProperties, ExtendedResourceProperties,
MongoDBCollectionCreateUpdateParameters,
MongoDBCollectionResource,
SqlContainerCreateUpdateParameters, SqlContainerCreateUpdateParameters,
SqlContainerResource SqlContainerResource
} from "../../Utils/arm/generatedClients/2020-04-01/types"; } from "../../Utils/arm/generatedClients/2020-04-01/types";
@@ -26,8 +23,10 @@ import {
getGremlinGraph getGremlinGraph
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources"; } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources"; import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { handleError } from "../ErrorHandlingUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logError } from "../Logger";
import { refreshCachedResources } from "../DataAccessUtilityBase";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
export async function updateCollection( export async function updateCollection(
@@ -52,14 +51,16 @@ export async function updateCollection(
.database(databaseId) .database(databaseId)
.container(collectionId) .container(collectionId)
.replace(newCollection as ContainerDefinition, options); .replace(newCollection as ContainerDefinition, options);
collection = sdkResponse.resource as Collection; collection = sdkResponse.resource as Collection;
} }
logConsoleInfo(`Successfully updated container ${collectionId}`); logConsoleInfo(`Successfully updated container ${collectionId}`);
await refreshCachedResources();
return collection; return collection;
} catch (error) { } catch (error) {
handleError(error, "UpdateCollection", `Failed to update container ${collectionId}`); logConsoleError(`Failed to update container ${collectionId}: ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "UpdateCollection", error.code);
sendNotificationForError(error);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();
@@ -79,6 +80,15 @@ async function updateCollectionWithARM(
switch (defaultExperience) { switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB: case DefaultAccountExperienceType.DocumentDB:
return updateSqlContainer(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection); return updateSqlContainer(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection);
case DefaultAccountExperienceType.MongoDB:
return updateMongoDBCollection(
databaseId,
collectionId,
subscriptionId,
resourceGroup,
accountName,
newCollection
);
case DefaultAccountExperienceType.Cassandra: case DefaultAccountExperienceType.Cassandra:
return updateCassandraTable(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection); return updateCassandraTable(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection);
case DefaultAccountExperienceType.Graph: case DefaultAccountExperienceType.Graph:
@@ -115,35 +125,26 @@ async function updateSqlContainer(
throw new Error(`Sql container to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`); throw new Error(`Sql container to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`);
} }
export async function updateMongoDBCollectionThroughRP( async function updateMongoDBCollection(
databaseId: string, databaseId: string,
collectionId: string, collectionId: string,
newCollection: MongoDBCollectionResource, subscriptionId: string,
updateOptions?: CreateUpdateOptions resourceGroup: string,
): Promise<MongoDBCollectionResource> { accountName: string,
const subscriptionId = userContext.subscriptionId; newCollection: Collection
const resourceGroup = userContext.resourceGroup; ): Promise<Collection> {
const accountName = userContext.databaseAccount.name;
const getResponse = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId); const getResponse = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) { if (getResponse && getResponse.properties && getResponse.properties.resource) {
const updateParams: MongoDBCollectionCreateUpdateParameters = { getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
properties: {
resource: newCollection,
options: updateOptions
}
};
const updateResponse = await createUpdateMongoDBCollection( const updateResponse = await createUpdateMongoDBCollection(
subscriptionId, subscriptionId,
resourceGroup, resourceGroup,
accountName, accountName,
databaseId, databaseId,
collectionId, collectionId,
updateParams getResponse as SqlContainerCreateUpdateParameters
); );
return updateResponse && (updateResponse.properties.resource as Collection);
return updateResponse && (updateResponse.properties.resource as MongoDBCollectionResource);
} }
throw new Error( throw new Error(

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