Compare commits

...

24 Commits

Author SHA1 Message Date
Steve Faulkner
a266fdc89c Create codeql-analysis.yml 2020-10-14 14:58:44 -05:00
victor-meng
39f7ef331a Use RP to read databases for all API (#280)
Since I've cleaned up all the places that use `_rid` and `_self` of a database in my previous commit (d525afa142), we are safe to use RP to read databases for all APIs except Tables.

For Tables API, since RP doesn't support any database level operations, I have to hardcode the read database response. The only property we need in the Tables database is the id which is the string that we display in the resource tree.
2020-10-14 18:10:14 +00:00
victor-meng
9933a4988a Use SDK calls to read and update shared throughput for Tables API (#278)
RP does not supporting reading or updating database level throughput for Tables API so we have to switch back to using SDK calls for these operations.
2020-10-13 22:49:30 +00:00
victor-meng
d525afa142 Refactor code that uses the _rid and _self of a database or collection (#267) 2020-10-13 13:29:39 -07:00
Steve Faulkner
cfb9a0b321 Refactor NotificationsClient (#270) 2020-10-12 22:10:28 -05:00
Tanuj Mittal
3b64d75322 Add Report Abuse dialog for public gallery notebooks (#265)
![image](https://user-images.githubusercontent.com/693092/95408825-3975a680-08d5-11eb-812b-80f922ab9fc8.png)
2020-10-12 23:48:05 +00:00
Steve Faulkner
daba1c4ed4 Remove AutoMerge Github Action (#273)
Instead, we will be trying out the official Microsoft bot https://docs.opensource.microsoft.com/tools/fabricbot/index.html
2020-10-12 21:19:02 +00:00
victor-meng
a698e08638 Remove database offers cache and get offer directly from database (#268)
Currently we maintain a cache of all database offers which can be stale since we have moved to lazy loading the offers. Instead of reading the offer from the cache, we should just find the selected database and get the offer directly by calling `database.offer()`.
2020-10-12 21:00:47 +00:00
Garrett Ausfeldt
88d630fef4 Add summary to each table of the DataTable for narrator context (#238)
The DataTable control creates 3 tables in the DOM, one for the header, one for the body and one for the footer. Because of this when navigating through the tablem it says "leaving table", when it is really the same table. It seems this is not the default and because of the option **dom: "RZlfrtip"**, the DataTable is created this way. 

If I remove that setting, it will only create one table, BUT other things break, because there is a lot of custom DOM manipulation assuming the DOM was the way it was before (gross). This make me question if we wanted this on purpose to maybe solve a cross browser scrolling issue.

Instead I decided to leave it as is, until migrating to Microsoft's Fluent UI is prioritized. However I did add a summary attribute to each table, so that when listening to the narrator, it make more sense when leaving one part of the table into another part of the table. While not optimal, it should at least satisfy accessibility concern of it being confusing.
2020-10-12 20:30:37 +00:00
Armando Trejo Oliver
5ffa746adb Escape quotes in identifiers in CQL queries 2020-10-12 13:00:11 -05:00
Srinath Narayanan
a9a57f4ba9 Added telelmetry for settings v2 and v1 (#269)
* Added telelmetry for settings v2 and v1

* format errors fixed

* renamed actions
2020-10-12 09:01:00 -07:00
Steve Faulkner
47cc6fd7a8 Set default backend endpoint (#271) 2020-10-12 09:23:57 -05:00
Steve Faulkner
14cdf19efb Remove Explorer.isEmulator (#256) 2020-10-09 11:18:50 -05:00
Steve Faulkner
5191ae3f3a All events should trigger and ADO build 2020-10-09 09:47:52 -05:00
Steve Faulkner
ba862a8106 Remove jquery.contextMenu (#248) 2020-10-08 18:19:24 -05:00
Steve Faulkner
fe085b3e5a Restore AppInsights fetch telemetry (#263) 2020-10-08 17:55:02 -05:00
Srinath Narayanan
8028734cb0 Fixed settingsV2 bugs and added experimentation (#264)
* inital commit for flight tests

- FIxed bugs with settingstab v2

* minor edits

* removed console log

* fixed bug with autoscale throughput step increase

* resolved PR comments

* fixed compile error

* Added comment
2020-10-08 14:32:54 -07:00
Armando Trejo Oliver
444f663733 Update README.md (#255) 2020-10-08 15:57:38 -05:00
Laurent Nguyen
8c1ca35420 Fix double-scrollbar bug 2020-10-08 14:04:02 -05:00
Laurent Nguyen
b69174788d Add more Telemetry to Data Explorer (#242)
* Add Telemetry to command bar buttons

* Count and report # of files/notebooks/directories in myNotebook to telemetry

* Add resource tree clicks to Telemetry

* Log to Telemetry: opened notebook cell counts by type, kernelspec name

* Fix unit test

* Move Telemetry processor call in notebook traceNotebookTelemetry action from reducer to epic. Use action to trace other info.

* Fix react duplicate key error

* Log notebook cell context menu actions

* Reformat and cleanup

* Move resource tree tracing code out of render(). Only call once when tree is updated

* Fix build issues
2020-10-08 10:53:01 +02:00
Laurent Nguyen
ff03c79399 Fix horizontal scrollbar in notebook cell input issue (#260)
* Fix horizontal scrollbar in notebook cell input issue

* Cell input overflow visible
2020-10-08 09:17:46 +02:00
victor-meng
0382628249 Move update offers call to RP 2020-10-07 17:25:21 -05:00
Steve Faulkner
d346ebe054 Fix Blackforest Origin check (#261) 2020-10-07 15:11:11 -05:00
Zachary Foster
f5ecb8a04f Fixes e2e test input focus swapping (#262)
* Remove redundant E2E tests

* Remove deps

* Fixes e2e tests hopefully

Co-authored-by: Steve Faulkner <471400+southpolesteve@users.noreply.github.com>
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2020-10-07 15:41:38 -04:00
123 changed files with 1952 additions and 3433 deletions

View File

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

View File

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

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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[];
};

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -77,6 +77,9 @@ exports[`GalleryViewerComponent renders 1`] = `
selectedKey={0}
/>
</StackItem>
<StackItem>
<InfoComponent />
</StackItem>
</Stack>
</Stack>
</PivotItem>

View File

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

View File

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

View File

@@ -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, () => {});
};
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,9 +17,6 @@ describe("IndexingPolicyComponent", () => {
},
indexingPolicyContent: initialIndexingPolicyContent,
indexingPolicyContentBaseline: initialIndexingPolicyContent,
onIndexingPolicyElementFocusChange: () => {
return;
},
onIndexingPolicyContentChange: () => {
return;
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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="" />
}))
}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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", () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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", () => {

View File

@@ -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>(() => {

View File

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

View File

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

View File

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

View File

@@ -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", () => {

View File

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

View File

@@ -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", () => {

View File

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

View File

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

View 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"""');
});
});

View 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}"`;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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", () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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