mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-29 22:02:01 +00:00
Compare commits
24 Commits
remove-red
...
codescan
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a266fdc89c | ||
|
|
39f7ef331a | ||
|
|
9933a4988a | ||
|
|
d525afa142 | ||
|
|
cfb9a0b321 | ||
|
|
3b64d75322 | ||
|
|
daba1c4ed4 | ||
|
|
a698e08638 | ||
|
|
88d630fef4 | ||
|
|
5ffa746adb | ||
|
|
a9a57f4ba9 | ||
|
|
47cc6fd7a8 | ||
|
|
14cdf19efb | ||
|
|
5191ae3f3a | ||
|
|
ba862a8106 | ||
|
|
fe085b3e5a | ||
|
|
8028734cb0 | ||
|
|
444f663733 | ||
|
|
8c1ca35420 | ||
|
|
b69174788d | ||
|
|
ff03c79399 | ||
|
|
0382628249 | ||
|
|
d346ebe054 | ||
|
|
f5ecb8a04f |
29
.github/workflows/automerge.yml
vendored
29
.github/workflows/automerge.yml
vendored
@@ -1,29 +0,0 @@
|
||||
name: automerge
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- unlabeled
|
||||
- synchronize
|
||||
- opened
|
||||
- edited
|
||||
- ready_for_review
|
||||
- reopened
|
||||
- unlocked
|
||||
pull_request_review:
|
||||
types:
|
||||
- submitted
|
||||
check_suite:
|
||||
types:
|
||||
- completed
|
||||
status: {}
|
||||
jobs:
|
||||
automerge:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: automerge
|
||||
uses: "pascalgn/automerge-action@v0.11.0"
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.AUTOMERGE_GITHUB_PAT }}"
|
||||
MERGE_METHOD: "squash"
|
||||
MERGE_COMMIT_MESSAGE: "pull-request-title"
|
||||
72
.github/workflows/ci.yml
vendored
72
.github/workflows/ci.yml
vendored
@@ -105,74 +105,6 @@ jobs:
|
||||
EMULATOR_ENDPOINT: https://0.0.0.0:8081/
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
||||
endtoendsql:
|
||||
name: "End To End Tests | SQL"
|
||||
needs: [lint, format, compile, unittest]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: Restore Cypress Binary Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress-binary-cache
|
||||
- run: npm ci
|
||||
- name: End to End Tests
|
||||
run: |
|
||||
npm start &
|
||||
cd cypress
|
||||
npm ci
|
||||
node cleanup.js
|
||||
npm run wait-for-server
|
||||
npx cypress run --browser chrome --headless --spec "./integration/dataexplorer/SQL/*"
|
||||
shell: bash
|
||||
env:
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
||||
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
|
||||
- uses: actions/upload-artifact@v2
|
||||
name: videos
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
path: "**/*.mp4"
|
||||
endtoendmongo:
|
||||
name: "End To End Tests | Mongo"
|
||||
needs: [lint, format, compile, unittest]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: Restore Cypress Binary Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress-binary-cache
|
||||
- name: End to End Tests
|
||||
run: |
|
||||
npm ci
|
||||
npm start &
|
||||
cd cypress
|
||||
npm ci
|
||||
node cleanup.js
|
||||
npm run wait-for-server
|
||||
npx cypress run --browser chrome --headless --spec "./integration/dataexplorer/MONGO/*"
|
||||
shell: bash
|
||||
env:
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
||||
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: ${{ failure() }}
|
||||
name: videos
|
||||
with:
|
||||
path: "**/*.mp4"
|
||||
accessibility:
|
||||
name: "Accessibility | Hosted"
|
||||
needs: [lint, format, compile, unittest]
|
||||
@@ -221,7 +153,7 @@ jobs:
|
||||
nuget:
|
||||
name: Publish Nuget
|
||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo]
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendpuppeteer]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||
@@ -245,7 +177,7 @@ jobs:
|
||||
nugetmpac:
|
||||
name: Publish Nuget MPAC
|
||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo]
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendpuppeteer]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||
|
||||
71
.github/workflows/codeql-analysis.yml
vendored
Normal file
71
.github/workflows/codeql-analysis.yml
vendored
Normal 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
|
||||
@@ -1,4 +1,4 @@
|
||||
# CosmosDB Explorer
|
||||
# Cosmos DB Explorer
|
||||
|
||||
UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), https://cosmos.azure.com/, and the [Cosmos DB Emulator](https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator)
|
||||
|
||||
|
||||
@@ -3,11 +3,8 @@
|
||||
# Add steps that build, run tests, deploy, and more:
|
||||
# https://aka.ms/yaml
|
||||
|
||||
trigger:
|
||||
- master
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
vmImage: "ubuntu-latest"
|
||||
|
||||
steps:
|
||||
- task: ComponentGovernanceComponentDetection@0
|
||||
- task: ComponentGovernanceComponentDetection@0
|
||||
|
||||
177
externals/jquery.contextMenu.css
vendored
177
externals/jquery.contextMenu.css
vendored
@@ -1,177 +0,0 @@
|
||||
/*!
|
||||
* jQuery contextMenu - Plugin for simple contextMenu handling
|
||||
*
|
||||
* Version: 1.6.6
|
||||
*
|
||||
* Authors: Rodney Rehm, Addy Osmani (patches for FF)
|
||||
* Web: http://medialize.github.com/jQuery-contextMenu/
|
||||
*
|
||||
* Licensed under
|
||||
* MIT License http://www.opensource.org/licenses/mit-license
|
||||
* GPL v3 http://opensource.org/licenses/GPL-3.0
|
||||
*
|
||||
*/
|
||||
|
||||
.context-menu-list {
|
||||
z-index: 1001;
|
||||
position: fixed;
|
||||
background: white;
|
||||
border: solid 1px gainsboro;
|
||||
box-shadow: 4px 4px 4px -2px #888888;
|
||||
padding: 8px 0px 8px 0px;
|
||||
line-height: 25px;
|
||||
width: 254px;
|
||||
list-style: none;
|
||||
margin-left: -10px;
|
||||
outline: 0px #fff;
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
padding: 2px 2px 2px 31px;
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: -moz-none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.context-menu-separator {
|
||||
padding-bottom: 0;
|
||||
border-bottom: 1px solid #DDD;
|
||||
}
|
||||
|
||||
.context-menu-item>label>input,
|
||||
.context-menu-item>label>textarea {
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
margin-left: -10px;
|
||||
}
|
||||
|
||||
.context-menu-item:hover {
|
||||
cursor: pointer;
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
|
||||
.context-menu-item.disabled {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.context-menu-input.hover,
|
||||
.context-menu-item.disabled.hover {
|
||||
cursor: default;
|
||||
background-color: #EEE;
|
||||
}
|
||||
|
||||
.context-menu-submenu:after {
|
||||
content: ">";
|
||||
color: #666;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 3px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
||||
/* icons
|
||||
#protip:
|
||||
In case you want to use sprites for icons (which I would suggest you do) have a look at
|
||||
http://css-tricks.com/13224-pseudo-spriting/ to get an idea of how to implement
|
||||
.context-menu-item.icon:before {}
|
||||
*/
|
||||
|
||||
.context-menu-item.icon {
|
||||
min-height: 18px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 10px 7px;
|
||||
}
|
||||
|
||||
.context-menu-item.icon:hover {
|
||||
min-height: 18px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 10px 7px;
|
||||
}
|
||||
|
||||
|
||||
/*.context-menu-item.icon-edit {
|
||||
background-image: url(images/page_white_edit.png);
|
||||
}
|
||||
|
||||
.context-menu-item.icon-cut {
|
||||
background-image: url(images/cut.png);
|
||||
}
|
||||
|
||||
.context-menu-item.icon-copy {
|
||||
background-image: url(images/page_white_copy.png);
|
||||
}
|
||||
|
||||
.context-menu-item.icon-paste {
|
||||
background-image: url(images/page_white_paste.png);
|
||||
}
|
||||
|
||||
.context-menu-item.icon-delete {
|
||||
background-image: url(images/page_white_delete.png);
|
||||
}
|
||||
|
||||
.context-menu-item.icon-add {
|
||||
background-image: url(images/page_white_add.png);
|
||||
}
|
||||
|
||||
.context-menu-item.icon-quit {
|
||||
background-image: url(images/door.png);
|
||||
}*/
|
||||
|
||||
|
||||
/* vertically align inside labels */
|
||||
|
||||
.context-menu-input>label>* {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
|
||||
/* position checkboxes and radios as icons */
|
||||
|
||||
.context-menu-input>label>input[type="checkbox"],
|
||||
.context-menu-input>label>input[type="radio"] {
|
||||
margin-left: -17px;
|
||||
}
|
||||
|
||||
.context-menu-input>label>span {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.context-menu-input>label,
|
||||
.context-menu-input>label>input[type="text"],
|
||||
.context-menu-input>label>textarea,
|
||||
.context-menu-input>label>select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-ms-box-sizing: border-box;
|
||||
-o-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.context-menu-input>label>textarea {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.context-menu-item>.context-menu-list {
|
||||
display: none;
|
||||
/* re-positioned by js */
|
||||
right: -5px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
|
||||
/*.context-menu-item.hover>.context-menu-list {
|
||||
display: block;
|
||||
padding-left: 5px;
|
||||
}*/
|
||||
|
||||
.context-menu-accesskey {
|
||||
text-decoration: underline;
|
||||
}
|
||||
1686
externals/jquery.contextMenu.js
vendored
1686
externals/jquery.contextMenu.js
vendored
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ const isCI = require("is-ci");
|
||||
module.exports = {
|
||||
launch: {
|
||||
headless: isCI,
|
||||
slowMo: 30,
|
||||
slowMo: 55,
|
||||
defaultViewport: null,
|
||||
ignoreHTTPSErrors: true,
|
||||
args: ["--disable-web-security"]
|
||||
|
||||
@@ -2082,7 +2082,8 @@ a:link {
|
||||
.resourceTreeAndTabs {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
92
package-lock.json
generated
92
package-lock.json
generated
@@ -2326,83 +2326,83 @@
|
||||
}
|
||||
},
|
||||
"@microsoft/applicationinsights-analytics-js": {
|
||||
"version": "2.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-analytics-js/-/applicationinsights-analytics-js-2.5.8.tgz",
|
||||
"integrity": "sha512-2RsftiXa5ojNXuHRIC7RybWbN+Z7TMrLK2XGTza1Wq/KHRJNB48WmQuxjd6SzsNguqxRoHsH0sUogIwlK+NO8A==",
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-analytics-js/-/applicationinsights-analytics-js-2.5.9.tgz",
|
||||
"integrity": "sha512-9L3fb1H1as+J3J2j2EDx1HEMdrucjgR4INqahy+ZAxDPFvR3HCOedYzx645zObBIPu7QkH2LAjPk4fuNGHR1rg==",
|
||||
"requires": {
|
||||
"@microsoft/applicationinsights-common": "2.5.8",
|
||||
"@microsoft/applicationinsights-core-js": "2.5.8",
|
||||
"@microsoft/applicationinsights-shims": "1.0.1",
|
||||
"@microsoft/applicationinsights-common": "2.5.9",
|
||||
"@microsoft/applicationinsights-core-js": "2.5.9",
|
||||
"@microsoft/applicationinsights-shims": "1.0.3",
|
||||
"@microsoft/dynamicproto-js": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"@microsoft/applicationinsights-channel-js": {
|
||||
"version": "2.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-2.5.8.tgz",
|
||||
"integrity": "sha512-etVGzhNluflTikOpW9dlDxdg+B+8gt/nxmGeXqti9Hsqq0fTxcY9bmNklFIEKhOIwai4DodgJjflaDdgR0ObUQ==",
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-2.5.9.tgz",
|
||||
"integrity": "sha512-NAQ/2wWmD+gaIZDCMzzwxm8RcbswDvUO5BYeuW9UHJaFuEZ9o9xpztKVz32u4CMv7OI/mLOqnmR4rb0d+kUMwQ==",
|
||||
"requires": {
|
||||
"@microsoft/applicationinsights-common": "2.5.8",
|
||||
"@microsoft/applicationinsights-core-js": "2.5.8",
|
||||
"@microsoft/applicationinsights-shims": "1.0.1",
|
||||
"@microsoft/applicationinsights-common": "2.5.9",
|
||||
"@microsoft/applicationinsights-core-js": "2.5.9",
|
||||
"@microsoft/applicationinsights-shims": "1.0.3",
|
||||
"@microsoft/dynamicproto-js": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"@microsoft/applicationinsights-common": {
|
||||
"version": "2.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-2.5.8.tgz",
|
||||
"integrity": "sha512-RIMAJTsF0H5wiRZxYIF9H8sbMe2W5Ig4yMuH0/lho69DcNZpHf8p6PSa4Qhhli0AnoWYfLE7/WlWO1eR5SkByw==",
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-2.5.9.tgz",
|
||||
"integrity": "sha512-dKmXO9m55uRDhpoa0P7l+BApf+lsrqjgoLeKv+ABM8ygIyd9JH6CDcdaT3af+kUFtt9Oj3ChyfueKr1EVOdGkQ==",
|
||||
"requires": {
|
||||
"@microsoft/applicationinsights-core-js": "2.5.8",
|
||||
"@microsoft/applicationinsights-shims": "1.0.1"
|
||||
"@microsoft/applicationinsights-core-js": "2.5.9",
|
||||
"@microsoft/applicationinsights-shims": "1.0.3"
|
||||
}
|
||||
},
|
||||
"@microsoft/applicationinsights-core-js": {
|
||||
"version": "2.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.5.8.tgz",
|
||||
"integrity": "sha512-NxxIViHKuqgpla+KdQk7Qy48Dm8lN4oy2mMEpv9kM5GW5MBJ8nZ4A5RV1kokF3kXuDmTUTHlWBXeLR8hauA3qQ==",
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.5.9.tgz",
|
||||
"integrity": "sha512-KE9h1wmC/Ckm7jYjsMF1SEWQnk0v0CRzZq1upSARgPH7BgmyClXz1kdnLtuTWz8Aha8IIH9dW2hUOfPCdR+BpQ==",
|
||||
"requires": {
|
||||
"@microsoft/applicationinsights-shims": "1.0.1",
|
||||
"@microsoft/applicationinsights-shims": "1.0.3",
|
||||
"@microsoft/dynamicproto-js": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"@microsoft/applicationinsights-dependencies-js": {
|
||||
"version": "2.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-dependencies-js/-/applicationinsights-dependencies-js-2.5.8.tgz",
|
||||
"integrity": "sha512-jnXpjE/bnlLeew78OsN8aPTLOPZQJ4y1MOO8R3E+eUXdproD2TemynSk5kUfrMdry91DZOBZnrmJ2NCB+g5ArQ==",
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-dependencies-js/-/applicationinsights-dependencies-js-2.5.9.tgz",
|
||||
"integrity": "sha512-pNM/dkUOscV0ul/YJe928+77EBtRkRXO/le/VWzlunoUFaEEo4pirc7NycvPx9w/KxA62JMEogbQsWE6nAmqPg==",
|
||||
"requires": {
|
||||
"@microsoft/applicationinsights-common": "2.5.8",
|
||||
"@microsoft/applicationinsights-core-js": "2.5.8",
|
||||
"@microsoft/applicationinsights-shims": "1.0.1",
|
||||
"@microsoft/applicationinsights-common": "2.5.9",
|
||||
"@microsoft/applicationinsights-core-js": "2.5.9",
|
||||
"@microsoft/applicationinsights-shims": "1.0.3",
|
||||
"@microsoft/dynamicproto-js": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"@microsoft/applicationinsights-properties-js": {
|
||||
"version": "2.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-properties-js/-/applicationinsights-properties-js-2.5.8.tgz",
|
||||
"integrity": "sha512-BAOSguXe07ua6SqYmAW+8+vVvBZb4qmmUP6s5zc7kNMMgEoEGsCn2VHmM8wHHxq4J/TmYxIqwoufS+XKbUvCeQ==",
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-properties-js/-/applicationinsights-properties-js-2.5.9.tgz",
|
||||
"integrity": "sha512-mZxaC8CZsURn38IwsPaUx+o9QXQU2vm81THZL+1Lc+7scPo55ATDTFgZ2awIj7CdTp69oGzUkpB7maOn6+OVOw==",
|
||||
"requires": {
|
||||
"@microsoft/applicationinsights-common": "2.5.8",
|
||||
"@microsoft/applicationinsights-core-js": "2.5.8",
|
||||
"@microsoft/applicationinsights-shims": "1.0.1"
|
||||
"@microsoft/applicationinsights-common": "2.5.9",
|
||||
"@microsoft/applicationinsights-core-js": "2.5.9",
|
||||
"@microsoft/applicationinsights-shims": "1.0.3"
|
||||
}
|
||||
},
|
||||
"@microsoft/applicationinsights-shims": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-1.0.1.tgz",
|
||||
"integrity": "sha512-nPjUBSpvX5Dnkovp2lZzrg0/uSvYNtbAclWwVP7t8J1hy5OJ3xr3KPNaz79+b84G16Rj861ybau9Gbk7inXkTg=="
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-1.0.3.tgz",
|
||||
"integrity": "sha512-+S17aqEkOYpyBpmclhgwcEplwnxSo5AxYBdRg38GBobI1GKPSpZfnLssLzcjJ6XZCS5tqB5xjyTZs6gHj7ZJWQ=="
|
||||
},
|
||||
"@microsoft/applicationinsights-web": {
|
||||
"version": "2.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web/-/applicationinsights-web-2.5.8.tgz",
|
||||
"integrity": "sha512-ZxCUkBJrCFNHa0LgWWbtwqu4TxJPlukuSvDrdLv6XV1yX2ETq6Q1kw/IUEtKhbtNbTZQ8aJ+x8Nc/iqbssdXTA==",
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web/-/applicationinsights-web-2.5.9.tgz",
|
||||
"integrity": "sha512-dxg5XXbQqjWw9QmGdgbd7knb1qFA58FFYj9ObqRmlqiihk25kper7H15HH8LaV0lV6goClmBWc9KsNGA2veyeA==",
|
||||
"requires": {
|
||||
"@microsoft/applicationinsights-analytics-js": "2.5.8",
|
||||
"@microsoft/applicationinsights-channel-js": "2.5.8",
|
||||
"@microsoft/applicationinsights-common": "2.5.8",
|
||||
"@microsoft/applicationinsights-core-js": "2.5.8",
|
||||
"@microsoft/applicationinsights-dependencies-js": "2.5.8",
|
||||
"@microsoft/applicationinsights-properties-js": "2.5.8",
|
||||
"@microsoft/applicationinsights-shims": "1.0.1",
|
||||
"@microsoft/applicationinsights-analytics-js": "2.5.9",
|
||||
"@microsoft/applicationinsights-channel-js": "2.5.9",
|
||||
"@microsoft/applicationinsights-common": "2.5.9",
|
||||
"@microsoft/applicationinsights-core-js": "2.5.9",
|
||||
"@microsoft/applicationinsights-dependencies-js": "2.5.9",
|
||||
"@microsoft/applicationinsights-properties-js": "2.5.9",
|
||||
"@microsoft/applicationinsights-shims": "1.0.3",
|
||||
"@microsoft/dynamicproto-js": "^1.0.0"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"@azure/cosmos-language-service": "0.0.4",
|
||||
"@jupyterlab/services": "6.0.0-rc.2",
|
||||
"@jupyterlab/terminal": "3.0.0-rc.2",
|
||||
"@microsoft/applicationinsights-web": "2.5.8",
|
||||
"@microsoft/applicationinsights-web": "2.5.9",
|
||||
"@nteract/commutable": "7.3.2",
|
||||
"@nteract/connected-components": "6.8.2",
|
||||
"@nteract/core": "15.1.0",
|
||||
@@ -88,8 +88,8 @@
|
||||
"styled-components": "4.3.2",
|
||||
"text-encoding": "0.7.0",
|
||||
"underscore": "1.9.1",
|
||||
"utility-types": "3.10.0",
|
||||
"url-polyfill": "1.1.7",
|
||||
"utility-types": "3.10.0",
|
||||
"webcrypto-liner": "1.1.4",
|
||||
"webfontloader": "1.6.28",
|
||||
"whatwg-fetch": "3.0.0"
|
||||
|
||||
@@ -117,7 +117,6 @@ export class Features {
|
||||
public static readonly enableGalleryPublish = "enablegallerypublish";
|
||||
public static readonly enableCodeOfConduct = "enablecodeofconduct";
|
||||
public static readonly enableLinkInjection = "enablelinkinjection";
|
||||
public static readonly enableSettingsV2 = "enablesettingsv2";
|
||||
public static readonly enableSpark = "enablespark";
|
||||
public static readonly livyEndpoint = "livyendpoint";
|
||||
public static readonly notebookServerUrl = "notebookserverurl";
|
||||
@@ -131,6 +130,11 @@ export class Features {
|
||||
public static readonly enableSDKoperations = "enablesdkoperations";
|
||||
}
|
||||
|
||||
// flight names returned from the portal are always lowercase
|
||||
export class Flights {
|
||||
public static readonly SettingsV2 = "settingsv2";
|
||||
}
|
||||
|
||||
export class AfecFeatures {
|
||||
public static readonly Spark = "spark-public-preview";
|
||||
public static readonly Notebooks = "sparknotebooks-public-preview";
|
||||
|
||||
@@ -72,22 +72,6 @@ export function getPartitionKeyHeader(partitionKeyDefinition: DataModels.Partiti
|
||||
return [partitionKeyValue];
|
||||
}
|
||||
|
||||
export function updateOffer(
|
||||
offer: DataModels.Offer,
|
||||
newOffer: DataModels.Offer,
|
||||
options?: RequestOptions
|
||||
): Q.Promise<DataModels.Offer> {
|
||||
return Q(
|
||||
client()
|
||||
.offer(offer.id)
|
||||
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
|
||||
.replace((newOffer as unknown) as OfferDefinition, options)
|
||||
.then(response => {
|
||||
return Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => response.resource);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function updateDocument(
|
||||
collection: ViewModels.CollectionBase,
|
||||
documentId: DocumentId,
|
||||
|
||||
@@ -157,41 +157,6 @@ export function updateDocument(
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function updateOffer(
|
||||
offer: DataModels.Offer,
|
||||
newOffer: DataModels.Offer,
|
||||
options: RequestOptions
|
||||
): Q.Promise<DataModels.Offer> {
|
||||
var deferred = Q.defer<any>();
|
||||
const clearMessage = logConsoleProgress(`Updating offer for resource ${offer.resource}`);
|
||||
DataAccessUtilityBase.updateOffer(offer, newOffer, options)
|
||||
.then(
|
||||
(replacedOffer: DataModels.Offer) => {
|
||||
logConsoleInfo(`Successfully updated offer for resource ${offer.resource}`);
|
||||
deferred.resolve(replacedOffer);
|
||||
},
|
||||
(error: any) => {
|
||||
logConsoleError(`Error updating offer for resource ${offer.resource}: ${JSON.stringify(error)}`);
|
||||
Logger.logError(
|
||||
JSON.stringify({
|
||||
oldOffer: offer,
|
||||
newOffer: newOffer,
|
||||
error: error
|
||||
}),
|
||||
"UpdateOffer",
|
||||
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();
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import "jquery";
|
||||
import * as Q from "q";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||
import { userContext } from "../UserContext";
|
||||
|
||||
export class NotificationsClientBase {
|
||||
private _extensionEndpoint: string;
|
||||
private _notificationsApiSuffix: string;
|
||||
|
||||
protected constructor(notificationsApiSuffix: string) {
|
||||
this._notificationsApiSuffix = notificationsApiSuffix;
|
||||
}
|
||||
|
||||
public fetchNotifications(): Q.Promise<DataModels.Notification[]> {
|
||||
const deferred: Q.Deferred<DataModels.Notification[]> = Q.defer<DataModels.Notification[]>();
|
||||
const databaseAccount = userContext.databaseAccount;
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const url = `${this._extensionEndpoint}${this._notificationsApiSuffix}?accountName=${databaseAccount.name}&subscriptionId=${subscriptionId}&resourceGroup=${resourceGroup}`;
|
||||
const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||
const headers: any = {};
|
||||
headers[authorizationHeader.header] = authorizationHeader.token;
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "GET",
|
||||
headers: headers,
|
||||
cache: false
|
||||
}).then(
|
||||
(notifications: DataModels.Notification[], textStatus: string, xhr: JQueryXHR<any>) => {
|
||||
deferred.resolve(notifications);
|
||||
},
|
||||
(xhr: JQueryXHR<any>, textStatus: string, error: any) => {
|
||||
deferred.reject(xhr.responseText);
|
||||
}
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public setExtensionEndpoint(extensionEndpoint: string): void {
|
||||
this._extensionEndpoint = extensionEndpoint;
|
||||
}
|
||||
}
|
||||
41
src/Common/PortalNotifications.ts
Normal file
41
src/Common/PortalNotifications.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||
import { userContext } from "../UserContext";
|
||||
import { configContext, Platform } from "../ConfigContext";
|
||||
|
||||
const notificationsPath = () => {
|
||||
switch (configContext.platform) {
|
||||
case Platform.Hosted:
|
||||
return "/api/guest/notifications";
|
||||
case Platform.Portal:
|
||||
return "/api/notifications";
|
||||
default:
|
||||
throw new Error(`Unknown platform: ${configContext.platform}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchPortalNotifications = async (): Promise<DataModels.Notification[]> => {
|
||||
if (configContext.platform === Platform.Emulator) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const databaseAccount = userContext.databaseAccount;
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const url = `${configContext.BACKEND_ENDPOINT}${notificationsPath()}?accountName=${
|
||||
databaseAccount.name
|
||||
}&subscriptionId=${subscriptionId}&resourceGroup=${resourceGroup}`;
|
||||
const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||
const headers = { [authorizationHeader.header]: authorizationHeader.token };
|
||||
|
||||
const response = await window.fetch(url, {
|
||||
headers
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
|
||||
return (await response.json()) as DataModels.Notification[];
|
||||
};
|
||||
@@ -20,26 +20,14 @@ export const readDatabaseOffer = async (
|
||||
const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`);
|
||||
let offerId = params.offerId;
|
||||
if (!offerId) {
|
||||
if (
|
||||
window.authType === AuthType.AAD &&
|
||||
!userContext.useSDKOperations &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||
) {
|
||||
try {
|
||||
offerId = await getDatabaseOfferIdWithARM(params.databaseId);
|
||||
} catch (error) {
|
||||
clearMessage();
|
||||
if (error.code !== "NotFound") {
|
||||
throw new error();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
offerId = await getDatabaseOfferIdWithSDK(params.databaseResourceId);
|
||||
if (!offerId) {
|
||||
clearMessage();
|
||||
return undefined;
|
||||
}
|
||||
offerId = await (window.authType === AuthType.AAD &&
|
||||
!userContext.useSDKOperations &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||
? getDatabaseOfferIdWithARM(params.databaseId)
|
||||
: getDatabaseOfferIdWithSDK(params.databaseResourceId));
|
||||
if (!offerId) {
|
||||
clearMessage();
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,24 +63,32 @@ const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> =>
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const accountName = userContext.databaseAccount.name;
|
||||
const defaultExperience = userContext.defaultExperience;
|
||||
switch (defaultExperience) {
|
||||
case DefaultAccountExperienceType.DocumentDB:
|
||||
rpResponse = await getSqlDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
|
||||
break;
|
||||
case DefaultAccountExperienceType.MongoDB:
|
||||
rpResponse = await getMongoDBDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Cassandra:
|
||||
rpResponse = await getCassandraKeyspaceThroughput(subscriptionId, resourceGroup, accountName, databaseId);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Graph:
|
||||
rpResponse = await getGremlinDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||
}
|
||||
|
||||
return rpResponse?.name;
|
||||
try {
|
||||
switch (defaultExperience) {
|
||||
case DefaultAccountExperienceType.DocumentDB:
|
||||
rpResponse = await getSqlDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
|
||||
break;
|
||||
case DefaultAccountExperienceType.MongoDB:
|
||||
rpResponse = await getMongoDBDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Cassandra:
|
||||
rpResponse = await getCassandraKeyspaceThroughput(subscriptionId, resourceGroup, accountName, databaseId);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Graph:
|
||||
rpResponse = await getGremlinDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||
}
|
||||
|
||||
return rpResponse?.name;
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const getDatabaseOfferIdWithSDK = async (databaseResourceId: string): Promise<string> => {
|
||||
|
||||
@@ -12,16 +12,14 @@ import { sendNotificationForError } from "./sendNotificationForError";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export async function readDatabases(): Promise<DataModels.Database[]> {
|
||||
if (userContext.defaultExperience === DefaultAccountExperienceType.Table) {
|
||||
return [{ id: "TablesDB" } as DataModels.Database];
|
||||
}
|
||||
|
||||
let databases: DataModels.Database[];
|
||||
const clearMessage = logConsoleProgress(`Querying databases`);
|
||||
try {
|
||||
if (
|
||||
window.authType === AuthType.AAD &&
|
||||
!userContext.useSDKOperations &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Cassandra
|
||||
) {
|
||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
||||
databases = await readDatabasesWithARM();
|
||||
} else {
|
||||
const sdkResponse = await client()
|
||||
|
||||
@@ -41,6 +41,7 @@ export async function updateCollection(
|
||||
try {
|
||||
if (
|
||||
window.authType === AuthType.AAD &&
|
||||
!userContext.useSDKOperations &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||
) {
|
||||
@@ -52,16 +53,18 @@ export async function updateCollection(
|
||||
.replace(newCollection as ContainerDefinition, options);
|
||||
collection = sdkResponse.resource as Collection;
|
||||
}
|
||||
|
||||
logConsoleInfo(`Successfully updated container ${collectionId}`);
|
||||
await refreshCachedResources();
|
||||
return collection;
|
||||
} catch (error) {
|
||||
logConsoleError(`Failed to update container ${collectionId}: ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "UpdateCollection", error.code);
|
||||
sendNotificationForError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
logConsoleInfo(`Successfully updated container ${collectionId}`);
|
||||
clearMessage();
|
||||
await refreshCachedResources();
|
||||
return collection;
|
||||
}
|
||||
|
||||
async function updateCollectionWithARM(
|
||||
|
||||
421
src/Common/dataAccess/updateOffer.ts
Normal file
421
src/Common/dataAccess/updateOffer.ts
Normal file
@@ -0,0 +1,421 @@
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { Offer, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||
import { OfferDefinition } from "@azure/cosmos";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { ThroughputSettingsUpdateParameters } from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||
import { client } from "../CosmosClient";
|
||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { logError } from "../Logger";
|
||||
import { readCollectionOffer } from "./readCollectionOffer";
|
||||
import { readDatabaseOffer } from "./readDatabaseOffer";
|
||||
import { refreshCachedOffers, refreshCachedResources } from "../DataAccessUtilityBase";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
import {
|
||||
updateSqlDatabaseThroughput,
|
||||
migrateSqlDatabaseToAutoscale,
|
||||
migrateSqlDatabaseToManualThroughput,
|
||||
migrateSqlContainerToAutoscale,
|
||||
migrateSqlContainerToManualThroughput,
|
||||
updateSqlContainerThroughput
|
||||
} from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import {
|
||||
updateCassandraKeyspaceThroughput,
|
||||
migrateCassandraKeyspaceToAutoscale,
|
||||
migrateCassandraKeyspaceToManualThroughput,
|
||||
migrateCassandraTableToAutoscale,
|
||||
migrateCassandraTableToManualThroughput,
|
||||
updateCassandraTableThroughput
|
||||
} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
||||
import {
|
||||
updateMongoDBDatabaseThroughput,
|
||||
migrateMongoDBDatabaseToAutoscale,
|
||||
migrateMongoDBDatabaseToManualThroughput,
|
||||
migrateMongoDBCollectionToAutoscale,
|
||||
migrateMongoDBCollectionToManualThroughput,
|
||||
updateMongoDBCollectionThroughput
|
||||
} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||
import {
|
||||
updateGremlinDatabaseThroughput,
|
||||
migrateGremlinDatabaseToAutoscale,
|
||||
migrateGremlinDatabaseToManualThroughput,
|
||||
migrateGremlinGraphToAutoscale,
|
||||
migrateGremlinGraphToManualThroughput,
|
||||
updateGremlinGraphThroughput
|
||||
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||
import { userContext } from "../../UserContext";
|
||||
import {
|
||||
migrateTableToAutoscale,
|
||||
migrateTableToManualThroughput,
|
||||
updateTableThroughput
|
||||
} from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
|
||||
|
||||
export const updateOffer = async (params: UpdateOfferParams): Promise<Offer> => {
|
||||
let updatedOffer: Offer;
|
||||
const offerResourceText: string = params.collectionId
|
||||
? `collection ${params.collectionId}`
|
||||
: `database ${params.databaseId}`;
|
||||
const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`);
|
||||
|
||||
try {
|
||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
||||
if (params.collectionId) {
|
||||
updatedOffer = await updateCollectionOfferWithARM(params);
|
||||
} else if (userContext.defaultExperience === DefaultAccountExperienceType.Table) {
|
||||
// update table's database offer with SDK since RP doesn't support it
|
||||
updatedOffer = await updateOfferWithSDK(params);
|
||||
} else {
|
||||
updatedOffer = await updateDatabaseOfferWithARM(params);
|
||||
}
|
||||
} else {
|
||||
updatedOffer = await updateOfferWithSDK(params);
|
||||
}
|
||||
await refreshCachedOffers();
|
||||
await refreshCachedResources();
|
||||
logConsoleInfo(`Successfully updated offer for ${offerResourceText}`);
|
||||
return updatedOffer;
|
||||
} catch (error) {
|
||||
logConsoleError(`Error updating offer for ${offerResourceText}: ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "UpdateCollection", error.code);
|
||||
sendNotificationForError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
|
||||
const updateCollectionOfferWithARM = async (params: UpdateOfferParams): Promise<Offer> => {
|
||||
try {
|
||||
switch (userContext.defaultExperience) {
|
||||
case DefaultAccountExperienceType.DocumentDB:
|
||||
await updateSqlContainerOffer(params);
|
||||
break;
|
||||
case DefaultAccountExperienceType.MongoDB:
|
||||
await updateMongoCollectionOffer(params);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Cassandra:
|
||||
await updateCassandraTableOffer(params);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Graph:
|
||||
await updateGremlinGraphOffer(params);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Table:
|
||||
await updateTableOffer(params);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported default experience type: ${userContext.defaultExperience}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== "MethodNotAllowed") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return await readCollectionOffer({
|
||||
collectionId: params.collectionId,
|
||||
databaseId: params.databaseId,
|
||||
offerId: params.currentOffer.id
|
||||
});
|
||||
};
|
||||
|
||||
const updateDatabaseOfferWithARM = async (params: UpdateOfferParams): Promise<Offer> => {
|
||||
try {
|
||||
switch (userContext.defaultExperience) {
|
||||
case DefaultAccountExperienceType.DocumentDB:
|
||||
await updateSqlDatabaseOffer(params);
|
||||
break;
|
||||
case DefaultAccountExperienceType.MongoDB:
|
||||
await updateMongoDatabaseOffer(params);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Cassandra:
|
||||
await updateCassandraKeyspaceOffer(params);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Graph:
|
||||
await updateGremlinDatabaseOffer(params);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported default experience type: ${userContext.defaultExperience}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== "MethodNotAllowed") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return await readDatabaseOffer({
|
||||
databaseId: params.databaseId,
|
||||
offerId: params.currentOffer.id
|
||||
});
|
||||
};
|
||||
|
||||
const updateSqlContainerOffer = async (params: UpdateOfferParams): Promise<void> => {
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const accountName = userContext.databaseAccount.name;
|
||||
|
||||
if (params.migrateToAutoPilot) {
|
||||
await migrateSqlContainerToAutoscale(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
params.databaseId,
|
||||
params.collectionId
|
||||
);
|
||||
} else if (params.migrateToManual) {
|
||||
await migrateSqlContainerToManualThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
params.databaseId,
|
||||
params.collectionId
|
||||
);
|
||||
} else {
|
||||
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
|
||||
await updateSqlContainerThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
params.databaseId,
|
||||
params.collectionId,
|
||||
body
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const updateMongoCollectionOffer = async (params: UpdateOfferParams): Promise<void> => {
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const accountName = userContext.databaseAccount.name;
|
||||
|
||||
if (params.migrateToAutoPilot) {
|
||||
await migrateMongoDBCollectionToAutoscale(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
params.databaseId,
|
||||
params.collectionId
|
||||
);
|
||||
} else if (params.migrateToManual) {
|
||||
await migrateMongoDBCollectionToManualThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
params.databaseId,
|
||||
params.collectionId
|
||||
);
|
||||
} else {
|
||||
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
|
||||
await updateMongoDBCollectionThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
params.databaseId,
|
||||
params.collectionId,
|
||||
body
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const updateCassandraTableOffer = async (params: UpdateOfferParams): Promise<void> => {
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const accountName = userContext.databaseAccount.name;
|
||||
|
||||
if (params.migrateToAutoPilot) {
|
||||
await migrateCassandraTableToAutoscale(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
params.databaseId,
|
||||
params.collectionId
|
||||
);
|
||||
} else if (params.migrateToManual) {
|
||||
await migrateCassandraTableToManualThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
params.databaseId,
|
||||
params.collectionId
|
||||
);
|
||||
} else {
|
||||
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
|
||||
await updateCassandraTableThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
params.databaseId,
|
||||
params.collectionId,
|
||||
body
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const updateGremlinGraphOffer = async (params: UpdateOfferParams): Promise<void> => {
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const accountName = userContext.databaseAccount.name;
|
||||
|
||||
if (params.migrateToAutoPilot) {
|
||||
await migrateGremlinGraphToAutoscale(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
params.databaseId,
|
||||
params.collectionId
|
||||
);
|
||||
} else if (params.migrateToManual) {
|
||||
await migrateGremlinGraphToManualThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
params.databaseId,
|
||||
params.collectionId
|
||||
);
|
||||
} else {
|
||||
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
|
||||
await updateGremlinGraphThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
params.databaseId,
|
||||
params.collectionId,
|
||||
body
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const updateTableOffer = async (params: UpdateOfferParams): Promise<void> => {
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const accountName = userContext.databaseAccount.name;
|
||||
|
||||
if (params.migrateToAutoPilot) {
|
||||
await migrateTableToAutoscale(subscriptionId, resourceGroup, accountName, params.collectionId);
|
||||
} else if (params.migrateToManual) {
|
||||
await migrateTableToManualThroughput(subscriptionId, resourceGroup, accountName, params.collectionId);
|
||||
} else {
|
||||
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
|
||||
await updateTableThroughput(subscriptionId, resourceGroup, accountName, params.collectionId, body);
|
||||
}
|
||||
};
|
||||
|
||||
const updateSqlDatabaseOffer = async (params: UpdateOfferParams): Promise<void> => {
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const accountName = userContext.databaseAccount.name;
|
||||
|
||||
if (params.migrateToAutoPilot) {
|
||||
await migrateSqlDatabaseToAutoscale(subscriptionId, resourceGroup, accountName, params.databaseId);
|
||||
} else if (params.migrateToManual) {
|
||||
await migrateSqlDatabaseToManualThroughput(subscriptionId, resourceGroup, accountName, params.databaseId);
|
||||
} else {
|
||||
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
|
||||
await updateSqlDatabaseThroughput(subscriptionId, resourceGroup, accountName, params.databaseId, body);
|
||||
}
|
||||
};
|
||||
|
||||
const updateMongoDatabaseOffer = async (params: UpdateOfferParams): Promise<void> => {
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const accountName = userContext.databaseAccount.name;
|
||||
|
||||
if (params.migrateToAutoPilot) {
|
||||
await migrateMongoDBDatabaseToAutoscale(subscriptionId, resourceGroup, accountName, params.databaseId);
|
||||
} else if (params.migrateToManual) {
|
||||
await migrateMongoDBDatabaseToManualThroughput(subscriptionId, resourceGroup, accountName, params.databaseId);
|
||||
} else {
|
||||
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
|
||||
await updateMongoDBDatabaseThroughput(subscriptionId, resourceGroup, accountName, params.databaseId, body);
|
||||
}
|
||||
};
|
||||
|
||||
const updateCassandraKeyspaceOffer = async (params: UpdateOfferParams): Promise<void> => {
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const accountName = userContext.databaseAccount.name;
|
||||
|
||||
if (params.migrateToAutoPilot) {
|
||||
await migrateCassandraKeyspaceToAutoscale(subscriptionId, resourceGroup, accountName, params.databaseId);
|
||||
} else if (params.migrateToManual) {
|
||||
await migrateCassandraKeyspaceToManualThroughput(subscriptionId, resourceGroup, accountName, params.databaseId);
|
||||
} else {
|
||||
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
|
||||
await updateCassandraKeyspaceThroughput(subscriptionId, resourceGroup, accountName, params.databaseId, body);
|
||||
}
|
||||
};
|
||||
|
||||
const updateGremlinDatabaseOffer = async (params: UpdateOfferParams): Promise<void> => {
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const accountName = userContext.databaseAccount.name;
|
||||
|
||||
if (params.migrateToAutoPilot) {
|
||||
await migrateGremlinDatabaseToAutoscale(subscriptionId, resourceGroup, accountName, params.databaseId);
|
||||
} else if (params.migrateToManual) {
|
||||
await migrateGremlinDatabaseToManualThroughput(subscriptionId, resourceGroup, accountName, params.databaseId);
|
||||
} else {
|
||||
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
|
||||
await updateGremlinDatabaseThroughput(subscriptionId, resourceGroup, accountName, params.databaseId, body);
|
||||
}
|
||||
};
|
||||
|
||||
const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpdateParameters => {
|
||||
const body: ThroughputSettingsUpdateParameters = {
|
||||
properties: {
|
||||
resource: {}
|
||||
}
|
||||
};
|
||||
|
||||
if (params.autopilotThroughput) {
|
||||
body.properties.resource.autoscaleSettings = {
|
||||
maxThroughput: params.autopilotThroughput
|
||||
};
|
||||
} else {
|
||||
body.properties.resource.throughput = params.manualThroughput;
|
||||
}
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> => {
|
||||
const currentOffer = params.currentOffer;
|
||||
const newOffer: Offer = {
|
||||
content: {
|
||||
offerThroughput: undefined,
|
||||
offerIsRUPerMinuteThroughputEnabled: false
|
||||
},
|
||||
_etag: undefined,
|
||||
_ts: undefined,
|
||||
_rid: currentOffer._rid,
|
||||
_self: currentOffer._self,
|
||||
id: currentOffer.id,
|
||||
offerResourceId: currentOffer.offerResourceId,
|
||||
offerVersion: currentOffer.offerVersion,
|
||||
offerType: currentOffer.offerType,
|
||||
resource: currentOffer.resource
|
||||
};
|
||||
|
||||
if (params.autopilotThroughput) {
|
||||
newOffer.content.offerAutopilotSettings = {
|
||||
maxThroughput: params.autopilotThroughput
|
||||
};
|
||||
} else {
|
||||
newOffer.content.offerThroughput = params.manualThroughput;
|
||||
}
|
||||
|
||||
const options: RequestOptions = {};
|
||||
if (params.migrateToAutoPilot) {
|
||||
options.initialHeaders[HttpHeaders.migrateOfferToAutopilot] = "true";
|
||||
delete newOffer.content.offerAutopilotSettings;
|
||||
} else if (params.migrateToManual) {
|
||||
options.initialHeaders[HttpHeaders.migrateOfferToManualThroughput] = "true";
|
||||
newOffer.content.offerAutopilotSettings = { maxThroughput: 0 };
|
||||
}
|
||||
|
||||
const sdkResponse = await client()
|
||||
.offer(params.currentOffer.id)
|
||||
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
|
||||
.replace((newOffer as unknown) as OfferDefinition, options);
|
||||
return sdkResponse?.resource;
|
||||
};
|
||||
@@ -34,6 +34,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||
allowedParentFrameOrigins: [
|
||||
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*portal\\.microsoftazure.de$`,
|
||||
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
|
||||
`^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$`
|
||||
@@ -50,7 +51,8 @@ let configContext: Readonly<ConfigContext> = {
|
||||
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
|
||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
|
||||
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/settings/applications/1189306
|
||||
JUNO_ENDPOINT: "https://tools.cosmos.azure.com"
|
||||
JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com"
|
||||
};
|
||||
|
||||
export function resetConfigContext(): void {
|
||||
|
||||
@@ -303,6 +303,16 @@ export interface ReadCollectionOfferParams {
|
||||
offerId?: string;
|
||||
}
|
||||
|
||||
export interface UpdateOfferParams {
|
||||
currentOffer: Offer;
|
||||
databaseId: string;
|
||||
autopilotThroughput: number;
|
||||
manualThroughput: number;
|
||||
collectionId?: string;
|
||||
migrateToAutoPilot?: boolean;
|
||||
migrateToManual?: boolean;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
id: string;
|
||||
kind: string;
|
||||
|
||||
@@ -85,7 +85,7 @@ export interface Database extends TreeNode {
|
||||
collapseDatabase(): void;
|
||||
|
||||
loadCollections(): Promise<void>;
|
||||
findCollectionWithId(collectionRid: string): Collection;
|
||||
findCollectionWithId(collectionId: string): Collection;
|
||||
openAddCollection(database: Database, event: MouseEvent): void;
|
||||
onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void;
|
||||
onSettingsClick: () => void;
|
||||
@@ -266,7 +266,6 @@ export interface TabOptions {
|
||||
tabKind: CollectionTabKind;
|
||||
title: string;
|
||||
tabPath: string;
|
||||
selfLink: string;
|
||||
isActive: ko.Observable<boolean>;
|
||||
hashLocation: string;
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]) => void;
|
||||
@@ -305,7 +304,6 @@ export interface QueryTabOptions extends TabOptions {
|
||||
export interface ScriptTabOption extends TabOptions {
|
||||
resource: any;
|
||||
isNew: boolean;
|
||||
collectionSelfLink?: string;
|
||||
partitionKey?: DataModels.PartitionKey;
|
||||
}
|
||||
|
||||
@@ -388,6 +386,7 @@ export interface DataExplorerInputsFrame {
|
||||
dataExplorerVersion?: string;
|
||||
isAuthWithresourceToken?: boolean;
|
||||
defaultCollectionThroughput?: CollectionCreationDefaults;
|
||||
flights?: readonly string[];
|
||||
}
|
||||
|
||||
export interface CollectionCreationDefaults {
|
||||
|
||||
42
src/Definitions/jquery.contextmenu.d.ts
vendored
42
src/Definitions/jquery.contextmenu.d.ts
vendored
@@ -1,42 +0,0 @@
|
||||
// Type definitions for jQuery contextMenu 1.7.0
|
||||
// Project: http://medialize.github.com/jQuery-contextMenu/
|
||||
// Definitions by: Natan Vivo <https://github.com/nvivo/>
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
|
||||
/// <reference path="jquery.d.ts" />
|
||||
|
||||
interface JQueryContextMenuOptions {
|
||||
selector: string;
|
||||
appendTo?: string;
|
||||
trigger?: string;
|
||||
autoHide?: boolean;
|
||||
delay?: number;
|
||||
determinePosition?: (menu: JQuery) => void;
|
||||
position?: (opt: JQuery, x: number, y: number) => void;
|
||||
positionSubmenu?: (menu: JQuery) => void;
|
||||
zIndex?: number;
|
||||
animation?: {
|
||||
duration?: number;
|
||||
show?: string;
|
||||
hide?: string;
|
||||
};
|
||||
events?: {
|
||||
show?: () => void;
|
||||
hide?: () => void;
|
||||
};
|
||||
callback?: (key: any, options: any) => any;
|
||||
items?: any;
|
||||
build?: (triggerElement: JQuery, e: Event) => any;
|
||||
reposition?: boolean;
|
||||
className?: string;
|
||||
itemClickEvent?: string;
|
||||
}
|
||||
|
||||
interface JQueryStatic {
|
||||
contextMenu(options?: JQueryContextMenuOptions): JQuery;
|
||||
contextMenu(type: string, selector?: any): JQuery;
|
||||
}
|
||||
|
||||
interface JQuery {
|
||||
contextMenu(options?: any): JQuery;
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { Dialog, DialogType, DialogFooter, IDialogProps } from "office-ui-fabric
|
||||
import { IButtonProps, PrimaryButton, DefaultButton } from "office-ui-fabric-react/lib/Button";
|
||||
import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField";
|
||||
import { Link } from "office-ui-fabric-react/lib/Link";
|
||||
import { FontIcon } from "office-ui-fabric-react";
|
||||
import { ChoiceGroup, FontIcon, IChoiceGroupProps } from "office-ui-fabric-react";
|
||||
|
||||
export interface TextFieldProps extends ITextFieldProps {
|
||||
label: string;
|
||||
@@ -24,6 +24,7 @@ export interface DialogProps {
|
||||
subText: string;
|
||||
isModal: boolean;
|
||||
visible: boolean;
|
||||
choiceGroupProps?: IChoiceGroupProps;
|
||||
textFieldProps?: TextFieldProps;
|
||||
linkProps?: LinkProps;
|
||||
primaryButtonText: string;
|
||||
@@ -65,6 +66,7 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
|
||||
minWidth: DIALOG_MIN_WIDTH,
|
||||
maxWidth: DIALOG_MAX_WIDTH
|
||||
};
|
||||
const choiceGroupProps: IChoiceGroupProps = this.props.choiceGroupProps;
|
||||
const textFieldProps: ITextFieldProps = this.props.textFieldProps;
|
||||
const linkProps: LinkProps = this.props.linkProps;
|
||||
const primaryButtonProps: IButtonProps = {
|
||||
@@ -82,6 +84,7 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
|
||||
|
||||
return (
|
||||
<Dialog {...dialogProps}>
|
||||
{choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />}
|
||||
{textFieldProps && <TextField {...textFieldProps} />}
|
||||
{linkProps && (
|
||||
<Link href={linkProps.linkUrl} target="_blank">
|
||||
|
||||
@@ -55,7 +55,6 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
||||
label: "Enable Injecting Notebook Viewer Link into the first cell",
|
||||
value: "true"
|
||||
},
|
||||
{ key: "feature.enablesettingsv2", label: "Enable SettingsV2 Tab", value: "true" },
|
||||
{ key: "feature.canexceedmaximumvalue", label: "Can exceed max value", value: "true" },
|
||||
{
|
||||
key: "feature.enablefixedcollectionwithsharedthroughput",
|
||||
|
||||
@@ -178,12 +178,6 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
className="checkboxRow"
|
||||
horizontalAlign="space-between"
|
||||
>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enablesettingsv2"
|
||||
label="Enable SettingsV2 Tab"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.canexceedmaximumvalue"
|
||||
|
||||
@@ -227,7 +227,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
<Stack.Item styles={{ root: { minWidth: 200 } }}>
|
||||
<Dropdown options={this.sortingOptions} selectedKey={this.state.sortBy} onChange={this.onDropdownChange} />
|
||||
</Stack.Item>
|
||||
{this.props.container?.isGalleryPublishEnabled() && (
|
||||
{(!this.props.container || this.props.container.isGalleryPublishEnabled()) && (
|
||||
<Stack.Item>
|
||||
<InfoComponent />
|
||||
</Stack.Item>
|
||||
|
||||
@@ -3,10 +3,14 @@ import { Icon, Label, Stack, HoverCard, HoverCardType, Link } from "office-ui-fa
|
||||
import { CodeOfConductEndpoints } from "../../../../Common/Constants";
|
||||
import "./InfoComponent.less";
|
||||
|
||||
export class InfoComponent extends React.Component {
|
||||
private getInfoPanel = (iconName: string, labelText: string, url: string): JSX.Element => {
|
||||
export interface InfoComponentProps {
|
||||
onReportAbuseClick?: () => void;
|
||||
}
|
||||
|
||||
export class InfoComponent extends React.Component<InfoComponentProps> {
|
||||
private getInfoPanel = (iconName: string, labelText: string, url?: string, onClick?: () => void): JSX.Element => {
|
||||
return (
|
||||
<Link href={url} target="_blank">
|
||||
<Link href={url} target={url && "_blank"} onClick={onClick}>
|
||||
<div className="infoPanel">
|
||||
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} />
|
||||
<Label className="infoLabel">{labelText}</Label>
|
||||
@@ -25,6 +29,11 @@ export class InfoComponent extends React.Component {
|
||||
<Stack.Item>
|
||||
{this.getInfoPanel("KnowledgeArticle", "Microsoft Terms of Use", CodeOfConductEndpoints.termsOfUse)}
|
||||
</Stack.Item>
|
||||
{this.props.onReportAbuseClick !== undefined && (
|
||||
<Stack.Item>
|
||||
{this.getInfoPanel("ReportHacked", "Report Abuse", undefined, () => this.props.onReportAbuseClick())}
|
||||
</Stack.Item>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -77,6 +77,9 @@ exports[`GalleryViewerComponent renders 1`] = `
|
||||
selectedKey={0}
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<InfoComponent />
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
|
||||
@@ -25,7 +25,8 @@ describe("NotebookMetadataComponent", () => {
|
||||
onTagClick: undefined,
|
||||
onDownloadClick: undefined,
|
||||
onFavoriteClick: undefined,
|
||||
onUnfavoriteClick: undefined
|
||||
onUnfavoriteClick: undefined,
|
||||
onReportAbuseClick: undefined
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
|
||||
@@ -54,7 +55,8 @@ describe("NotebookMetadataComponent", () => {
|
||||
onTagClick: undefined,
|
||||
onDownloadClick: undefined,
|
||||
onFavoriteClick: undefined,
|
||||
onUnfavoriteClick: undefined
|
||||
onUnfavoriteClick: undefined,
|
||||
onReportAbuseClick: undefined
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
|
||||
|
||||
@@ -17,6 +17,7 @@ import { IGalleryItem } from "../../../Juno/JunoClient";
|
||||
import { FileSystemUtil } from "../../Notebook/FileSystemUtil";
|
||||
import "./NotebookViewerComponent.less";
|
||||
import CosmosDBLogo from "../../../../images/CosmosDB-logo.svg";
|
||||
import { InfoComponent } from "../NotebookGallery/InfoComponent/InfoComponent";
|
||||
|
||||
export interface NotebookMetadataComponentProps {
|
||||
data: IGalleryItem;
|
||||
@@ -26,6 +27,7 @@ export interface NotebookMetadataComponentProps {
|
||||
onFavoriteClick: () => void;
|
||||
onUnfavoriteClick: () => void;
|
||||
onDownloadClick: () => void;
|
||||
onReportAbuseClick: () => void;
|
||||
}
|
||||
|
||||
export class NotebookMetadataComponent extends React.Component<NotebookMetadataComponentProps> {
|
||||
@@ -41,24 +43,39 @@ export class NotebookMetadataComponent extends React.Component<NotebookMetadataC
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 30 }}>
|
||||
<Text variant="xxLarge" nowrap>
|
||||
{FileSystemUtil.stripExtension(this.props.data.name, "ipynb")}
|
||||
</Text>
|
||||
<Text>
|
||||
{this.props.isFavorite !== undefined && (
|
||||
<>
|
||||
<IconButton
|
||||
iconProps={{ iconName: this.props.isFavorite ? "HeartFill" : "Heart" }}
|
||||
onClick={this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick}
|
||||
/>
|
||||
{this.props.data.favorites} likes
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
<Stack.Item>
|
||||
<Text variant="xxLarge" nowrap>
|
||||
{FileSystemUtil.stripExtension(this.props.data.name, "ipynb")}
|
||||
</Text>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<Text>
|
||||
{this.props.isFavorite !== undefined && (
|
||||
<>
|
||||
<IconButton
|
||||
iconProps={{ iconName: this.props.isFavorite ? "HeartFill" : "Heart" }}
|
||||
onClick={this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick}
|
||||
/>
|
||||
{this.props.data.favorites} likes
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</Stack.Item>
|
||||
|
||||
{this.props.downloadButtonText && (
|
||||
<PrimaryButton text={this.props.downloadButtonText} onClick={this.props.onDownloadClick} />
|
||||
<Stack.Item>
|
||||
<PrimaryButton text={this.props.downloadButtonText} onClick={this.props.onDownloadClick} />
|
||||
</Stack.Item>
|
||||
)}
|
||||
|
||||
<Stack.Item grow>
|
||||
<></>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<InfoComponent onReportAbuseClick={this.props.onReportAbuseClick} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
|
||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 10 }}>
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
*/
|
||||
import { Notebook } from "@nteract/commutable";
|
||||
import { createContentRef } from "@nteract/core";
|
||||
import { Icon, Link, ProgressIndicator } from "office-ui-fabric-react";
|
||||
import { IChoiceGroupProps, Icon, Link, ProgressIndicator } from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { contents } from "rx-jupyter";
|
||||
import * as Logger from "../../../Common/Logger";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
|
||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
@@ -15,12 +14,13 @@ import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationCon
|
||||
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
|
||||
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
|
||||
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
|
||||
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
|
||||
import { DialogComponent, DialogProps, TextFieldProps } from "../DialogReactComponent/DialogComponent";
|
||||
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
|
||||
import "./NotebookViewerComponent.less";
|
||||
import Explorer from "../../Explorer";
|
||||
import { NotebookV4 } from "@nteract/commutable/lib/v4";
|
||||
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
|
||||
import { DialogHost } from "../../../Utils/GalleryUtils";
|
||||
|
||||
export interface NotebookViewerComponentProps {
|
||||
container?: Explorer;
|
||||
@@ -43,10 +43,8 @@ interface NotebookViewerComponentState {
|
||||
showProgressBar: boolean;
|
||||
}
|
||||
|
||||
export class NotebookViewerComponent extends React.Component<
|
||||
NotebookViewerComponentProps,
|
||||
NotebookViewerComponentState
|
||||
> {
|
||||
export class NotebookViewerComponent extends React.Component<NotebookViewerComponentProps, NotebookViewerComponentState>
|
||||
implements DialogHost {
|
||||
private clientManager: NotebookClientV2;
|
||||
private notebookComponentBootstrapper: NotebookComponentBootstrapper;
|
||||
|
||||
@@ -140,6 +138,7 @@ export class NotebookViewerComponent extends React.Component<
|
||||
onFavoriteClick={this.favoriteItem}
|
||||
onUnfavoriteClick={this.unfavoriteItem}
|
||||
onDownloadClick={this.downloadItem}
|
||||
onReportAbuseClick={this.state.galleryItem.isSample ? undefined : this.reportAbuse}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@@ -179,6 +178,39 @@ export class NotebookViewerComponent extends React.Component<
|
||||
};
|
||||
}
|
||||
|
||||
// DialogHost
|
||||
showOkCancelModalDialog(
|
||||
title: string,
|
||||
msg: string,
|
||||
okLabel: string,
|
||||
onOk: () => void,
|
||||
cancelLabel: string,
|
||||
onCancel: () => void,
|
||||
choiceGroupProps?: IChoiceGroupProps,
|
||||
textFieldProps?: TextFieldProps
|
||||
): void {
|
||||
this.setState({
|
||||
dialogProps: {
|
||||
isModal: true,
|
||||
visible: true,
|
||||
title,
|
||||
subText: msg,
|
||||
primaryButtonText: okLabel,
|
||||
secondaryButtonText: cancelLabel,
|
||||
onPrimaryButtonClick: () => {
|
||||
this.setState({ dialogProps: undefined });
|
||||
onOk && onOk();
|
||||
},
|
||||
onSecondaryButtonClick: () => {
|
||||
this.setState({ dialogProps: undefined });
|
||||
onCancel && onCancel();
|
||||
},
|
||||
choiceGroupProps,
|
||||
textFieldProps
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private favoriteItem = async (): Promise<void> => {
|
||||
GalleryUtils.favoriteItem(this.props.container, this.props.junoClient, this.state.galleryItem, item =>
|
||||
this.setState({ galleryItem: item, isFavorite: true })
|
||||
@@ -196,4 +228,8 @@ export class NotebookViewerComponent extends React.Component<
|
||||
this.setState({ galleryItem: item })
|
||||
);
|
||||
};
|
||||
|
||||
private reportAbuse = (): void => {
|
||||
GalleryUtils.reportAbuse(this.props.junoClient, this.state.galleryItem, this, () => {});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,26 +17,38 @@ exports[`NotebookMetadataComponent renders liked notebook 1`] = `
|
||||
}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Text
|
||||
nowrap={true}
|
||||
variant="xxLarge"
|
||||
>
|
||||
name
|
||||
</Text>
|
||||
<Text>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "HeartFill",
|
||||
<StackItem>
|
||||
<Text
|
||||
nowrap={true}
|
||||
variant="xxLarge"
|
||||
>
|
||||
name
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<Text>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "HeartFill",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
0
|
||||
likes
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<CustomizedPrimaryButton
|
||||
text="Download"
|
||||
/>
|
||||
0
|
||||
likes
|
||||
</Text>
|
||||
<CustomizedPrimaryButton
|
||||
text="Download"
|
||||
</StackItem>
|
||||
<StackItem
|
||||
grow={true}
|
||||
/>
|
||||
<StackItem>
|
||||
<InfoComponent />
|
||||
</StackItem>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
@@ -117,26 +129,38 @@ exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
|
||||
}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Text
|
||||
nowrap={true}
|
||||
variant="xxLarge"
|
||||
>
|
||||
name
|
||||
</Text>
|
||||
<Text>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Heart",
|
||||
<StackItem>
|
||||
<Text
|
||||
nowrap={true}
|
||||
variant="xxLarge"
|
||||
>
|
||||
name
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<Text>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Heart",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
0
|
||||
likes
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<CustomizedPrimaryButton
|
||||
text="Download"
|
||||
/>
|
||||
0
|
||||
likes
|
||||
</Text>
|
||||
<CustomizedPrimaryButton
|
||||
text="Download"
|
||||
</StackItem>
|
||||
<StackItem
|
||||
grow={true}
|
||||
/>
|
||||
<StackItem>
|
||||
<InfoComponent />
|
||||
</StackItem>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
|
||||
@@ -6,7 +6,7 @@ import SettingsTabV2 from "../../Tabs/SettingsTabV2";
|
||||
import { collection } from "./TestUtils";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import ko from "knockout";
|
||||
import { TtlType, isDirty, TtlOnNoDefault, TtlOn, TtlOff } from "./SettingsUtils";
|
||||
import { TtlType, isDirty } from "./SettingsUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
|
||||
jest.mock("../../../Common/dataAccess/updateCollection", () => ({
|
||||
@@ -20,8 +20,8 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
|
||||
geospatialConfig: undefined
|
||||
} as DataModels.Collection)
|
||||
}));
|
||||
import { updateOffer } from "../../../Common/DocumentClientUtilityBase";
|
||||
jest.mock("../../../Common/DocumentClientUtilityBase", () => ({
|
||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||
jest.mock("../../../Common/dataAccess/updateOffer", () => ({
|
||||
updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer)
|
||||
}));
|
||||
|
||||
@@ -33,7 +33,6 @@ describe("SettingsComponent", () => {
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
node: undefined,
|
||||
selfLink: undefined,
|
||||
hashLocation: "settings",
|
||||
isActive: ko.observable(false),
|
||||
onUpdateTabsButtons: undefined
|
||||
@@ -103,10 +102,7 @@ describe("SettingsComponent", () => {
|
||||
let settingsComponentInstance = new SettingsComponent(baseProps);
|
||||
expect(settingsComponentInstance.shouldShowKeyspaceSharedThroughputMessage()).toEqual(false);
|
||||
|
||||
const newContainer = new Explorer({
|
||||
notificationsClient: undefined,
|
||||
isEmulator: false
|
||||
});
|
||||
const newContainer = new Explorer();
|
||||
newContainer.isPreferredApiCassandra = ko.computed(() => true);
|
||||
|
||||
const newCollection = { ...collection };
|
||||
@@ -147,10 +143,7 @@ describe("SettingsComponent", () => {
|
||||
let settingsComponentInstance = new SettingsComponent(baseProps);
|
||||
expect(settingsComponentInstance.hasConflictResolution()).toEqual(undefined);
|
||||
|
||||
const newContainer = new Explorer({
|
||||
notificationsClient: undefined,
|
||||
isEmulator: false
|
||||
});
|
||||
const newContainer = new Explorer();
|
||||
newContainer.databaseAccount = ko.observable({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
@@ -220,13 +213,6 @@ describe("SettingsComponent", () => {
|
||||
expect(isDirty(state.throughput, state.throughputBaseline)).toEqual(false);
|
||||
});
|
||||
|
||||
it("getTtlValue", async () => {
|
||||
const settingsComponentInstance = new SettingsComponent(baseProps);
|
||||
expect(settingsComponentInstance.getTtlValue(TtlType.OnNoDefault)).toEqual(TtlOnNoDefault);
|
||||
expect(settingsComponentInstance.getTtlValue(TtlType.On)).toEqual(TtlOn);
|
||||
expect(settingsComponentInstance.getTtlValue(TtlType.Off)).toEqual(TtlOff);
|
||||
});
|
||||
|
||||
it("getAnalyticalStorageTtl", () => {
|
||||
const newCollection = { ...collection };
|
||||
newCollection.analyticalStorageTtl = ko.observable(10);
|
||||
|
||||
@@ -6,11 +6,11 @@ import * as SharedConstants from "../../../Shared/Constants";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import DiscardIcon from "../../../../images/discard.svg";
|
||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||
import { traceStart, traceFailure, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { traceStart, traceFailure, traceSuccess, trace } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import Explorer from "../../Explorer";
|
||||
import { updateOffer } from "../../../Common/DocumentClientUtilityBase";
|
||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import { userContext } from "../../../UserContext";
|
||||
@@ -27,9 +27,6 @@ import {
|
||||
SettingsV2TabTypes,
|
||||
getTabTitle,
|
||||
isDirty,
|
||||
TtlOff,
|
||||
TtlOn,
|
||||
TtlOnNoDefault,
|
||||
parseConflictResolutionMode,
|
||||
parseConflictResolutionProcedure
|
||||
} from "./SettingsUtils";
|
||||
@@ -38,7 +35,7 @@ import {
|
||||
ConflictResolutionComponentProps
|
||||
} from "./SettingsSubComponents/ConflictResolutionComponent";
|
||||
import { SubSettingsComponent, SubSettingsComponentProps } from "./SettingsSubComponents/SubSettingsComponent";
|
||||
import { Pivot, PivotItem, IPivotProps, IPivotItemProps, IChoiceGroupOption } from "office-ui-fabric-react";
|
||||
import { Pivot, PivotItem, IPivotProps, IPivotItemProps } from "office-ui-fabric-react";
|
||||
import "./SettingsComponent.less";
|
||||
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
|
||||
|
||||
@@ -85,7 +82,6 @@ export interface SettingsComponentState {
|
||||
indexingPolicyContent: DataModels.IndexingPolicy;
|
||||
indexingPolicyContentBaseline: DataModels.IndexingPolicy;
|
||||
shouldDiscardIndexingPolicy: boolean;
|
||||
indexingPolicyElementFocussed: boolean;
|
||||
isIndexingPolicyDirty: boolean;
|
||||
|
||||
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode;
|
||||
@@ -102,7 +98,6 @@ export interface SettingsComponentState {
|
||||
|
||||
export class SettingsComponent extends React.Component<SettingsComponentProps, SettingsComponentState> {
|
||||
private static readonly sixMonthsInSeconds = 15768000;
|
||||
private static readonly zeroSeconds = 0;
|
||||
|
||||
public saveSettingsButton: ButtonV2;
|
||||
public discardSettingsChangesButton: ButtonV2;
|
||||
@@ -127,8 +122,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.changeFeedPolicyVisible = this.collection?.container.isFeatureEnabled(
|
||||
Constants.Features.enableChangeFeedPolicy
|
||||
);
|
||||
// Mongo container with system partition key still treat as "Fixed"
|
||||
|
||||
// Mongo container with system partition key still treat as "Fixed"
|
||||
this.isFixedContainer =
|
||||
!this.collection.partitionKey ||
|
||||
(this.container.isPreferredApiMongoDB() && this.collection.partitionKey.systemKey);
|
||||
@@ -160,7 +155,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
|
||||
indexingPolicyContent: undefined,
|
||||
indexingPolicyContentBaseline: undefined,
|
||||
indexingPolicyElementFocussed: false,
|
||||
shouldDiscardIndexingPolicy: false,
|
||||
isIndexingPolicyDirty: false,
|
||||
|
||||
@@ -270,7 +264,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.props.settingsTab.isExecutionError(false);
|
||||
|
||||
this.props.settingsTab.isExecuting(true);
|
||||
const startKey: number = traceStart(Action.UpdateSettings, {
|
||||
const startKey: number = traceStart(Action.SettingsV2Updated, {
|
||||
databaseAccountName: this.container.databaseAccount()?.name,
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
@@ -411,7 +405,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
||||
} catch (error) {
|
||||
traceFailure(
|
||||
Action.UpdateSettings,
|
||||
Action.SettingsV2Updated,
|
||||
{
|
||||
databaseAccountName: this.container.databaseAccount().name,
|
||||
databaseName: this.collection && this.collection.databaseId,
|
||||
@@ -426,7 +420,21 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
const updatedOffer: DataModels.Offer = await updateOffer(this.collection.offer(), newOffer, headerOptions);
|
||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||
databaseId: this.collection.databaseId,
|
||||
collectionId: this.collection.id(),
|
||||
currentOffer: this.collection.offer(),
|
||||
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
||||
manualThroughput: this.state.isAutoPilotSelected ? undefined : newThroughput
|
||||
};
|
||||
if (this.hasProvisioningTypeChanged()) {
|
||||
if (this.state.isAutoPilotSelected) {
|
||||
updateOfferParams.migrateToAutoPilot = true;
|
||||
} else {
|
||||
updateOfferParams.migrateToManual = true;
|
||||
}
|
||||
}
|
||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||
this.collection.offer(updatedOffer);
|
||||
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
||||
if (this.state.isAutoPilotSelected) {
|
||||
@@ -446,7 +454,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.setBaseline();
|
||||
this.setState({ wasAutopilotOriginallySet: this.state.isAutoPilotSelected });
|
||||
traceSuccess(
|
||||
Action.UpdateSettings,
|
||||
Action.SettingsV2Updated,
|
||||
{
|
||||
databaseAccountName: this.container.databaseAccount()?.name,
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
@@ -460,7 +468,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.props.settingsTab.isExecutionError(true);
|
||||
console.error(reason);
|
||||
traceFailure(
|
||||
Action.UpdateSettings,
|
||||
Action.SettingsV2Updated,
|
||||
{
|
||||
databaseAccountName: this.container.databaseAccount()?.name,
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
@@ -474,6 +482,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
};
|
||||
|
||||
public onRevertClick = (): void => {
|
||||
trace(Action.SettingsV2Discarded, ActionModifiers.Mark, {
|
||||
message: "Settings Discarded"
|
||||
});
|
||||
|
||||
this.setState({
|
||||
throughput: this.state.throughputBaseline,
|
||||
timeToLive: this.state.timeToLiveBaseline,
|
||||
@@ -504,9 +516,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private onScaleDiscardableChange = (isScaleDiscardable: boolean): void =>
|
||||
this.setState({ isScaleDiscardable: isScaleDiscardable });
|
||||
|
||||
private onIndexingPolicyElementFocusChange = (indexingPolicyElementFocussed: boolean): void =>
|
||||
this.setState({ indexingPolicyElementFocussed: indexingPolicyElementFocussed });
|
||||
|
||||
private onIndexingPolicyContentChange = (newIndexingPolicy: DataModels.IndexingPolicy): void =>
|
||||
this.setState({ indexingPolicyContent: newIndexingPolicy });
|
||||
|
||||
@@ -530,79 +539,34 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
}
|
||||
};
|
||||
|
||||
private onConflictResolutionPolicyModeChange = (
|
||||
event?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
): void =>
|
||||
this.setState({
|
||||
conflictResolutionPolicyMode:
|
||||
DataModels.ConflictResolutionMode[option.key as keyof typeof DataModels.ConflictResolutionMode]
|
||||
});
|
||||
private onConflictResolutionPolicyModeChange = (newMode: DataModels.ConflictResolutionMode): void =>
|
||||
this.setState({ conflictResolutionPolicyMode: newMode });
|
||||
|
||||
private onConflictResolutionPolicyPathChange = (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => this.setState({ conflictResolutionPolicyPath: newValue });
|
||||
private onConflictResolutionPolicyPathChange = (newPath: string): void =>
|
||||
this.setState({ conflictResolutionPolicyPath: newPath });
|
||||
|
||||
private onConflictResolutionPolicyProcedureChange = (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => this.setState({ conflictResolutionPolicyProcedure: newValue });
|
||||
private onConflictResolutionPolicyProcedureChange = (newProcedure: string): void =>
|
||||
this.setState({ conflictResolutionPolicyProcedure: newProcedure });
|
||||
|
||||
private onConflictResolutionDirtyChange = (isConflictResolutionDirty: boolean): void =>
|
||||
this.setState({ isConflictResolutionDirty: isConflictResolutionDirty });
|
||||
|
||||
public getTtlValue = (value: string): TtlType => {
|
||||
switch (value) {
|
||||
case TtlOn:
|
||||
return TtlType.On;
|
||||
case TtlOff:
|
||||
return TtlType.Off;
|
||||
case TtlOnNoDefault:
|
||||
return TtlType.OnNoDefault;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
private onTtlChange = (newTtl: TtlType): void => this.setState({ timeToLive: newTtl });
|
||||
|
||||
private onTtlChange = (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, option?: IChoiceGroupOption): void =>
|
||||
this.setState({ timeToLive: this.getTtlValue(option.key) });
|
||||
|
||||
private onTimeToLiveSecondsChange = (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => {
|
||||
let newTimeToLiveSeconds = parseInt(newValue);
|
||||
newTimeToLiveSeconds = isNaN(newTimeToLiveSeconds) ? SettingsComponent.zeroSeconds : newTimeToLiveSeconds;
|
||||
private onTimeToLiveSecondsChange = (newTimeToLiveSeconds: number): void =>
|
||||
this.setState({ timeToLiveSeconds: newTimeToLiveSeconds });
|
||||
};
|
||||
|
||||
private onGeoSpatialConfigTypeChange = (
|
||||
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
): void =>
|
||||
this.setState({ geospatialConfigType: GeospatialConfigType[option.key as keyof typeof GeospatialConfigType] });
|
||||
private onGeoSpatialConfigTypeChange = (newGeoSpatialConfigType: GeospatialConfigType): void =>
|
||||
this.setState({ geospatialConfigType: newGeoSpatialConfigType });
|
||||
|
||||
private onAnalyticalStorageTtlSelectionChange = (
|
||||
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
): void => this.setState({ analyticalStorageTtlSelection: this.getTtlValue(option.key) });
|
||||
private onAnalyticalStorageTtlSelectionChange = (newAnalyticalStorageTtlSelection: TtlType): void =>
|
||||
this.setState({ analyticalStorageTtlSelection: newAnalyticalStorageTtlSelection });
|
||||
|
||||
private onAnalyticalStorageTtlSecondsChange = (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => {
|
||||
let newAnalyticalStorageTtlSeconds = parseInt(newValue);
|
||||
newAnalyticalStorageTtlSeconds = isNaN(newAnalyticalStorageTtlSeconds)
|
||||
? SettingsComponent.zeroSeconds
|
||||
: newAnalyticalStorageTtlSeconds;
|
||||
private onAnalyticalStorageTtlSecondsChange = (newAnalyticalStorageTtlSeconds: number): void =>
|
||||
this.setState({ analyticalStorageTtlSeconds: newAnalyticalStorageTtlSeconds });
|
||||
};
|
||||
|
||||
private onChangeFeedPolicyChange = (
|
||||
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
): void =>
|
||||
this.setState({ changeFeedPolicy: ChangeFeedPolicyState[option.key as keyof typeof ChangeFeedPolicyState] });
|
||||
private onChangeFeedPolicyChange = (newChangeFeedPolicy: ChangeFeedPolicyState): void =>
|
||||
this.setState({ changeFeedPolicy: newChangeFeedPolicy });
|
||||
|
||||
private onSubSettingsSaveableChange = (isSubSettingsSaveable: boolean): void =>
|
||||
this.setState({ isSubSettingsSaveable: isSubSettingsSaveable });
|
||||
@@ -830,7 +794,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
resetShouldDiscardIndexingPolicy: this.resetShouldDiscardIndexingPolicy,
|
||||
indexingPolicyContent: this.state.indexingPolicyContent,
|
||||
indexingPolicyContentBaseline: this.state.indexingPolicyContentBaseline,
|
||||
onIndexingPolicyElementFocusChange: this.onIndexingPolicyElementFocusChange,
|
||||
onIndexingPolicyContentChange: this.onIndexingPolicyContentChange,
|
||||
logIndexingPolicySuccessMessage: this.logIndexingPolicySuccessMessage,
|
||||
onIndexingPolicyDirtyChange: this.onIndexingPolicyDirtyChange
|
||||
|
||||
@@ -18,24 +18,15 @@ export interface ConflictResolutionComponentProps {
|
||||
container: Explorer;
|
||||
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode;
|
||||
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode;
|
||||
onConflictResolutionPolicyModeChange: (
|
||||
event?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
) => void;
|
||||
onConflictResolutionPolicyModeChange: (newMode: DataModels.ConflictResolutionMode) => void;
|
||||
conflictResolutionPolicyPath: string;
|
||||
conflictResolutionPolicyPathBaseline: string;
|
||||
|
||||
onConflictResolutionPolicyPathChange: (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
) => void;
|
||||
onConflictResolutionPolicyPathChange: (newPath: string) => void;
|
||||
conflictResolutionPolicyProcedure: string;
|
||||
conflictResolutionPolicyProcedureBaseline: string;
|
||||
|
||||
onConflictResolutionPolicyProcedureChange: (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
) => void;
|
||||
onConflictResolutionPolicyProcedureChange: (newProcedure: string) => void;
|
||||
onConflictResolutionDirtyChange: (isConflictResolutionDirty: boolean) => void;
|
||||
}
|
||||
|
||||
@@ -77,12 +68,30 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
|
||||
return false;
|
||||
};
|
||||
|
||||
private onConflictResolutionPolicyModeChange = (
|
||||
event?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
): void =>
|
||||
this.props.onConflictResolutionPolicyModeChange(
|
||||
DataModels.ConflictResolutionMode[option.key as keyof typeof DataModels.ConflictResolutionMode]
|
||||
);
|
||||
|
||||
private onConflictResolutionPolicyPathChange = (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => this.props.onConflictResolutionPolicyPathChange(newValue);
|
||||
|
||||
private onConflictResolutionPolicyProcedureChange = (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => this.props.onConflictResolutionPolicyProcedureChange(newValue);
|
||||
|
||||
private getConflictResolutionModeComponent = (): JSX.Element => (
|
||||
<ChoiceGroup
|
||||
label="Mode"
|
||||
selectedKey={this.props.conflictResolutionPolicyMode}
|
||||
options={this.conflictResolutionChoiceGroupOptions}
|
||||
onChange={this.props.onConflictResolutionPolicyModeChange}
|
||||
onChange={this.onConflictResolutionPolicyModeChange}
|
||||
styles={getChoiceGroupStyles(
|
||||
this.props.conflictResolutionPolicyMode,
|
||||
this.props.conflictResolutionPolicyModeBaseline
|
||||
@@ -104,7 +113,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
|
||||
this.props.conflictResolutionPolicyPathBaseline
|
||||
)}
|
||||
value={this.props.conflictResolutionPolicyPath}
|
||||
onChange={this.props.onConflictResolutionPolicyPathChange}
|
||||
onChange={this.onConflictResolutionPolicyPathChange}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -122,7 +131,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
|
||||
this.props.conflictResolutionPolicyProcedureBaseline
|
||||
)}
|
||||
value={this.props.conflictResolutionPolicyProcedure}
|
||||
onChange={this.props.onConflictResolutionPolicyProcedureChange}
|
||||
onChange={this.onConflictResolutionPolicyProcedureChange}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -17,9 +17,6 @@ describe("IndexingPolicyComponent", () => {
|
||||
},
|
||||
indexingPolicyContent: initialIndexingPolicyContent,
|
||||
indexingPolicyContentBaseline: initialIndexingPolicyContent,
|
||||
onIndexingPolicyElementFocusChange: () => {
|
||||
return;
|
||||
},
|
||||
onIndexingPolicyContentChange: () => {
|
||||
return;
|
||||
},
|
||||
|
||||
@@ -10,7 +10,6 @@ export interface IndexingPolicyComponentProps {
|
||||
resetShouldDiscardIndexingPolicy: () => void;
|
||||
indexingPolicyContent: DataModels.IndexingPolicy;
|
||||
indexingPolicyContentBaseline: DataModels.IndexingPolicy;
|
||||
onIndexingPolicyElementFocusChange: (indexingPolicyContentFocussed: boolean) => void;
|
||||
onIndexingPolicyContentChange: (newIndexingPolicy: DataModels.IndexingPolicy) => void;
|
||||
logIndexingPolicySuccessMessage: () => void;
|
||||
onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void;
|
||||
@@ -89,8 +88,6 @@ export class IndexingPolicyComponent extends React.Component<
|
||||
ariaLabel: "Indexing Policy"
|
||||
});
|
||||
if (this.indexingPolicyEditor) {
|
||||
this.indexingPolicyEditor.onDidFocusEditorText(() => this.props.onIndexingPolicyElementFocusChange(true));
|
||||
this.indexingPolicyEditor.onDidBlurEditorText(() => this.props.onIndexingPolicyElementFocusChange(false));
|
||||
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
|
||||
indexingPolicyEditorModel.onDidChangeContent(this.onEditorContentChange.bind(this));
|
||||
this.props.logIndexingPolicySuccessMessage();
|
||||
|
||||
@@ -12,10 +12,7 @@ import * as SharedConstants from "../../../../Shared/Constants";
|
||||
import ko from "knockout";
|
||||
|
||||
describe("ScaleComponent", () => {
|
||||
const nonNationalCloudContainer = new Explorer({
|
||||
notificationsClient: undefined,
|
||||
isEmulator: false
|
||||
});
|
||||
const nonNationalCloudContainer = new Explorer();
|
||||
nonNationalCloudContainer.getPlatformType = () => PlatformType.Portal;
|
||||
nonNationalCloudContainer.isRunningOnNationalCloud = () => false;
|
||||
|
||||
@@ -88,10 +85,7 @@ describe("ScaleComponent", () => {
|
||||
});
|
||||
|
||||
it("autoScale enabled", () => {
|
||||
const newContainer = new Explorer({
|
||||
notificationsClient: undefined,
|
||||
isEmulator: false
|
||||
});
|
||||
const newContainer = new Explorer();
|
||||
|
||||
newContainer.databaseAccount({
|
||||
id: undefined,
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { getMaxRUs, getMinRUs, hasDatabaseSharedThroughput } from "../SettingsUtils";
|
||||
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
|
||||
import { Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
|
||||
import { configContext, Platform } from "../../../../ConfigContext";
|
||||
|
||||
export interface ScaleComponentProps {
|
||||
collection: ViewModels.Collection;
|
||||
@@ -43,7 +44,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
private isEmulator: boolean;
|
||||
constructor(props: ScaleComponentProps) {
|
||||
super(props);
|
||||
this.isEmulator = this.props.container.isEmulator;
|
||||
this.isEmulator = configContext.platform === Platform.Emulator;
|
||||
}
|
||||
|
||||
public isAutoScaleEnabled = (): boolean => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { SubSettingsComponent, SubSettingsComponentProps } from "./SubSettingsComponent";
|
||||
import { container, collection } from "../TestUtils";
|
||||
import { TtlType, GeospatialConfigType, ChangeFeedPolicyState } from "../SettingsUtils";
|
||||
import { TtlType, GeospatialConfigType, ChangeFeedPolicyState, TtlOnNoDefault, TtlOn, TtlOff } from "../SettingsUtils";
|
||||
import ko from "knockout";
|
||||
import Explorer from "../../../Explorer";
|
||||
|
||||
@@ -105,10 +105,7 @@ describe("SubSettingsComponent", () => {
|
||||
});
|
||||
|
||||
it("partitionKey not visible", () => {
|
||||
const newContainer = new Explorer({
|
||||
notificationsClient: undefined,
|
||||
isEmulator: false
|
||||
});
|
||||
const newContainer = new Explorer();
|
||||
|
||||
newContainer.isPreferredApiCassandra = ko.computed(() => true);
|
||||
const props = { ...baseProps, container: newContainer };
|
||||
@@ -133,4 +130,11 @@ describe("SubSettingsComponent", () => {
|
||||
expect(isComponentDirtyResult.isSaveable).toEqual(true);
|
||||
expect(isComponentDirtyResult.isDiscardable).toEqual(true);
|
||||
});
|
||||
|
||||
it("getTtlValue", async () => {
|
||||
const subSettingsComponentInstance = new SubSettingsComponent(baseProps);
|
||||
expect(subSettingsComponentInstance.getTtlValue(TtlType.OnNoDefault)).toEqual(TtlOnNoDefault);
|
||||
expect(subSettingsComponentInstance.getTtlValue(TtlType.On)).toEqual(TtlOn);
|
||||
expect(subSettingsComponentInstance.getTtlValue(TtlType.Off)).toEqual(TtlOff);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,11 @@ import {
|
||||
TtlType,
|
||||
ChangeFeedPolicyState,
|
||||
isDirty,
|
||||
IsComponentDirtyResult
|
||||
IsComponentDirtyResult,
|
||||
TtlOn,
|
||||
TtlOff,
|
||||
TtlOnNoDefault,
|
||||
getSanitizedInputValue
|
||||
} from "../SettingsUtils";
|
||||
import Explorer from "../../../Explorer";
|
||||
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||
@@ -37,40 +41,28 @@ export interface SubSettingsComponentProps {
|
||||
timeToLive: TtlType;
|
||||
timeToLiveBaseline: TtlType;
|
||||
|
||||
onTtlChange: (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, option?: IChoiceGroupOption) => void;
|
||||
onTtlChange: (newTtl: TtlType) => void;
|
||||
timeToLiveSeconds: number;
|
||||
timeToLiveSecondsBaseline: number;
|
||||
onTimeToLiveSecondsChange: (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
) => void;
|
||||
onTimeToLiveSecondsChange: (newTimeToLiveSeconds: number) => void;
|
||||
|
||||
geospatialConfigType: GeospatialConfigType;
|
||||
geospatialConfigTypeBaseline: GeospatialConfigType;
|
||||
onGeoSpatialConfigTypeChange: (
|
||||
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
) => void;
|
||||
onGeoSpatialConfigTypeChange: (newGeoSpatialConfigType: GeospatialConfigType) => void;
|
||||
|
||||
isAnalyticalStorageEnabled: boolean;
|
||||
analyticalStorageTtlSelection: TtlType;
|
||||
analyticalStorageTtlSelectionBaseline: TtlType;
|
||||
onAnalyticalStorageTtlSelectionChange: (
|
||||
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
) => void;
|
||||
onAnalyticalStorageTtlSelectionChange: (newAnalyticalStorageTtlSelection: TtlType) => void;
|
||||
|
||||
analyticalStorageTtlSeconds: number;
|
||||
analyticalStorageTtlSecondsBaseline: number;
|
||||
onAnalyticalStorageTtlSecondsChange: (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
) => void;
|
||||
onAnalyticalStorageTtlSecondsChange: (newAnalyticalStorageTtlSeconds: number) => void;
|
||||
|
||||
changeFeedPolicyVisible: boolean;
|
||||
changeFeedPolicy: ChangeFeedPolicyState;
|
||||
changeFeedPolicyBaseline: ChangeFeedPolicyState;
|
||||
onChangeFeedPolicyChange: (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, option?: IChoiceGroupOption) => void;
|
||||
onChangeFeedPolicyChange: (newChangeFeedPolicyState: ChangeFeedPolicyState) => void;
|
||||
onSubSettingsSaveableChange: (isSubSettingsSaveable: boolean) => void;
|
||||
onSubSettingsDiscardableChange: (isSubSettingsDiscardable: boolean) => void;
|
||||
}
|
||||
@@ -139,6 +131,54 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
{ key: TtlType.On, text: "On" }
|
||||
];
|
||||
|
||||
public getTtlValue = (value: string): TtlType => {
|
||||
switch (value) {
|
||||
case TtlOn:
|
||||
return TtlType.On;
|
||||
case TtlOff:
|
||||
return TtlType.Off;
|
||||
case TtlOnNoDefault:
|
||||
return TtlType.OnNoDefault;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
private onTtlChange = (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, option?: IChoiceGroupOption): void =>
|
||||
this.props.onTtlChange(this.getTtlValue(option.key));
|
||||
|
||||
private onTimeToLiveSecondsChange = (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => {
|
||||
const newTimeToLiveSeconds = getSanitizedInputValue(newValue, Int32.Max);
|
||||
this.props.onTimeToLiveSecondsChange(newTimeToLiveSeconds);
|
||||
};
|
||||
|
||||
private onGeoSpatialConfigTypeChange = (
|
||||
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
): void =>
|
||||
this.props.onGeoSpatialConfigTypeChange(GeospatialConfigType[option.key as keyof typeof GeospatialConfigType]);
|
||||
|
||||
private onAnalyticalStorageTtlSelectionChange = (
|
||||
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
): void => this.props.onAnalyticalStorageTtlSelectionChange(this.getTtlValue(option.key));
|
||||
|
||||
private onAnalyticalStorageTtlSecondsChange = (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => {
|
||||
const newAnalyticalStorageTtlSeconds = getSanitizedInputValue(newValue, Int32.Max);
|
||||
this.props.onAnalyticalStorageTtlSecondsChange(newAnalyticalStorageTtlSeconds);
|
||||
};
|
||||
|
||||
private onChangeFeedPolicyChange = (
|
||||
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
): void =>
|
||||
this.props.onChangeFeedPolicyChange(ChangeFeedPolicyState[option.key as keyof typeof ChangeFeedPolicyState]);
|
||||
|
||||
private getTtlComponent = (): JSX.Element => (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<ChoiceGroup
|
||||
@@ -146,7 +186,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
label="Time to Live"
|
||||
selectedKey={this.props.timeToLive}
|
||||
options={this.ttlChoiceGroupOptions}
|
||||
onChange={this.props.onTtlChange}
|
||||
onChange={this.onTtlChange}
|
||||
styles={getChoiceGroupStyles(this.props.timeToLive, this.props.timeToLiveBaseline)}
|
||||
/>
|
||||
{isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && (
|
||||
@@ -163,7 +203,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
min={1}
|
||||
max={Int32.Max}
|
||||
value={this.props.timeToLiveSeconds?.toString()}
|
||||
onChange={this.props.onTimeToLiveSecondsChange}
|
||||
onChange={this.onTimeToLiveSecondsChange}
|
||||
suffix="second(s)"
|
||||
/>
|
||||
)}
|
||||
@@ -183,7 +223,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
label="Analytical Storage Time to Live"
|
||||
selectedKey={this.props.analyticalStorageTtlSelection}
|
||||
options={this.analyticalTtlChoiceGroupOptions}
|
||||
onChange={this.props.onAnalyticalStorageTtlSelectionChange}
|
||||
onChange={this.onAnalyticalStorageTtlSelectionChange}
|
||||
styles={getChoiceGroupStyles(
|
||||
this.props.analyticalStorageTtlSelection,
|
||||
this.props.analyticalStorageTtlSelectionBaseline
|
||||
@@ -202,7 +242,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
max={Int32.Max}
|
||||
value={this.props.analyticalStorageTtlSeconds?.toString()}
|
||||
suffix="second(s)"
|
||||
onChange={this.props.onAnalyticalStorageTtlSecondsChange}
|
||||
onChange={this.onAnalyticalStorageTtlSecondsChange}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -219,7 +259,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
label="Geospatial Configuration"
|
||||
selectedKey={this.props.geospatialConfigType}
|
||||
options={this.geoSpatialConfigTypeChoiceGroupOptions}
|
||||
onChange={this.props.onGeoSpatialConfigTypeChange}
|
||||
onChange={this.onGeoSpatialConfigTypeChange}
|
||||
styles={getChoiceGroupStyles(this.props.geospatialConfigType, this.props.geospatialConfigTypeBaseline)}
|
||||
/>
|
||||
);
|
||||
@@ -241,7 +281,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
id="changeFeedPolicy"
|
||||
selectedKey={this.props.changeFeedPolicy}
|
||||
options={this.changeFeedChoiceGroupOptions}
|
||||
onChange={this.props.onChangeFeedPolicyChange}
|
||||
onChange={this.onChangeFeedPolicyChange}
|
||||
styles={getChoiceGroupStyles(this.props.changeFeedPolicy, this.props.changeFeedPolicyBaseline)}
|
||||
aria-labelledby={labelId}
|
||||
/>
|
||||
|
||||
@@ -26,9 +26,10 @@ import {
|
||||
MessageBarType
|
||||
} from "office-ui-fabric-react";
|
||||
import { ToolTipLabelComponent } from "../ToolTipLabelComponent";
|
||||
import { IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
|
||||
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
|
||||
import * as SharedConstants from "../../../../../Shared/Constants";
|
||||
import * as DataModels from "../../../../../Contracts/DataModels";
|
||||
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||
|
||||
export interface ThroughputInputAutoPilotV3Props {
|
||||
databaseAccount: DataModels.DatabaseAccount;
|
||||
@@ -71,9 +72,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
> {
|
||||
private shouldCheckComponentIsDirty = true;
|
||||
private static readonly defaultStep = 100;
|
||||
private static readonly zeroThroughput = 0;
|
||||
private step: number;
|
||||
private choiceGroupFixedStyle = getChoiceGroupStyles(undefined, undefined);
|
||||
private throughputInputMaxValue: number;
|
||||
private autoPilotInputMaxValue: number;
|
||||
private options: IChoiceGroupOption[] = [
|
||||
{ key: "true", text: "Autoscale" },
|
||||
{ key: "false", text: "Manual" }
|
||||
@@ -140,6 +141,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
};
|
||||
|
||||
this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep;
|
||||
this.throughputInputMaxValue = this.props.canExceedMaximumValue ? Int32.Max : this.props.maximum;
|
||||
this.autoPilotInputMaxValue = Int32.Max;
|
||||
}
|
||||
|
||||
public hasProvisioningTypeChanged = (): boolean =>
|
||||
@@ -200,8 +203,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => {
|
||||
let newThroughput = parseInt(newValue);
|
||||
newThroughput = isNaN(newThroughput) ? ThroughputInputAutoPilotV3Component.zeroThroughput : newThroughput;
|
||||
const newThroughput = getSanitizedInputValue(newValue, this.autoPilotInputMaxValue);
|
||||
this.props.onMaxAutoPilotThroughputChange(newThroughput);
|
||||
};
|
||||
|
||||
@@ -209,9 +211,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => {
|
||||
let newThroughput = parseInt(newValue);
|
||||
newThroughput = isNaN(newThroughput) ? ThroughputInputAutoPilotV3Component.zeroThroughput : newThroughput;
|
||||
|
||||
const newThroughput = getSanitizedInputValue(newValue, this.throughputInputMaxValue);
|
||||
if (this.overrideWithAutoPilotSettings()) {
|
||||
this.props.onMaxAutoPilotThroughputChange(newThroughput);
|
||||
} else {
|
||||
@@ -245,7 +245,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
onChange={this.onChoiceGroupChange}
|
||||
required={this.props.showAsMandatory}
|
||||
ariaLabelledBy={labelId}
|
||||
styles={this.choiceGroupFixedStyle}
|
||||
styles={getChoiceGroupStyles(this.props.wasAutopilotOriginallySet, this.props.isAutoPilotSelected)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
@@ -270,8 +270,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
key="auto pilot throughput input"
|
||||
styles={getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)}
|
||||
disabled={this.overrideWithProvisionedThroughputSettings()}
|
||||
step={this.step}
|
||||
min={AutoPilotUtils.minAutoPilotThroughput}
|
||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
|
||||
onChange={this.onAutoPilotThroughputChange}
|
||||
/>
|
||||
@@ -298,8 +297,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
styles={getTextFieldStyles(this.props.throughput, this.props.throughputBaseline)}
|
||||
disabled={this.overrideWithAutoPilotSettings()}
|
||||
step={this.step}
|
||||
min={this.props.minimum}
|
||||
max={this.props.canExceedMaximumValue ? undefined : this.props.maximum}
|
||||
value={
|
||||
this.overrideWithAutoPilotSettings()
|
||||
? this.props.maxAutoPilotThroughputBaseline?.toString()
|
||||
|
||||
@@ -81,10 +81,10 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
Object {
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
"borderColor": undefined,
|
||||
},
|
||||
".ms-ChoiceField-field.is-checked::before": Object {
|
||||
"borderColor": "",
|
||||
"borderColor": undefined,
|
||||
},
|
||||
".ms-ChoiceField-wrapper label": Object {
|
||||
"fontFamily": undefined,
|
||||
@@ -113,10 +113,9 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
id="autopilotInput"
|
||||
key="auto pilot throughput input"
|
||||
label="Max RU/s"
|
||||
min={4000}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
step={100}
|
||||
step={1000}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
@@ -219,7 +218,6 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||
disabled={false}
|
||||
id="throughputInput"
|
||||
key="provisioned throughput input"
|
||||
min={10000}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
step={100}
|
||||
@@ -375,7 +373,6 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||
disabled={false}
|
||||
id="throughputInput"
|
||||
key="provisioned throughput input"
|
||||
min={10000}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
step={100}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { collection, container } from "./TestUtils";
|
||||
import {
|
||||
getMaxRUs,
|
||||
getMinRUs,
|
||||
getSanitizedInputValue,
|
||||
hasDatabaseSharedThroughput,
|
||||
isDirty,
|
||||
isDirtyTypes,
|
||||
@@ -86,4 +87,11 @@ describe("SettingsUtils", () => {
|
||||
expect(isDirty(baseline, current)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("getSanitizedInputValue", () => {
|
||||
const max = 100;
|
||||
expect(getSanitizedInputValue("", max)).toEqual(0);
|
||||
expect(getSanitizedInputValue("999", max)).toEqual(99);
|
||||
expect(getSanitizedInputValue("10", max)).toEqual(10);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as PricingUtils from "../../../Utils/PricingUtils";
|
||||
|
||||
import Explorer from "../../Explorer";
|
||||
|
||||
const zeroValue = 0;
|
||||
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy;
|
||||
export const TtlOff = "off";
|
||||
export const TtlOn = "on";
|
||||
@@ -129,6 +130,16 @@ export const parseConflictResolutionProcedure = (procedureFromBackEnd: string):
|
||||
return procedureFromBackEnd;
|
||||
};
|
||||
|
||||
export const getSanitizedInputValue = (newValueString: string, max: number): number => {
|
||||
let newValue = parseInt(newValueString);
|
||||
if (isNaN(newValue)) {
|
||||
newValue = zeroValue;
|
||||
} else if (newValue > max) {
|
||||
newValue = Math.floor(newValue / 10);
|
||||
}
|
||||
return newValue;
|
||||
};
|
||||
|
||||
export const isDirty = (current: isDirtyTypes, baseline: isDirtyTypes): boolean => {
|
||||
const currentType = typeof current;
|
||||
const baselineType = typeof baseline;
|
||||
|
||||
@@ -3,10 +3,7 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import Explorer from "../../Explorer";
|
||||
import ko from "knockout";
|
||||
|
||||
export const container = new Explorer({
|
||||
notificationsClient: undefined,
|
||||
isEmulator: false
|
||||
});
|
||||
export const container = new Explorer();
|
||||
|
||||
export const collection = ({
|
||||
container: container,
|
||||
|
||||
@@ -84,9 +84,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"visible": [Function],
|
||||
},
|
||||
AddCollectionPane {
|
||||
"_databaseOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"_isSynapseLinkEnabled": [Function],
|
||||
"autoPilotThroughput": [Function],
|
||||
"autoPilotTiersList": [Function],
|
||||
@@ -586,9 +583,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"_refreshSparkEnabledStateForAccount": [Function],
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"addCollectionPane": AddCollectionPane {
|
||||
"_databaseOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"_isSynapseLinkEnabled": [Function],
|
||||
"autoPilotThroughput": [Function],
|
||||
"autoPilotTiersList": [Function],
|
||||
@@ -982,7 +976,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isCodeOfConductEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEmulator": false,
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isGalleryPublishEnabled": [Function],
|
||||
@@ -1058,7 +1051,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"parameters": [Function],
|
||||
},
|
||||
"notificationConsoleData": [Function],
|
||||
"notificationsClient": undefined,
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"onSwitchToConnectionString": [Function],
|
||||
@@ -1398,9 +1390,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"visible": [Function],
|
||||
},
|
||||
AddCollectionPane {
|
||||
"_databaseOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"_isSynapseLinkEnabled": [Function],
|
||||
"autoPilotThroughput": [Function],
|
||||
"autoPilotTiersList": [Function],
|
||||
@@ -1900,9 +1889,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"_refreshSparkEnabledStateForAccount": [Function],
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"addCollectionPane": AddCollectionPane {
|
||||
"_databaseOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"_isSynapseLinkEnabled": [Function],
|
||||
"autoPilotThroughput": [Function],
|
||||
"autoPilotTiersList": [Function],
|
||||
@@ -2296,7 +2282,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isCodeOfConductEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEmulator": false,
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isGalleryPublishEnabled": [Function],
|
||||
@@ -2372,7 +2357,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"parameters": [Function],
|
||||
},
|
||||
"notificationConsoleData": [Function],
|
||||
"notificationsClient": undefined,
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"onSwitchToConnectionString": [Function],
|
||||
@@ -2725,9 +2709,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"visible": [Function],
|
||||
},
|
||||
AddCollectionPane {
|
||||
"_databaseOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"_isSynapseLinkEnabled": [Function],
|
||||
"autoPilotThroughput": [Function],
|
||||
"autoPilotTiersList": [Function],
|
||||
@@ -3227,9 +3208,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"_refreshSparkEnabledStateForAccount": [Function],
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"addCollectionPane": AddCollectionPane {
|
||||
"_databaseOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"_isSynapseLinkEnabled": [Function],
|
||||
"autoPilotThroughput": [Function],
|
||||
"autoPilotTiersList": [Function],
|
||||
@@ -3623,7 +3601,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isCodeOfConductEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEmulator": false,
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isGalleryPublishEnabled": [Function],
|
||||
@@ -3699,7 +3676,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"parameters": [Function],
|
||||
},
|
||||
"notificationConsoleData": [Function],
|
||||
"notificationsClient": undefined,
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"onSwitchToConnectionString": [Function],
|
||||
@@ -4039,9 +4015,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"visible": [Function],
|
||||
},
|
||||
AddCollectionPane {
|
||||
"_databaseOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"_isSynapseLinkEnabled": [Function],
|
||||
"autoPilotThroughput": [Function],
|
||||
"autoPilotTiersList": [Function],
|
||||
@@ -4541,9 +4514,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"_refreshSparkEnabledStateForAccount": [Function],
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"addCollectionPane": AddCollectionPane {
|
||||
"_databaseOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"_isSynapseLinkEnabled": [Function],
|
||||
"autoPilotThroughput": [Function],
|
||||
"autoPilotTiersList": [Function],
|
||||
@@ -4937,7 +4907,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isCodeOfConductEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEmulator": false,
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isGalleryPublishEnabled": [Function],
|
||||
@@ -5013,7 +4982,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"parameters": [Function],
|
||||
},
|
||||
"notificationConsoleData": [Function],
|
||||
"notificationsClient": undefined,
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"onSwitchToConnectionString": [Function],
|
||||
@@ -5325,7 +5293,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
logIndexingPolicySuccessMessage={[Function]}
|
||||
onIndexingPolicyContentChange={[Function]}
|
||||
onIndexingPolicyDirtyChange={[Function]}
|
||||
onIndexingPolicyElementFocusChange={[Function]}
|
||||
resetShouldDiscardIndexingPolicy={[Function]}
|
||||
shouldDiscardIndexingPolicy={false}
|
||||
/>
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
import TriangleDownIcon from "../../../../images/Triangle-down.svg";
|
||||
import TriangleRightIcon from "../../../../images/Triangle-right.svg";
|
||||
import LoadingIndicator_3Squares from "../../../../images/LoadingIndicator_3Squares.gif";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
|
||||
export interface TreeNodeMenuItem {
|
||||
label: string;
|
||||
@@ -276,7 +278,12 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
text: menuItem.label,
|
||||
disabled: menuItem.isDisabled,
|
||||
className: menuItem.styleClass,
|
||||
onClick: menuItem.onClick,
|
||||
onClick: () => {
|
||||
menuItem.onClick();
|
||||
TelemetryProcessor.trace(Action.ClickResourceTreeNodeContextMenuItem, ActionModifiers.Mark, {
|
||||
label: menuItem.label
|
||||
});
|
||||
},
|
||||
onRenderIcon: (props: any) => <img src={menuItem.iconSrc} alt="" />
|
||||
}))
|
||||
}}
|
||||
|
||||
@@ -191,7 +191,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
||||
"className": undefined,
|
||||
"disabled": true,
|
||||
"key": "menuLabel",
|
||||
"onClick": undefined,
|
||||
"onClick": [Function],
|
||||
"onRenderIcon": [Function],
|
||||
"text": "menuLabel",
|
||||
},
|
||||
|
||||
@@ -82,12 +82,12 @@ import { toRawContentUri, fromContentUri } from "../Utils/GitHubUtils";
|
||||
import UserDefinedFunction from "./Tree/UserDefinedFunction";
|
||||
import StoredProcedure from "./Tree/StoredProcedure";
|
||||
import Trigger from "./Tree/Trigger";
|
||||
import { NotificationsClientBase } from "../Common/NotificationsClientBase";
|
||||
import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
|
||||
import TabsBase from "./Tabs/TabsBase";
|
||||
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
|
||||
import { updateUserContext, userContext } from "../UserContext";
|
||||
import { stringToBlob } from "../Utils/BlobUtils";
|
||||
import { IChoiceGroupProps } from "office-ui-fabric-react";
|
||||
|
||||
BindingHandlersRegisterer.registerBindingHandlers();
|
||||
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
||||
@@ -98,10 +98,6 @@ enum ShareAccessToggleState {
|
||||
Read
|
||||
}
|
||||
|
||||
interface ExplorerOptions {
|
||||
notificationsClient: NotificationsClientBase;
|
||||
isEmulator: boolean;
|
||||
}
|
||||
interface AdHocAccessData {
|
||||
readWriteUrl: string;
|
||||
readUrl: string;
|
||||
@@ -135,14 +131,12 @@ export default class Explorer {
|
||||
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
|
||||
public isEnableMongoCapabilityPresent: ko.Computed<boolean>;
|
||||
public isServerlessEnabled: ko.Computed<boolean>;
|
||||
public isEmulator: boolean;
|
||||
public isAccountReady: ko.Observable<boolean>;
|
||||
public canSaveQueries: ko.Computed<boolean>;
|
||||
public features: ko.Observable<any>;
|
||||
public serverId: ko.Observable<string>;
|
||||
public armEndpoint: ko.Observable<string>;
|
||||
public isTryCosmosDBSubscription: ko.Observable<boolean>;
|
||||
public notificationsClient: NotificationsClientBase;
|
||||
public queriesClient: QueriesClient;
|
||||
public tableDataClient: TableDataClient;
|
||||
public splitter: Splitter;
|
||||
@@ -212,7 +206,7 @@ export default class Explorer {
|
||||
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
||||
public isCodeOfConductEnabled: ko.Computed<boolean>;
|
||||
public isLinkInjectionEnabled: ko.Computed<boolean>;
|
||||
public isSettingsV2Enabled: ko.Computed<boolean>;
|
||||
public isSettingsV2Enabled: ko.Observable<boolean>;
|
||||
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
||||
public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
|
||||
@@ -271,7 +265,7 @@ export default class Explorer {
|
||||
|
||||
private static readonly MaxNbDatabasesToAutoExpand = 5;
|
||||
|
||||
constructor(options: ExplorerOptions) {
|
||||
constructor() {
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
|
||||
dataExplorerArea: Constants.Areas.ResourceTree
|
||||
});
|
||||
@@ -377,8 +371,6 @@ export default class Explorer {
|
||||
}
|
||||
});
|
||||
this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>();
|
||||
this.notificationsClient = options.notificationsClient;
|
||||
this.isEmulator = options.isEmulator;
|
||||
|
||||
this.features = ko.observable();
|
||||
this.serverId = ko.observable<string>();
|
||||
@@ -421,7 +413,8 @@ export default class Explorer {
|
||||
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
|
||||
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
|
||||
);
|
||||
this.isSettingsV2Enabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSettingsV2));
|
||||
//this.isSettingsV2Enabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSettingsV2));
|
||||
this.isSettingsV2Enabled = ko.observable(false);
|
||||
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
|
||||
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||
@@ -1868,7 +1861,7 @@ export default class Explorer {
|
||||
return null;
|
||||
}
|
||||
if (this.selectedNode().nodeKind === "Database") {
|
||||
return _.find(this.databases(), (database: ViewModels.Database) => database.rid === this.selectedNode().rid);
|
||||
return _.find(this.databases(), (database: ViewModels.Database) => database.id() === this.selectedNode().id());
|
||||
}
|
||||
return this.findSelectedCollection().database;
|
||||
}
|
||||
@@ -1911,7 +1904,6 @@ export default class Explorer {
|
||||
this.features(inputs.features);
|
||||
this.serverId(inputs.serverId);
|
||||
this.armEndpoint(EnvironmentUtility.normalizeArmEndpointUri(inputs.csmEndpoint || configContext.ARM_ENDPOINT));
|
||||
this.notificationsClient.setExtensionEndpoint(configContext.BACKEND_ENDPOINT);
|
||||
this.databaseAccount(databaseAccount);
|
||||
this.subscriptionType(inputs.subscriptionType);
|
||||
this.quotaId(inputs.quotaId);
|
||||
@@ -1919,6 +1911,7 @@ export default class Explorer {
|
||||
this.flight(inputs.addCollectionDefaultFlight);
|
||||
this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription);
|
||||
this.isAuthWithResourceToken(inputs.isAuthWithresourceToken);
|
||||
this.setFeatureFlagsFromFlights(inputs.flights);
|
||||
|
||||
if (!!inputs.dataExplorerVersion) {
|
||||
this.parentFrameDataExplorerVersion(inputs.dataExplorerVersion);
|
||||
@@ -1953,12 +1946,20 @@ export default class Explorer {
|
||||
return Q();
|
||||
}
|
||||
|
||||
public findSelectedCollection(): ViewModels.Collection {
|
||||
if (this.selectedNode().nodeKind === "Collection") {
|
||||
return this.findSelectedCollectionForSelectedNode();
|
||||
} else {
|
||||
return this.findSelectedCollectionForSubNode();
|
||||
public setFeatureFlagsFromFlights(flights: readonly string[]): void {
|
||||
if (!flights) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (flights.indexOf(Constants.Flights.SettingsV2) !== -1) {
|
||||
this.isSettingsV2Enabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
public findSelectedCollection(): ViewModels.Collection {
|
||||
return (this.selectedNode().nodeKind === "Collection"
|
||||
? this.selectedNode()
|
||||
: this.selectedNode().collection) as ViewModels.Collection;
|
||||
}
|
||||
|
||||
// TODO: Refactor below methods, minimize dependencies and add unit tests where necessary
|
||||
@@ -2075,11 +2076,11 @@ export default class Explorer {
|
||||
});
|
||||
databasesToLoad.forEach(async (database: ViewModels.Database) => {
|
||||
await database.loadCollections();
|
||||
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.rid === database.rid);
|
||||
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.id() === database.id());
|
||||
if (isNewDatabase) {
|
||||
database.expandDatabase();
|
||||
}
|
||||
this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === database.rid);
|
||||
this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().id() === database.id());
|
||||
});
|
||||
|
||||
Q.all(loadCollectionPromises).done(
|
||||
@@ -2191,21 +2192,11 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
private findSelectedCollectionForSelectedNode(): ViewModels.Collection {
|
||||
return this.findCollection(this.selectedNode().rid);
|
||||
}
|
||||
|
||||
public findCollection(rid: string): ViewModels.Collection {
|
||||
for (let i = 0; i < this.databases().length; i++) {
|
||||
const database = this.databases()[i];
|
||||
for (let j = 0; j < database.collections().length; j++) {
|
||||
const collection = database.collections()[j];
|
||||
if (collection.rid === rid) {
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
public findCollection(databaseId: string, collectionId: string): ViewModels.Collection {
|
||||
const database: ViewModels.Database = this.databases().find(
|
||||
(database: ViewModels.Database) => database.id() === databaseId
|
||||
);
|
||||
return database?.collections().find((collection: ViewModels.Collection) => collection.id() === collectionId);
|
||||
}
|
||||
|
||||
public isLastCollection(): boolean {
|
||||
@@ -2229,7 +2220,7 @@ export default class Explorer {
|
||||
const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => {
|
||||
const databaseExists = _.some(
|
||||
this.databases(),
|
||||
(existingDatabase: ViewModels.Database) => existingDatabase.rid === database._rid
|
||||
(existingDatabase: ViewModels.Database) => existingDatabase.id() === database.id
|
||||
);
|
||||
return !databaseExists;
|
||||
});
|
||||
@@ -2241,7 +2232,7 @@ export default class Explorer {
|
||||
ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => {
|
||||
const databasePresentInUpdatedList = _.some(
|
||||
updatedDatabaseList,
|
||||
(db: DataModels.Database) => db._rid === database.rid
|
||||
(db: DataModels.Database) => db.id === database.id()
|
||||
);
|
||||
if (!databasePresentInUpdatedList) {
|
||||
databasesToDelete.push(database);
|
||||
@@ -2263,7 +2254,7 @@ export default class Explorer {
|
||||
const databasesToKeep: ViewModels.Database[] = [];
|
||||
|
||||
ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => {
|
||||
const shouldRemoveDatabase = _.some(databasesToRemove, (db: ViewModels.Database) => db.rid === database.rid);
|
||||
const shouldRemoveDatabase = _.some(databasesToRemove, (db: ViewModels.Database) => db.id === database.id);
|
||||
if (!shouldRemoveDatabase) {
|
||||
databasesToKeep.push(database);
|
||||
}
|
||||
@@ -2272,19 +2263,6 @@ export default class Explorer {
|
||||
this.databases(databasesToKeep);
|
||||
}
|
||||
|
||||
private findSelectedCollectionForSubNode(): ViewModels.Collection {
|
||||
for (let i = 0; i < this.databases().length; i++) {
|
||||
const database = this.databases()[i];
|
||||
for (let j = 0; j < database.collections().length; j++) {
|
||||
const collection = database.collections()[j];
|
||||
if (this.selectedNode().collection && collection.rid === this.selectedNode().collection.rid) {
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to upload notebook, but notebook is not enabled";
|
||||
@@ -2376,42 +2354,16 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public showOkCancelModalDialog(
|
||||
title: string,
|
||||
msg: string,
|
||||
okLabel: string,
|
||||
onOk: () => void,
|
||||
cancelLabel: string,
|
||||
onCancel: () => void
|
||||
): void {
|
||||
this._dialogProps({
|
||||
isModal: true,
|
||||
visible: true,
|
||||
title,
|
||||
subText: msg,
|
||||
primaryButtonText: okLabel,
|
||||
secondaryButtonText: cancelLabel,
|
||||
onPrimaryButtonClick: () => {
|
||||
this._closeModalDialog();
|
||||
onOk && onOk();
|
||||
},
|
||||
onSecondaryButtonClick: () => {
|
||||
this._closeModalDialog();
|
||||
onCancel && onCancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public showOkCancelTextFieldModalDialog(
|
||||
title: string,
|
||||
msg: string,
|
||||
okLabel: string,
|
||||
onOk: () => void,
|
||||
cancelLabel: string,
|
||||
onCancel: () => void,
|
||||
textFieldProps: TextFieldProps,
|
||||
choiceGroupProps?: IChoiceGroupProps,
|
||||
textFieldProps?: TextFieldProps,
|
||||
isPrimaryButtonDisabled?: boolean
|
||||
): void {
|
||||
let textFieldValue: string = null;
|
||||
this._dialogProps({
|
||||
isModal: true,
|
||||
visible: true,
|
||||
@@ -2427,8 +2379,9 @@ export default class Explorer {
|
||||
this._closeModalDialog();
|
||||
onCancel && onCancel();
|
||||
},
|
||||
primaryButtonDisabled: isPrimaryButtonDisabled,
|
||||
textFieldProps
|
||||
choiceGroupProps,
|
||||
textFieldProps,
|
||||
primaryButtonDisabled: isPrimaryButtonDisabled
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2469,7 +2422,6 @@ export default class Explorer {
|
||||
title: notebookContentItem.name,
|
||||
tabPath: notebookContentItem.path,
|
||||
collection: null,
|
||||
selfLink: null,
|
||||
masterKey: userContext.masterKey || "",
|
||||
hashLocation: "notebooks",
|
||||
isActive: ko.observable(false),
|
||||
@@ -2921,7 +2873,6 @@ export default class Explorer {
|
||||
title: title,
|
||||
tabPath: title,
|
||||
collection: null,
|
||||
selfLink: null,
|
||||
hashLocation: hashLocation,
|
||||
isActive: ko.observable(false),
|
||||
isTabsContentExpanded: ko.observable(true),
|
||||
@@ -2965,7 +2916,6 @@ export default class Explorer {
|
||||
title: title,
|
||||
tabPath: title,
|
||||
documentClientUtility: null,
|
||||
selfLink: null,
|
||||
isActive: ko.observable(false),
|
||||
hashLocation: hashLocation,
|
||||
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
||||
@@ -3008,7 +2958,6 @@ export default class Explorer {
|
||||
tabPath: title,
|
||||
documentClientUtility: null,
|
||||
collection: null,
|
||||
selfLink: null,
|
||||
hashLocation: hashLocation,
|
||||
isActive: ko.observable(false),
|
||||
isTabsContentExpanded: ko.observable(true),
|
||||
|
||||
@@ -141,8 +141,6 @@ describe("getPkIdFromDocumentId", () => {
|
||||
});
|
||||
|
||||
describe("GraphExplorer", () => {
|
||||
const COLLECTION_RID = "collectionRid";
|
||||
const COLLECTION_SELF_LINK = "collectionSelfLink";
|
||||
const gremlinRU = 789.12;
|
||||
|
||||
const createMockProps = (): GraphExplorerProps => {
|
||||
@@ -160,8 +158,6 @@ describe("GraphExplorer", () => {
|
||||
onIsValidQueryChange: (isValidQuery: boolean): void => {},
|
||||
|
||||
collectionPartitionKeyProperty: "collectionPartitionKeyProperty",
|
||||
collectionRid: COLLECTION_RID,
|
||||
collectionSelfLink: COLLECTION_SELF_LINK,
|
||||
graphBackendEndpoint: "graphBackendEndpoint",
|
||||
databaseId: "databaseId",
|
||||
collectionId: "collectionId",
|
||||
|
||||
@@ -47,8 +47,6 @@ export interface GraphExplorerProps {
|
||||
onIsValidQueryChange: (isValidQuery: boolean) => void;
|
||||
|
||||
collectionPartitionKeyProperty: string;
|
||||
collectionRid: string;
|
||||
collectionSelfLink: string;
|
||||
graphBackendEndpoint: string;
|
||||
databaseId: string;
|
||||
collectionId: string;
|
||||
@@ -1761,7 +1759,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${queryInfoStr}`);
|
||||
|
||||
return queryDocumentsPage(
|
||||
this.props.collectionRid,
|
||||
this.props.collectionId,
|
||||
this.currentDocDBQueryInfo.iterator,
|
||||
this.currentDocDBQueryInfo.index,
|
||||
{
|
||||
|
||||
@@ -17,8 +17,6 @@ interface Parameter {
|
||||
graphConfig?: GraphConfig;
|
||||
|
||||
collectionPartitionKeyProperty: string;
|
||||
collectionRid: string;
|
||||
collectionSelfLink: string;
|
||||
graphBackendEndpoint: string;
|
||||
databaseId: string;
|
||||
collectionId: string;
|
||||
@@ -49,8 +47,6 @@ export class GraphExplorerAdapter implements ReactAdapter {
|
||||
onIsGraphDisplayed={this.params.onIsGraphDisplayed}
|
||||
onResetDefaultGraphConfigValues={this.params.onResetDefaultGraphConfigValues}
|
||||
collectionPartitionKeyProperty={this.params.collectionPartitionKeyProperty}
|
||||
collectionRid={this.params.collectionRid}
|
||||
collectionSelfLink={this.params.collectionSelfLink}
|
||||
graphBackendEndpoint={this.params.graphBackendEndpoint}
|
||||
databaseId={this.params.databaseId}
|
||||
collectionId={this.params.collectionId}
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { CommandBarComponentButtonFactory } from "./CommandBarComponentButtonFactory";
|
||||
import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
|
||||
import { StyleConstants } from "../../../Common/Constants";
|
||||
import { CommandBarUtil } from "./CommandBarUtil";
|
||||
import * as CommandBarUtil from "./CommandBarUtil";
|
||||
import Explorer from "../../Explorer";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ export class CommandBarComponentButtonFactory {
|
||||
buttons.push(fullScreenButton);
|
||||
}
|
||||
|
||||
if (!container.hasOwnProperty("isEmulator") || !container.isEmulator) {
|
||||
if (configContext.platform !== Platform.Emulator) {
|
||||
const label = "Feedback";
|
||||
const feedbackButtonOptions: CommandButtonComponentProps = {
|
||||
iconSrc: FeedbackIcon,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CommandBarUtil } from "./CommandBarUtil";
|
||||
import * as CommandBarUtil from "./CommandBarUtil";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
@@ -8,7 +8,7 @@ describe("CommandBarUtil tests", () => {
|
||||
return {
|
||||
iconSrc: "icon",
|
||||
iconAlt: "label",
|
||||
onCommandClick: (e: React.SyntheticEvent): void => {},
|
||||
onCommandClick: jest.fn(),
|
||||
commandButtonLabel: "label",
|
||||
ariaLabel: "ariaLabel",
|
||||
hasPopup: true,
|
||||
@@ -29,11 +29,14 @@ describe("CommandBarUtil tests", () => {
|
||||
expect(!converted.split);
|
||||
expect(converted.iconProps.imageProps.src).toEqual(btn.iconSrc);
|
||||
expect(converted.iconProps.imageProps.alt).toEqual(btn.iconAlt);
|
||||
expect(converted.onClick).toEqual(btn.onCommandClick);
|
||||
expect(converted.text).toEqual(btn.commandButtonLabel);
|
||||
expect(converted.ariaLabel).toEqual(btn.ariaLabel);
|
||||
expect(converted.disabled).toEqual(btn.disabled);
|
||||
expect(converted.className).toEqual(btn.className);
|
||||
|
||||
// Click gets called
|
||||
converted.onClick();
|
||||
expect(btn.onCommandClick).toBeCalled();
|
||||
});
|
||||
|
||||
it("should convert NavbarButtonConfig to split button", () => {
|
||||
|
||||
@@ -11,177 +11,187 @@ import ChevronDownIcon from "../../../../images/Chevron_down.svg";
|
||||
import { ArcadiaMenuPicker } from "../../Controls/Arcadia/ArcadiaMenuPicker";
|
||||
import { MemoryTrackerComponent } from "./MemoryTrackerComponent";
|
||||
import { MemoryUsageInfo } from "../../../Contracts/DataModels";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
|
||||
/**
|
||||
* Utilities for CommandBar
|
||||
* Convert our NavbarButtonConfig to UI Fabric buttons
|
||||
* @param btns
|
||||
*/
|
||||
export class CommandBarUtil {
|
||||
/**
|
||||
* Convert our NavbarButtonConfig to UI Fabric buttons
|
||||
* @param btns
|
||||
*/
|
||||
public static convertButton(btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] {
|
||||
const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
|
||||
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
|
||||
const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
|
||||
|
||||
return btns
|
||||
.filter(btn => btn)
|
||||
.map(
|
||||
(btn: CommandButtonComponentProps, index: number): ICommandBarItemProps => {
|
||||
if (btn.isDivider) {
|
||||
return CommandBarUtil.createDivider(btn.commandButtonLabel);
|
||||
}
|
||||
return btns
|
||||
.filter(btn => btn)
|
||||
.map(
|
||||
(btn: CommandButtonComponentProps, index: number): ICommandBarItemProps => {
|
||||
if (btn.isDivider) {
|
||||
return createDivider(btn.commandButtonLabel);
|
||||
}
|
||||
|
||||
const isSplit = !!btn.children && btn.children.length > 0;
|
||||
|
||||
const result: ICommandBarItemProps = {
|
||||
iconProps: {
|
||||
style: {
|
||||
width: StyleConstants.CommandBarIconWidth, // 16
|
||||
alignSelf: btn.iconName ? "baseline" : undefined
|
||||
},
|
||||
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
|
||||
iconName: btn.iconName
|
||||
const isSplit = !!btn.children && btn.children.length > 0;
|
||||
const label = btn.commandButtonLabel || btn.tooltipText;
|
||||
const result: ICommandBarItemProps = {
|
||||
iconProps: {
|
||||
style: {
|
||||
width: StyleConstants.CommandBarIconWidth, // 16
|
||||
alignSelf: btn.iconName ? "baseline" : undefined
|
||||
},
|
||||
onClick: btn.onCommandClick,
|
||||
key: `${btn.commandButtonLabel}${index}`,
|
||||
text: btn.commandButtonLabel || btn.tooltipText,
|
||||
"data-test": btn.commandButtonLabel || btn.tooltipText,
|
||||
title: btn.tooltipText,
|
||||
name: btn.commandButtonLabel || btn.tooltipText,
|
||||
disabled: btn.disabled,
|
||||
ariaLabel: btn.ariaLabel,
|
||||
buttonStyles: {
|
||||
root: {
|
||||
backgroundColor: backgroundColor,
|
||||
height: buttonHeightPx,
|
||||
paddingRight: 0,
|
||||
paddingLeft: 0,
|
||||
minWidth: 24,
|
||||
marginLeft: isSplit ? 0 : 5,
|
||||
marginRight: isSplit ? 0 : 5
|
||||
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
|
||||
iconName: btn.iconName
|
||||
},
|
||||
onClick: (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
|
||||
btn.onCommandClick(ev);
|
||||
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label });
|
||||
},
|
||||
key: `${btn.commandButtonLabel}${index}`,
|
||||
text: label,
|
||||
"data-test": label,
|
||||
title: btn.tooltipText,
|
||||
name: label,
|
||||
disabled: btn.disabled,
|
||||
ariaLabel: btn.ariaLabel,
|
||||
buttonStyles: {
|
||||
root: {
|
||||
backgroundColor: backgroundColor,
|
||||
height: buttonHeightPx,
|
||||
paddingRight: 0,
|
||||
paddingLeft: 0,
|
||||
minWidth: 24,
|
||||
marginLeft: isSplit ? 0 : 5,
|
||||
marginRight: isSplit ? 0 : 5
|
||||
},
|
||||
rootDisabled: {
|
||||
backgroundColor: backgroundColor,
|
||||
pointerEvents: "auto"
|
||||
},
|
||||
splitButtonMenuButton: {
|
||||
backgroundColor: backgroundColor,
|
||||
selectors: {
|
||||
":hover": { backgroundColor: StyleConstants.AccentLight }
|
||||
},
|
||||
rootDisabled: {
|
||||
backgroundColor: backgroundColor,
|
||||
pointerEvents: "auto"
|
||||
},
|
||||
splitButtonMenuButton: {
|
||||
backgroundColor: backgroundColor,
|
||||
selectors: {
|
||||
":hover": { backgroundColor: StyleConstants.AccentLight }
|
||||
},
|
||||
width: 16
|
||||
},
|
||||
label: { fontSize: StyleConstants.mediumFontSize },
|
||||
rootHovered: { backgroundColor: StyleConstants.AccentLight },
|
||||
rootPressed: { backgroundColor: StyleConstants.AccentLight },
|
||||
splitButtonMenuButtonExpanded: {
|
||||
backgroundColor: StyleConstants.AccentExtra,
|
||||
selectors: {
|
||||
":hover": { backgroundColor: StyleConstants.AccentLight }
|
||||
}
|
||||
},
|
||||
splitButtonDivider: {
|
||||
display: "none"
|
||||
},
|
||||
icon: {
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0
|
||||
},
|
||||
splitButtonContainer: {
|
||||
marginLeft: 5,
|
||||
marginRight: 5
|
||||
width: 16
|
||||
},
|
||||
label: { fontSize: StyleConstants.mediumFontSize },
|
||||
rootHovered: { backgroundColor: StyleConstants.AccentLight },
|
||||
rootPressed: { backgroundColor: StyleConstants.AccentLight },
|
||||
splitButtonMenuButtonExpanded: {
|
||||
backgroundColor: StyleConstants.AccentExtra,
|
||||
selectors: {
|
||||
":hover": { backgroundColor: StyleConstants.AccentLight }
|
||||
}
|
||||
},
|
||||
className: btn.className,
|
||||
id: btn.id
|
||||
splitButtonDivider: {
|
||||
display: "none"
|
||||
},
|
||||
icon: {
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0
|
||||
},
|
||||
splitButtonContainer: {
|
||||
marginLeft: 5,
|
||||
marginRight: 5
|
||||
}
|
||||
},
|
||||
className: btn.className,
|
||||
id: btn.id
|
||||
};
|
||||
|
||||
if (isSplit) {
|
||||
// It's a split button
|
||||
result.split = true;
|
||||
|
||||
result.subMenuProps = {
|
||||
items: convertButton(btn.children, backgroundColor),
|
||||
styles: {
|
||||
list: {
|
||||
// TODO Figure out how to do it the proper way with subComponentStyles.
|
||||
// TODO Remove all this crazy styling once we adopt Ui-Fabric Azure themes
|
||||
selectors: {
|
||||
".ms-ContextualMenu-itemText": { fontSize: StyleConstants.mediumFontSize },
|
||||
".ms-ContextualMenu-link:hover": { backgroundColor: StyleConstants.AccentLight },
|
||||
".ms-ContextualMenu-icon": { width: 16, height: 16 }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (isSplit) {
|
||||
// It's a split button
|
||||
result.split = true;
|
||||
|
||||
result.subMenuProps = {
|
||||
items: CommandBarUtil.convertButton(btn.children, backgroundColor),
|
||||
styles: {
|
||||
list: {
|
||||
// TODO Figure out how to do it the proper way with subComponentStyles.
|
||||
// TODO Remove all this crazy styling once we adopt Ui-Fabric Azure themes
|
||||
selectors: {
|
||||
".ms-ContextualMenu-itemText": { fontSize: StyleConstants.mediumFontSize },
|
||||
".ms-ContextualMenu-link:hover": { backgroundColor: StyleConstants.AccentLight },
|
||||
".ms-ContextualMenu-icon": { width: 16, height: 16 }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
result.menuIconProps = {
|
||||
iconType: IconType.image,
|
||||
style: {
|
||||
width: 12,
|
||||
paddingLeft: 1,
|
||||
paddingTop: 6
|
||||
},
|
||||
imageProps: { src: ChevronDownIcon, alt: btn.iconAlt }
|
||||
};
|
||||
}
|
||||
|
||||
if (btn.isDropdown) {
|
||||
const selectedChild = _.find(btn.children, child => child.dropdownItemKey === btn.dropdownSelectedKey);
|
||||
result.name = selectedChild?.commandButtonLabel || btn.dropdownPlaceholder;
|
||||
|
||||
const dropdownStyles: Partial<IDropdownStyles> = {
|
||||
root: { margin: 5 },
|
||||
dropdown: { width: btn.dropdownWidth },
|
||||
title: { fontSize: 12, height: 30, lineHeight: 28 },
|
||||
dropdownItem: { fontSize: 12, lineHeight: 28, minHeight: 30 },
|
||||
dropdownItemSelected: { fontSize: 12, lineHeight: 28, minHeight: 30 }
|
||||
};
|
||||
|
||||
result.commandBarButtonAs = (props: IComponentAsProps<ICommandBarItemProps>) => {
|
||||
return (
|
||||
<Dropdown
|
||||
placeholder={btn.dropdownPlaceholder}
|
||||
defaultSelectedKey={btn.dropdownSelectedKey}
|
||||
onChange={(event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption, index?: number): void =>
|
||||
btn.children[index].onCommandClick(event)
|
||||
}
|
||||
options={btn.children.map((child: CommandButtonComponentProps) => ({
|
||||
key: child.dropdownItemKey,
|
||||
text: child.commandButtonLabel
|
||||
}))}
|
||||
styles={dropdownStyles}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
if (btn.isArcadiaPicker && btn.arcadiaProps) {
|
||||
result.commandBarButtonAs = () => <ArcadiaMenuPicker {...btn.arcadiaProps} />;
|
||||
}
|
||||
|
||||
return result;
|
||||
result.menuIconProps = {
|
||||
iconType: IconType.image,
|
||||
style: {
|
||||
width: 12,
|
||||
paddingLeft: 1,
|
||||
paddingTop: 6
|
||||
},
|
||||
imageProps: { src: ChevronDownIcon, alt: btn.iconAlt }
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static createDivider(key: string): ICommandBarItemProps {
|
||||
return {
|
||||
onRender: () => (
|
||||
<div className="dividerContainer">
|
||||
<span />
|
||||
</div>
|
||||
),
|
||||
iconOnly: true,
|
||||
disabled: true,
|
||||
key: key
|
||||
};
|
||||
}
|
||||
if (btn.isDropdown) {
|
||||
const selectedChild = _.find(btn.children, child => child.dropdownItemKey === btn.dropdownSelectedKey);
|
||||
result.name = selectedChild?.commandButtonLabel || btn.dropdownPlaceholder;
|
||||
|
||||
public static createMemoryTracker(key: string, memoryUsageInfo: Observable<MemoryUsageInfo>): ICommandBarItemProps {
|
||||
return {
|
||||
key,
|
||||
onRender: () => <MemoryTrackerComponent memoryUsageInfo={memoryUsageInfo} />
|
||||
};
|
||||
}
|
||||
}
|
||||
const dropdownStyles: Partial<IDropdownStyles> = {
|
||||
root: { margin: 5 },
|
||||
dropdown: { width: btn.dropdownWidth },
|
||||
title: { fontSize: 12, height: 30, lineHeight: 28 },
|
||||
dropdownItem: { fontSize: 12, lineHeight: 28, minHeight: 30 },
|
||||
dropdownItemSelected: { fontSize: 12, lineHeight: 28, minHeight: 30 }
|
||||
};
|
||||
|
||||
const onDropdownChange = (
|
||||
event: React.FormEvent<HTMLDivElement>,
|
||||
option?: IDropdownOption,
|
||||
index?: number
|
||||
): void => {
|
||||
btn.children[index].onCommandClick(event);
|
||||
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label: option.text });
|
||||
};
|
||||
|
||||
result.commandBarButtonAs = (props: IComponentAsProps<ICommandBarItemProps>) => {
|
||||
return (
|
||||
<Dropdown
|
||||
placeholder={btn.dropdownPlaceholder}
|
||||
defaultSelectedKey={btn.dropdownSelectedKey}
|
||||
onChange={onDropdownChange}
|
||||
options={btn.children.map((child: CommandButtonComponentProps) => ({
|
||||
key: child.dropdownItemKey,
|
||||
text: child.commandButtonLabel
|
||||
}))}
|
||||
styles={dropdownStyles}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
if (btn.isArcadiaPicker && btn.arcadiaProps) {
|
||||
result.commandBarButtonAs = () => <ArcadiaMenuPicker {...btn.arcadiaProps} />;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const createDivider = (key: string): ICommandBarItemProps => {
|
||||
return {
|
||||
onRender: () => (
|
||||
<div className="dividerContainer">
|
||||
<span />
|
||||
</div>
|
||||
),
|
||||
iconOnly: true,
|
||||
disabled: true,
|
||||
key: key
|
||||
};
|
||||
};
|
||||
|
||||
export const createMemoryTracker = (
|
||||
key: string,
|
||||
memoryUsageInfo: Observable<MemoryUsageInfo>
|
||||
): ICommandBarItemProps => {
|
||||
return {
|
||||
key,
|
||||
onRender: () => <MemoryTrackerComponent memoryUsageInfo={memoryUsageInfo} />
|
||||
};
|
||||
};
|
||||
|
||||
@@ -15,7 +15,10 @@ export interface OpenNotebookItem {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export type OpenCollectionItem = string;
|
||||
export interface OpenCollectionItem {
|
||||
databaseId: string;
|
||||
collectionId: string;
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
type: Type;
|
||||
@@ -121,7 +124,11 @@ export class MostRecentActivity {
|
||||
public onItemClicked(item: Item) {
|
||||
switch (item.type) {
|
||||
case Type.OpenCollection: {
|
||||
const collection = this.container.findCollection(item.data as OpenCollectionItem);
|
||||
const openCollectionitem = item.data as OpenCollectionItem;
|
||||
const collection = this.container.findCollection(
|
||||
openCollectionitem.databaseId,
|
||||
openCollectionitem.collectionId
|
||||
);
|
||||
if (collection) {
|
||||
collection.openTab();
|
||||
}
|
||||
|
||||
@@ -32,24 +32,27 @@ import {
|
||||
import { message, JupyterMessage, Channels, createMessage, childOf, ofMessageType } from "@nteract/messaging";
|
||||
import { sessions, kernels } from "rx-jupyter";
|
||||
import { RecordOf } from "immutable";
|
||||
import { AnyAction } from "redux";
|
||||
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import * as CdbActions from "./actions";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action as TelemetryAction } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { CdbAppState } from "./types";
|
||||
import { decryptJWTToken } from "../../../Utils/AuthorizationUtils";
|
||||
import * as TextFile from "./contents/file/text-file";
|
||||
import { NotebookUtil } from "../NotebookUtil";
|
||||
import { FileSystemUtil } from "../FileSystemUtil";
|
||||
import * as cdbActions from "../NotebookComponent/actions";
|
||||
import { Areas } from "../../../Common/Constants";
|
||||
|
||||
interface NotebookServiceConfig extends JupyterServerConfig {
|
||||
userPuid?: string;
|
||||
}
|
||||
|
||||
const logToTelemetry = (state: CdbAppState, title: string, error?: string) => {
|
||||
const logFailureToTelemetry = (state: CdbAppState, title: string, error?: string) => {
|
||||
TelemetryProcessor.traceFailure(TelemetryAction.NotebookErrorNotification, {
|
||||
databaseAccountName: state.cdb.databaseAccountName,
|
||||
defaultExperience: state.cdb.defaultExperience,
|
||||
@@ -311,7 +314,7 @@ export const launchWebSocketKernelEpic = (
|
||||
kernelSpecToLaunch = currentKernelspecs.defaultKernelName;
|
||||
const msg = `No kernelspec name specified to launch, using default kernel: ${kernelSpecToLaunch}`;
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
|
||||
logToTelemetry(state$.value, "Launching alternate kernel", msg);
|
||||
logFailureToTelemetry(state$.value, "Launching alternate kernel", msg);
|
||||
} else {
|
||||
return of(
|
||||
actions.launchKernelFailed({
|
||||
@@ -337,7 +340,7 @@ export const launchWebSocketKernelEpic = (
|
||||
msg += ` Using default kernel: ${kernelSpecToLaunch}`;
|
||||
}
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
|
||||
logToTelemetry(state$.value, "Launching alternate kernel", msg);
|
||||
logFailureToTelemetry(state$.value, "Launching alternate kernel", msg);
|
||||
}
|
||||
|
||||
const sessionPayload = {
|
||||
@@ -634,7 +637,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
|
||||
const title = "Kernel restart";
|
||||
const msg = "Kernel successfully restarted";
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
|
||||
logToTelemetry(state$.value, title, msg);
|
||||
logFailureToTelemetry(state$.value, title, msg);
|
||||
break;
|
||||
}
|
||||
case actions.RESTART_KERNEL_FAILED:
|
||||
@@ -645,7 +648,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
|
||||
const title = "Save failure";
|
||||
const msg = `Failed to save notebook: ${(action as actions.SaveFailed).payload.error}`;
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
||||
logToTelemetry(state$.value, title, msg);
|
||||
logFailureToTelemetry(state$.value, title, msg);
|
||||
break;
|
||||
}
|
||||
case actions.FETCH_CONTENT_FAILED: {
|
||||
@@ -654,7 +657,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
|
||||
const title = "Fetching content failure";
|
||||
const msg = `Failed to fetch notebook content: ${filepath}, error: ${typedAction.payload.error}`;
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
||||
logToTelemetry(state$.value, title, msg);
|
||||
logFailureToTelemetry(state$.value, title, msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -679,7 +682,7 @@ const handleKernelConnectionLostEpic = (
|
||||
|
||||
const msg = "Notebook was disconnected from kernel";
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
||||
logToTelemetry(state, "Error", "Kernel connection error");
|
||||
logFailureToTelemetry(state, "Error", "Kernel connection error");
|
||||
|
||||
const host = selectors.currentHost(state);
|
||||
const serverConfig: NotebookServiceConfig = selectors.serverConfig(host as RecordOf<JupyterHostRecordProps>);
|
||||
@@ -692,7 +695,7 @@ const handleKernelConnectionLostEpic = (
|
||||
const msg =
|
||||
"Restarted kernel too many times. Please reload the page to enable Data Explorer to restart the kernel automatically.";
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
||||
logToTelemetry(state, "Kernel restart error", msg);
|
||||
logFailureToTelemetry(state, "Kernel restart error", msg);
|
||||
|
||||
const explorer = window.dataExplorer;
|
||||
if (explorer) {
|
||||
@@ -844,6 +847,105 @@ const closeContentFailedToFetchEpic = (
|
||||
);
|
||||
};
|
||||
|
||||
const traceNotebookTelemetryEpic = (
|
||||
action$: Observable<cdbActions.TraceNotebookTelemetryAction>,
|
||||
state$: StateObservable<CdbAppState>
|
||||
): Observable<{}> => {
|
||||
return action$.pipe(
|
||||
ofType(cdbActions.TRACE_NOTEBOOK_TELEMETRY),
|
||||
mergeMap((action: cdbActions.TraceNotebookTelemetryAction) => {
|
||||
const state = state$.value;
|
||||
|
||||
TelemetryProcessor.trace(action.payload.action, action.payload.actionModifier, {
|
||||
...action.payload.data,
|
||||
databaseAccountName: state.cdb.databaseAccountName,
|
||||
defaultExperience: state.cdb.defaultExperience,
|
||||
dataExplorerArea: Areas.Notebook
|
||||
});
|
||||
return EMPTY;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Log notebook information to telemetry
|
||||
* # raw cells, # markdown cells, # code cells, total
|
||||
* @param action$
|
||||
* @param state$
|
||||
*/
|
||||
const traceNotebookInfoEpic = (
|
||||
action$: Observable<actions.FetchContentFulfilled>,
|
||||
state$: StateObservable<AppState>
|
||||
): Observable<{} | cdbActions.TraceNotebookTelemetryAction> => {
|
||||
return action$.pipe(
|
||||
ofType(actions.FETCH_CONTENT_FULFILLED),
|
||||
mergeMap((action: { payload: any }) => {
|
||||
const state = state$.value;
|
||||
const contentRef = action.payload.contentRef;
|
||||
const model = selectors.model(state, { contentRef });
|
||||
|
||||
// If it's not a notebook, we shouldn't be here
|
||||
if (!model || model.type !== "notebook") {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
const dataToLog = {
|
||||
nbCodeCells: 0,
|
||||
nbRawCells: 0,
|
||||
nbMarkdownCells: 0,
|
||||
nbCells: 0
|
||||
};
|
||||
for (let [id, cell] of selectors.notebook.cellMap(model)) {
|
||||
switch (cell.cell_type) {
|
||||
case "code":
|
||||
dataToLog.nbCodeCells++;
|
||||
break;
|
||||
case "markdown":
|
||||
dataToLog.nbMarkdownCells++;
|
||||
break;
|
||||
case "raw":
|
||||
dataToLog.nbRawCells++;
|
||||
break;
|
||||
}
|
||||
dataToLog.nbCells++;
|
||||
}
|
||||
|
||||
return of(
|
||||
cdbActions.traceNotebookTelemetry({
|
||||
action: TelemetryAction.NotebooksFetched,
|
||||
actionModifier: ActionModifiers.Mark,
|
||||
data: dataToLog
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Log Kernel spec to start
|
||||
* @param action$
|
||||
* @param state$
|
||||
*/
|
||||
const traceNotebookKernelEpic = (
|
||||
action$: Observable<AnyAction>,
|
||||
state$: StateObservable<AppState>
|
||||
): Observable<cdbActions.TraceNotebookTelemetryAction> => {
|
||||
return action$.pipe(
|
||||
ofType(actions.LAUNCH_KERNEL_SUCCESSFUL),
|
||||
mergeMap((action: { payload: any; type: string }) => {
|
||||
return of(
|
||||
cdbActions.traceNotebookTelemetry({
|
||||
action: TelemetryAction.NotebooksKernelSpecName,
|
||||
actionModifier: ActionModifiers.Mark,
|
||||
data: {
|
||||
kernelSpecName: action.payload.kernel.name
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const allEpics = [
|
||||
addInitialCodeCellEpic,
|
||||
focusInitialCodeCellEpic,
|
||||
@@ -856,5 +958,8 @@ export const allEpics = [
|
||||
executeFocusedCellAndFocusNextEpic,
|
||||
closeUnsupportedMimetypesEpic,
|
||||
closeContentFailedToFetchEpic,
|
||||
restartWebSocketKernelEpic
|
||||
restartWebSocketKernelEpic,
|
||||
traceNotebookTelemetryEpic,
|
||||
traceNotebookInfoEpic,
|
||||
traceNotebookKernelEpic
|
||||
];
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { actions, CoreRecord, reducers as nteractReducers } from "@nteract/core";
|
||||
import { Action } from "redux";
|
||||
import { Areas } from "../../../Common/Constants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import * as cdbActions from "./actions";
|
||||
import { CdbRecord } from "./types";
|
||||
|
||||
@@ -72,17 +70,6 @@ export const cdbReducer = (state: CdbRecord, action: Action) => {
|
||||
return state.set("hoveredCellId", typedAction.payload.cellId);
|
||||
}
|
||||
|
||||
case cdbActions.TRACE_NOTEBOOK_TELEMETRY: {
|
||||
const typedAction = action as cdbActions.TraceNotebookTelemetryAction;
|
||||
TelemetryProcessor.trace(typedAction.payload.action, typedAction.payload.actionModifier, {
|
||||
...typedAction.payload.data,
|
||||
databaseAccountName: state.databaseAccountName,
|
||||
defaultExperience: state.defaultExperience,
|
||||
dataExplorerArea: Areas.Notebook
|
||||
});
|
||||
return state;
|
||||
}
|
||||
|
||||
case cdbActions.UPDATE_NOTEBOOK_PARENT_DOM_ELTS: {
|
||||
const typedAction = action as cdbActions.UpdateNotebookParentDomEltAction;
|
||||
var parentEltsMap = state.get("currentNotebookParentElements");
|
||||
|
||||
@@ -166,7 +166,7 @@ export default class NotebookManager {
|
||||
private promptForCommitMsg = (title: string, primaryButtonLabel: string) => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
let commitMsg = "Committed from Azure Cosmos DB Notebooks";
|
||||
this.params.container.showOkCancelTextFieldModalDialog(
|
||||
this.params.container.showOkCancelModalDialog(
|
||||
title || "Commit",
|
||||
undefined,
|
||||
primaryButtonLabel || "Commit",
|
||||
@@ -181,6 +181,7 @@ export default class NotebookManager {
|
||||
},
|
||||
"Cancel",
|
||||
() => reject(new Error("Commit dialog canceled")),
|
||||
undefined,
|
||||
{
|
||||
label: "Commit message",
|
||||
autoAdjustHeight: true,
|
||||
|
||||
@@ -14,6 +14,8 @@ import { CellToolbarContext } from "@nteract/stateful-components";
|
||||
import { CellType, CellId } from "@nteract/commutable";
|
||||
import * as selectors from "@nteract/selectors";
|
||||
import { RecordOf } from "immutable";
|
||||
import * as cdbActions from "../NotebookComponent/actions";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
|
||||
export interface ComponentProps {
|
||||
contentRef: ContentRef;
|
||||
@@ -29,6 +31,7 @@ interface DispatchProps {
|
||||
moveCell: (destinationId: CellId, above: boolean) => void;
|
||||
clearOutputs: () => void;
|
||||
deleteCell: () => void;
|
||||
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: any) => void;
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
@@ -48,12 +51,18 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
|
||||
{
|
||||
key: "Run",
|
||||
text: "Run",
|
||||
onClick: this.props.executeCell
|
||||
onClick: () => {
|
||||
this.props.executeCell();
|
||||
this.props.traceNotebookTelemetry(Action.NotebooksExecuteCellFromMenu, ActionModifiers.Mark);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "Clear Outputs",
|
||||
text: "Clear Outputs",
|
||||
onClick: this.props.clearOutputs
|
||||
onClick: () => {
|
||||
this.props.clearOutputs();
|
||||
this.props.traceNotebookTelemetry(Action.NotebooksClearOutputsFromMenu, ActionModifiers.Mark);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "Divider",
|
||||
@@ -64,31 +73,43 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
|
||||
|
||||
items = items.concat([
|
||||
{
|
||||
key: "Divider",
|
||||
key: "Divider2",
|
||||
itemType: ContextualMenuItemType.Divider
|
||||
},
|
||||
{
|
||||
key: "Insert Code Cell Above",
|
||||
text: "Insert Code Cell Above",
|
||||
onClick: this.props.insertCodeCellAbove
|
||||
onClick: () => {
|
||||
this.props.insertCodeCellAbove();
|
||||
this.props.traceNotebookTelemetry(Action.NotebooksInsertCodeCellAboveFromMenu, ActionModifiers.Mark);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "Insert Code Cell Below",
|
||||
text: "Insert Code Cell Below",
|
||||
onClick: this.props.insertCodeCellBelow
|
||||
onClick: () => {
|
||||
this.props.insertCodeCellBelow();
|
||||
this.props.traceNotebookTelemetry(Action.NotebooksInsertCodeCellBelowFromMenu, ActionModifiers.Mark);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "Insert Text Cell Above",
|
||||
text: "Insert Text Cell Above",
|
||||
onClick: this.props.insertTextCellAbove
|
||||
onClick: () => {
|
||||
this.props.insertTextCellAbove();
|
||||
this.props.traceNotebookTelemetry(Action.NotebooksInsertTextCellAboveFromMenu, ActionModifiers.Mark);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "Insert Text Cell Below",
|
||||
text: "Insert Text Cell Below",
|
||||
onClick: this.props.insertTextCellBelow
|
||||
onClick: () => {
|
||||
this.props.insertTextCellBelow();
|
||||
this.props.traceNotebookTelemetry(Action.NotebooksInsertTextCellBelowFromMenu, ActionModifiers.Mark);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "Divider",
|
||||
key: "Divider3",
|
||||
itemType: ContextualMenuItemType.Divider
|
||||
}
|
||||
]);
|
||||
@@ -98,7 +119,10 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
|
||||
moveItems.push({
|
||||
key: "Move Cell Up",
|
||||
text: "Move Cell Up",
|
||||
onClick: () => this.props.moveCell(this.props.cellIdAbove, true)
|
||||
onClick: () => {
|
||||
this.props.moveCell(this.props.cellIdAbove, true);
|
||||
this.props.traceNotebookTelemetry(Action.NotebooksMoveCellUpFromMenu, ActionModifiers.Mark);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -106,13 +130,16 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
|
||||
moveItems.push({
|
||||
key: "Move Cell Down",
|
||||
text: "Move Cell Down",
|
||||
onClick: () => this.props.moveCell(this.props.cellIdBelow, false)
|
||||
onClick: () => {
|
||||
this.props.moveCell(this.props.cellIdBelow, false);
|
||||
this.props.traceNotebookTelemetry(Action.NotebooksMoveCellDownFromMenu, ActionModifiers.Mark);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (moveItems.length > 0) {
|
||||
moveItems.push({
|
||||
key: "Divider",
|
||||
key: "Divider4",
|
||||
itemType: ContextualMenuItemType.Divider
|
||||
});
|
||||
items = items.concat(moveItems);
|
||||
@@ -121,7 +148,10 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
|
||||
items.push({
|
||||
key: "Delete Cell",
|
||||
text: "Delete Cell",
|
||||
onClick: this.props.deleteCell
|
||||
onClick: () => {
|
||||
this.props.deleteCell();
|
||||
this.props.traceNotebookTelemetry(Action.DeleteCellFromMenu, ActionModifiers.Mark);
|
||||
}
|
||||
});
|
||||
|
||||
const menuItemLabel = "More";
|
||||
@@ -156,7 +186,9 @@ const mapDispatchToProps = (
|
||||
moveCell: (destinationId: CellId, above: boolean) =>
|
||||
dispatch(actions.moveCell({ id, contentRef, destinationId, above })),
|
||||
clearOutputs: () => dispatch(actions.clearOutputs({ id, contentRef })),
|
||||
deleteCell: () => dispatch(actions.deleteCell({ id, contentRef }))
|
||||
deleteCell: () => dispatch(actions.deleteCell({ id, contentRef })),
|
||||
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: any) =>
|
||||
dispatch(cdbActions.traceNotebookTelemetry({ action, actionModifier, data }))
|
||||
});
|
||||
|
||||
const makeMapStateToProps = (state: AppState, ownProps: ComponentProps): ((state: AppState) => StateProps) => {
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
.nteract-cell-input .nteract-cell-source {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/** Adaptation for the R kernel's inline lists **/
|
||||
|
||||
@@ -40,7 +40,7 @@ describe("Add Collection Pane", () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
||||
explorer = new Explorer();
|
||||
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||
});
|
||||
|
||||
|
||||
@@ -101,12 +101,10 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
public showUpsellMessage: ko.PureComputed<boolean>;
|
||||
public shouldCreateMongoWildcardIndex: ko.Observable<boolean>;
|
||||
|
||||
private _databaseOffers: HashMap<DataModels.Offer>;
|
||||
private _isSynapseLinkEnabled: ko.Computed<boolean>;
|
||||
|
||||
constructor(options: AddCollectionPaneOptions) {
|
||||
super(options);
|
||||
this._databaseOffers = new HashMap<DataModels.Offer>();
|
||||
this.hasAutoPilotV2FeatureFlag = ko.pureComputed(() => this.container.hasAutoPilotV2FeatureFlag());
|
||||
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText(this.hasAutoPilotV2FeatureFlag()));
|
||||
this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled());
|
||||
@@ -327,7 +325,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
|
||||
this.canRequestSupport = ko.pureComputed(() => {
|
||||
if (
|
||||
!this.container.isEmulator &&
|
||||
configContext.platform !== Platform.Emulator &&
|
||||
!this.container.isTryCosmosDBSubscription() &&
|
||||
this.container.getPlatformType() !== PlatformType.Portal
|
||||
) {
|
||||
@@ -339,7 +337,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
});
|
||||
|
||||
this.costsVisible = ko.pureComputed(() => {
|
||||
return !this.container.isEmulator;
|
||||
return configContext.platform !== Platform.Emulator;
|
||||
});
|
||||
|
||||
this.maxCollectionsReached = ko.computed<boolean>(() => {
|
||||
@@ -481,7 +479,10 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
}
|
||||
|
||||
if (!this.databaseCreateNew()) {
|
||||
this.databaseHasSharedOffer(this._databaseOffers.has(selectedDatabaseId));
|
||||
const selectedDatabase: ViewModels.Database = this.container
|
||||
.databases()
|
||||
.find((database: ViewModels.Database) => database.id() === selectedDatabaseId);
|
||||
this.databaseHasSharedOffer(!!selectedDatabase?.offer());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -749,15 +750,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
}
|
||||
|
||||
private _onDatabasesChange(newDatabaseIds: ViewModels.Database[]) {
|
||||
const cachedDatabaseIdsList = _.map(newDatabaseIds, (database: ViewModels.Database) => {
|
||||
if (database && database.offer && database.offer()) {
|
||||
this._databaseOffers.set(database.id(), database.offer());
|
||||
}
|
||||
|
||||
return database.id();
|
||||
});
|
||||
|
||||
this.databaseIds(cachedDatabaseIdsList);
|
||||
this.databaseIds(newDatabaseIds?.map((database: ViewModels.Database) => database.id()));
|
||||
}
|
||||
|
||||
private _computeOfferThroughput(): number {
|
||||
|
||||
@@ -40,10 +40,7 @@ describe("Add Database Pane", () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer({
|
||||
notificationsClient: null,
|
||||
isEmulator: false
|
||||
});
|
||||
explorer = new Explorer();
|
||||
});
|
||||
|
||||
it("should be true if subscription type is Benefits", () => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
|
||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
import { createDatabase } from "../../Common/dataAccess/createDatabase";
|
||||
import { PlatformType } from "../../PlatformType";
|
||||
import { configContext, Platform } from "../../ConfigContext";
|
||||
|
||||
export default class AddDatabasePane extends ContextualPaneBase {
|
||||
public defaultExperience: ko.Computed<string>;
|
||||
@@ -180,7 +181,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
|
||||
this.canRequestSupport = ko.pureComputed(() => {
|
||||
if (
|
||||
!this.container.isEmulator &&
|
||||
configContext.platform !== Platform.Emulator &&
|
||||
!this.container.isTryCosmosDBSubscription() &&
|
||||
this.container.getPlatformType() !== PlatformType.Portal
|
||||
) {
|
||||
@@ -203,7 +204,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
});
|
||||
|
||||
this.costsVisible = ko.pureComputed(() => {
|
||||
return !this.container.isEmulator;
|
||||
return configContext.platform !== Platform.Emulator;
|
||||
});
|
||||
|
||||
this.throughputSpendAckVisible = ko.pureComputed<boolean>(() => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
|
||||
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
|
||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
import { HashMap } from "../../Common/HashMap";
|
||||
import { configContext, Platform } from "../../ConfigContext";
|
||||
|
||||
export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
public createTableQuery: ko.Observable<string>;
|
||||
@@ -231,11 +232,11 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
});
|
||||
|
||||
this.costsVisible = ko.pureComputed(() => {
|
||||
return !this.container.isEmulator;
|
||||
return configContext.platform !== Platform.Emulator;
|
||||
});
|
||||
|
||||
this.canRequestSupport = ko.pureComputed(() => {
|
||||
if (!this.container.isEmulator && !this.container.isTryCosmosDBSubscription()) {
|
||||
if (configContext.platform !== Platform.Emulator && !this.container.isTryCosmosDBSubscription()) {
|
||||
const offerThroughput: number = this.throughput();
|
||||
return offerThroughput <= 100000;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
let explorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
||||
explorer = new Explorer();
|
||||
});
|
||||
|
||||
it("should be true if 1 database and 1 collection", () => {
|
||||
@@ -56,7 +56,7 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
|
||||
describe("shouldRecordFeedback()", () => {
|
||||
it("should return true if last collection and database does not have shared throughput else false", () => {
|
||||
let fakeExplorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
||||
let fakeExplorer = new Explorer();
|
||||
fakeExplorer.isNotificationConsoleExpanded = ko.observable<boolean>(false);
|
||||
fakeExplorer.refreshAllDatabases = () => Q.resolve();
|
||||
|
||||
|
||||
@@ -66,7 +66,11 @@ export default class DeleteCollectionConfirmationPane extends ContextualPaneBase
|
||||
this.isExecuting(false);
|
||||
this.close();
|
||||
this.container.selectedNode(selectedCollection.database);
|
||||
this.container.tabsManager?.closeTabsByComparator(tab => tab.node && tab.node.rid === selectedCollection.rid);
|
||||
this.container.tabsManager?.closeTabsByComparator(
|
||||
tab =>
|
||||
tab.node?.id() === selectedCollection.id() &&
|
||||
(tab.node as ViewModels.Collection).databaseId === selectedCollection.databaseId
|
||||
);
|
||||
this.container.refreshAllDatabases();
|
||||
this.resetData();
|
||||
TelemetryProcessor.traceSuccess(
|
||||
|
||||
@@ -22,7 +22,7 @@ describe("Delete Database Confirmation Pane", () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
||||
explorer = new Explorer();
|
||||
});
|
||||
|
||||
it("should be true if only 1 database", () => {
|
||||
|
||||
@@ -69,12 +69,16 @@ export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
|
||||
this.isExecuting(false);
|
||||
this.close();
|
||||
this.container.refreshAllDatabases();
|
||||
this.container.tabsManager.closeTabsByComparator(tab => tab.node && tab.node.rid === selectedDatabase.rid);
|
||||
this.container.tabsManager.closeTabsByComparator(tab => tab.node?.id() === selectedDatabase.id());
|
||||
this.container.selectedNode(null);
|
||||
selectedDatabase
|
||||
.collections()
|
||||
.forEach((collection: ViewModels.Collection) =>
|
||||
this.container.tabsManager.closeTabsByComparator(tab => tab.node && tab.node.rid === collection.rid)
|
||||
this.container.tabsManager.closeTabsByComparator(
|
||||
tab =>
|
||||
tab.node?.id() === collection.id() &&
|
||||
(tab.node as ViewModels.Collection).databaseId === collection.databaseId
|
||||
)
|
||||
);
|
||||
this.resetData();
|
||||
TelemetryProcessor.traceSuccess(
|
||||
|
||||
@@ -7,7 +7,7 @@ describe("Settings Pane", () => {
|
||||
let explorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
||||
explorer = new Explorer();
|
||||
});
|
||||
|
||||
it("should be true for SQL API", () => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import Explorer from "../Explorer";
|
||||
jest.mock("../Explorer");
|
||||
|
||||
const createExplorer = () => {
|
||||
const mock = new Explorer({} as any);
|
||||
const mock = new Explorer();
|
||||
mock.selectedNode = ko.observable();
|
||||
mock.isNotebookEnabled = ko.observable(false);
|
||||
mock.addCollectionText = ko.observable("add collection");
|
||||
|
||||
@@ -71,6 +71,8 @@ export var htmlSelectors = {
|
||||
dataTableScrollContainerSelector: ".dataTables_scroll",
|
||||
dataTableHeaderTypeSelector: "table thead th",
|
||||
dataTablePaginationButtonSelector: ".paginate_button",
|
||||
dataTableHeaderTableSelector: "#storageTable_wrapper .dataTables_scrollHeadInner table",
|
||||
dataTableBodyTableSelector: "#storageTable_wrapper .dataTables_scrollBody table",
|
||||
searchInputField: ".search-input",
|
||||
uploadDropdownSelector: "#upload-dropdown",
|
||||
navigationDropdownSelector: "#navigation-dropdown",
|
||||
|
||||
18
src/Explorer/Tables/CqlUtilities.test.ts
Normal file
18
src/Explorer/Tables/CqlUtilities.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { getQuotedCqlIdentifier } from "./CqlUtilities";
|
||||
|
||||
describe("getQuotedCqlIdentifier", () => {
|
||||
it("undefined id", () => {
|
||||
const result = getQuotedCqlIdentifier(undefined);
|
||||
expect(result).toBe(undefined);
|
||||
});
|
||||
|
||||
it("id with no quotes", () => {
|
||||
const result = getQuotedCqlIdentifier("foo");
|
||||
expect(result).toBe('"foo"');
|
||||
});
|
||||
|
||||
it("id with quotes", () => {
|
||||
const result = getQuotedCqlIdentifier('"foo"');
|
||||
expect(result).toBe('"""foo"""');
|
||||
});
|
||||
});
|
||||
12
src/Explorer/Tables/CqlUtilities.ts
Normal file
12
src/Explorer/Tables/CqlUtilities.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export function getQuotedCqlIdentifier(identifier: string): string {
|
||||
let result = identifier;
|
||||
if (!identifier) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (identifier.includes('"')) {
|
||||
result = identifier.replace(/"/g, '""');
|
||||
}
|
||||
|
||||
return `"${result}"`;
|
||||
}
|
||||
@@ -143,6 +143,21 @@ function createDataTable(
|
||||
fnInitComplete: initializeTable,
|
||||
fnDrawCallback: updateSelectionStatus
|
||||
});
|
||||
|
||||
(tableEntityListViewModel.table.table(0).container() as Element)
|
||||
.querySelectorAll(Constants.htmlSelectors.dataTableHeaderTableSelector)
|
||||
.forEach(table => {
|
||||
table.setAttribute(
|
||||
"summary",
|
||||
`Header for sorting results for container ${tableEntityListViewModel.queryTablesTab.collection.id()}`
|
||||
);
|
||||
});
|
||||
|
||||
(tableEntityListViewModel.table.table(0).container() as Element)
|
||||
.querySelectorAll(Constants.htmlSelectors.dataTableBodyTableSelector)
|
||||
.forEach(table => {
|
||||
table.setAttribute("summary", `Results for container ${tableEntityListViewModel.queryTablesTab.collection.id()}`);
|
||||
});
|
||||
}
|
||||
|
||||
function bindColumn(data: any, type: string, full: any) {
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
import Q from "q";
|
||||
|
||||
import * as Constants from "../Constants";
|
||||
import TableCommands from "./TableCommands";
|
||||
import TableEntityListViewModel from "./TableEntityListViewModel";
|
||||
|
||||
/*
|
||||
* ContextMenu view representation
|
||||
*/
|
||||
export default class DataTableContextMenu {
|
||||
public viewModel: TableEntityListViewModel;
|
||||
|
||||
// There is one context menu for each selector on each tab and they should all be registered here.
|
||||
// Once the context menus are registered, we should access them through this instance.
|
||||
public static Instance: { [key: string]: { contextMenu: DataTableContextMenu } } = {};
|
||||
|
||||
private _tableCommands: TableCommands;
|
||||
|
||||
constructor(viewModel: TableEntityListViewModel, tableCommands: TableCommands) {
|
||||
this.viewModel = viewModel;
|
||||
this._tableCommands = tableCommands;
|
||||
|
||||
this.registerTableBodyContextMenu();
|
||||
this.registerTableHeaderContextMenu();
|
||||
|
||||
DataTableContextMenu.Instance[viewModel.queryTablesTab.tabId] = { contextMenu: this };
|
||||
}
|
||||
|
||||
public unregisterContextMenu(selector: string): void {
|
||||
$.contextMenu("destroy", "div#" + this.viewModel.queryTablesTab.tabId + ".tab-pane " + selector);
|
||||
}
|
||||
|
||||
public registerTableBodyContextMenu(): void {
|
||||
// Localize
|
||||
$.contextMenu({
|
||||
selector:
|
||||
"div#" + this.viewModel.queryTablesTab.tabId + ".tab-pane " + Constants.htmlSelectors.dataTableBodyRowSelector,
|
||||
callback: this.bodyContextMenuSelect,
|
||||
items: {
|
||||
edit: {
|
||||
name: "Edit",
|
||||
cmd: TableCommands.editEntityCommand,
|
||||
icon: "edit-entity",
|
||||
disabled: () => !this.isEnabled(TableCommands.editEntityCommand)
|
||||
},
|
||||
delete: {
|
||||
name: "Delete",
|
||||
cmd: TableCommands.deleteEntitiesCommand,
|
||||
icon: "delete-entity",
|
||||
disabled: () => !this.isEnabled(TableCommands.deleteEntitiesCommand)
|
||||
},
|
||||
reorder: {
|
||||
name: "Reorder Columns Based on Schema",
|
||||
cmd: TableCommands.reorderColumnsCommand,
|
||||
icon: "shift-non-empty-columns-left",
|
||||
disabled: () => !this.isEnabled(TableCommands.reorderColumnsCommand)
|
||||
},
|
||||
reset: {
|
||||
name: "Reset Columns",
|
||||
cmd: TableCommands.resetColumnsCommand,
|
||||
icon: "reset-column-order"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public registerTableHeaderContextMenu(): void {
|
||||
// Localize
|
||||
$.contextMenu({
|
||||
selector:
|
||||
"div#" + this.viewModel.queryTablesTab.tabId + ".tab-pane " + Constants.htmlSelectors.dataTableHeadRowSelector,
|
||||
callback: this.headerContextMenuSelect,
|
||||
items: {
|
||||
customizeColumns: {
|
||||
name: "Column Options",
|
||||
cmd: TableCommands.customizeColumnsCommand,
|
||||
icon: "customize-columns"
|
||||
},
|
||||
reset: {
|
||||
name: "Reset Columns",
|
||||
cmd: TableCommands.resetColumnsCommand,
|
||||
icon: "reset-column-order"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private isEnabled(commandName: string): boolean {
|
||||
return this._tableCommands.isEnabled(commandName, this.viewModel.selected());
|
||||
}
|
||||
|
||||
private headerContextMenuSelect = (key: any, options: any): void => {
|
||||
var promise: Q.Promise<any> = null;
|
||||
|
||||
switch (key) {
|
||||
case TableCommands.customizeColumnsCommand:
|
||||
promise = this._tableCommands.customizeColumnsCommand(this.viewModel);
|
||||
break;
|
||||
case TableCommands.resetColumnsCommand:
|
||||
promise = Q.resolve(this._tableCommands.resetColumns(this.viewModel));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (promise) {
|
||||
promise.then(() => {
|
||||
this.viewModel.focusDataTable();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private bodyContextMenuSelect = (key: any, options: any): void => {
|
||||
var promise: Q.Promise<any> = null;
|
||||
|
||||
switch (key) {
|
||||
case TableCommands.editEntityCommand:
|
||||
promise = this._tableCommands.editEntityCommand(this.viewModel);
|
||||
break;
|
||||
case TableCommands.deleteEntitiesCommand:
|
||||
promise = this._tableCommands.deleteEntitiesCommand(this.viewModel);
|
||||
break;
|
||||
case TableCommands.reorderColumnsCommand:
|
||||
promise = this._tableCommands.reorderColumnsBasedOnSelectedEntities(this.viewModel);
|
||||
break;
|
||||
case TableCommands.resetColumnsCommand:
|
||||
promise = Q.resolve(this._tableCommands.resetColumns(this.viewModel));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (promise) {
|
||||
promise.then(() => {
|
||||
this.viewModel.focusDataTable();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A context menu factory to construct the one context menu for each tab/table view model.
|
||||
*/
|
||||
public static contextMenuFactory(viewModel: TableEntityListViewModel, tableCommands: TableCommands) {
|
||||
if (!DataTableContextMenu.Instance[viewModel.queryTablesTab.tabId]) {
|
||||
DataTableContextMenu.Instance[viewModel.queryTablesTab.tabId] = {
|
||||
contextMenu: new DataTableContextMenu(viewModel, tableCommands)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,18 +41,6 @@ export default class DataTableOperationManager {
|
||||
this.tryOpenEditor();
|
||||
};
|
||||
|
||||
private contextMenu = (event: JQueryEventObject) => {
|
||||
var elem: JQuery = $(event.currentTarget);
|
||||
this.updateLastSelectedItem(elem, event.shiftKey);
|
||||
|
||||
this.applyContextMenuSelection(elem);
|
||||
setTimeout(function() {
|
||||
$(".context-menu-list")
|
||||
.attr("tabindex", -1)
|
||||
.focus();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
private keyDown = (event: JQueryEventObject): boolean => {
|
||||
var isUpArrowKey: boolean = event.keyCode === Constants.keyCodes.UpArrow,
|
||||
isDownArrowKey: boolean = event.keyCode === Constants.keyCodes.DownArrow,
|
||||
@@ -293,7 +281,6 @@ export default class DataTableOperationManager {
|
||||
public bind() {
|
||||
this.dataTable.on("click", "tr", this.click);
|
||||
this.dataTable.on("dblclick", "tr", this.doubleClick);
|
||||
this.dataTable.on("contextmenu", "tr", this.contextMenu);
|
||||
this.dataTable.on("keydown", "td", this.keyDown);
|
||||
this.dataTable.on("keyup", "td", this.keyUp);
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import Q from "q";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { CassandraTableKey, CassandraAPIDataClient } from "../TableDataClient";
|
||||
import DataTableViewModel from "./DataTableViewModel";
|
||||
import DataTableContextMenu from "./DataTableContextMenu";
|
||||
import * as DataTableUtilities from "./DataTableUtilities";
|
||||
import { getQuotedCqlIdentifier } from "../CqlUtilities";
|
||||
import TableCommands from "./TableCommands";
|
||||
import TableEntityCache from "./TableEntityCache";
|
||||
import * as Constants from "../Constants";
|
||||
@@ -56,11 +56,11 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
||||
this.cache = new TableEntityCache();
|
||||
this.queryErrorMessage = ko.observable<string>();
|
||||
this.queryTablesTab = queryTablesTab;
|
||||
// Enable Context menu for the data table.
|
||||
DataTableContextMenu.contextMenuFactory(this, tableCommands);
|
||||
this.id = `tableEntityListViewModel${this.queryTablesTab.tabId}`;
|
||||
this.cqlQuery = ko.observable<string>(
|
||||
`SELECT * FROM ${this.queryTablesTab.collection.databaseId}.${this.queryTablesTab.collection.id()}`
|
||||
`SELECT * FROM ${getQuotedCqlIdentifier(this.queryTablesTab.collection.databaseId)}.${getQuotedCqlIdentifier(
|
||||
this.queryTablesTab.collection.id()
|
||||
)}`
|
||||
);
|
||||
this.oDataQuery = ko.observable<string>();
|
||||
this.sqlQuery = ko.observable<string>("SELECT * FROM c");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as ko from "knockout";
|
||||
import * as CustomTimestampHelper from "./CustomTimestampHelper";
|
||||
import { getQuotedCqlIdentifier } from "../CqlUtilities";
|
||||
import QueryClauseViewModel from "./QueryClauseViewModel";
|
||||
import ClauseGroup from "./ClauseGroup";
|
||||
import ClauseGroupViewModel from "./ClauseGroupViewModel";
|
||||
@@ -237,7 +238,7 @@ export default class QueryBuilderViewModel {
|
||||
public getCqlFilterFromClauses = (): string => {
|
||||
const databaseId = this._queryViewModel.queryTablesTab.collection.databaseId;
|
||||
const collectionId = this._queryViewModel.queryTablesTab.collection.id();
|
||||
const tableToQuery = `${databaseId}.${collectionId}`;
|
||||
const tableToQuery = `${getQuotedCqlIdentifier(databaseId)}.${getQuotedCqlIdentifier(collectionId)}`;
|
||||
var filterString: string = `SELECT * FROM ${tableToQuery}`;
|
||||
if (this._queryViewModel.selectText() && this._queryViewModel.selectText().length > 0) {
|
||||
filterString = "SELECT";
|
||||
|
||||
@@ -7,6 +7,7 @@ import TableEntityListViewModel from "../DataTable/TableEntityListViewModel";
|
||||
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
||||
import * as DataTableUtilities from "../DataTable/DataTableUtilities";
|
||||
import { KeyCodes } from "../../../Common/Constants";
|
||||
import { getQuotedCqlIdentifier } from "../CqlUtilities";
|
||||
|
||||
export default class QueryViewModel {
|
||||
public topValueLimitMessage: string = "Please input a number between 0 and 1000.";
|
||||
@@ -189,7 +190,9 @@ export default class QueryViewModel {
|
||||
this._tableEntityListViewModel.oDataQuery("");
|
||||
this._tableEntityListViewModel.sqlQuery("SELECT * FROM c");
|
||||
this._tableEntityListViewModel.cqlQuery(
|
||||
`SELECT * FROM ${this.queryTablesTab.collection.databaseId}.${this.queryTablesTab.collection.id()}`
|
||||
`SELECT * FROM ${getQuotedCqlIdentifier(this.queryTablesTab.collection.databaseId)}.${getQuotedCqlIdentifier(
|
||||
this.queryTablesTab.collection.id()
|
||||
)}`
|
||||
);
|
||||
return this._tableEntityListViewModel.reloadTable(false);
|
||||
};
|
||||
|
||||
@@ -58,7 +58,6 @@ export default class ConflictsTab extends TabsBase {
|
||||
private _documentsIterator: MinimalQueryIterator;
|
||||
private _container: Explorer;
|
||||
private _acceptButtonLabel: ko.Observable<string> = ko.observable("Save");
|
||||
protected _selfLink: string;
|
||||
|
||||
constructor(options: ViewModels.ConflictsTabOptions) {
|
||||
super(options);
|
||||
@@ -74,7 +73,6 @@ export default class ConflictsTab extends TabsBase {
|
||||
this.selectedConflictCurrent = editable.observable<any>("");
|
||||
this.partitionKey = options.partitionKey || (this.collection && this.collection.partitionKey);
|
||||
this.conflictIds = options.conflictIds;
|
||||
this._selfLink = options.selfLink || (this.collection && this.collection.self);
|
||||
this.partitionKeyPropertyHeader =
|
||||
(this.collection && this.collection.partitionKeyPropertyHeader) || this._getPartitionKeyPropertyHeader();
|
||||
this.partitionKeyProperty = !!this.partitionKeyPropertyHeader
|
||||
|
||||
@@ -16,10 +16,11 @@ import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import { PlatformType } from "../../PlatformType";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import Explorer from "../Explorer";
|
||||
import { updateOffer } from "../../Common/DocumentClientUtilityBase";
|
||||
import { updateOffer } from "../../Common/dataAccess/updateOffer";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { updateOfferThroughputBeyondLimit } from "../../Common/dataAccess/updateOfferThroughputBeyondLimit";
|
||||
import { configContext, Platform } from "../../ConfigContext";
|
||||
|
||||
const updateThroughputBeyondLimitWarningMessage: string = `
|
||||
You are about to request an increase in throughput beyond the pre-allocated capacity.
|
||||
@@ -196,7 +197,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
});
|
||||
|
||||
this.costsVisible = ko.computed(() => {
|
||||
return !this.container.isEmulator;
|
||||
return configContext.platform !== Platform.Emulator;
|
||||
});
|
||||
|
||||
this.shouldDisplayPortalUsePrompt = ko.pureComputed<boolean>(
|
||||
@@ -207,7 +208,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
);
|
||||
this.canRequestSupport = ko.pureComputed(() => {
|
||||
if (
|
||||
!!this.container.isEmulator ||
|
||||
configContext.platform === Platform.Emulator ||
|
||||
this.container.getPlatformType() === PlatformType.Hosted ||
|
||||
this.canThroughputExceedMaximumValue()
|
||||
) {
|
||||
@@ -455,8 +456,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
this._buildCommandBarOptions();
|
||||
}
|
||||
|
||||
public onSaveClick = (): Q.Promise<any> => {
|
||||
let promises: Q.Promise<void>[] = [];
|
||||
public onSaveClick = async (): Promise<any> => {
|
||||
this.isExecutionError(false);
|
||||
|
||||
this.isExecuting(true);
|
||||
@@ -470,163 +470,81 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
|
||||
const headerOptions: RequestOptions = { initialHeaders: {} };
|
||||
|
||||
if (this.isAutoPilotSelected()) {
|
||||
const offer = this.database.offer();
|
||||
let offerAutopilotSettings: any = {};
|
||||
if (!this.hasAutoPilotV2FeatureFlag()) {
|
||||
offerAutopilotSettings.maxThroughput = this.autoPilotThroughput();
|
||||
try {
|
||||
if (this.isAutoPilotSelected()) {
|
||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||
databaseId: this.database.id(),
|
||||
currentOffer: this.database.offer(),
|
||||
autopilotThroughput: this.autoPilotThroughput(),
|
||||
manualThroughput: undefined,
|
||||
migrateToAutoPilot: this._hasProvisioningTypeChanged()
|
||||
};
|
||||
|
||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||
this.database.offer(updatedOffer);
|
||||
this.database.offer.valueHasMutated();
|
||||
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
||||
} else {
|
||||
offerAutopilotSettings.tier = this.selectedAutoPilotTier();
|
||||
}
|
||||
const newOffer: DataModels.Offer = {
|
||||
content: {
|
||||
offerThroughput: undefined,
|
||||
offerIsRUPerMinuteThroughputEnabled: false,
|
||||
offerAutopilotSettings
|
||||
},
|
||||
_etag: undefined,
|
||||
_ts: undefined,
|
||||
_rid: offer._rid,
|
||||
_self: offer._self,
|
||||
id: offer.id,
|
||||
offerResourceId: offer.offerResourceId,
|
||||
offerVersion: offer.offerVersion,
|
||||
offerType: offer.offerType,
|
||||
resource: offer.resource
|
||||
};
|
||||
if (this.throughput.editableIsDirty() || this.isAutoPilotSelected.editableIsDirty()) {
|
||||
const originalThroughputValue = this.throughput.getEditableOriginalValue();
|
||||
const newThroughput = this.throughput();
|
||||
|
||||
// user has changed from provisioned --> autoscale
|
||||
if (!this.hasAutoPilotV2FeatureFlag() && this._hasProvisioningTypeChanged()) {
|
||||
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToAutopilot] = "true";
|
||||
delete newOffer.content.offerAutopilotSettings;
|
||||
}
|
||||
|
||||
const updateOfferPromise = updateOffer(this.database.offer(), newOffer, headerOptions).then(
|
||||
(updatedOffer: DataModels.Offer) => {
|
||||
this.database.offer(updatedOffer);
|
||||
this.database.offer.valueHasMutated();
|
||||
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
||||
}
|
||||
);
|
||||
promises.push(updateOfferPromise);
|
||||
} else {
|
||||
if (this.throughput.editableIsDirty() || this.isAutoPilotSelected.editableIsDirty()) {
|
||||
const offer = this.database.offer();
|
||||
const originalThroughputValue = this.throughput.getEditableOriginalValue();
|
||||
const newThroughput = this.throughput();
|
||||
|
||||
if (
|
||||
this.canThroughputExceedMaximumValue() &&
|
||||
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million
|
||||
) {
|
||||
const requestPayload = {
|
||||
subscriptionId: userContext.subscriptionId,
|
||||
databaseAccountName: userContext.databaseAccount.name,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
databaseName: this.database.id(),
|
||||
throughput: newThroughput,
|
||||
offerIsRUPerMinuteThroughputEnabled: false
|
||||
};
|
||||
const updateOfferBeyondLimitPromise = updateOfferThroughputBeyondLimit(requestPayload).then(
|
||||
() => {
|
||||
this.database.offer().content.offerThroughput = originalThroughputValue;
|
||||
this.throughput(originalThroughputValue);
|
||||
this.notificationStatusInfo(
|
||||
throughputApplyDelayedMessage(this.isAutoPilotSelected(), newThroughput, this.database.id())
|
||||
);
|
||||
this.throughput.valueHasMutated(); // force component re-render
|
||||
},
|
||||
(error: any) => {
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.UpdateSettings,
|
||||
{
|
||||
databaseAccountName: this.container.databaseAccount().name,
|
||||
databaseName: this.database && this.database.id(),
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
error: error
|
||||
},
|
||||
startKey
|
||||
);
|
||||
}
|
||||
);
|
||||
promises.push(Q(updateOfferBeyondLimitPromise));
|
||||
} else {
|
||||
const newOffer: DataModels.Offer = {
|
||||
content: {
|
||||
offerThroughput: newThroughput,
|
||||
if (
|
||||
this.canThroughputExceedMaximumValue() &&
|
||||
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million
|
||||
) {
|
||||
const requestPayload = {
|
||||
subscriptionId: userContext.subscriptionId,
|
||||
databaseAccountName: userContext.databaseAccount.name,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
databaseName: this.database.id(),
|
||||
throughput: newThroughput,
|
||||
offerIsRUPerMinuteThroughputEnabled: false
|
||||
},
|
||||
_etag: undefined,
|
||||
_ts: undefined,
|
||||
_rid: offer._rid,
|
||||
_self: offer._self,
|
||||
id: offer.id,
|
||||
offerResourceId: offer.offerResourceId,
|
||||
offerVersion: offer.offerVersion,
|
||||
offerType: offer.offerType,
|
||||
resource: offer.resource
|
||||
};
|
||||
};
|
||||
await updateOfferThroughputBeyondLimit(requestPayload);
|
||||
this.database.offer().content.offerThroughput = originalThroughputValue;
|
||||
this.throughput(originalThroughputValue);
|
||||
this.notificationStatusInfo(
|
||||
throughputApplyDelayedMessage(this.isAutoPilotSelected(), newThroughput, this.database.id())
|
||||
);
|
||||
this.throughput.valueHasMutated(); // force component re-render
|
||||
} else {
|
||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||
databaseId: this.database.id(),
|
||||
currentOffer: this.database.offer(),
|
||||
autopilotThroughput: undefined,
|
||||
manualThroughput: newThroughput,
|
||||
migrateToManual: this._hasProvisioningTypeChanged()
|
||||
};
|
||||
|
||||
// user has changed from autoscale --> provisioned
|
||||
if (!this.hasAutoPilotV2FeatureFlag() && this._hasProvisioningTypeChanged()) {
|
||||
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true";
|
||||
newOffer.content.offerAutopilotSettings = { maxThroughput: 0 };
|
||||
const updatedOffer = await updateOffer(updateOfferParams);
|
||||
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
||||
this.database.offer(updatedOffer);
|
||||
this.database.offer.valueHasMutated();
|
||||
}
|
||||
|
||||
const updateOfferPromise = updateOffer(this.database.offer(), newOffer, headerOptions).then(
|
||||
(updatedOffer: DataModels.Offer) => {
|
||||
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
||||
this.database.offer(updatedOffer);
|
||||
this.database.offer.valueHasMutated();
|
||||
}
|
||||
);
|
||||
|
||||
promises.push(updateOfferPromise);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (promises.length === 0) {
|
||||
} catch (error) {
|
||||
this.container.isRefreshingExplorer(false);
|
||||
this.isExecutionError(true);
|
||||
console.error(error);
|
||||
this.displayedError(ErrorParserUtility.parse(error)[0].message);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.UpdateSettings,
|
||||
{
|
||||
databaseAccountName: this.container.databaseAccount().name,
|
||||
databaseName: this.database && this.database.id(),
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
error: error
|
||||
},
|
||||
startKey
|
||||
);
|
||||
} finally {
|
||||
this.isExecuting(false);
|
||||
}
|
||||
|
||||
return Q.all(promises)
|
||||
.then(
|
||||
() => {
|
||||
this.container.isRefreshingExplorer(false);
|
||||
this._setBaseline();
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.UpdateSettings,
|
||||
{
|
||||
databaseAccountName: this.container.databaseAccount().name,
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle()
|
||||
},
|
||||
startKey
|
||||
);
|
||||
},
|
||||
(reason: any) => {
|
||||
this.container.isRefreshingExplorer(false);
|
||||
this.isExecutionError(true);
|
||||
console.error(reason);
|
||||
this.displayedError(ErrorParserUtility.parse(reason)[0].message);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.UpdateSettings,
|
||||
{
|
||||
databaseAccountName: this.container.databaseAccount().name,
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle()
|
||||
},
|
||||
startKey
|
||||
);
|
||||
}
|
||||
)
|
||||
.finally(() => this.isExecuting(false));
|
||||
};
|
||||
|
||||
public onRevertClick = (): Q.Promise<any> => {
|
||||
|
||||
@@ -15,7 +15,6 @@ describe("Documents tab", () => {
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
selfLink: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable<boolean>(false),
|
||||
|
||||
@@ -27,15 +26,9 @@ describe("Documents tab", () => {
|
||||
});
|
||||
|
||||
describe("showPartitionKey", () => {
|
||||
const explorer = new Explorer({
|
||||
notificationsClient: null,
|
||||
isEmulator: false
|
||||
});
|
||||
const explorer = new Explorer();
|
||||
|
||||
const mongoExplorer = new Explorer({
|
||||
notificationsClient: null,
|
||||
isEmulator: false
|
||||
});
|
||||
const mongoExplorer = new Explorer();
|
||||
mongoExplorer.defaultExperience(Constants.DefaultAccountExperience.MongoDB);
|
||||
|
||||
const collectionWithoutPartitionKey = <ViewModels.Collection>(<unknown>{
|
||||
@@ -95,7 +88,6 @@ describe("Documents tab", () => {
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
selfLink: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable<boolean>(false),
|
||||
|
||||
@@ -113,7 +105,6 @@ describe("Documents tab", () => {
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
selfLink: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable<boolean>(false),
|
||||
|
||||
@@ -131,7 +122,6 @@ describe("Documents tab", () => {
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
selfLink: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable<boolean>(false),
|
||||
|
||||
@@ -149,7 +139,6 @@ describe("Documents tab", () => {
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
selfLink: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable<boolean>(false),
|
||||
|
||||
@@ -167,7 +156,6 @@ describe("Documents tab", () => {
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
selfLink: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable<boolean>(false),
|
||||
|
||||
|
||||
@@ -70,7 +70,6 @@ export default class DocumentsTab extends TabsBase {
|
||||
|
||||
private _documentsIterator: QueryIterator<ItemDefinition & Resource>;
|
||||
private _resourceTokenPartitionKey: string;
|
||||
protected _selfLink: string;
|
||||
|
||||
constructor(options: ViewModels.DocumentsTabOptions) {
|
||||
super(options);
|
||||
@@ -91,7 +90,6 @@ export default class DocumentsTab extends TabsBase {
|
||||
this.partitionKey = options.partitionKey || (this.collection && this.collection.partitionKey);
|
||||
this._resourceTokenPartitionKey = options.resourceTokenPartitionKey;
|
||||
this.documentIds = options.documentIds;
|
||||
this._selfLink = options.selfLink || (this.collection && this.collection.self);
|
||||
|
||||
this.partitionKeyPropertyHeader =
|
||||
(this.collection && this.collection.partitionKeyPropertyHeader) || this._getPartitionKeyPropertyHeader();
|
||||
|
||||
@@ -91,8 +91,6 @@ export default class GraphTab extends TabsBase {
|
||||
onIsFilterQueryLoading: (isFilterQueryLoading: boolean): void => this.isFilterQueryLoading(isFilterQueryLoading),
|
||||
onIsValidQuery: (isValidQuery: boolean): void => this.isValidQuery(isValidQuery),
|
||||
collectionPartitionKeyProperty: options.collectionPartitionKeyProperty,
|
||||
collectionRid: this.rid,
|
||||
collectionSelfLink: options.selfLink,
|
||||
graphBackendEndpoint: GraphTab.getGremlinEndpoint(options.account),
|
||||
databaseId: options.databaseId,
|
||||
collectionId: options.collectionId,
|
||||
|
||||
@@ -24,7 +24,6 @@ describe("Query Tab", () => {
|
||||
database: database,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
selfLink: "",
|
||||
isActive: ko.observable<boolean>(false),
|
||||
hashLocation: "",
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||
@@ -49,7 +48,7 @@ describe("Query Tab", () => {
|
||||
let explorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
||||
explorer = new Explorer();
|
||||
});
|
||||
|
||||
it("should be true for accounts using SQL API", () => {
|
||||
@@ -69,7 +68,7 @@ describe("Query Tab", () => {
|
||||
let explorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
||||
explorer = new Explorer();
|
||||
});
|
||||
|
||||
it("should be visible when using a supported API", () => {
|
||||
|
||||
@@ -55,7 +55,6 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
|
||||
protected monacoSettings: ViewModels.MonacoEditorSettings;
|
||||
private _executeQueryButtonTitle: ko.Observable<string>;
|
||||
protected _iterator: MinimalQueryIterator;
|
||||
private _selfLink: string;
|
||||
private _isSaveQueriesEnabled: ko.Computed<boolean>;
|
||||
private _resourceTokenPartitionKey: string;
|
||||
|
||||
@@ -86,7 +85,6 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
|
||||
this.errors = ko.observableArray<ViewModels.QueryError>([]);
|
||||
this._partitionKey = options.partitionKey;
|
||||
this._resourceTokenPartitionKey = options.resourceTokenPartitionKey;
|
||||
this._selfLink = options.selfLink;
|
||||
this.splitterId = this.tabId + "_splitter";
|
||||
this.isPreferredApiMongoDB = false;
|
||||
this.aggregatedQueryMetrics = ko.observable<DataModels.QueryMetrics>();
|
||||
|
||||
@@ -64,7 +64,6 @@ describe("Settings tab", () => {
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
selfLink: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: new Collection(
|
||||
@@ -79,7 +78,7 @@ describe("Settings tab", () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
||||
explorer = new Explorer();
|
||||
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||
});
|
||||
|
||||
@@ -178,7 +177,7 @@ describe("Settings tab", () => {
|
||||
let explorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
||||
explorer = new Explorer();
|
||||
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||
});
|
||||
|
||||
@@ -187,7 +186,6 @@ describe("Settings tab", () => {
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
selfLink: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||
@@ -209,8 +207,6 @@ describe("Settings tab", () => {
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
|
||||
selfLink: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||
@@ -227,8 +223,6 @@ describe("Settings tab", () => {
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
|
||||
selfLink: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||
@@ -256,7 +250,7 @@ describe("Settings tab", () => {
|
||||
let explorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
||||
explorer = new Explorer();
|
||||
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||
});
|
||||
|
||||
@@ -265,8 +259,6 @@ describe("Settings tab", () => {
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
|
||||
selfLink: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||
@@ -281,8 +273,6 @@ describe("Settings tab", () => {
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
|
||||
selfLink: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||
@@ -306,8 +296,6 @@ describe("Settings tab", () => {
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
|
||||
selfLink: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||
@@ -337,10 +325,7 @@ describe("Settings tab", () => {
|
||||
}
|
||||
|
||||
function getCollection(defaultApi: string, partitionKeyOption: PartitionKeyOption) {
|
||||
const explorer = new Explorer({
|
||||
notificationsClient: null,
|
||||
isEmulator: false
|
||||
});
|
||||
const explorer = new Explorer();
|
||||
explorer.defaultExperience(defaultApi);
|
||||
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||
|
||||
@@ -383,8 +368,6 @@ describe("Settings tab", () => {
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
|
||||
selfLink: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: getCollection(defaultApi, partitionKeyOption),
|
||||
@@ -470,10 +453,7 @@ describe("Settings tab", () => {
|
||||
|
||||
describe("AutoPilot", () => {
|
||||
function getCollection(autoPilotTier: DataModels.AutopilotTier) {
|
||||
const explorer = new Explorer({
|
||||
notificationsClient: null,
|
||||
isEmulator: false
|
||||
});
|
||||
const explorer = new Explorer();
|
||||
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
|
||||
|
||||
explorer.databaseAccount({
|
||||
@@ -526,8 +506,6 @@ describe("Settings tab", () => {
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
|
||||
selfLink: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: getCollection(autoPilotTier),
|
||||
|
||||
@@ -13,15 +13,16 @@ import Q from "q";
|
||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||
import TabsBase from "./TabsBase";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import { PlatformType } from "../../PlatformType";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import Explorer from "../Explorer";
|
||||
import { updateOffer } from "../../Common/DocumentClientUtilityBase";
|
||||
import { updateOffer } from "../../Common/dataAccess/updateOffer";
|
||||
import { updateCollection } from "../../Common/dataAccess/updateCollection";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { updateOfferThroughputBeyondLimit } from "../../Common/dataAccess/updateOfferThroughputBeyondLimit";
|
||||
import { configContext, Platform } from "../../ConfigContext";
|
||||
|
||||
const ttlWarning: string = `
|
||||
The system will automatically delete items based on the TTL value (in seconds) you provide, without needing a delete operation explicitly issued by a client application.
|
||||
@@ -454,7 +455,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
||||
});
|
||||
|
||||
this.rupmVisible = ko.computed(() => {
|
||||
if (this.container.isEmulator) {
|
||||
if (configContext.platform === Platform.Emulator) {
|
||||
return false;
|
||||
}
|
||||
if (this.container.isFeatureEnabled(Constants.Features.enableRupm)) {
|
||||
@@ -484,7 +485,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
||||
});
|
||||
|
||||
this.costsVisible = ko.computed(() => {
|
||||
return !this.container.isEmulator;
|
||||
return configContext.platform !== Platform.Emulator;
|
||||
});
|
||||
|
||||
this.isTryCosmosDBSubscription = ko.computed<boolean>(() => {
|
||||
@@ -500,7 +501,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
||||
});
|
||||
|
||||
this.canRequestSupport = ko.pureComputed(() => {
|
||||
if (this.container.isEmulator) {
|
||||
if (configContext.platform === Platform.Emulator) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -711,7 +712,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
||||
}
|
||||
|
||||
const isThroughputGreaterThanMaxRus = this.throughput() > this.maxRUs();
|
||||
const isEmulator = this.container.isEmulator;
|
||||
const isEmulator = configContext.platform === Platform.Emulator;
|
||||
if (isThroughputGreaterThanMaxRus && isEmulator) {
|
||||
return false;
|
||||
}
|
||||
@@ -881,7 +882,8 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
||||
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||
|
||||
const throughputExceedsMaxValue: boolean = !this.container.isEmulator && this.throughput() > this.maxRUs();
|
||||
const throughputExceedsMaxValue: boolean =
|
||||
configContext.platform !== Platform.Emulator && this.throughput() > this.maxRUs();
|
||||
|
||||
const ttlOptionDirty: boolean = this.timeToLive.editableIsDirty();
|
||||
const ttlOrIndexingPolicyFieldsDirty: boolean =
|
||||
@@ -1175,7 +1177,21 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
||||
);
|
||||
this.throughput.valueHasMutated(); // force component re-render
|
||||
} else {
|
||||
const updatedOffer: DataModels.Offer = await updateOffer(this.collection.offer(), newOffer, headerOptions);
|
||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||
databaseId: this.collection.databaseId,
|
||||
collectionId: this.collection.id(),
|
||||
currentOffer: this.collection.offer(),
|
||||
autopilotThroughput: this.isAutoPilotSelected() ? this.autoPilotThroughput() : undefined,
|
||||
manualThroughput: this.isAutoPilotSelected() ? undefined : newThroughput
|
||||
};
|
||||
if (this._hasProvisioningTypeChanged()) {
|
||||
if (this.isAutoPilotSelected()) {
|
||||
updateOfferParams.migrateToAutoPilot = true;
|
||||
} else {
|
||||
updateOfferParams.migrateToManual = true;
|
||||
}
|
||||
}
|
||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||
this.collection.offer(updatedOffer);
|
||||
this.collection.offer.valueHasMutated();
|
||||
}
|
||||
@@ -1217,6 +1233,9 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
||||
};
|
||||
|
||||
public onRevertClick = (): Q.Promise<any> => {
|
||||
TelemetryProcessor.trace(Action.DiscardSettings, ActionModifiers.Mark, {
|
||||
message: "Settings Discarded"
|
||||
});
|
||||
this.throughput.setBaseline(this.throughput.getEditableOriginalValue());
|
||||
this.timeToLive.setBaseline(this.timeToLive.getEditableOriginalValue());
|
||||
this.timeToLiveSeconds.setBaseline(this.timeToLiveSeconds.getEditableOriginalValue());
|
||||
|
||||
@@ -9,6 +9,7 @@ export default class SettingsTabV2 extends TabsBase {
|
||||
|
||||
constructor(options: ViewModels.TabOptions) {
|
||||
super(options);
|
||||
this.tabId = "SettingsV2-" + this.tabId;
|
||||
const props: SettingsComponentProps = {
|
||||
settingsTab: this
|
||||
};
|
||||
|
||||
@@ -88,7 +88,8 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
databaseAccountName: this.getContainer().databaseAccount().name,
|
||||
defaultExperience: this.getContainer().defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle()
|
||||
tabTitle: this.tabTitle(),
|
||||
tabId: this.tabId
|
||||
});
|
||||
}
|
||||
|
||||
@@ -145,7 +146,8 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
databaseAccountName: this.getContainer().databaseAccount().name,
|
||||
defaultExperience: this.getContainer().defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle()
|
||||
tabTitle: this.tabTitle(),
|
||||
tabId: this.tabId
|
||||
});
|
||||
return Q();
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ describe("Tabs manager tests", () => {
|
||||
let documentsTab: DocumentsTab;
|
||||
|
||||
beforeAll(() => {
|
||||
explorer = new Explorer({ notificationsClient: undefined, isEmulator: false });
|
||||
explorer = new Explorer();
|
||||
explorer.databaseAccount = ko.observable<DataModels.DatabaseAccount>({
|
||||
id: "test",
|
||||
name: "test",
|
||||
@@ -50,7 +50,6 @@ describe("Tabs manager tests", () => {
|
||||
database,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
selfLink: "",
|
||||
isActive: ko.observable<boolean>(false),
|
||||
hashLocation: "",
|
||||
onUpdateTabsButtons: undefined
|
||||
@@ -63,7 +62,6 @@ describe("Tabs manager tests", () => {
|
||||
collection,
|
||||
title: "",
|
||||
tabPath: "",
|
||||
selfLink: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable<boolean>(false),
|
||||
onUpdateTabsButtons: undefined
|
||||
|
||||
@@ -40,6 +40,7 @@ import { configContext } from "../../ConfigContext";
|
||||
import Explorer from "../Explorer";
|
||||
import { userContext } from "../../UserContext";
|
||||
import TabsBase from "../Tabs/TabsBase";
|
||||
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
|
||||
|
||||
export default class Collection implements ViewModels.Collection {
|
||||
public nodeKind: string;
|
||||
@@ -238,7 +239,9 @@ export default class Collection implements ViewModels.Collection {
|
||||
this.expandCollection();
|
||||
}
|
||||
this.container.onUpdateTabsButtons([]);
|
||||
this.container.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.rid === this.rid);
|
||||
this.container.tabsManager.refreshActiveTab(
|
||||
tab => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
|
||||
);
|
||||
}
|
||||
|
||||
public collapseCollection() {
|
||||
@@ -289,7 +292,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
|
||||
const documentsTabs: DocumentsTab[] = this.container.tabsManager.getTabs(
|
||||
ViewModels.CollectionTabKind.Documents,
|
||||
tab => tab.collection && tab.collection.rid === this.rid
|
||||
tab => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
|
||||
) as DocumentsTab[];
|
||||
let documentsTab: DocumentsTab = documentsTabs && documentsTabs[0];
|
||||
|
||||
@@ -311,8 +314,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
documentIds: ko.observableArray<DocumentId>([]),
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "Items",
|
||||
|
||||
selfLink: this.self,
|
||||
isActive: ko.observable<boolean>(false),
|
||||
collection: this,
|
||||
node: this,
|
||||
@@ -340,7 +341,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
|
||||
const conflictsTabs: ConflictsTab[] = this.container.tabsManager.getTabs(
|
||||
ViewModels.CollectionTabKind.Conflicts,
|
||||
tab => tab.collection && tab.collection.rid === this.rid
|
||||
tab => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
|
||||
) as ConflictsTab[];
|
||||
let conflictsTab: ConflictsTab = conflictsTabs && conflictsTabs[0];
|
||||
|
||||
@@ -362,8 +363,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
conflictIds: ko.observableArray<ConflictId>([]),
|
||||
tabKind: ViewModels.CollectionTabKind.Conflicts,
|
||||
title: "Conflicts",
|
||||
|
||||
selfLink: this.self,
|
||||
isActive: ko.observable<boolean>(false),
|
||||
collection: this,
|
||||
node: this,
|
||||
@@ -397,7 +396,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
|
||||
const queryTablesTabs: QueryTablesTab[] = this.container.tabsManager.getTabs(
|
||||
ViewModels.CollectionTabKind.QueryTables,
|
||||
tab => tab.collection && tab.collection.rid === this.rid
|
||||
tab => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
|
||||
) as QueryTablesTab[];
|
||||
let queryTablesTab: QueryTablesTab = queryTablesTabs && queryTablesTabs[0];
|
||||
|
||||
@@ -426,7 +425,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
collection: this,
|
||||
|
||||
node: this,
|
||||
selfLink: this.self,
|
||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/entities`,
|
||||
isActive: ko.observable(false),
|
||||
onLoadStartKey: startKey,
|
||||
@@ -451,7 +449,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
|
||||
const graphTabs: GraphTab[] = this.container.tabsManager.getTabs(
|
||||
ViewModels.CollectionTabKind.Graph,
|
||||
tab => tab.collection && tab.collection.rid === this.rid
|
||||
tab => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
|
||||
) as GraphTab[];
|
||||
let graphTab: GraphTab = graphTabs && graphTabs[0];
|
||||
|
||||
@@ -477,7 +475,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
tabPath: "",
|
||||
|
||||
collection: this,
|
||||
selfLink: this.self,
|
||||
masterKey: userContext.masterKey || "",
|
||||
collectionPartitionKeyProperty: this.partitionKeyProperty,
|
||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/graphs`,
|
||||
@@ -507,7 +504,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
|
||||
const mongoDocumentsTabs: MongoDocumentsTab[] = this.container.tabsManager.getTabs(
|
||||
ViewModels.CollectionTabKind.Documents,
|
||||
tab => tab.collection && tab.collection.rid === this.rid
|
||||
tab => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
|
||||
) as MongoDocumentsTab[];
|
||||
let mongoDocumentsTab: MongoDocumentsTab = mongoDocumentsTabs && mongoDocumentsTabs[0];
|
||||
|
||||
@@ -534,7 +531,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
collection: this,
|
||||
|
||||
node: this,
|
||||
selfLink: this.self,
|
||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoDocuments`,
|
||||
isActive: ko.observable(false),
|
||||
onLoadStartKey: startKey,
|
||||
@@ -560,7 +556,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
const tabTitle = !this.offer() ? "Settings" : "Scale & Settings";
|
||||
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
|
||||
const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Settings, tab => {
|
||||
return tab.collection && tab.collection.rid === this.rid;
|
||||
return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id();
|
||||
});
|
||||
|
||||
const traceStartData = {
|
||||
@@ -578,7 +574,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
tabPath: "",
|
||||
collection: this,
|
||||
node: this,
|
||||
selfLink: this.self,
|
||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/settings`,
|
||||
isActive: ko.observable(false),
|
||||
onUpdateTabsButtons: this.container.onUpdateTabsButtons
|
||||
@@ -671,7 +666,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
tabPath: "",
|
||||
collection: this,
|
||||
node: this,
|
||||
selfLink: this.self,
|
||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/query`,
|
||||
isActive: ko.observable(false),
|
||||
queryText: queryText,
|
||||
@@ -703,7 +697,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
tabPath: "",
|
||||
collection: this,
|
||||
node: this,
|
||||
selfLink: this.self,
|
||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoQuery`,
|
||||
isActive: ko.observable(false),
|
||||
partitionKey: collection.partitionKey,
|
||||
@@ -734,7 +727,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
title: title,
|
||||
tabPath: "",
|
||||
collection: this,
|
||||
selfLink: this.self,
|
||||
masterKey: userContext.masterKey || "",
|
||||
collectionPartitionKeyProperty: this.partitionKeyProperty,
|
||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/graphs`,
|
||||
@@ -758,7 +750,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
collection: this,
|
||||
node: this,
|
||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoShell`,
|
||||
selfLink: this.self,
|
||||
isActive: ko.observable(false),
|
||||
onUpdateTabsButtons: this.container.onUpdateTabsButtons
|
||||
});
|
||||
@@ -821,7 +812,9 @@ export default class Collection implements ViewModels.Collection {
|
||||
} else {
|
||||
this.expandStoredProcedures();
|
||||
}
|
||||
this.container.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.rid === this.rid);
|
||||
this.container.tabsManager.refreshActiveTab(
|
||||
tab => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
|
||||
);
|
||||
}
|
||||
|
||||
public expandStoredProcedures() {
|
||||
@@ -878,7 +871,9 @@ export default class Collection implements ViewModels.Collection {
|
||||
} else {
|
||||
this.expandUserDefinedFunctions();
|
||||
}
|
||||
this.container.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.rid === this.rid);
|
||||
this.container.tabsManager.refreshActiveTab(
|
||||
tab => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
|
||||
);
|
||||
}
|
||||
|
||||
public expandUserDefinedFunctions() {
|
||||
@@ -935,7 +930,9 @@ export default class Collection implements ViewModels.Collection {
|
||||
} else {
|
||||
this.expandTriggers();
|
||||
}
|
||||
this.container.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.rid === this.rid);
|
||||
this.container.tabsManager.refreshActiveTab(
|
||||
tab => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
|
||||
);
|
||||
}
|
||||
|
||||
public expandTriggers() {
|
||||
@@ -1028,26 +1025,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
this.uploadFiles(event.originalEvent.dataTransfer.files);
|
||||
}
|
||||
|
||||
public isCollectionNodeSelected(): boolean {
|
||||
return (
|
||||
this.isSubNodeSelected(ViewModels.CollectionTabKind.Query) ||
|
||||
(!this.isCollectionExpanded() &&
|
||||
this.container.selectedNode &&
|
||||
this.container.selectedNode() &&
|
||||
this.container.selectedNode().rid === this.rid &&
|
||||
this.container.selectedNode().nodeKind === "Collection")
|
||||
);
|
||||
}
|
||||
|
||||
public isSubNodeSelected(nodeKind: ViewModels.CollectionTabKind): boolean {
|
||||
return (
|
||||
this.container.selectedNode &&
|
||||
this.container.selectedNode() &&
|
||||
this.container.selectedNode().rid === this.rid &&
|
||||
this.selectedSubnodeKind() === nodeKind
|
||||
);
|
||||
}
|
||||
|
||||
public onDeleteCollectionContextMenuClick(source: ViewModels.Collection, event: MouseEvent | KeyboardEvent) {
|
||||
this.container.deleteCollectionConfirmationPane.open();
|
||||
}
|
||||
@@ -1213,7 +1190,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
}
|
||||
|
||||
const deferred: Q.Deferred<DataModels.Notification> = Q.defer<DataModels.Notification>();
|
||||
this.container.notificationsClient.fetchNotifications().then(
|
||||
fetchPortalNotifications().then(
|
||||
(notifications: DataModels.Notification[]) => {
|
||||
if (!notifications || notifications.length === 0) {
|
||||
deferred.resolve(undefined);
|
||||
@@ -1283,10 +1260,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
});
|
||||
}
|
||||
|
||||
protected _getOfferForCollection(offers: DataModels.Offer[], collection: DataModels.Collection): DataModels.Offer {
|
||||
return _.find(offers, (offer: DataModels.Offer) => offer.resource.indexOf(collection._rid) >= 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Top-level method that will open the correct tab type depending on account API
|
||||
*/
|
||||
|
||||
@@ -14,6 +14,8 @@ import * as Logger from "../../Common/Logger";
|
||||
import Explorer from "../Explorer";
|
||||
import { readCollections } from "../../Common/dataAccess/readCollections";
|
||||
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
|
||||
|
||||
export default class Database implements ViewModels.Database {
|
||||
public nodeKind: string;
|
||||
@@ -55,7 +57,7 @@ export default class Database implements ViewModels.Database {
|
||||
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
|
||||
const matchingTabs = this.container.tabsManager.getTabs(
|
||||
ViewModels.CollectionTabKind.DatabaseSettings,
|
||||
tab => tab.rid === this.rid
|
||||
tab => tab.node?.id() === this.id()
|
||||
);
|
||||
let settingsTab: DatabaseSettingsTab = matchingTabs && (matchingTabs[0] as DatabaseSettingsTab);
|
||||
if (!settingsTab) {
|
||||
@@ -77,7 +79,6 @@ export default class Database implements ViewModels.Database {
|
||||
rid: this.rid,
|
||||
database: this,
|
||||
hashLocation: `${Constants.HashRoutePrefixes.databasesWithId(this.id())}/settings`,
|
||||
selfLink: this.self,
|
||||
isActive: ko.observable(false),
|
||||
onLoadStartKey: startKey,
|
||||
onUpdateTabsButtons: this.container.onUpdateTabsButtons
|
||||
@@ -126,8 +127,8 @@ export default class Database implements ViewModels.Database {
|
||||
!this.isDatabaseExpanded() &&
|
||||
this.container.selectedNode &&
|
||||
this.container.selectedNode() &&
|
||||
this.container.selectedNode().rid === this.rid &&
|
||||
this.container.selectedNode().nodeKind === "Database"
|
||||
this.container.selectedNode().nodeKind === "Database" &&
|
||||
this.container.selectedNode().id() === this.id()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -215,8 +216,8 @@ export default class Database implements ViewModels.Database {
|
||||
}
|
||||
|
||||
const deferred: Q.Deferred<DataModels.Notification> = Q.defer<DataModels.Notification>();
|
||||
this.container.notificationsClient.fetchNotifications().then(
|
||||
(notifications: DataModels.Notification[]) => {
|
||||
fetchPortalNotifications().then(
|
||||
notifications => {
|
||||
if (!notifications || notifications.length === 0) {
|
||||
deferred.resolve(undefined);
|
||||
return;
|
||||
@@ -260,7 +261,7 @@ export default class Database implements ViewModels.Database {
|
||||
(collection: DataModels.Collection) => {
|
||||
const collectionExists = _.some(
|
||||
this.collections(),
|
||||
(existingCollection: Collection) => existingCollection.rid === collection._rid
|
||||
(existingCollection: Collection) => existingCollection.id() === collection.id
|
||||
);
|
||||
return !collectionExists;
|
||||
}
|
||||
@@ -270,7 +271,7 @@ export default class Database implements ViewModels.Database {
|
||||
ko.utils.arrayForEach(this.collections(), (collection: Collection) => {
|
||||
const collectionPresentInUpdatedList = _.some(
|
||||
updatedCollectionsList,
|
||||
(coll: DataModels.Collection) => coll._rid === collection.rid
|
||||
(coll: DataModels.Collection) => coll.id === collection.id()
|
||||
);
|
||||
if (!collectionPresentInUpdatedList) {
|
||||
collectionsToDelete.push(collection);
|
||||
@@ -296,7 +297,7 @@ export default class Database implements ViewModels.Database {
|
||||
const collectionsToKeep: Collection[] = [];
|
||||
|
||||
ko.utils.arrayForEach(this.collections(), (collection: Collection) => {
|
||||
const shouldRemoveCollection = _.some(collectionsToRemove, (coll: Collection) => coll.rid === collection.rid);
|
||||
const shouldRemoveCollection = _.some(collectionsToRemove, (coll: Collection) => coll.id() === collection.id());
|
||||
if (!shouldRemoveCollection) {
|
||||
collectionsToKeep.push(collection);
|
||||
}
|
||||
|
||||
@@ -94,7 +94,6 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
||||
tabPath: "",
|
||||
collection: this,
|
||||
node: this,
|
||||
selfLink: this.self,
|
||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/query`,
|
||||
isActive: ko.observable(false),
|
||||
queryText: queryText,
|
||||
@@ -121,7 +120,9 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
||||
|
||||
const documentsTabs: DocumentsTab[] = this.container.tabsManager.getTabs(
|
||||
ViewModels.CollectionTabKind.Documents,
|
||||
(tab: TabsBase) => tab.collection && tab.collection.rid === this.rid
|
||||
(tab: TabsBase) =>
|
||||
tab.collection?.id() === this.id() &&
|
||||
(tab.collection as ViewModels.CollectionBase).databaseId === this.databaseId
|
||||
) as DocumentsTab[];
|
||||
let documentsTab: DocumentsTab = documentsTabs && documentsTabs[0];
|
||||
|
||||
@@ -143,7 +144,6 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
||||
documentIds: ko.observableArray<DocumentId>([]),
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: "Items",
|
||||
selfLink: this.self,
|
||||
isActive: ko.observable<boolean>(false),
|
||||
collection: this,
|
||||
node: this,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user