mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-07 19:46:53 +00:00
Compare commits
37 Commits
remove-red
...
jbunster/r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3dd25bdb4a | ||
|
|
e09730d782 | ||
|
|
09a95fded4 | ||
|
|
7ffa18a190 | ||
|
|
30353c26f3 | ||
|
|
a293a87528 | ||
|
|
34d8704071 | ||
|
|
23714831bd | ||
|
|
9a5d46b6e0 | ||
|
|
b9245101bc | ||
|
|
274deb13be | ||
|
|
9d50577800 | ||
|
|
bd00e5eb9b | ||
|
|
821f665e78 | ||
|
|
39f7ef331a | ||
|
|
9933a4988a | ||
|
|
d525afa142 | ||
|
|
cfb9a0b321 | ||
|
|
3b64d75322 | ||
|
|
daba1c4ed4 | ||
|
|
a698e08638 | ||
|
|
88d630fef4 | ||
|
|
5ffa746adb | ||
|
|
a9a57f4ba9 | ||
|
|
47cc6fd7a8 | ||
|
|
14cdf19efb | ||
|
|
5191ae3f3a | ||
|
|
ba862a8106 | ||
|
|
fe085b3e5a | ||
|
|
8028734cb0 | ||
|
|
444f663733 | ||
|
|
8c1ca35420 | ||
|
|
b69174788d | ||
|
|
ff03c79399 | ||
|
|
0382628249 | ||
|
|
d346ebe054 | ||
|
|
f5ecb8a04f |
29
.github/workflows/automerge.yml
vendored
29
.github/workflows/automerge.yml
vendored
@@ -1,29 +0,0 @@
|
||||
name: automerge
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- unlabeled
|
||||
- synchronize
|
||||
- opened
|
||||
- edited
|
||||
- ready_for_review
|
||||
- reopened
|
||||
- unlocked
|
||||
pull_request_review:
|
||||
types:
|
||||
- submitted
|
||||
check_suite:
|
||||
types:
|
||||
- completed
|
||||
status: {}
|
||||
jobs:
|
||||
automerge:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: automerge
|
||||
uses: "pascalgn/automerge-action@v0.11.0"
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.AUTOMERGE_GITHUB_PAT }}"
|
||||
MERGE_METHOD: "squash"
|
||||
MERGE_COMMIT_MESSAGE: "pull-request-title"
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ trigger:
|
||||
- master
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
vmImage: "ubuntu-latest"
|
||||
|
||||
steps:
|
||||
- task: ComponentGovernanceComponentDetection@0
|
||||
- task: ComponentGovernanceComponentDetection@0
|
||||
|
||||
177
externals/jquery.contextMenu.css
vendored
177
externals/jquery.contextMenu.css
vendored
@@ -1,177 +0,0 @@
|
||||
/*!
|
||||
* jQuery contextMenu - Plugin for simple contextMenu handling
|
||||
*
|
||||
* Version: 1.6.6
|
||||
*
|
||||
* Authors: Rodney Rehm, Addy Osmani (patches for FF)
|
||||
* Web: http://medialize.github.com/jQuery-contextMenu/
|
||||
*
|
||||
* Licensed under
|
||||
* MIT License http://www.opensource.org/licenses/mit-license
|
||||
* GPL v3 http://opensource.org/licenses/GPL-3.0
|
||||
*
|
||||
*/
|
||||
|
||||
.context-menu-list {
|
||||
z-index: 1001;
|
||||
position: fixed;
|
||||
background: white;
|
||||
border: solid 1px gainsboro;
|
||||
box-shadow: 4px 4px 4px -2px #888888;
|
||||
padding: 8px 0px 8px 0px;
|
||||
line-height: 25px;
|
||||
width: 254px;
|
||||
list-style: none;
|
||||
margin-left: -10px;
|
||||
outline: 0px #fff;
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
padding: 2px 2px 2px 31px;
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: -moz-none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.context-menu-separator {
|
||||
padding-bottom: 0;
|
||||
border-bottom: 1px solid #DDD;
|
||||
}
|
||||
|
||||
.context-menu-item>label>input,
|
||||
.context-menu-item>label>textarea {
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
margin-left: -10px;
|
||||
}
|
||||
|
||||
.context-menu-item:hover {
|
||||
cursor: pointer;
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
|
||||
.context-menu-item.disabled {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.context-menu-input.hover,
|
||||
.context-menu-item.disabled.hover {
|
||||
cursor: default;
|
||||
background-color: #EEE;
|
||||
}
|
||||
|
||||
.context-menu-submenu:after {
|
||||
content: ">";
|
||||
color: #666;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 3px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
||||
/* icons
|
||||
#protip:
|
||||
In case you want to use sprites for icons (which I would suggest you do) have a look at
|
||||
http://css-tricks.com/13224-pseudo-spriting/ to get an idea of how to implement
|
||||
.context-menu-item.icon:before {}
|
||||
*/
|
||||
|
||||
.context-menu-item.icon {
|
||||
min-height: 18px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 10px 7px;
|
||||
}
|
||||
|
||||
.context-menu-item.icon:hover {
|
||||
min-height: 18px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 10px 7px;
|
||||
}
|
||||
|
||||
|
||||
/*.context-menu-item.icon-edit {
|
||||
background-image: url(images/page_white_edit.png);
|
||||
}
|
||||
|
||||
.context-menu-item.icon-cut {
|
||||
background-image: url(images/cut.png);
|
||||
}
|
||||
|
||||
.context-menu-item.icon-copy {
|
||||
background-image: url(images/page_white_copy.png);
|
||||
}
|
||||
|
||||
.context-menu-item.icon-paste {
|
||||
background-image: url(images/page_white_paste.png);
|
||||
}
|
||||
|
||||
.context-menu-item.icon-delete {
|
||||
background-image: url(images/page_white_delete.png);
|
||||
}
|
||||
|
||||
.context-menu-item.icon-add {
|
||||
background-image: url(images/page_white_add.png);
|
||||
}
|
||||
|
||||
.context-menu-item.icon-quit {
|
||||
background-image: url(images/door.png);
|
||||
}*/
|
||||
|
||||
|
||||
/* vertically align inside labels */
|
||||
|
||||
.context-menu-input>label>* {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
|
||||
/* position checkboxes and radios as icons */
|
||||
|
||||
.context-menu-input>label>input[type="checkbox"],
|
||||
.context-menu-input>label>input[type="radio"] {
|
||||
margin-left: -17px;
|
||||
}
|
||||
|
||||
.context-menu-input>label>span {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.context-menu-input>label,
|
||||
.context-menu-input>label>input[type="text"],
|
||||
.context-menu-input>label>textarea,
|
||||
.context-menu-input>label>select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-ms-box-sizing: border-box;
|
||||
-o-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.context-menu-input>label>textarea {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.context-menu-item>.context-menu-list {
|
||||
display: none;
|
||||
/* re-positioned by js */
|
||||
right: -5px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
|
||||
/*.context-menu-item.hover>.context-menu-list {
|
||||
display: block;
|
||||
padding-left: 5px;
|
||||
}*/
|
||||
|
||||
.context-menu-accesskey {
|
||||
text-decoration: underline;
|
||||
}
|
||||
1686
externals/jquery.contextMenu.js
vendored
1686
externals/jquery.contextMenu.js
vendored
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ const isCI = require("is-ci");
|
||||
module.exports = {
|
||||
launch: {
|
||||
headless: isCI,
|
||||
slowMo: 30,
|
||||
slowMo: 55,
|
||||
defaultViewport: null,
|
||||
ignoreHTTPSErrors: true,
|
||||
args: ["--disable-web-security"]
|
||||
|
||||
@@ -2082,7 +2082,8 @@ a:link {
|
||||
.resourceTreeAndTabs {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
92
package-lock.json
generated
92
package-lock.json
generated
@@ -2326,83 +2326,83 @@
|
||||
}
|
||||
},
|
||||
"@microsoft/applicationinsights-analytics-js": {
|
||||
"version": "2.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-analytics-js/-/applicationinsights-analytics-js-2.5.8.tgz",
|
||||
"integrity": "sha512-2RsftiXa5ojNXuHRIC7RybWbN+Z7TMrLK2XGTza1Wq/KHRJNB48WmQuxjd6SzsNguqxRoHsH0sUogIwlK+NO8A==",
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-analytics-js/-/applicationinsights-analytics-js-2.5.9.tgz",
|
||||
"integrity": "sha512-9L3fb1H1as+J3J2j2EDx1HEMdrucjgR4INqahy+ZAxDPFvR3HCOedYzx645zObBIPu7QkH2LAjPk4fuNGHR1rg==",
|
||||
"requires": {
|
||||
"@microsoft/applicationinsights-common": "2.5.8",
|
||||
"@microsoft/applicationinsights-core-js": "2.5.8",
|
||||
"@microsoft/applicationinsights-shims": "1.0.1",
|
||||
"@microsoft/applicationinsights-common": "2.5.9",
|
||||
"@microsoft/applicationinsights-core-js": "2.5.9",
|
||||
"@microsoft/applicationinsights-shims": "1.0.3",
|
||||
"@microsoft/dynamicproto-js": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"@microsoft/applicationinsights-channel-js": {
|
||||
"version": "2.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-2.5.8.tgz",
|
||||
"integrity": "sha512-etVGzhNluflTikOpW9dlDxdg+B+8gt/nxmGeXqti9Hsqq0fTxcY9bmNklFIEKhOIwai4DodgJjflaDdgR0ObUQ==",
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-2.5.9.tgz",
|
||||
"integrity": "sha512-NAQ/2wWmD+gaIZDCMzzwxm8RcbswDvUO5BYeuW9UHJaFuEZ9o9xpztKVz32u4CMv7OI/mLOqnmR4rb0d+kUMwQ==",
|
||||
"requires": {
|
||||
"@microsoft/applicationinsights-common": "2.5.8",
|
||||
"@microsoft/applicationinsights-core-js": "2.5.8",
|
||||
"@microsoft/applicationinsights-shims": "1.0.1",
|
||||
"@microsoft/applicationinsights-common": "2.5.9",
|
||||
"@microsoft/applicationinsights-core-js": "2.5.9",
|
||||
"@microsoft/applicationinsights-shims": "1.0.3",
|
||||
"@microsoft/dynamicproto-js": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"@microsoft/applicationinsights-common": {
|
||||
"version": "2.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-2.5.8.tgz",
|
||||
"integrity": "sha512-RIMAJTsF0H5wiRZxYIF9H8sbMe2W5Ig4yMuH0/lho69DcNZpHf8p6PSa4Qhhli0AnoWYfLE7/WlWO1eR5SkByw==",
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-2.5.9.tgz",
|
||||
"integrity": "sha512-dKmXO9m55uRDhpoa0P7l+BApf+lsrqjgoLeKv+ABM8ygIyd9JH6CDcdaT3af+kUFtt9Oj3ChyfueKr1EVOdGkQ==",
|
||||
"requires": {
|
||||
"@microsoft/applicationinsights-core-js": "2.5.8",
|
||||
"@microsoft/applicationinsights-shims": "1.0.1"
|
||||
"@microsoft/applicationinsights-core-js": "2.5.9",
|
||||
"@microsoft/applicationinsights-shims": "1.0.3"
|
||||
}
|
||||
},
|
||||
"@microsoft/applicationinsights-core-js": {
|
||||
"version": "2.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.5.8.tgz",
|
||||
"integrity": "sha512-NxxIViHKuqgpla+KdQk7Qy48Dm8lN4oy2mMEpv9kM5GW5MBJ8nZ4A5RV1kokF3kXuDmTUTHlWBXeLR8hauA3qQ==",
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.5.9.tgz",
|
||||
"integrity": "sha512-KE9h1wmC/Ckm7jYjsMF1SEWQnk0v0CRzZq1upSARgPH7BgmyClXz1kdnLtuTWz8Aha8IIH9dW2hUOfPCdR+BpQ==",
|
||||
"requires": {
|
||||
"@microsoft/applicationinsights-shims": "1.0.1",
|
||||
"@microsoft/applicationinsights-shims": "1.0.3",
|
||||
"@microsoft/dynamicproto-js": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"@microsoft/applicationinsights-dependencies-js": {
|
||||
"version": "2.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-dependencies-js/-/applicationinsights-dependencies-js-2.5.8.tgz",
|
||||
"integrity": "sha512-jnXpjE/bnlLeew78OsN8aPTLOPZQJ4y1MOO8R3E+eUXdproD2TemynSk5kUfrMdry91DZOBZnrmJ2NCB+g5ArQ==",
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-dependencies-js/-/applicationinsights-dependencies-js-2.5.9.tgz",
|
||||
"integrity": "sha512-pNM/dkUOscV0ul/YJe928+77EBtRkRXO/le/VWzlunoUFaEEo4pirc7NycvPx9w/KxA62JMEogbQsWE6nAmqPg==",
|
||||
"requires": {
|
||||
"@microsoft/applicationinsights-common": "2.5.8",
|
||||
"@microsoft/applicationinsights-core-js": "2.5.8",
|
||||
"@microsoft/applicationinsights-shims": "1.0.1",
|
||||
"@microsoft/applicationinsights-common": "2.5.9",
|
||||
"@microsoft/applicationinsights-core-js": "2.5.9",
|
||||
"@microsoft/applicationinsights-shims": "1.0.3",
|
||||
"@microsoft/dynamicproto-js": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"@microsoft/applicationinsights-properties-js": {
|
||||
"version": "2.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-properties-js/-/applicationinsights-properties-js-2.5.8.tgz",
|
||||
"integrity": "sha512-BAOSguXe07ua6SqYmAW+8+vVvBZb4qmmUP6s5zc7kNMMgEoEGsCn2VHmM8wHHxq4J/TmYxIqwoufS+XKbUvCeQ==",
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-properties-js/-/applicationinsights-properties-js-2.5.9.tgz",
|
||||
"integrity": "sha512-mZxaC8CZsURn38IwsPaUx+o9QXQU2vm81THZL+1Lc+7scPo55ATDTFgZ2awIj7CdTp69oGzUkpB7maOn6+OVOw==",
|
||||
"requires": {
|
||||
"@microsoft/applicationinsights-common": "2.5.8",
|
||||
"@microsoft/applicationinsights-core-js": "2.5.8",
|
||||
"@microsoft/applicationinsights-shims": "1.0.1"
|
||||
"@microsoft/applicationinsights-common": "2.5.9",
|
||||
"@microsoft/applicationinsights-core-js": "2.5.9",
|
||||
"@microsoft/applicationinsights-shims": "1.0.3"
|
||||
}
|
||||
},
|
||||
"@microsoft/applicationinsights-shims": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-1.0.1.tgz",
|
||||
"integrity": "sha512-nPjUBSpvX5Dnkovp2lZzrg0/uSvYNtbAclWwVP7t8J1hy5OJ3xr3KPNaz79+b84G16Rj861ybau9Gbk7inXkTg=="
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-1.0.3.tgz",
|
||||
"integrity": "sha512-+S17aqEkOYpyBpmclhgwcEplwnxSo5AxYBdRg38GBobI1GKPSpZfnLssLzcjJ6XZCS5tqB5xjyTZs6gHj7ZJWQ=="
|
||||
},
|
||||
"@microsoft/applicationinsights-web": {
|
||||
"version": "2.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web/-/applicationinsights-web-2.5.8.tgz",
|
||||
"integrity": "sha512-ZxCUkBJrCFNHa0LgWWbtwqu4TxJPlukuSvDrdLv6XV1yX2ETq6Q1kw/IUEtKhbtNbTZQ8aJ+x8Nc/iqbssdXTA==",
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web/-/applicationinsights-web-2.5.9.tgz",
|
||||
"integrity": "sha512-dxg5XXbQqjWw9QmGdgbd7knb1qFA58FFYj9ObqRmlqiihk25kper7H15HH8LaV0lV6goClmBWc9KsNGA2veyeA==",
|
||||
"requires": {
|
||||
"@microsoft/applicationinsights-analytics-js": "2.5.8",
|
||||
"@microsoft/applicationinsights-channel-js": "2.5.8",
|
||||
"@microsoft/applicationinsights-common": "2.5.8",
|
||||
"@microsoft/applicationinsights-core-js": "2.5.8",
|
||||
"@microsoft/applicationinsights-dependencies-js": "2.5.8",
|
||||
"@microsoft/applicationinsights-properties-js": "2.5.8",
|
||||
"@microsoft/applicationinsights-shims": "1.0.1",
|
||||
"@microsoft/applicationinsights-analytics-js": "2.5.9",
|
||||
"@microsoft/applicationinsights-channel-js": "2.5.9",
|
||||
"@microsoft/applicationinsights-common": "2.5.9",
|
||||
"@microsoft/applicationinsights-core-js": "2.5.9",
|
||||
"@microsoft/applicationinsights-dependencies-js": "2.5.9",
|
||||
"@microsoft/applicationinsights-properties-js": "2.5.9",
|
||||
"@microsoft/applicationinsights-shims": "1.0.3",
|
||||
"@microsoft/dynamicproto-js": "^1.0.0"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"@azure/cosmos-language-service": "0.0.4",
|
||||
"@jupyterlab/services": "6.0.0-rc.2",
|
||||
"@jupyterlab/terminal": "3.0.0-rc.2",
|
||||
"@microsoft/applicationinsights-web": "2.5.8",
|
||||
"@microsoft/applicationinsights-web": "2.5.9",
|
||||
"@nteract/commutable": "7.3.2",
|
||||
"@nteract/connected-components": "6.8.2",
|
||||
"@nteract/core": "15.1.0",
|
||||
@@ -88,8 +88,8 @@
|
||||
"styled-components": "4.3.2",
|
||||
"text-encoding": "0.7.0",
|
||||
"underscore": "1.9.1",
|
||||
"utility-types": "3.10.0",
|
||||
"url-polyfill": "1.1.7",
|
||||
"utility-types": "3.10.0",
|
||||
"webcrypto-liner": "1.1.4",
|
||||
"webfontloader": "1.6.28",
|
||||
"whatwg-fetch": "3.0.0"
|
||||
|
||||
@@ -117,7 +117,6 @@ export class Features {
|
||||
public static readonly enableGalleryPublish = "enablegallerypublish";
|
||||
public static readonly enableCodeOfConduct = "enablecodeofconduct";
|
||||
public static readonly enableLinkInjection = "enablelinkinjection";
|
||||
public static readonly enableSettingsV2 = "enablesettingsv2";
|
||||
public static readonly enableSpark = "enablespark";
|
||||
public static readonly livyEndpoint = "livyendpoint";
|
||||
public static readonly notebookServerUrl = "notebookserverurl";
|
||||
@@ -131,6 +130,11 @@ export class Features {
|
||||
public static readonly enableSDKoperations = "enablesdkoperations";
|
||||
}
|
||||
|
||||
// flight names returned from the portal are always lowercase
|
||||
export class Flights {
|
||||
public static readonly SettingsV2 = "settingsv2";
|
||||
}
|
||||
|
||||
export class AfecFeatures {
|
||||
public static readonly Spark = "spark-public-preview";
|
||||
public static readonly Notebooks = "sparknotebooks-public-preview";
|
||||
|
||||
@@ -112,7 +112,6 @@ describe("endpoint", () => {
|
||||
|
||||
describe("requestPlugin", () => {
|
||||
beforeEach(() => {
|
||||
delete window.dataExplorerPlatform;
|
||||
resetConfigContext();
|
||||
});
|
||||
|
||||
|
||||
@@ -72,22 +72,6 @@ export function getPartitionKeyHeader(partitionKeyDefinition: DataModels.Partiti
|
||||
return [partitionKeyValue];
|
||||
}
|
||||
|
||||
export function updateOffer(
|
||||
offer: DataModels.Offer,
|
||||
newOffer: DataModels.Offer,
|
||||
options?: RequestOptions
|
||||
): Q.Promise<DataModels.Offer> {
|
||||
return Q(
|
||||
client()
|
||||
.offer(offer.id)
|
||||
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
|
||||
.replace((newOffer as unknown) as OfferDefinition, options)
|
||||
.then(response => {
|
||||
return Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => response.resource);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function updateDocument(
|
||||
collection: ViewModels.CollectionBase,
|
||||
documentId: DocumentId,
|
||||
|
||||
@@ -157,41 +157,6 @@ export function updateDocument(
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function updateOffer(
|
||||
offer: DataModels.Offer,
|
||||
newOffer: DataModels.Offer,
|
||||
options: RequestOptions
|
||||
): Q.Promise<DataModels.Offer> {
|
||||
var deferred = Q.defer<any>();
|
||||
const clearMessage = logConsoleProgress(`Updating offer for resource ${offer.resource}`);
|
||||
DataAccessUtilityBase.updateOffer(offer, newOffer, options)
|
||||
.then(
|
||||
(replacedOffer: DataModels.Offer) => {
|
||||
logConsoleInfo(`Successfully updated offer for resource ${offer.resource}`);
|
||||
deferred.resolve(replacedOffer);
|
||||
},
|
||||
(error: any) => {
|
||||
logConsoleError(`Error updating offer for resource ${offer.resource}: ${JSON.stringify(error)}`);
|
||||
Logger.logError(
|
||||
JSON.stringify({
|
||||
oldOffer: offer,
|
||||
newOffer: newOffer,
|
||||
error: error
|
||||
}),
|
||||
"UpdateOffer",
|
||||
error.code
|
||||
);
|
||||
sendNotificationForError(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
clearMessage();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
|
||||
var deferred = Q.defer<any>();
|
||||
const entityName = getEntityName();
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import "jquery";
|
||||
import * as Q from "q";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||
import { userContext } from "../UserContext";
|
||||
|
||||
export class NotificationsClientBase {
|
||||
private _extensionEndpoint: string;
|
||||
private _notificationsApiSuffix: string;
|
||||
|
||||
protected constructor(notificationsApiSuffix: string) {
|
||||
this._notificationsApiSuffix = notificationsApiSuffix;
|
||||
}
|
||||
|
||||
public fetchNotifications(): Q.Promise<DataModels.Notification[]> {
|
||||
const deferred: Q.Deferred<DataModels.Notification[]> = Q.defer<DataModels.Notification[]>();
|
||||
const databaseAccount = userContext.databaseAccount;
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const url = `${this._extensionEndpoint}${this._notificationsApiSuffix}?accountName=${databaseAccount.name}&subscriptionId=${subscriptionId}&resourceGroup=${resourceGroup}`;
|
||||
const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||
const headers: any = {};
|
||||
headers[authorizationHeader.header] = authorizationHeader.token;
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "GET",
|
||||
headers: headers,
|
||||
cache: false
|
||||
}).then(
|
||||
(notifications: DataModels.Notification[], textStatus: string, xhr: JQueryXHR<any>) => {
|
||||
deferred.resolve(notifications);
|
||||
},
|
||||
(xhr: JQueryXHR<any>, textStatus: string, error: any) => {
|
||||
deferred.reject(xhr.responseText);
|
||||
}
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public setExtensionEndpoint(extensionEndpoint: string): void {
|
||||
this._extensionEndpoint = extensionEndpoint;
|
||||
}
|
||||
}
|
||||
41
src/Common/PortalNotifications.ts
Normal file
41
src/Common/PortalNotifications.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||
import { userContext } from "../UserContext";
|
||||
import { configContext, Platform } from "../ConfigContext";
|
||||
|
||||
const notificationsPath = () => {
|
||||
switch (configContext.platform) {
|
||||
case Platform.Hosted:
|
||||
return "/api/guest/notifications";
|
||||
case Platform.Portal:
|
||||
return "/api/notifications";
|
||||
default:
|
||||
throw new Error(`Unknown platform: ${configContext.platform}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchPortalNotifications = async (): Promise<DataModels.Notification[]> => {
|
||||
if (configContext.platform === Platform.Emulator) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const databaseAccount = userContext.databaseAccount;
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const url = `${configContext.BACKEND_ENDPOINT}${notificationsPath()}?accountName=${
|
||||
databaseAccount.name
|
||||
}&subscriptionId=${subscriptionId}&resourceGroup=${resourceGroup}`;
|
||||
const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||
const headers = { [authorizationHeader.header]: authorizationHeader.token };
|
||||
|
||||
const response = await window.fetch(url, {
|
||||
headers
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
|
||||
return (await response.json()) as DataModels.Notification[];
|
||||
};
|
||||
@@ -1,28 +1,76 @@
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import {
|
||||
SqlStoredProcedureCreateUpdateParameters,
|
||||
SqlStoredProcedureResource
|
||||
} from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||
import { client } from "../CosmosClient";
|
||||
import {
|
||||
createUpdateSqlStoredProcedure,
|
||||
getSqlStoredProcedure
|
||||
} from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export async function createStoredProcedure(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
storedProcedure: StoredProcedureDefinition
|
||||
): Promise<StoredProcedureDefinition & Resource> {
|
||||
let createdStoredProcedure: StoredProcedureDefinition & Resource;
|
||||
const clearMessage = logConsoleProgress(`Creating stored procedure ${storedProcedure.id}`);
|
||||
try {
|
||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
||||
try {
|
||||
const getResponse = await getSqlStoredProcedure(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
databaseId,
|
||||
collectionId,
|
||||
storedProcedure.id
|
||||
);
|
||||
if (getResponse?.properties?.resource) {
|
||||
throw new Error(
|
||||
`Create stored procedure failed: stored procedure with id ${storedProcedure.id} already exists`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const createSprocParams: SqlStoredProcedureCreateUpdateParameters = {
|
||||
properties: {
|
||||
resource: storedProcedure as SqlStoredProcedureResource,
|
||||
options: {}
|
||||
}
|
||||
};
|
||||
const rpResponse = await createUpdateSqlStoredProcedure(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
databaseId,
|
||||
collectionId,
|
||||
storedProcedure.id,
|
||||
createSprocParams
|
||||
);
|
||||
return rpResponse && (rpResponse.properties?.resource as StoredProcedureDefinition & Resource);
|
||||
}
|
||||
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.storedProcedures.create(storedProcedure);
|
||||
createdStoredProcedure = response.resource;
|
||||
return response?.resource;
|
||||
} catch (error) {
|
||||
logConsoleError(`Error while creating stored procedure ${storedProcedure.id}:\n ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "CreateStoredProcedure", error.code);
|
||||
sendNotificationForError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
|
||||
clearMessage();
|
||||
return createdStoredProcedure;
|
||||
}
|
||||
|
||||
@@ -1,28 +1,71 @@
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Resource, TriggerDefinition } from "@azure/cosmos";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import {
|
||||
SqlTriggerCreateUpdateParameters,
|
||||
SqlTriggerResource
|
||||
} from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||
import { client } from "../CosmosClient";
|
||||
import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export async function createTrigger(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
trigger: TriggerDefinition
|
||||
): Promise<TriggerDefinition & Resource> {
|
||||
let createdTrigger: TriggerDefinition & Resource;
|
||||
const clearMessage = logConsoleProgress(`Creating trigger ${trigger.id}`);
|
||||
try {
|
||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
||||
try {
|
||||
const getResponse = await getSqlTrigger(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
databaseId,
|
||||
collectionId,
|
||||
trigger.id
|
||||
);
|
||||
if (getResponse?.properties?.resource) {
|
||||
throw new Error(`Create trigger failed: trigger with id ${trigger.id} already exists`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const createTriggerParams: SqlTriggerCreateUpdateParameters = {
|
||||
properties: {
|
||||
resource: trigger as SqlTriggerResource,
|
||||
options: {}
|
||||
}
|
||||
};
|
||||
const rpResponse = await createUpdateSqlTrigger(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
databaseId,
|
||||
collectionId,
|
||||
trigger.id,
|
||||
createTriggerParams
|
||||
);
|
||||
return rpResponse && (rpResponse.properties?.resource as TriggerDefinition & Resource);
|
||||
}
|
||||
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.triggers.create(trigger);
|
||||
createdTrigger = response.resource;
|
||||
return response.resource;
|
||||
} catch (error) {
|
||||
logConsoleError(`Error while creating trigger ${trigger.id}:\n ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "CreateTrigger", error.code);
|
||||
sendNotificationForError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
|
||||
clearMessage();
|
||||
return createdTrigger;
|
||||
}
|
||||
|
||||
@@ -1,28 +1,76 @@
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import {
|
||||
SqlUserDefinedFunctionCreateUpdateParameters,
|
||||
SqlUserDefinedFunctionResource
|
||||
} from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||
import { client } from "../CosmosClient";
|
||||
import {
|
||||
createUpdateSqlUserDefinedFunction,
|
||||
getSqlUserDefinedFunction
|
||||
} from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export async function createUserDefinedFunction(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
userDefinedFunction: UserDefinedFunctionDefinition
|
||||
): Promise<UserDefinedFunctionDefinition & Resource> {
|
||||
let createdUserDefinedFunction: UserDefinedFunctionDefinition & Resource;
|
||||
const clearMessage = logConsoleProgress(`Creating user defined function ${userDefinedFunction.id}`);
|
||||
try {
|
||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
||||
try {
|
||||
const getResponse = await getSqlUserDefinedFunction(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
databaseId,
|
||||
collectionId,
|
||||
userDefinedFunction.id
|
||||
);
|
||||
if (getResponse?.properties?.resource) {
|
||||
throw new Error(
|
||||
`Create user defined function failed: user defined function with id ${userDefinedFunction.id} already exists`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const createUDFParams: SqlUserDefinedFunctionCreateUpdateParameters = {
|
||||
properties: {
|
||||
resource: userDefinedFunction as SqlUserDefinedFunctionResource,
|
||||
options: {}
|
||||
}
|
||||
};
|
||||
const rpResponse = await createUpdateSqlUserDefinedFunction(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
databaseId,
|
||||
collectionId,
|
||||
userDefinedFunction.id,
|
||||
createUDFParams
|
||||
);
|
||||
return rpResponse && (rpResponse.properties?.resource as UserDefinedFunctionDefinition & Resource);
|
||||
}
|
||||
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.userDefinedFunctions.create(userDefinedFunction);
|
||||
createdUserDefinedFunction = response.resource;
|
||||
return response?.resource;
|
||||
} catch (error) {
|
||||
logConsoleError(`Error while creating user defined function ${userDefinedFunction.id}:\n ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "CreateUserupdateUserDefinedFunction", error.code);
|
||||
sendNotificationForError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
|
||||
clearMessage();
|
||||
return createdUserDefinedFunction;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { client } from "../CosmosClient";
|
||||
import { deleteSqlStoredProcedure } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export async function deleteStoredProcedure(
|
||||
databaseId: string,
|
||||
@@ -10,17 +13,28 @@ export async function deleteStoredProcedure(
|
||||
): Promise<void> {
|
||||
const clearMessage = logConsoleProgress(`Deleting stored procedure ${storedProcedureId}`);
|
||||
try {
|
||||
await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.storedProcedure(storedProcedureId)
|
||||
.delete();
|
||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
||||
await deleteSqlStoredProcedure(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
databaseId,
|
||||
collectionId,
|
||||
storedProcedureId
|
||||
);
|
||||
} else {
|
||||
await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.storedProcedure(storedProcedureId)
|
||||
.delete();
|
||||
}
|
||||
} catch (error) {
|
||||
logConsoleError(`Error while deleting stored procedure ${storedProcedureId}:\n ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "DeleteStoredProcedure", error.code);
|
||||
sendNotificationForError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
|
||||
clearMessage();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { client } from "../CosmosClient";
|
||||
import { deleteSqlTrigger } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export async function deleteTrigger(databaseId: string, collectionId: string, triggerId: string): Promise<void> {
|
||||
const clearMessage = logConsoleProgress(`Deleting trigger ${triggerId}`);
|
||||
try {
|
||||
await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.trigger(triggerId)
|
||||
.delete();
|
||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
||||
await deleteSqlTrigger(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
databaseId,
|
||||
collectionId,
|
||||
triggerId
|
||||
);
|
||||
} else {
|
||||
await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.trigger(triggerId)
|
||||
.delete();
|
||||
}
|
||||
} catch (error) {
|
||||
logConsoleError(`Error while deleting trigger ${triggerId}:\n ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "DeleteTrigger", error.code);
|
||||
sendNotificationForError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
|
||||
clearMessage();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { client } from "../CosmosClient";
|
||||
import { deleteSqlUserDefinedFunction } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export async function deleteUserDefinedFunction(databaseId: string, collectionId: string, id: string): Promise<void> {
|
||||
const clearMessage = logConsoleProgress(`Deleting user defined function ${id}`);
|
||||
try {
|
||||
await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.userDefinedFunction(id)
|
||||
.delete();
|
||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
||||
await deleteSqlUserDefinedFunction(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
databaseId,
|
||||
collectionId,
|
||||
id
|
||||
);
|
||||
} else {
|
||||
await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.userDefinedFunction(id)
|
||||
.delete();
|
||||
}
|
||||
} catch (error) {
|
||||
logConsoleError(`Error while deleting user defined function ${id}:\n ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "DeleteUserDefinedFunction", error.code);
|
||||
sendNotificationForError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
|
||||
clearMessage();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -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> => {
|
||||
|
||||
@@ -18,9 +18,7 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
||||
if (
|
||||
window.authType === AuthType.AAD &&
|
||||
!userContext.useSDKOperations &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Cassandra
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||
) {
|
||||
databases = await readDatabasesWithARM();
|
||||
} else {
|
||||
|
||||
@@ -13,10 +13,13 @@ export const readOffers = async (): Promise<Offer[]> => {
|
||||
const clearMessage = logConsoleProgress(`Querying offers`);
|
||||
try {
|
||||
if (configContext.platform === Platform.Portal) {
|
||||
return sendCachedDataMessage<Offer[]>(MessageTypes.AllOffers, [
|
||||
const offers = sendCachedDataMessage<Offer[]>(MessageTypes.AllOffers, [
|
||||
userContext.databaseAccount.id,
|
||||
ClientDefaults.portalCacheTimeoutMs
|
||||
]);
|
||||
clearMessage();
|
||||
|
||||
return offers;
|
||||
}
|
||||
} catch (error) {
|
||||
// If error getting cached Offers, continue on and read via SDK
|
||||
|
||||
@@ -1,27 +1,41 @@
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { listSqlStoredProcedures } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export async function readStoredProcedures(
|
||||
databaseId: string,
|
||||
collectionId: string
|
||||
): Promise<(StoredProcedureDefinition & Resource)[]> {
|
||||
let sprocs: (StoredProcedureDefinition & Resource)[];
|
||||
const clearMessage = logConsoleProgress(`Querying stored procedures for container ${collectionId}`);
|
||||
try {
|
||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
||||
const rpResponse = await listSqlStoredProcedures(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
return rpResponse?.value?.map(sproc => sproc.properties?.resource as StoredProcedureDefinition & Resource);
|
||||
}
|
||||
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.storedProcedures.readAll()
|
||||
.fetchAll();
|
||||
sprocs = response.resources;
|
||||
return response?.resources;
|
||||
} catch (error) {
|
||||
logConsoleError(`Failed to query stored procedures for container ${collectionId}: ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "ReadStoredProcedures", error.code);
|
||||
sendNotificationForError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
clearMessage();
|
||||
return sprocs;
|
||||
}
|
||||
|
||||
@@ -1,27 +1,41 @@
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Resource, TriggerDefinition } from "@azure/cosmos";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { listSqlTriggers } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export async function readTriggers(
|
||||
databaseId: string,
|
||||
collectionId: string
|
||||
): Promise<(TriggerDefinition & Resource)[]> {
|
||||
let triggers: (TriggerDefinition & Resource)[];
|
||||
const clearMessage = logConsoleProgress(`Querying triggers for container ${collectionId}`);
|
||||
try {
|
||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
||||
const rpResponse = await listSqlTriggers(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
return rpResponse?.value?.map(trigger => trigger.properties?.resource as TriggerDefinition & Resource);
|
||||
}
|
||||
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.triggers.readAll()
|
||||
.fetchAll();
|
||||
triggers = response.resources;
|
||||
return response?.resources;
|
||||
} catch (error) {
|
||||
logConsoleError(`Failed to query triggers for container ${collectionId}: ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "ReadTriggers", error.code);
|
||||
sendNotificationForError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
clearMessage();
|
||||
return triggers;
|
||||
}
|
||||
|
||||
@@ -1,27 +1,41 @@
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { listSqlUserDefinedFunctions } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export async function readUserDefinedFunctions(
|
||||
databaseId: string,
|
||||
collectionId: string
|
||||
): Promise<(UserDefinedFunctionDefinition & Resource)[]> {
|
||||
let udfs: (UserDefinedFunctionDefinition & Resource)[];
|
||||
const clearMessage = logConsoleProgress(`Querying user defined functions for container ${collectionId}`);
|
||||
try {
|
||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
||||
const rpResponse = await listSqlUserDefinedFunctions(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
return rpResponse?.value?.map(udf => udf.properties?.resource as UserDefinedFunctionDefinition & Resource);
|
||||
}
|
||||
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.userDefinedFunctions.readAll()
|
||||
.fetchAll();
|
||||
udfs = response.resources;
|
||||
return response?.resources;
|
||||
} catch (error) {
|
||||
logConsoleError(`Failed to query user defined functions for container ${collectionId}: ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "ReadUserDefinedFunctions", error.code);
|
||||
sendNotificationForError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
clearMessage();
|
||||
return udfs;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
425
src/Common/dataAccess/updateOffer.ts
Normal file
425
src/Common/dataAccess/updateOffer.ts
Normal file
@@ -0,0 +1,425 @@
|
||||
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;
|
||||
};
|
||||
@@ -1,29 +1,71 @@
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import {
|
||||
SqlStoredProcedureCreateUpdateParameters,
|
||||
SqlStoredProcedureResource
|
||||
} from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||
import { client } from "../CosmosClient";
|
||||
import {
|
||||
createUpdateSqlStoredProcedure,
|
||||
getSqlStoredProcedure
|
||||
} from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export async function updateStoredProcedure(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
storedProcedure: StoredProcedureDefinition
|
||||
): Promise<StoredProcedureDefinition & Resource> {
|
||||
let updatedStoredProcedure: StoredProcedureDefinition & Resource;
|
||||
const clearMessage = logConsoleProgress(`Updating stored procedure ${storedProcedure.id}`);
|
||||
try {
|
||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
||||
const getResponse = await getSqlStoredProcedure(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
databaseId,
|
||||
collectionId,
|
||||
storedProcedure.id
|
||||
);
|
||||
|
||||
if (getResponse?.properties?.resource) {
|
||||
const createSprocParams: SqlStoredProcedureCreateUpdateParameters = {
|
||||
properties: {
|
||||
resource: storedProcedure as SqlStoredProcedureResource,
|
||||
options: {}
|
||||
}
|
||||
};
|
||||
const rpResponse = await createUpdateSqlStoredProcedure(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
databaseId,
|
||||
collectionId,
|
||||
storedProcedure.id,
|
||||
createSprocParams
|
||||
);
|
||||
return rpResponse && (rpResponse.properties?.resource as StoredProcedureDefinition & Resource);
|
||||
}
|
||||
|
||||
throw new Error(`Failed to update stored procedure: ${storedProcedure.id} does not exist.`);
|
||||
}
|
||||
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.storedProcedure(storedProcedure.id)
|
||||
.replace(storedProcedure);
|
||||
updatedStoredProcedure = response.resource;
|
||||
return response?.resource;
|
||||
} catch (error) {
|
||||
logConsoleError(`Error while updating stored procedure ${storedProcedure.id}:\n ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "UpdateStoredProcedure", error.code);
|
||||
const errorMessage = error.code === "NotFound" ? `${storedProcedure.id} does not exist.` : JSON.stringify(error);
|
||||
logConsoleError(`Error while updating stored procedure ${storedProcedure.id}:\n ${errorMessage}`);
|
||||
logError(errorMessage, "UpdateStoredProcedure", error.code);
|
||||
sendNotificationForError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
|
||||
clearMessage();
|
||||
return updatedStoredProcedure;
|
||||
}
|
||||
|
||||
@@ -1,29 +1,68 @@
|
||||
import { AuthType } from "../../AuthType";
|
||||
import {
|
||||
SqlTriggerCreateUpdateParameters,
|
||||
SqlTriggerResource
|
||||
} from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||
import { TriggerDefinition } from "@azure/cosmos";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export async function updateTrigger(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
trigger: TriggerDefinition
|
||||
): Promise<TriggerDefinition> {
|
||||
let updatedTrigger: TriggerDefinition;
|
||||
const clearMessage = logConsoleProgress(`Updating trigger ${trigger.id}`);
|
||||
try {
|
||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
||||
const getResponse = await getSqlTrigger(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
databaseId,
|
||||
collectionId,
|
||||
trigger.id
|
||||
);
|
||||
|
||||
if (getResponse?.properties?.resource) {
|
||||
const createTriggerParams: SqlTriggerCreateUpdateParameters = {
|
||||
properties: {
|
||||
resource: trigger as SqlTriggerResource,
|
||||
options: {}
|
||||
}
|
||||
};
|
||||
const rpResponse = await createUpdateSqlTrigger(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
databaseId,
|
||||
collectionId,
|
||||
trigger.id,
|
||||
createTriggerParams
|
||||
);
|
||||
return rpResponse && (rpResponse.properties?.resource as TriggerDefinition);
|
||||
}
|
||||
|
||||
throw new Error(`Failed to update trigger: ${trigger.id} does not exist.`);
|
||||
}
|
||||
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.trigger(trigger.id)
|
||||
.replace(trigger);
|
||||
updatedTrigger = response.resource;
|
||||
return response?.resource;
|
||||
} catch (error) {
|
||||
logConsoleError(`Error while updating trigger ${trigger.id}:\n ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "UpdateTrigger", error.code);
|
||||
const errorMessage = error.code === "NotFound" ? `${trigger.id} does not exist.` : JSON.stringify(error);
|
||||
logConsoleError(`Error while updating trigger ${trigger.id}:\n ${errorMessage}`);
|
||||
logError(errorMessage, "UpdateTrigger", error.code);
|
||||
sendNotificationForError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
|
||||
clearMessage();
|
||||
return updatedTrigger;
|
||||
}
|
||||
|
||||
@@ -1,29 +1,72 @@
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import {
|
||||
SqlUserDefinedFunctionCreateUpdateParameters,
|
||||
SqlUserDefinedFunctionResource
|
||||
} from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||
import { client } from "../CosmosClient";
|
||||
import {
|
||||
createUpdateSqlUserDefinedFunction,
|
||||
getSqlUserDefinedFunction
|
||||
} from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export async function updateUserDefinedFunction(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
userDefinedFunction: UserDefinedFunctionDefinition
|
||||
): Promise<UserDefinedFunctionDefinition & Resource> {
|
||||
let updatedUserDefinedFunction: UserDefinedFunctionDefinition & Resource;
|
||||
const clearMessage = logConsoleProgress(`Updating user defined function ${userDefinedFunction.id}`);
|
||||
try {
|
||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
||||
const getResponse = await getSqlUserDefinedFunction(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
databaseId,
|
||||
collectionId,
|
||||
userDefinedFunction.id
|
||||
);
|
||||
|
||||
if (getResponse?.properties?.resource) {
|
||||
const createUDFParams: SqlUserDefinedFunctionCreateUpdateParameters = {
|
||||
properties: {
|
||||
resource: userDefinedFunction as SqlUserDefinedFunctionResource,
|
||||
options: {}
|
||||
}
|
||||
};
|
||||
const rpResponse = await createUpdateSqlUserDefinedFunction(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
databaseId,
|
||||
collectionId,
|
||||
userDefinedFunction.id,
|
||||
createUDFParams
|
||||
);
|
||||
return rpResponse && (rpResponse.properties?.resource as UserDefinedFunctionDefinition & Resource);
|
||||
}
|
||||
|
||||
throw new Error(`Failed to update user defined function: ${userDefinedFunction.id} does not exist.`);
|
||||
}
|
||||
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.userDefinedFunction(userDefinedFunction.id)
|
||||
.replace(userDefinedFunction);
|
||||
updatedUserDefinedFunction = response.resource;
|
||||
return response?.resource;
|
||||
} catch (error) {
|
||||
logConsoleError(`Error while updating user defined function ${userDefinedFunction.id}:\n ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "UpdateUserupdateUserDefinedFunction", error.code);
|
||||
const errorMessage =
|
||||
error.code === "NotFound" ? `${userDefinedFunction.id} does not exist.` : JSON.stringify(error);
|
||||
logConsoleError(`Error while updating user defined function ${userDefinedFunction.id}:\n ${errorMessage}`);
|
||||
logError(errorMessage, "UpdateUserupdateUserDefinedFunction", error.code);
|
||||
sendNotificationForError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
|
||||
clearMessage();
|
||||
return updatedUserDefinedFunction;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||
allowedParentFrameOrigins: [
|
||||
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*portal\\.microsoftazure.de$`,
|
||||
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
|
||||
`^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$`
|
||||
@@ -50,7 +51,8 @@ let configContext: Readonly<ConfigContext> = {
|
||||
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
|
||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
|
||||
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/settings/applications/1189306
|
||||
JUNO_ENDPOINT: "https://tools.cosmos.azure.com"
|
||||
JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com"
|
||||
};
|
||||
|
||||
export function resetConfigContext(): void {
|
||||
|
||||
@@ -303,6 +303,16 @@ export interface ReadCollectionOfferParams {
|
||||
offerId?: string;
|
||||
}
|
||||
|
||||
export interface UpdateOfferParams {
|
||||
currentOffer: Offer;
|
||||
databaseId: string;
|
||||
autopilotThroughput: number;
|
||||
manualThroughput: number;
|
||||
collectionId?: string;
|
||||
migrateToAutoPilot?: boolean;
|
||||
migrateToManual?: boolean;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
id: string;
|
||||
kind: string;
|
||||
|
||||
@@ -85,7 +85,7 @@ export interface Database extends TreeNode {
|
||||
collapseDatabase(): void;
|
||||
|
||||
loadCollections(): Promise<void>;
|
||||
findCollectionWithId(collectionRid: string): Collection;
|
||||
findCollectionWithId(collectionId: string): Collection;
|
||||
openAddCollection(database: Database, event: MouseEvent): void;
|
||||
onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void;
|
||||
onSettingsClick: () => void;
|
||||
@@ -266,7 +266,6 @@ export interface TabOptions {
|
||||
tabKind: CollectionTabKind;
|
||||
title: string;
|
||||
tabPath: string;
|
||||
selfLink: string;
|
||||
isActive: ko.Observable<boolean>;
|
||||
hashLocation: string;
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]) => void;
|
||||
@@ -305,7 +304,6 @@ export interface QueryTabOptions extends TabOptions {
|
||||
export interface ScriptTabOption extends TabOptions {
|
||||
resource: any;
|
||||
isNew: boolean;
|
||||
collectionSelfLink?: string;
|
||||
partitionKey?: DataModels.PartitionKey;
|
||||
}
|
||||
|
||||
@@ -388,6 +386,7 @@ export interface DataExplorerInputsFrame {
|
||||
dataExplorerVersion?: string;
|
||||
isAuthWithresourceToken?: boolean;
|
||||
defaultCollectionThroughput?: CollectionCreationDefaults;
|
||||
flights?: readonly string[];
|
||||
}
|
||||
|
||||
export interface CollectionCreationDefaults {
|
||||
|
||||
42
src/Definitions/jquery.contextmenu.d.ts
vendored
42
src/Definitions/jquery.contextmenu.d.ts
vendored
@@ -1,42 +0,0 @@
|
||||
// Type definitions for jQuery contextMenu 1.7.0
|
||||
// Project: http://medialize.github.com/jQuery-contextMenu/
|
||||
// Definitions by: Natan Vivo <https://github.com/nvivo/>
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
|
||||
/// <reference path="jquery.d.ts" />
|
||||
|
||||
interface JQueryContextMenuOptions {
|
||||
selector: string;
|
||||
appendTo?: string;
|
||||
trigger?: string;
|
||||
autoHide?: boolean;
|
||||
delay?: number;
|
||||
determinePosition?: (menu: JQuery) => void;
|
||||
position?: (opt: JQuery, x: number, y: number) => void;
|
||||
positionSubmenu?: (menu: JQuery) => void;
|
||||
zIndex?: number;
|
||||
animation?: {
|
||||
duration?: number;
|
||||
show?: string;
|
||||
hide?: string;
|
||||
};
|
||||
events?: {
|
||||
show?: () => void;
|
||||
hide?: () => void;
|
||||
};
|
||||
callback?: (key: any, options: any) => any;
|
||||
items?: any;
|
||||
build?: (triggerElement: JQuery, e: Event) => any;
|
||||
reposition?: boolean;
|
||||
className?: string;
|
||||
itemClickEvent?: string;
|
||||
}
|
||||
|
||||
interface JQueryStatic {
|
||||
contextMenu(options?: JQueryContextMenuOptions): JQuery;
|
||||
contextMenu(type: string, selector?: any): JQuery;
|
||||
}
|
||||
|
||||
interface JQuery {
|
||||
contextMenu(options?: any): JQuery;
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { Dialog, DialogType, DialogFooter, IDialogProps } from "office-ui-fabric
|
||||
import { IButtonProps, PrimaryButton, DefaultButton } from "office-ui-fabric-react/lib/Button";
|
||||
import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField";
|
||||
import { Link } from "office-ui-fabric-react/lib/Link";
|
||||
import { FontIcon } from "office-ui-fabric-react";
|
||||
import { ChoiceGroup, FontIcon, IChoiceGroupProps } from "office-ui-fabric-react";
|
||||
|
||||
export interface TextFieldProps extends ITextFieldProps {
|
||||
label: string;
|
||||
@@ -24,6 +24,7 @@ export interface DialogProps {
|
||||
subText: string;
|
||||
isModal: boolean;
|
||||
visible: boolean;
|
||||
choiceGroupProps?: IChoiceGroupProps;
|
||||
textFieldProps?: TextFieldProps;
|
||||
linkProps?: LinkProps;
|
||||
primaryButtonText: string;
|
||||
@@ -65,6 +66,7 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
|
||||
minWidth: DIALOG_MIN_WIDTH,
|
||||
maxWidth: DIALOG_MAX_WIDTH
|
||||
};
|
||||
const choiceGroupProps: IChoiceGroupProps = this.props.choiceGroupProps;
|
||||
const textFieldProps: ITextFieldProps = this.props.textFieldProps;
|
||||
const linkProps: LinkProps = this.props.linkProps;
|
||||
const primaryButtonProps: IButtonProps = {
|
||||
@@ -82,6 +84,7 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
|
||||
|
||||
return (
|
||||
<Dialog {...dialogProps}>
|
||||
{choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />}
|
||||
{textFieldProps && <TextField {...textFieldProps} />}
|
||||
{linkProps && (
|
||||
<Link href={linkProps.linkUrl} target="_blank">
|
||||
|
||||
@@ -55,7 +55,6 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
||||
label: "Enable Injecting Notebook Viewer Link into the first cell",
|
||||
value: "true"
|
||||
},
|
||||
{ key: "feature.enablesettingsv2", label: "Enable SettingsV2 Tab", value: "true" },
|
||||
{ key: "feature.canexceedmaximumvalue", label: "Can exceed max value", value: "true" },
|
||||
{
|
||||
key: "feature.enablefixedcollectionwithsharedthroughput",
|
||||
|
||||
@@ -178,12 +178,6 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
className="checkboxRow"
|
||||
horizontalAlign="space-between"
|
||||
>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enablesettingsv2"
|
||||
label="Enable SettingsV2 Tab"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.canexceedmaximumvalue"
|
||||
|
||||
@@ -18,7 +18,9 @@ describe("GalleryCardComponent", () => {
|
||||
downloads: 0,
|
||||
favorites: 0,
|
||||
views: 0,
|
||||
newCellId: undefined
|
||||
newCellId: undefined,
|
||||
policyViolations: undefined,
|
||||
pendingScanJobIds: undefined
|
||||
},
|
||||
isFavorite: false,
|
||||
showDownload: true,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Dropdown,
|
||||
FocusZone,
|
||||
FontWeights,
|
||||
IDropdownOption,
|
||||
IPageSpecification,
|
||||
IPivotItemProps,
|
||||
@@ -11,7 +12,8 @@ import {
|
||||
Pivot,
|
||||
PivotItem,
|
||||
SearchBox,
|
||||
Stack
|
||||
Stack,
|
||||
Text
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import * as Logger from "../../../Common/Logger";
|
||||
@@ -151,7 +153,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
// explicitly checking if isCodeOfConductAccepted is not false, as it is initially undefined.
|
||||
// Displaying code of conduct component on gallery load should not be the default behavior.
|
||||
if (this.state.isCodeOfConductAccepted !== false) {
|
||||
tabs.push(this.createTab(GalleryTab.Published, this.state.publishedNotebooks));
|
||||
tabs.push(this.createPublishedNotebooksTab(GalleryTab.Published, this.state.publishedNotebooks));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,10 +199,59 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
||||
return {
|
||||
tab,
|
||||
content: this.createTabContent(data)
|
||||
content: this.createSearchBarHeader(this.createCardsTabContent(data))
|
||||
};
|
||||
}
|
||||
|
||||
private createPublishedNotebooksTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
|
||||
return {
|
||||
tab,
|
||||
content: this.createPublishedNotebooksTabContent(data)
|
||||
};
|
||||
};
|
||||
|
||||
private createPublishedNotebooksTabContent = (data: IGalleryItem[]): JSX.Element => {
|
||||
const { published, underReview, removed } = GalleryUtils.filterPublishedNotebooks(data);
|
||||
const content = (
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
{published?.length > 0 &&
|
||||
this.createPublishedNotebooksSectionContent(
|
||||
undefined,
|
||||
"You have successfully published the following notebook(s) to public gallery and shared with other Azure Cosmos DB users.",
|
||||
this.createCardsTabContent(published)
|
||||
)}
|
||||
{underReview?.length > 0 &&
|
||||
this.createPublishedNotebooksSectionContent(
|
||||
"Under Review",
|
||||
"Content of a notebook you published is currently being scanned for illegal content. It will not be available to public gallery until the review is completed (may take a few days)",
|
||||
this.createCardsTabContent(underReview)
|
||||
)}
|
||||
{removed?.length > 0 &&
|
||||
this.createPublishedNotebooksSectionContent(
|
||||
"Removed",
|
||||
"These notebooks were found to contain illegal content and has been taken down.",
|
||||
this.createPolicyViolationsListContent(removed)
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
return this.createSearchBarHeader(content);
|
||||
};
|
||||
|
||||
private createPublishedNotebooksSectionContent = (
|
||||
title: string,
|
||||
description: string,
|
||||
content: JSX.Element
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: 5 }}>
|
||||
{title && <Text styles={{ root: { fontWeight: FontWeights.semibold } }}>{title}</Text>}
|
||||
{description && <Text>{description}</Text>}
|
||||
{content}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
private createPublicGalleryTabContent(data: IGalleryItem[], acceptedCodeOfConduct: boolean): JSX.Element {
|
||||
return acceptedCodeOfConduct === false ? (
|
||||
<CodeOfConductComponent
|
||||
@@ -210,11 +261,11 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
this.createTabContent(data)
|
||||
this.createSearchBarHeader(this.createCardsTabContent(data))
|
||||
);
|
||||
}
|
||||
|
||||
private createTabContent(data: IGalleryItem[]): JSX.Element {
|
||||
private createSearchBarHeader(content: JSX.Element): JSX.Element {
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
<Stack horizontal tokens={{ childrenGap: 20, padding: 10 }}>
|
||||
@@ -227,13 +278,13 @@ 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>
|
||||
)}
|
||||
</Stack>
|
||||
{data && this.createCardsTabContent(data)}
|
||||
<Stack.Item>{content}</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -251,6 +302,25 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
);
|
||||
}
|
||||
|
||||
private createPolicyViolationsListContent(data: IGalleryItem[]): JSX.Element {
|
||||
return (
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Policy violations</th>
|
||||
</tr>
|
||||
{data.map(item => (
|
||||
<tr key={`policy-violations-tr-${item.id}`}>
|
||||
<td>{item.name}</td>
|
||||
<td>{item.policyViolations.join(", ")}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
private loadTabContent(tab: GalleryTab, searchText: string, sortBy: SortBy, offline: boolean): void {
|
||||
switch (tab) {
|
||||
case GalleryTab.OfficialSamples:
|
||||
|
||||
@@ -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 && (
|
||||
<Stack.Item>
|
||||
{this.getInfoPanel("ReportHacked", "Report Abuse", undefined, () => this.props.onReportAbuseClick())}
|
||||
</Stack.Item>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -77,7 +77,25 @@ exports[`GalleryViewerComponent renders 1`] = `
|
||||
selectedKey={0}
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<InfoComponent />
|
||||
</StackItem>
|
||||
</Stack>
|
||||
<StackItem>
|
||||
<FocusZone
|
||||
direction={2}
|
||||
isCircularNavigation={false}
|
||||
shouldRaiseClicks={true}
|
||||
>
|
||||
<List
|
||||
getPageSpecification={[Function]}
|
||||
onRenderCell={[Function]}
|
||||
renderedWindowsAhead={3}
|
||||
renderedWindowsBehind={2}
|
||||
startIndex={0}
|
||||
/>
|
||||
</FocusZone>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
</StyledPivotBase>
|
||||
|
||||
@@ -18,14 +18,17 @@ describe("NotebookMetadataComponent", () => {
|
||||
downloads: 0,
|
||||
favorites: 0,
|
||||
views: 0,
|
||||
newCellId: undefined
|
||||
newCellId: undefined,
|
||||
policyViolations: undefined,
|
||||
pendingScanJobIds: undefined
|
||||
},
|
||||
isFavorite: false,
|
||||
downloadButtonText: "Download",
|
||||
onTagClick: undefined,
|
||||
onDownloadClick: undefined,
|
||||
onFavoriteClick: undefined,
|
||||
onUnfavoriteClick: undefined
|
||||
onUnfavoriteClick: undefined,
|
||||
onReportAbuseClick: undefined
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
|
||||
@@ -47,14 +50,17 @@ describe("NotebookMetadataComponent", () => {
|
||||
downloads: 0,
|
||||
favorites: 0,
|
||||
views: 0,
|
||||
newCellId: undefined
|
||||
newCellId: undefined,
|
||||
policyViolations: undefined,
|
||||
pendingScanJobIds: undefined
|
||||
},
|
||||
isFavorite: true,
|
||||
downloadButtonText: "Download",
|
||||
onTagClick: undefined,
|
||||
onDownloadClick: undefined,
|
||||
onFavoriteClick: undefined,
|
||||
onUnfavoriteClick: undefined
|
||||
onUnfavoriteClick: undefined,
|
||||
onReportAbuseClick: undefined
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
|
||||
|
||||
@@ -17,6 +17,7 @@ import { IGalleryItem } from "../../../Juno/JunoClient";
|
||||
import { FileSystemUtil } from "../../Notebook/FileSystemUtil";
|
||||
import "./NotebookViewerComponent.less";
|
||||
import CosmosDBLogo from "../../../../images/CosmosDB-logo.svg";
|
||||
import { InfoComponent } from "../NotebookGallery/InfoComponent/InfoComponent";
|
||||
|
||||
export interface NotebookMetadataComponentProps {
|
||||
data: IGalleryItem;
|
||||
@@ -26,6 +27,7 @@ export interface NotebookMetadataComponentProps {
|
||||
onFavoriteClick: () => void;
|
||||
onUnfavoriteClick: () => void;
|
||||
onDownloadClick: () => void;
|
||||
onReportAbuseClick: () => void;
|
||||
}
|
||||
|
||||
export class NotebookMetadataComponent extends React.Component<NotebookMetadataComponentProps> {
|
||||
@@ -41,24 +43,39 @@ export class NotebookMetadataComponent extends React.Component<NotebookMetadataC
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 30 }}>
|
||||
<Text variant="xxLarge" nowrap>
|
||||
{FileSystemUtil.stripExtension(this.props.data.name, "ipynb")}
|
||||
</Text>
|
||||
<Text>
|
||||
{this.props.isFavorite !== undefined && (
|
||||
<>
|
||||
<IconButton
|
||||
iconProps={{ iconName: this.props.isFavorite ? "HeartFill" : "Heart" }}
|
||||
onClick={this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick}
|
||||
/>
|
||||
{this.props.data.favorites} likes
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
<Stack.Item>
|
||||
<Text variant="xxLarge" nowrap>
|
||||
{FileSystemUtil.stripExtension(this.props.data.name, "ipynb")}
|
||||
</Text>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<Text>
|
||||
{this.props.isFavorite !== undefined && (
|
||||
<>
|
||||
<IconButton
|
||||
iconProps={{ iconName: this.props.isFavorite ? "HeartFill" : "Heart" }}
|
||||
onClick={this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick}
|
||||
/>
|
||||
{this.props.data.favorites} likes
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</Stack.Item>
|
||||
|
||||
{this.props.downloadButtonText && (
|
||||
<PrimaryButton text={this.props.downloadButtonText} onClick={this.props.onDownloadClick} />
|
||||
<Stack.Item>
|
||||
<PrimaryButton text={this.props.downloadButtonText} onClick={this.props.onDownloadClick} />
|
||||
</Stack.Item>
|
||||
)}
|
||||
|
||||
<Stack.Item grow>
|
||||
<></>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<InfoComponent onReportAbuseClick={this.props.onReportAbuseClick} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
|
||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 10 }}>
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
*/
|
||||
import { Notebook } from "@nteract/commutable";
|
||||
import { createContentRef } from "@nteract/core";
|
||||
import { Icon, Link, ProgressIndicator } from "office-ui-fabric-react";
|
||||
import { IChoiceGroupProps, Icon, Link, ProgressIndicator } from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { contents } from "rx-jupyter";
|
||||
import * as Logger from "../../../Common/Logger";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
|
||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
@@ -15,12 +14,13 @@ import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationCon
|
||||
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
|
||||
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
|
||||
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
|
||||
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
|
||||
import { DialogComponent, DialogProps, TextFieldProps } from "../DialogReactComponent/DialogComponent";
|
||||
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
|
||||
import "./NotebookViewerComponent.less";
|
||||
import Explorer from "../../Explorer";
|
||||
import { NotebookV4 } from "@nteract/commutable/lib/v4";
|
||||
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
|
||||
import { DialogHost } from "../../../Utils/GalleryUtils";
|
||||
|
||||
export interface NotebookViewerComponentProps {
|
||||
container?: Explorer;
|
||||
@@ -43,10 +43,8 @@ interface NotebookViewerComponentState {
|
||||
showProgressBar: boolean;
|
||||
}
|
||||
|
||||
export class NotebookViewerComponent extends React.Component<
|
||||
NotebookViewerComponentProps,
|
||||
NotebookViewerComponentState
|
||||
> {
|
||||
export class NotebookViewerComponent extends React.Component<NotebookViewerComponentProps, NotebookViewerComponentState>
|
||||
implements DialogHost {
|
||||
private clientManager: NotebookClientV2;
|
||||
private notebookComponentBootstrapper: NotebookComponentBootstrapper;
|
||||
|
||||
@@ -140,6 +138,7 @@ export class NotebookViewerComponent extends React.Component<
|
||||
onFavoriteClick={this.favoriteItem}
|
||||
onUnfavoriteClick={this.unfavoriteItem}
|
||||
onDownloadClick={this.downloadItem}
|
||||
onReportAbuseClick={this.state.galleryItem.isSample ? undefined : this.reportAbuse}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@@ -179,6 +178,39 @@ export class NotebookViewerComponent extends React.Component<
|
||||
};
|
||||
}
|
||||
|
||||
// DialogHost
|
||||
showOkCancelModalDialog(
|
||||
title: string,
|
||||
msg: string,
|
||||
okLabel: string,
|
||||
onOk: () => void,
|
||||
cancelLabel: string,
|
||||
onCancel: () => void,
|
||||
choiceGroupProps?: IChoiceGroupProps,
|
||||
textFieldProps?: TextFieldProps
|
||||
): void {
|
||||
this.setState({
|
||||
dialogProps: {
|
||||
isModal: true,
|
||||
visible: true,
|
||||
title,
|
||||
subText: msg,
|
||||
primaryButtonText: okLabel,
|
||||
secondaryButtonText: cancelLabel,
|
||||
onPrimaryButtonClick: () => {
|
||||
this.setState({ dialogProps: undefined });
|
||||
onOk && onOk();
|
||||
},
|
||||
onSecondaryButtonClick: () => {
|
||||
this.setState({ dialogProps: undefined });
|
||||
onCancel && onCancel();
|
||||
},
|
||||
choiceGroupProps,
|
||||
textFieldProps
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private favoriteItem = async (): Promise<void> => {
|
||||
GalleryUtils.favoriteItem(this.props.container, this.props.junoClient, this.state.galleryItem, item =>
|
||||
this.setState({ galleryItem: item, isFavorite: true })
|
||||
@@ -196,4 +228,8 @@ export class NotebookViewerComponent extends React.Component<
|
||||
this.setState({ galleryItem: item })
|
||||
);
|
||||
};
|
||||
|
||||
private reportAbuse = (): void => {
|
||||
GalleryUtils.reportAbuse(this.props.junoClient, this.state.galleryItem, this, () => {});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,26 +17,38 @@ exports[`NotebookMetadataComponent renders liked notebook 1`] = `
|
||||
}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Text
|
||||
nowrap={true}
|
||||
variant="xxLarge"
|
||||
>
|
||||
name
|
||||
</Text>
|
||||
<Text>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "HeartFill",
|
||||
<StackItem>
|
||||
<Text
|
||||
nowrap={true}
|
||||
variant="xxLarge"
|
||||
>
|
||||
name
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<Text>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "HeartFill",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
0
|
||||
likes
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<CustomizedPrimaryButton
|
||||
text="Download"
|
||||
/>
|
||||
0
|
||||
likes
|
||||
</Text>
|
||||
<CustomizedPrimaryButton
|
||||
text="Download"
|
||||
</StackItem>
|
||||
<StackItem
|
||||
grow={true}
|
||||
/>
|
||||
<StackItem>
|
||||
<InfoComponent />
|
||||
</StackItem>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
@@ -117,26 +129,38 @@ exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
|
||||
}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Text
|
||||
nowrap={true}
|
||||
variant="xxLarge"
|
||||
>
|
||||
name
|
||||
</Text>
|
||||
<Text>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Heart",
|
||||
<StackItem>
|
||||
<Text
|
||||
nowrap={true}
|
||||
variant="xxLarge"
|
||||
>
|
||||
name
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<Text>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Heart",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
0
|
||||
likes
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<CustomizedPrimaryButton
|
||||
text="Download"
|
||||
/>
|
||||
0
|
||||
likes
|
||||
</Text>
|
||||
<CustomizedPrimaryButton
|
||||
text="Download"
|
||||
</StackItem>
|
||||
<StackItem
|
||||
grow={true}
|
||||
/>
|
||||
<StackItem>
|
||||
<InfoComponent />
|
||||
</StackItem>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
|
||||
@@ -6,7 +6,7 @@ import SettingsTabV2 from "../../Tabs/SettingsTabV2";
|
||||
import { collection } from "./TestUtils";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import ko from "knockout";
|
||||
import { TtlType, isDirty, TtlOnNoDefault, TtlOn, TtlOff } from "./SettingsUtils";
|
||||
import { TtlType, isDirty } from "./SettingsUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
|
||||
jest.mock("../../../Common/dataAccess/updateCollection", () => ({
|
||||
@@ -20,8 +20,8 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
|
||||
geospatialConfig: undefined
|
||||
} as DataModels.Collection)
|
||||
}));
|
||||
import { updateOffer } from "../../../Common/DocumentClientUtilityBase";
|
||||
jest.mock("../../../Common/DocumentClientUtilityBase", () => ({
|
||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||
jest.mock("../../../Common/dataAccess/updateOffer", () => ({
|
||||
updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer)
|
||||
}));
|
||||
|
||||
@@ -33,7 +33,6 @@ describe("SettingsComponent", () => {
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
node: undefined,
|
||||
selfLink: undefined,
|
||||
hashLocation: "settings",
|
||||
isActive: ko.observable(false),
|
||||
onUpdateTabsButtons: undefined
|
||||
@@ -103,10 +102,7 @@ describe("SettingsComponent", () => {
|
||||
let settingsComponentInstance = new SettingsComponent(baseProps);
|
||||
expect(settingsComponentInstance.shouldShowKeyspaceSharedThroughputMessage()).toEqual(false);
|
||||
|
||||
const newContainer = new Explorer({
|
||||
notificationsClient: undefined,
|
||||
isEmulator: false
|
||||
});
|
||||
const newContainer = new Explorer();
|
||||
newContainer.isPreferredApiCassandra = ko.computed(() => true);
|
||||
|
||||
const newCollection = { ...collection };
|
||||
@@ -147,10 +143,7 @@ describe("SettingsComponent", () => {
|
||||
let settingsComponentInstance = new SettingsComponent(baseProps);
|
||||
expect(settingsComponentInstance.hasConflictResolution()).toEqual(undefined);
|
||||
|
||||
const newContainer = new Explorer({
|
||||
notificationsClient: undefined,
|
||||
isEmulator: false
|
||||
});
|
||||
const newContainer = new Explorer();
|
||||
newContainer.databaseAccount = ko.observable({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
@@ -220,13 +213,6 @@ describe("SettingsComponent", () => {
|
||||
expect(isDirty(state.throughput, state.throughputBaseline)).toEqual(false);
|
||||
});
|
||||
|
||||
it("getTtlValue", async () => {
|
||||
const settingsComponentInstance = new SettingsComponent(baseProps);
|
||||
expect(settingsComponentInstance.getTtlValue(TtlType.OnNoDefault)).toEqual(TtlOnNoDefault);
|
||||
expect(settingsComponentInstance.getTtlValue(TtlType.On)).toEqual(TtlOn);
|
||||
expect(settingsComponentInstance.getTtlValue(TtlType.Off)).toEqual(TtlOff);
|
||||
});
|
||||
|
||||
it("getAnalyticalStorageTtl", () => {
|
||||
const newCollection = { ...collection };
|
||||
newCollection.analyticalStorageTtl = ko.observable(10);
|
||||
|
||||
@@ -6,11 +6,11 @@ import * as SharedConstants from "../../../Shared/Constants";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import DiscardIcon from "../../../../images/discard.svg";
|
||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||
import { traceStart, traceFailure, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { traceStart, traceFailure, traceSuccess, trace } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import Explorer from "../../Explorer";
|
||||
import { updateOffer } from "../../../Common/DocumentClientUtilityBase";
|
||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import { userContext } from "../../../UserContext";
|
||||
@@ -27,9 +27,6 @@ import {
|
||||
SettingsV2TabTypes,
|
||||
getTabTitle,
|
||||
isDirty,
|
||||
TtlOff,
|
||||
TtlOn,
|
||||
TtlOnNoDefault,
|
||||
parseConflictResolutionMode,
|
||||
parseConflictResolutionProcedure
|
||||
} from "./SettingsUtils";
|
||||
@@ -38,7 +35,7 @@ import {
|
||||
ConflictResolutionComponentProps
|
||||
} from "./SettingsSubComponents/ConflictResolutionComponent";
|
||||
import { SubSettingsComponent, SubSettingsComponentProps } from "./SettingsSubComponents/SubSettingsComponent";
|
||||
import { Pivot, PivotItem, IPivotProps, IPivotItemProps, IChoiceGroupOption } from "office-ui-fabric-react";
|
||||
import { Pivot, PivotItem, IPivotProps, IPivotItemProps } from "office-ui-fabric-react";
|
||||
import "./SettingsComponent.less";
|
||||
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
|
||||
|
||||
@@ -85,7 +82,6 @@ export interface SettingsComponentState {
|
||||
indexingPolicyContent: DataModels.IndexingPolicy;
|
||||
indexingPolicyContentBaseline: DataModels.IndexingPolicy;
|
||||
shouldDiscardIndexingPolicy: boolean;
|
||||
indexingPolicyElementFocussed: boolean;
|
||||
isIndexingPolicyDirty: boolean;
|
||||
|
||||
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode;
|
||||
@@ -102,7 +98,6 @@ export interface SettingsComponentState {
|
||||
|
||||
export class SettingsComponent extends React.Component<SettingsComponentProps, SettingsComponentState> {
|
||||
private static readonly sixMonthsInSeconds = 15768000;
|
||||
private static readonly zeroSeconds = 0;
|
||||
|
||||
public saveSettingsButton: ButtonV2;
|
||||
public discardSettingsChangesButton: ButtonV2;
|
||||
@@ -127,8 +122,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.changeFeedPolicyVisible = this.collection?.container.isFeatureEnabled(
|
||||
Constants.Features.enableChangeFeedPolicy
|
||||
);
|
||||
// Mongo container with system partition key still treat as "Fixed"
|
||||
|
||||
// Mongo container with system partition key still treat as "Fixed"
|
||||
this.isFixedContainer =
|
||||
!this.collection.partitionKey ||
|
||||
(this.container.isPreferredApiMongoDB() && this.collection.partitionKey.systemKey);
|
||||
@@ -160,7 +155,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
|
||||
indexingPolicyContent: undefined,
|
||||
indexingPolicyContentBaseline: undefined,
|
||||
indexingPolicyElementFocussed: false,
|
||||
shouldDiscardIndexingPolicy: false,
|
||||
isIndexingPolicyDirty: false,
|
||||
|
||||
@@ -270,7 +264,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.props.settingsTab.isExecutionError(false);
|
||||
|
||||
this.props.settingsTab.isExecuting(true);
|
||||
const startKey: number = traceStart(Action.UpdateSettings, {
|
||||
const startKey: number = traceStart(Action.SettingsV2Updated, {
|
||||
databaseAccountName: this.container.databaseAccount()?.name,
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
@@ -411,11 +405,11 @@ 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,
|
||||
collectionName: this.collection && this.collection.id(),
|
||||
databaseName: this.collection?.databaseId,
|
||||
collectionName: this.collection?.id(),
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.props.settingsTab.tabTitle(),
|
||||
@@ -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,9 +454,11 @@ 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,
|
||||
databaseName: this.collection?.databaseId,
|
||||
collectionName: this.collection?.id(),
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.props.settingsTab.tabTitle()
|
||||
@@ -460,12 +470,15 @@ 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,
|
||||
databaseName: this.collection?.databaseId,
|
||||
collectionName: this.collection?.id(),
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.props.settingsTab.tabTitle()
|
||||
tabTitle: this.props.settingsTab.tabTitle(),
|
||||
error: reason
|
||||
},
|
||||
startKey
|
||||
);
|
||||
@@ -474,6 +487,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 +521,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 +544,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 +799,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
resetShouldDiscardIndexingPolicy: this.resetShouldDiscardIndexingPolicy,
|
||||
indexingPolicyContent: this.state.indexingPolicyContent,
|
||||
indexingPolicyContentBaseline: this.state.indexingPolicyContentBaseline,
|
||||
onIndexingPolicyElementFocusChange: this.onIndexingPolicyElementFocusChange,
|
||||
onIndexingPolicyContentChange: this.onIndexingPolicyContentChange,
|
||||
logIndexingPolicySuccessMessage: this.logIndexingPolicySuccessMessage,
|
||||
onIndexingPolicyDirtyChange: this.onIndexingPolicyDirtyChange
|
||||
|
||||
@@ -18,24 +18,15 @@ export interface ConflictResolutionComponentProps {
|
||||
container: Explorer;
|
||||
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode;
|
||||
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode;
|
||||
onConflictResolutionPolicyModeChange: (
|
||||
event?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
) => void;
|
||||
onConflictResolutionPolicyModeChange: (newMode: DataModels.ConflictResolutionMode) => void;
|
||||
conflictResolutionPolicyPath: string;
|
||||
conflictResolutionPolicyPathBaseline: string;
|
||||
|
||||
onConflictResolutionPolicyPathChange: (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
) => void;
|
||||
onConflictResolutionPolicyPathChange: (newPath: string) => void;
|
||||
conflictResolutionPolicyProcedure: string;
|
||||
conflictResolutionPolicyProcedureBaseline: string;
|
||||
|
||||
onConflictResolutionPolicyProcedureChange: (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
) => void;
|
||||
onConflictResolutionPolicyProcedureChange: (newProcedure: string) => void;
|
||||
onConflictResolutionDirtyChange: (isConflictResolutionDirty: boolean) => void;
|
||||
}
|
||||
|
||||
@@ -77,12 +68,30 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
|
||||
return false;
|
||||
};
|
||||
|
||||
private onConflictResolutionPolicyModeChange = (
|
||||
event?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
): void =>
|
||||
this.props.onConflictResolutionPolicyModeChange(
|
||||
DataModels.ConflictResolutionMode[option.key as keyof typeof DataModels.ConflictResolutionMode]
|
||||
);
|
||||
|
||||
private onConflictResolutionPolicyPathChange = (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => this.props.onConflictResolutionPolicyPathChange(newValue);
|
||||
|
||||
private onConflictResolutionPolicyProcedureChange = (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => this.props.onConflictResolutionPolicyProcedureChange(newValue);
|
||||
|
||||
private getConflictResolutionModeComponent = (): JSX.Element => (
|
||||
<ChoiceGroup
|
||||
label="Mode"
|
||||
selectedKey={this.props.conflictResolutionPolicyMode}
|
||||
options={this.conflictResolutionChoiceGroupOptions}
|
||||
onChange={this.props.onConflictResolutionPolicyModeChange}
|
||||
onChange={this.onConflictResolutionPolicyModeChange}
|
||||
styles={getChoiceGroupStyles(
|
||||
this.props.conflictResolutionPolicyMode,
|
||||
this.props.conflictResolutionPolicyModeBaseline
|
||||
@@ -104,7 +113,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
|
||||
this.props.conflictResolutionPolicyPathBaseline
|
||||
)}
|
||||
value={this.props.conflictResolutionPolicyPath}
|
||||
onChange={this.props.onConflictResolutionPolicyPathChange}
|
||||
onChange={this.onConflictResolutionPolicyPathChange}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -122,7 +131,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
|
||||
this.props.conflictResolutionPolicyProcedureBaseline
|
||||
)}
|
||||
value={this.props.conflictResolutionPolicyProcedure}
|
||||
onChange={this.props.onConflictResolutionPolicyProcedureChange}
|
||||
onChange={this.onConflictResolutionPolicyProcedureChange}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -17,9 +17,6 @@ describe("IndexingPolicyComponent", () => {
|
||||
},
|
||||
indexingPolicyContent: initialIndexingPolicyContent,
|
||||
indexingPolicyContentBaseline: initialIndexingPolicyContent,
|
||||
onIndexingPolicyElementFocusChange: () => {
|
||||
return;
|
||||
},
|
||||
onIndexingPolicyContentChange: () => {
|
||||
return;
|
||||
},
|
||||
|
||||
@@ -10,7 +10,6 @@ export interface IndexingPolicyComponentProps {
|
||||
resetShouldDiscardIndexingPolicy: () => void;
|
||||
indexingPolicyContent: DataModels.IndexingPolicy;
|
||||
indexingPolicyContentBaseline: DataModels.IndexingPolicy;
|
||||
onIndexingPolicyElementFocusChange: (indexingPolicyContentFocussed: boolean) => void;
|
||||
onIndexingPolicyContentChange: (newIndexingPolicy: DataModels.IndexingPolicy) => void;
|
||||
logIndexingPolicySuccessMessage: () => void;
|
||||
onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void;
|
||||
@@ -89,8 +88,6 @@ export class IndexingPolicyComponent extends React.Component<
|
||||
ariaLabel: "Indexing Policy"
|
||||
});
|
||||
if (this.indexingPolicyEditor) {
|
||||
this.indexingPolicyEditor.onDidFocusEditorText(() => this.props.onIndexingPolicyElementFocusChange(true));
|
||||
this.indexingPolicyEditor.onDidBlurEditorText(() => this.props.onIndexingPolicyElementFocusChange(false));
|
||||
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
|
||||
indexingPolicyEditorModel.onDidChangeContent(this.onEditorContentChange.bind(this));
|
||||
this.props.logIndexingPolicySuccessMessage();
|
||||
|
||||
@@ -5,18 +5,13 @@ import { container, collection } from "../TestUtils";
|
||||
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
|
||||
import Explorer from "../../../Explorer";
|
||||
import * as Constants from "../../../../Common/Constants";
|
||||
import { PlatformType } from "../../../../PlatformType";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import { throughputUnit } from "../SettingsRenderUtils";
|
||||
import * as SharedConstants from "../../../../Shared/Constants";
|
||||
import ko from "knockout";
|
||||
|
||||
describe("ScaleComponent", () => {
|
||||
const nonNationalCloudContainer = new Explorer({
|
||||
notificationsClient: undefined,
|
||||
isEmulator: false
|
||||
});
|
||||
nonNationalCloudContainer.getPlatformType = () => PlatformType.Portal;
|
||||
const nonNationalCloudContainer = new Explorer();
|
||||
nonNationalCloudContainer.isRunningOnNationalCloud = () => false;
|
||||
|
||||
const targetThroughput = 6000;
|
||||
@@ -88,10 +83,7 @@ describe("ScaleComponent", () => {
|
||||
});
|
||||
|
||||
it("autoScale enabled", () => {
|
||||
const newContainer = new Explorer({
|
||||
notificationsClient: undefined,
|
||||
isEmulator: false
|
||||
});
|
||||
const newContainer = new Explorer();
|
||||
|
||||
newContainer.databaseAccount({
|
||||
id: undefined,
|
||||
@@ -125,7 +117,7 @@ describe("ScaleComponent", () => {
|
||||
|
||||
it("getThroughputTitle", () => {
|
||||
let scaleComponent = new ScaleComponent(baseProps);
|
||||
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - 40,000 RU/s)");
|
||||
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
|
||||
|
||||
let newProps = { ...baseProps, container: nonNationalCloudContainer };
|
||||
scaleComponent = new ScaleComponent(newProps);
|
||||
@@ -138,7 +130,7 @@ describe("ScaleComponent", () => {
|
||||
|
||||
it("canThroughputExceedMaximumValue", () => {
|
||||
let scaleComponent = new ScaleComponent(baseProps);
|
||||
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(false);
|
||||
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true);
|
||||
|
||||
const newProps = { ...baseProps, container: nonNationalCloudContainer };
|
||||
scaleComponent = new ScaleComponent(newProps);
|
||||
|
||||
@@ -5,7 +5,6 @@ import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import * as SharedConstants from "../../../../Shared/Constants";
|
||||
import Explorer from "../../../Explorer";
|
||||
import { PlatformType } from "../../../../PlatformType";
|
||||
import {
|
||||
getTextFieldStyles,
|
||||
subComponentStackProps,
|
||||
@@ -19,6 +18,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 +43,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 => {
|
||||
@@ -77,7 +77,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
};
|
||||
|
||||
public getMaxRUThroughputInputLimit = (): number => {
|
||||
if (this.props.container?.getPlatformType() === PlatformType.Hosted && this.props.collection.partitionKey) {
|
||||
if (configContext.platform === Platform.Hosted && this.props.collection.partitionKey) {
|
||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||
}
|
||||
|
||||
@@ -99,8 +99,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
|
||||
public canThroughputExceedMaximumValue = (): boolean => {
|
||||
const isPublicAzurePortal: boolean =
|
||||
this.props.container.getPlatformType() === PlatformType.Portal &&
|
||||
!this.props.container.isRunningOnNationalCloud();
|
||||
configContext.platform === Platform.Portal && !this.props.container.isRunningOnNationalCloud();
|
||||
const hasPartitionKey = !!this.props.collection.partitionKey;
|
||||
|
||||
return isPublicAzurePortal && hasPartitionKey;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { SubSettingsComponent, SubSettingsComponentProps } from "./SubSettingsComponent";
|
||||
import { container, collection } from "../TestUtils";
|
||||
import { TtlType, GeospatialConfigType, ChangeFeedPolicyState } from "../SettingsUtils";
|
||||
import { TtlType, GeospatialConfigType, ChangeFeedPolicyState, TtlOnNoDefault, TtlOn, TtlOff } from "../SettingsUtils";
|
||||
import ko from "knockout";
|
||||
import Explorer from "../../../Explorer";
|
||||
|
||||
@@ -105,10 +105,7 @@ describe("SubSettingsComponent", () => {
|
||||
});
|
||||
|
||||
it("partitionKey not visible", () => {
|
||||
const newContainer = new Explorer({
|
||||
notificationsClient: undefined,
|
||||
isEmulator: false
|
||||
});
|
||||
const newContainer = new Explorer();
|
||||
|
||||
newContainer.isPreferredApiCassandra = ko.computed(() => true);
|
||||
const props = { ...baseProps, container: newContainer };
|
||||
@@ -133,4 +130,11 @@ describe("SubSettingsComponent", () => {
|
||||
expect(isComponentDirtyResult.isSaveable).toEqual(true);
|
||||
expect(isComponentDirtyResult.isDiscardable).toEqual(true);
|
||||
});
|
||||
|
||||
it("getTtlValue", async () => {
|
||||
const subSettingsComponentInstance = new SubSettingsComponent(baseProps);
|
||||
expect(subSettingsComponentInstance.getTtlValue(TtlType.OnNoDefault)).toEqual(TtlOnNoDefault);
|
||||
expect(subSettingsComponentInstance.getTtlValue(TtlType.On)).toEqual(TtlOn);
|
||||
expect(subSettingsComponentInstance.getTtlValue(TtlType.Off)).toEqual(TtlOff);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,11 @@ import {
|
||||
TtlType,
|
||||
ChangeFeedPolicyState,
|
||||
isDirty,
|
||||
IsComponentDirtyResult
|
||||
IsComponentDirtyResult,
|
||||
TtlOn,
|
||||
TtlOff,
|
||||
TtlOnNoDefault,
|
||||
getSanitizedInputValue
|
||||
} from "../SettingsUtils";
|
||||
import Explorer from "../../../Explorer";
|
||||
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||
@@ -37,40 +41,28 @@ export interface SubSettingsComponentProps {
|
||||
timeToLive: TtlType;
|
||||
timeToLiveBaseline: TtlType;
|
||||
|
||||
onTtlChange: (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, option?: IChoiceGroupOption) => void;
|
||||
onTtlChange: (newTtl: TtlType) => void;
|
||||
timeToLiveSeconds: number;
|
||||
timeToLiveSecondsBaseline: number;
|
||||
onTimeToLiveSecondsChange: (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
) => void;
|
||||
onTimeToLiveSecondsChange: (newTimeToLiveSeconds: number) => void;
|
||||
|
||||
geospatialConfigType: GeospatialConfigType;
|
||||
geospatialConfigTypeBaseline: GeospatialConfigType;
|
||||
onGeoSpatialConfigTypeChange: (
|
||||
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
) => void;
|
||||
onGeoSpatialConfigTypeChange: (newGeoSpatialConfigType: GeospatialConfigType) => void;
|
||||
|
||||
isAnalyticalStorageEnabled: boolean;
|
||||
analyticalStorageTtlSelection: TtlType;
|
||||
analyticalStorageTtlSelectionBaseline: TtlType;
|
||||
onAnalyticalStorageTtlSelectionChange: (
|
||||
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
) => void;
|
||||
onAnalyticalStorageTtlSelectionChange: (newAnalyticalStorageTtlSelection: TtlType) => void;
|
||||
|
||||
analyticalStorageTtlSeconds: number;
|
||||
analyticalStorageTtlSecondsBaseline: number;
|
||||
onAnalyticalStorageTtlSecondsChange: (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
) => void;
|
||||
onAnalyticalStorageTtlSecondsChange: (newAnalyticalStorageTtlSeconds: number) => void;
|
||||
|
||||
changeFeedPolicyVisible: boolean;
|
||||
changeFeedPolicy: ChangeFeedPolicyState;
|
||||
changeFeedPolicyBaseline: ChangeFeedPolicyState;
|
||||
onChangeFeedPolicyChange: (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, option?: IChoiceGroupOption) => void;
|
||||
onChangeFeedPolicyChange: (newChangeFeedPolicyState: ChangeFeedPolicyState) => void;
|
||||
onSubSettingsSaveableChange: (isSubSettingsSaveable: boolean) => void;
|
||||
onSubSettingsDiscardableChange: (isSubSettingsDiscardable: boolean) => void;
|
||||
}
|
||||
@@ -139,6 +131,54 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
{ key: TtlType.On, text: "On" }
|
||||
];
|
||||
|
||||
public getTtlValue = (value: string): TtlType => {
|
||||
switch (value) {
|
||||
case TtlOn:
|
||||
return TtlType.On;
|
||||
case TtlOff:
|
||||
return TtlType.Off;
|
||||
case TtlOnNoDefault:
|
||||
return TtlType.OnNoDefault;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
private onTtlChange = (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, option?: IChoiceGroupOption): void =>
|
||||
this.props.onTtlChange(this.getTtlValue(option.key));
|
||||
|
||||
private onTimeToLiveSecondsChange = (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => {
|
||||
const newTimeToLiveSeconds = getSanitizedInputValue(newValue, Int32.Max);
|
||||
this.props.onTimeToLiveSecondsChange(newTimeToLiveSeconds);
|
||||
};
|
||||
|
||||
private onGeoSpatialConfigTypeChange = (
|
||||
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
): void =>
|
||||
this.props.onGeoSpatialConfigTypeChange(GeospatialConfigType[option.key as keyof typeof GeospatialConfigType]);
|
||||
|
||||
private onAnalyticalStorageTtlSelectionChange = (
|
||||
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
): void => this.props.onAnalyticalStorageTtlSelectionChange(this.getTtlValue(option.key));
|
||||
|
||||
private onAnalyticalStorageTtlSecondsChange = (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => {
|
||||
const newAnalyticalStorageTtlSeconds = getSanitizedInputValue(newValue, Int32.Max);
|
||||
this.props.onAnalyticalStorageTtlSecondsChange(newAnalyticalStorageTtlSeconds);
|
||||
};
|
||||
|
||||
private onChangeFeedPolicyChange = (
|
||||
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
): void =>
|
||||
this.props.onChangeFeedPolicyChange(ChangeFeedPolicyState[option.key as keyof typeof ChangeFeedPolicyState]);
|
||||
|
||||
private getTtlComponent = (): JSX.Element => (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<ChoiceGroup
|
||||
@@ -146,7 +186,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
label="Time to Live"
|
||||
selectedKey={this.props.timeToLive}
|
||||
options={this.ttlChoiceGroupOptions}
|
||||
onChange={this.props.onTtlChange}
|
||||
onChange={this.onTtlChange}
|
||||
styles={getChoiceGroupStyles(this.props.timeToLive, this.props.timeToLiveBaseline)}
|
||||
/>
|
||||
{isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && (
|
||||
@@ -163,7 +203,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
min={1}
|
||||
max={Int32.Max}
|
||||
value={this.props.timeToLiveSeconds?.toString()}
|
||||
onChange={this.props.onTimeToLiveSecondsChange}
|
||||
onChange={this.onTimeToLiveSecondsChange}
|
||||
suffix="second(s)"
|
||||
/>
|
||||
)}
|
||||
@@ -183,7 +223,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
label="Analytical Storage Time to Live"
|
||||
selectedKey={this.props.analyticalStorageTtlSelection}
|
||||
options={this.analyticalTtlChoiceGroupOptions}
|
||||
onChange={this.props.onAnalyticalStorageTtlSelectionChange}
|
||||
onChange={this.onAnalyticalStorageTtlSelectionChange}
|
||||
styles={getChoiceGroupStyles(
|
||||
this.props.analyticalStorageTtlSelection,
|
||||
this.props.analyticalStorageTtlSelectionBaseline
|
||||
@@ -202,7 +242,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
max={Int32.Max}
|
||||
value={this.props.analyticalStorageTtlSeconds?.toString()}
|
||||
suffix="second(s)"
|
||||
onChange={this.props.onAnalyticalStorageTtlSecondsChange}
|
||||
onChange={this.onAnalyticalStorageTtlSecondsChange}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -219,7 +259,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
label="Geospatial Configuration"
|
||||
selectedKey={this.props.geospatialConfigType}
|
||||
options={this.geoSpatialConfigTypeChoiceGroupOptions}
|
||||
onChange={this.props.onGeoSpatialConfigTypeChange}
|
||||
onChange={this.onGeoSpatialConfigTypeChange}
|
||||
styles={getChoiceGroupStyles(this.props.geospatialConfigType, this.props.geospatialConfigTypeBaseline)}
|
||||
/>
|
||||
);
|
||||
@@ -241,7 +281,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
id="changeFeedPolicy"
|
||||
selectedKey={this.props.changeFeedPolicy}
|
||||
options={this.changeFeedChoiceGroupOptions}
|
||||
onChange={this.props.onChangeFeedPolicyChange}
|
||||
onChange={this.onChangeFeedPolicyChange}
|
||||
styles={getChoiceGroupStyles(this.props.changeFeedPolicy, this.props.changeFeedPolicyBaseline)}
|
||||
aria-labelledby={labelId}
|
||||
/>
|
||||
|
||||
@@ -26,9 +26,10 @@ import {
|
||||
MessageBarType
|
||||
} from "office-ui-fabric-react";
|
||||
import { ToolTipLabelComponent } from "../ToolTipLabelComponent";
|
||||
import { IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
|
||||
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
|
||||
import * as SharedConstants from "../../../../../Shared/Constants";
|
||||
import * as DataModels from "../../../../../Contracts/DataModels";
|
||||
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||
|
||||
export interface ThroughputInputAutoPilotV3Props {
|
||||
databaseAccount: DataModels.DatabaseAccount;
|
||||
@@ -71,9 +72,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
> {
|
||||
private shouldCheckComponentIsDirty = true;
|
||||
private static readonly defaultStep = 100;
|
||||
private static readonly zeroThroughput = 0;
|
||||
private step: number;
|
||||
private choiceGroupFixedStyle = getChoiceGroupStyles(undefined, undefined);
|
||||
private throughputInputMaxValue: number;
|
||||
private autoPilotInputMaxValue: number;
|
||||
private options: IChoiceGroupOption[] = [
|
||||
{ key: "true", text: "Autoscale" },
|
||||
{ key: "false", text: "Manual" }
|
||||
@@ -140,6 +141,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
};
|
||||
|
||||
this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep;
|
||||
this.throughputInputMaxValue = this.props.canExceedMaximumValue ? Int32.Max : this.props.maximum;
|
||||
this.autoPilotInputMaxValue = Int32.Max;
|
||||
}
|
||||
|
||||
public hasProvisioningTypeChanged = (): boolean =>
|
||||
@@ -200,8 +203,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => {
|
||||
let newThroughput = parseInt(newValue);
|
||||
newThroughput = isNaN(newThroughput) ? ThroughputInputAutoPilotV3Component.zeroThroughput : newThroughput;
|
||||
const newThroughput = getSanitizedInputValue(newValue, this.autoPilotInputMaxValue);
|
||||
this.props.onMaxAutoPilotThroughputChange(newThroughput);
|
||||
};
|
||||
|
||||
@@ -209,9 +211,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => {
|
||||
let newThroughput = parseInt(newValue);
|
||||
newThroughput = isNaN(newThroughput) ? ThroughputInputAutoPilotV3Component.zeroThroughput : newThroughput;
|
||||
|
||||
const newThroughput = getSanitizedInputValue(newValue, this.throughputInputMaxValue);
|
||||
if (this.overrideWithAutoPilotSettings()) {
|
||||
this.props.onMaxAutoPilotThroughputChange(newThroughput);
|
||||
} else {
|
||||
@@ -245,7 +245,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
onChange={this.onChoiceGroupChange}
|
||||
required={this.props.showAsMandatory}
|
||||
ariaLabelledBy={labelId}
|
||||
styles={this.choiceGroupFixedStyle}
|
||||
styles={getChoiceGroupStyles(this.props.wasAutopilotOriginallySet, this.props.isAutoPilotSelected)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
@@ -270,8 +270,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
key="auto pilot throughput input"
|
||||
styles={getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)}
|
||||
disabled={this.overrideWithProvisionedThroughputSettings()}
|
||||
step={this.step}
|
||||
min={AutoPilotUtils.minAutoPilotThroughput}
|
||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
|
||||
onChange={this.onAutoPilotThroughputChange}
|
||||
/>
|
||||
@@ -298,8 +297,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
styles={getTextFieldStyles(this.props.throughput, this.props.throughputBaseline)}
|
||||
disabled={this.overrideWithAutoPilotSettings()}
|
||||
step={this.step}
|
||||
min={this.props.minimum}
|
||||
max={this.props.canExceedMaximumValue ? undefined : this.props.maximum}
|
||||
value={
|
||||
this.overrideWithAutoPilotSettings()
|
||||
? this.props.maxAutoPilotThroughputBaseline?.toString()
|
||||
|
||||
@@ -81,10 +81,10 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
Object {
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
"borderColor": undefined,
|
||||
},
|
||||
".ms-ChoiceField-field.is-checked::before": Object {
|
||||
"borderColor": "",
|
||||
"borderColor": undefined,
|
||||
},
|
||||
".ms-ChoiceField-wrapper label": Object {
|
||||
"fontFamily": undefined,
|
||||
@@ -113,10 +113,9 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
id="autopilotInput"
|
||||
key="auto pilot throughput input"
|
||||
label="Max RU/s"
|
||||
min={4000}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
step={100}
|
||||
step={1000}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
@@ -219,7 +218,6 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||
disabled={false}
|
||||
id="throughputInput"
|
||||
key="provisioned throughput input"
|
||||
min={10000}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
step={100}
|
||||
@@ -375,7 +373,6 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||
disabled={false}
|
||||
id="throughputInput"
|
||||
key="provisioned throughput input"
|
||||
min={10000}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
step={100}
|
||||
|
||||
@@ -39,13 +39,13 @@ exports[`ScaleComponent renders with correct intiial notification 1`] = `
|
||||
}
|
||||
>
|
||||
<ThroughputInputAutoPilotV3Component
|
||||
canExceedMaximumValue={false}
|
||||
canExceedMaximumValue={true}
|
||||
getThroughputWarningMessage={[Function]}
|
||||
isAutoPilotSelected={false}
|
||||
isEmulator={false}
|
||||
isEnabled={true}
|
||||
isFixed={false}
|
||||
label="Throughput (6,000 - 40,000 RU/s)"
|
||||
label="Throughput (6,000 - unlimited RU/s)"
|
||||
maxAutoPilotThroughput={4000}
|
||||
maxAutoPilotThroughputBaseline={4000}
|
||||
maximum={40000}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { collection, container } from "./TestUtils";
|
||||
import {
|
||||
getMaxRUs,
|
||||
getMinRUs,
|
||||
getSanitizedInputValue,
|
||||
hasDatabaseSharedThroughput,
|
||||
isDirty,
|
||||
isDirtyTypes,
|
||||
@@ -86,4 +87,11 @@ describe("SettingsUtils", () => {
|
||||
expect(isDirty(baseline, current)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("getSanitizedInputValue", () => {
|
||||
const max = 100;
|
||||
expect(getSanitizedInputValue("", max)).toEqual(0);
|
||||
expect(getSanitizedInputValue("999", max)).toEqual(99);
|
||||
expect(getSanitizedInputValue("10", max)).toEqual(10);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as PricingUtils from "../../../Utils/PricingUtils";
|
||||
|
||||
import Explorer from "../../Explorer";
|
||||
|
||||
const zeroValue = 0;
|
||||
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy;
|
||||
export const TtlOff = "off";
|
||||
export const TtlOn = "on";
|
||||
@@ -129,6 +130,16 @@ export const parseConflictResolutionProcedure = (procedureFromBackEnd: string):
|
||||
return procedureFromBackEnd;
|
||||
};
|
||||
|
||||
export const getSanitizedInputValue = (newValueString: string, max: number): number => {
|
||||
let newValue = parseInt(newValueString);
|
||||
if (isNaN(newValue)) {
|
||||
newValue = zeroValue;
|
||||
} else if (newValue > max) {
|
||||
newValue = Math.floor(newValue / 10);
|
||||
}
|
||||
return newValue;
|
||||
};
|
||||
|
||||
export const isDirty = (current: isDirtyTypes, baseline: isDirtyTypes): boolean => {
|
||||
const currentType = typeof current;
|
||||
const baselineType = typeof baseline;
|
||||
|
||||
@@ -3,10 +3,7 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import Explorer from "../../Explorer";
|
||||
import ko from "knockout";
|
||||
|
||||
export const container = new Explorer({
|
||||
notificationsClient: undefined,
|
||||
isEmulator: false
|
||||
});
|
||||
export const container = new Explorer();
|
||||
|
||||
export const collection = ({
|
||||
container: container,
|
||||
|
||||
@@ -84,9 +84,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"visible": [Function],
|
||||
},
|
||||
AddCollectionPane {
|
||||
"_databaseOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"_isSynapseLinkEnabled": [Function],
|
||||
"autoPilotThroughput": [Function],
|
||||
"autoPilotTiersList": [Function],
|
||||
@@ -586,9 +583,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"_refreshSparkEnabledStateForAccount": [Function],
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"addCollectionPane": AddCollectionPane {
|
||||
"_databaseOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"_isSynapseLinkEnabled": [Function],
|
||||
"autoPilotThroughput": [Function],
|
||||
"autoPilotTiersList": [Function],
|
||||
@@ -982,7 +976,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isCodeOfConductEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEmulator": false,
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isGalleryPublishEnabled": [Function],
|
||||
@@ -1058,7 +1051,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"parameters": [Function],
|
||||
},
|
||||
"notificationConsoleData": [Function],
|
||||
"notificationsClient": undefined,
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"onSwitchToConnectionString": [Function],
|
||||
@@ -1398,9 +1390,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"visible": [Function],
|
||||
},
|
||||
AddCollectionPane {
|
||||
"_databaseOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"_isSynapseLinkEnabled": [Function],
|
||||
"autoPilotThroughput": [Function],
|
||||
"autoPilotTiersList": [Function],
|
||||
@@ -1900,9 +1889,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"_refreshSparkEnabledStateForAccount": [Function],
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"addCollectionPane": AddCollectionPane {
|
||||
"_databaseOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"_isSynapseLinkEnabled": [Function],
|
||||
"autoPilotThroughput": [Function],
|
||||
"autoPilotTiersList": [Function],
|
||||
@@ -2296,7 +2282,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isCodeOfConductEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEmulator": false,
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isGalleryPublishEnabled": [Function],
|
||||
@@ -2372,7 +2357,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"parameters": [Function],
|
||||
},
|
||||
"notificationConsoleData": [Function],
|
||||
"notificationsClient": undefined,
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"onSwitchToConnectionString": [Function],
|
||||
@@ -2725,9 +2709,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"visible": [Function],
|
||||
},
|
||||
AddCollectionPane {
|
||||
"_databaseOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"_isSynapseLinkEnabled": [Function],
|
||||
"autoPilotThroughput": [Function],
|
||||
"autoPilotTiersList": [Function],
|
||||
@@ -3227,9 +3208,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"_refreshSparkEnabledStateForAccount": [Function],
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"addCollectionPane": AddCollectionPane {
|
||||
"_databaseOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"_isSynapseLinkEnabled": [Function],
|
||||
"autoPilotThroughput": [Function],
|
||||
"autoPilotTiersList": [Function],
|
||||
@@ -3623,7 +3601,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isCodeOfConductEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEmulator": false,
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isGalleryPublishEnabled": [Function],
|
||||
@@ -3699,7 +3676,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"parameters": [Function],
|
||||
},
|
||||
"notificationConsoleData": [Function],
|
||||
"notificationsClient": undefined,
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"onSwitchToConnectionString": [Function],
|
||||
@@ -4039,9 +4015,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"visible": [Function],
|
||||
},
|
||||
AddCollectionPane {
|
||||
"_databaseOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"_isSynapseLinkEnabled": [Function],
|
||||
"autoPilotThroughput": [Function],
|
||||
"autoPilotTiersList": [Function],
|
||||
@@ -4541,9 +4514,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"_refreshSparkEnabledStateForAccount": [Function],
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
"addCollectionPane": AddCollectionPane {
|
||||
"_databaseOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"_isSynapseLinkEnabled": [Function],
|
||||
"autoPilotThroughput": [Function],
|
||||
"autoPilotTiersList": [Function],
|
||||
@@ -4937,7 +4907,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isCodeOfConductEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEmulator": false,
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isGalleryPublishEnabled": [Function],
|
||||
@@ -5013,7 +4982,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"parameters": [Function],
|
||||
},
|
||||
"notificationConsoleData": [Function],
|
||||
"notificationsClient": undefined,
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"onSwitchToConnectionString": [Function],
|
||||
@@ -5325,7 +5293,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
logIndexingPolicySuccessMessage={[Function]}
|
||||
onIndexingPolicyContentChange={[Function]}
|
||||
onIndexingPolicyDirtyChange={[Function]}
|
||||
onIndexingPolicyElementFocusChange={[Function]}
|
||||
resetShouldDiscardIndexingPolicy={[Function]}
|
||||
shouldDiscardIndexingPolicy={false}
|
||||
/>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
<input
|
||||
class="throughputModeRadio nonFirstRadio"
|
||||
aria-label="Provisioned Throughput mode"
|
||||
aria-label="Manual mode"
|
||||
type="radio"
|
||||
role="radio"
|
||||
tabindex="0"
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
<input
|
||||
class="throughputModeRadio nonFirstRadio"
|
||||
aria-label="Provisioned Throughput mode"
|
||||
aria-label="Manual mode"
|
||||
type="radio"
|
||||
role="radio"
|
||||
tabindex="0"
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
import TriangleDownIcon from "../../../../images/Triangle-down.svg";
|
||||
import TriangleRightIcon from "../../../../images/Triangle-right.svg";
|
||||
import LoadingIndicator_3Squares from "../../../../images/LoadingIndicator_3Squares.gif";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
|
||||
export interface TreeNodeMenuItem {
|
||||
label: string;
|
||||
@@ -276,7 +278,12 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
text: menuItem.label,
|
||||
disabled: menuItem.isDisabled,
|
||||
className: menuItem.styleClass,
|
||||
onClick: menuItem.onClick,
|
||||
onClick: () => {
|
||||
menuItem.onClick();
|
||||
TelemetryProcessor.trace(Action.ClickResourceTreeNodeContextMenuItem, ActionModifiers.Mark, {
|
||||
label: menuItem.label
|
||||
});
|
||||
},
|
||||
onRenderIcon: (props: any) => <img src={menuItem.iconSrc} alt="" />
|
||||
}))
|
||||
}}
|
||||
|
||||
@@ -191,7 +191,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
||||
"className": undefined,
|
||||
"disabled": true,
|
||||
"key": "menuLabel",
|
||||
"onClick": undefined,
|
||||
"onClick": [Function],
|
||||
"onRenderIcon": [Function],
|
||||
"text": "menuLabel",
|
||||
},
|
||||
|
||||
@@ -37,7 +37,7 @@ import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer
|
||||
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane";
|
||||
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
|
||||
import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { configContext, updateConfigContext } from "../ConfigContext";
|
||||
import { configContext, Platform, updateConfigContext } from "../ConfigContext";
|
||||
import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
|
||||
@@ -58,7 +58,6 @@ import { NotebookUtil } from "./Notebook/NotebookUtil";
|
||||
import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager";
|
||||
import { NotificationConsoleComponentAdapter } from "./Menus/NotificationConsole/NotificationConsoleComponentAdapter";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import { PlatformType } from "../PlatformType";
|
||||
import { QueriesClient } from "../Common/QueriesClient";
|
||||
import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane";
|
||||
import { RenewAdHocAccessPane } from "./Panes/RenewAdHocAccessPane";
|
||||
@@ -82,12 +81,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 +97,6 @@ enum ShareAccessToggleState {
|
||||
Read
|
||||
}
|
||||
|
||||
interface ExplorerOptions {
|
||||
notificationsClient: NotificationsClientBase;
|
||||
isEmulator: boolean;
|
||||
}
|
||||
interface AdHocAccessData {
|
||||
readWriteUrl: string;
|
||||
readUrl: string;
|
||||
@@ -135,14 +130,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 +205,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 +264,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 +370,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 +412,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);
|
||||
@@ -572,9 +564,7 @@ export default class Explorer {
|
||||
|
||||
this.isHostedDataExplorerEnabled = ko.computed<boolean>(
|
||||
() =>
|
||||
this.getPlatformType() === PlatformType.Portal &&
|
||||
!this.isRunningOnNationalCloud() &&
|
||||
!this.isPreferredApiGraph()
|
||||
configContext.platform === Platform.Portal && !this.isRunningOnNationalCloud() && !this.isPreferredApiGraph()
|
||||
);
|
||||
this.isRightPanelV2Enabled = ko.computed<boolean>(() =>
|
||||
this.isFeatureEnabled(Constants.Features.enableRightPanelV2)
|
||||
@@ -1800,7 +1790,7 @@ export default class Explorer {
|
||||
const message: any = event.data.data;
|
||||
const inputs: ViewModels.DataExplorerInputsFrame = message.inputs;
|
||||
|
||||
const isRunningInPortal = window.dataExplorerPlatform == PlatformType.Portal;
|
||||
const isRunningInPortal = configContext.platform === Platform.Portal;
|
||||
const isRunningInDevMode = process.env.NODE_ENV === "development";
|
||||
if (inputs && configContext.BACKEND_ENDPOINT && isRunningInPortal && isRunningInDevMode) {
|
||||
inputs.extensionEndpoint = configContext.PROXY_PATH;
|
||||
@@ -1868,7 +1858,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 +1901,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 +1908,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 +1943,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
|
||||
@@ -2008,10 +2006,6 @@ export default class Explorer {
|
||||
this._panes.forEach((pane: ContextualPaneBase) => pane.close());
|
||||
}
|
||||
|
||||
public getPlatformType(): PlatformType {
|
||||
return window.dataExplorerPlatform;
|
||||
}
|
||||
|
||||
public isRunningOnNationalCloud(): boolean {
|
||||
return (
|
||||
this.serverId() === Constants.ServerIds.blackforest ||
|
||||
@@ -2075,11 +2069,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 +2185,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 +2213,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 +2225,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 +2247,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 +2256,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 +2347,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 +2372,9 @@ export default class Explorer {
|
||||
this._closeModalDialog();
|
||||
onCancel && onCancel();
|
||||
},
|
||||
primaryButtonDisabled: isPrimaryButtonDisabled,
|
||||
textFieldProps
|
||||
choiceGroupProps,
|
||||
textFieldProps,
|
||||
primaryButtonDisabled: isPrimaryButtonDisabled
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2469,7 +2415,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 +2866,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 +2909,6 @@ export default class Explorer {
|
||||
title: title,
|
||||
tabPath: title,
|
||||
documentClientUtility: null,
|
||||
selfLink: null,
|
||||
isActive: ko.observable(false),
|
||||
hashLocation: hashLocation,
|
||||
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
||||
@@ -3008,7 +2951,6 @@ export default class Explorer {
|
||||
tabPath: title,
|
||||
documentClientUtility: null,
|
||||
collection: null,
|
||||
selfLink: null,
|
||||
hashLocation: hashLocation,
|
||||
isActive: ko.observable(false),
|
||||
isTabsContentExpanded: ko.observable(true),
|
||||
|
||||
@@ -141,8 +141,6 @@ describe("getPkIdFromDocumentId", () => {
|
||||
});
|
||||
|
||||
describe("GraphExplorer", () => {
|
||||
const COLLECTION_RID = "collectionRid";
|
||||
const COLLECTION_SELF_LINK = "collectionSelfLink";
|
||||
const gremlinRU = 789.12;
|
||||
|
||||
const createMockProps = (): GraphExplorerProps => {
|
||||
@@ -160,8 +158,6 @@ describe("GraphExplorer", () => {
|
||||
onIsValidQueryChange: (isValidQuery: boolean): void => {},
|
||||
|
||||
collectionPartitionKeyProperty: "collectionPartitionKeyProperty",
|
||||
collectionRid: COLLECTION_RID,
|
||||
collectionSelfLink: COLLECTION_SELF_LINK,
|
||||
graphBackendEndpoint: "graphBackendEndpoint",
|
||||
databaseId: "databaseId",
|
||||
collectionId: "collectionId",
|
||||
|
||||
@@ -47,8 +47,6 @@ export interface GraphExplorerProps {
|
||||
onIsValidQueryChange: (isValidQuery: boolean) => void;
|
||||
|
||||
collectionPartitionKeyProperty: string;
|
||||
collectionRid: string;
|
||||
collectionSelfLink: string;
|
||||
graphBackendEndpoint: string;
|
||||
databaseId: string;
|
||||
collectionId: string;
|
||||
@@ -1761,7 +1759,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${queryInfoStr}`);
|
||||
|
||||
return queryDocumentsPage(
|
||||
this.props.collectionRid,
|
||||
this.props.collectionId,
|
||||
this.currentDocDBQueryInfo.iterator,
|
||||
this.currentDocDBQueryInfo.index,
|
||||
{
|
||||
|
||||
@@ -17,8 +17,6 @@ interface Parameter {
|
||||
graphConfig?: GraphConfig;
|
||||
|
||||
collectionPartitionKeyProperty: string;
|
||||
collectionRid: string;
|
||||
collectionSelfLink: string;
|
||||
graphBackendEndpoint: string;
|
||||
databaseId: string;
|
||||
collectionId: string;
|
||||
@@ -49,8 +47,6 @@ export class GraphExplorerAdapter implements ReactAdapter {
|
||||
onIsGraphDisplayed={this.params.onIsGraphDisplayed}
|
||||
onResetDefaultGraphConfigValues={this.params.onResetDefaultGraphConfigValues}
|
||||
collectionPartitionKeyProperty={this.params.collectionPartitionKeyProperty}
|
||||
collectionRid={this.params.collectionRid}
|
||||
collectionSelfLink={this.params.collectionSelfLink}
|
||||
graphBackendEndpoint={this.params.graphBackendEndpoint}
|
||||
databaseId={this.params.databaseId}
|
||||
collectionId={this.params.collectionId}
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { CommandBarComponentButtonFactory } from "./CommandBarComponentButtonFactory";
|
||||
import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
|
||||
import { StyleConstants } from "../../../Common/Constants";
|
||||
import { CommandBarUtil } from "./CommandBarUtil";
|
||||
import * as CommandBarUtil from "./CommandBarUtil";
|
||||
import Explorer from "../../Explorer";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { PlatformType } from "../../../PlatformType";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { Areas } from "../../../Common/Constants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
@@ -159,7 +158,7 @@ export class CommandBarComponentButtonFactory {
|
||||
|
||||
public static createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
if (window.dataExplorerPlatform === PlatformType.Hosted) {
|
||||
if (configContext.platform === Platform.Hosted) {
|
||||
return buttons;
|
||||
}
|
||||
|
||||
@@ -194,7 +193,7 @@ export class CommandBarComponentButtonFactory {
|
||||
buttons.push(fullScreenButton);
|
||||
}
|
||||
|
||||
if (!container.hasOwnProperty("isEmulator") || !container.isEmulator) {
|
||||
if (configContext.platform !== Platform.Emulator) {
|
||||
const label = "Feedback";
|
||||
const feedbackButtonOptions: CommandButtonComponentProps = {
|
||||
iconSrc: FeedbackIcon,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CommandBarUtil } from "./CommandBarUtil";
|
||||
import * as CommandBarUtil from "./CommandBarUtil";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
@@ -8,7 +8,7 @@ describe("CommandBarUtil tests", () => {
|
||||
return {
|
||||
iconSrc: "icon",
|
||||
iconAlt: "label",
|
||||
onCommandClick: (e: React.SyntheticEvent): void => {},
|
||||
onCommandClick: jest.fn(),
|
||||
commandButtonLabel: "label",
|
||||
ariaLabel: "ariaLabel",
|
||||
hasPopup: true,
|
||||
@@ -29,11 +29,14 @@ describe("CommandBarUtil tests", () => {
|
||||
expect(!converted.split);
|
||||
expect(converted.iconProps.imageProps.src).toEqual(btn.iconSrc);
|
||||
expect(converted.iconProps.imageProps.alt).toEqual(btn.iconAlt);
|
||||
expect(converted.onClick).toEqual(btn.onCommandClick);
|
||||
expect(converted.text).toEqual(btn.commandButtonLabel);
|
||||
expect(converted.ariaLabel).toEqual(btn.ariaLabel);
|
||||
expect(converted.disabled).toEqual(btn.disabled);
|
||||
expect(converted.className).toEqual(btn.className);
|
||||
|
||||
// Click gets called
|
||||
converted.onClick();
|
||||
expect(btn.onCommandClick).toBeCalled();
|
||||
});
|
||||
|
||||
it("should convert NavbarButtonConfig to split button", () => {
|
||||
|
||||
@@ -11,177 +11,187 @@ import ChevronDownIcon from "../../../../images/Chevron_down.svg";
|
||||
import { ArcadiaMenuPicker } from "../../Controls/Arcadia/ArcadiaMenuPicker";
|
||||
import { MemoryTrackerComponent } from "./MemoryTrackerComponent";
|
||||
import { MemoryUsageInfo } from "../../../Contracts/DataModels";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
|
||||
/**
|
||||
* Utilities for CommandBar
|
||||
* Convert our NavbarButtonConfig to UI Fabric buttons
|
||||
* @param btns
|
||||
*/
|
||||
export class CommandBarUtil {
|
||||
/**
|
||||
* Convert our NavbarButtonConfig to UI Fabric buttons
|
||||
* @param btns
|
||||
*/
|
||||
public static convertButton(btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] {
|
||||
const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
|
||||
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
|
||||
const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
|
||||
|
||||
return btns
|
||||
.filter(btn => btn)
|
||||
.map(
|
||||
(btn: CommandButtonComponentProps, index: number): ICommandBarItemProps => {
|
||||
if (btn.isDivider) {
|
||||
return CommandBarUtil.createDivider(btn.commandButtonLabel);
|
||||
}
|
||||
return btns
|
||||
.filter(btn => btn)
|
||||
.map(
|
||||
(btn: CommandButtonComponentProps, index: number): ICommandBarItemProps => {
|
||||
if (btn.isDivider) {
|
||||
return createDivider(btn.commandButtonLabel);
|
||||
}
|
||||
|
||||
const isSplit = !!btn.children && btn.children.length > 0;
|
||||
|
||||
const result: ICommandBarItemProps = {
|
||||
iconProps: {
|
||||
style: {
|
||||
width: StyleConstants.CommandBarIconWidth, // 16
|
||||
alignSelf: btn.iconName ? "baseline" : undefined
|
||||
},
|
||||
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
|
||||
iconName: btn.iconName
|
||||
const isSplit = !!btn.children && btn.children.length > 0;
|
||||
const label = btn.commandButtonLabel || btn.tooltipText;
|
||||
const result: ICommandBarItemProps = {
|
||||
iconProps: {
|
||||
style: {
|
||||
width: StyleConstants.CommandBarIconWidth, // 16
|
||||
alignSelf: btn.iconName ? "baseline" : undefined
|
||||
},
|
||||
onClick: btn.onCommandClick,
|
||||
key: `${btn.commandButtonLabel}${index}`,
|
||||
text: btn.commandButtonLabel || btn.tooltipText,
|
||||
"data-test": btn.commandButtonLabel || btn.tooltipText,
|
||||
title: btn.tooltipText,
|
||||
name: btn.commandButtonLabel || btn.tooltipText,
|
||||
disabled: btn.disabled,
|
||||
ariaLabel: btn.ariaLabel,
|
||||
buttonStyles: {
|
||||
root: {
|
||||
backgroundColor: backgroundColor,
|
||||
height: buttonHeightPx,
|
||||
paddingRight: 0,
|
||||
paddingLeft: 0,
|
||||
minWidth: 24,
|
||||
marginLeft: isSplit ? 0 : 5,
|
||||
marginRight: isSplit ? 0 : 5
|
||||
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
|
||||
iconName: btn.iconName
|
||||
},
|
||||
onClick: (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
|
||||
btn.onCommandClick(ev);
|
||||
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label });
|
||||
},
|
||||
key: `${btn.commandButtonLabel}${index}`,
|
||||
text: label,
|
||||
"data-test": label,
|
||||
title: btn.tooltipText,
|
||||
name: label,
|
||||
disabled: btn.disabled,
|
||||
ariaLabel: btn.ariaLabel,
|
||||
buttonStyles: {
|
||||
root: {
|
||||
backgroundColor: backgroundColor,
|
||||
height: buttonHeightPx,
|
||||
paddingRight: 0,
|
||||
paddingLeft: 0,
|
||||
minWidth: 24,
|
||||
marginLeft: isSplit ? 0 : 5,
|
||||
marginRight: isSplit ? 0 : 5
|
||||
},
|
||||
rootDisabled: {
|
||||
backgroundColor: backgroundColor,
|
||||
pointerEvents: "auto"
|
||||
},
|
||||
splitButtonMenuButton: {
|
||||
backgroundColor: backgroundColor,
|
||||
selectors: {
|
||||
":hover": { backgroundColor: StyleConstants.AccentLight }
|
||||
},
|
||||
rootDisabled: {
|
||||
backgroundColor: backgroundColor,
|
||||
pointerEvents: "auto"
|
||||
},
|
||||
splitButtonMenuButton: {
|
||||
backgroundColor: backgroundColor,
|
||||
selectors: {
|
||||
":hover": { backgroundColor: StyleConstants.AccentLight }
|
||||
},
|
||||
width: 16
|
||||
},
|
||||
label: { fontSize: StyleConstants.mediumFontSize },
|
||||
rootHovered: { backgroundColor: StyleConstants.AccentLight },
|
||||
rootPressed: { backgroundColor: StyleConstants.AccentLight },
|
||||
splitButtonMenuButtonExpanded: {
|
||||
backgroundColor: StyleConstants.AccentExtra,
|
||||
selectors: {
|
||||
":hover": { backgroundColor: StyleConstants.AccentLight }
|
||||
}
|
||||
},
|
||||
splitButtonDivider: {
|
||||
display: "none"
|
||||
},
|
||||
icon: {
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0
|
||||
},
|
||||
splitButtonContainer: {
|
||||
marginLeft: 5,
|
||||
marginRight: 5
|
||||
width: 16
|
||||
},
|
||||
label: { fontSize: StyleConstants.mediumFontSize },
|
||||
rootHovered: { backgroundColor: StyleConstants.AccentLight },
|
||||
rootPressed: { backgroundColor: StyleConstants.AccentLight },
|
||||
splitButtonMenuButtonExpanded: {
|
||||
backgroundColor: StyleConstants.AccentExtra,
|
||||
selectors: {
|
||||
":hover": { backgroundColor: StyleConstants.AccentLight }
|
||||
}
|
||||
},
|
||||
className: btn.className,
|
||||
id: btn.id
|
||||
splitButtonDivider: {
|
||||
display: "none"
|
||||
},
|
||||
icon: {
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0
|
||||
},
|
||||
splitButtonContainer: {
|
||||
marginLeft: 5,
|
||||
marginRight: 5
|
||||
}
|
||||
},
|
||||
className: btn.className,
|
||||
id: btn.id
|
||||
};
|
||||
|
||||
if (isSplit) {
|
||||
// It's a split button
|
||||
result.split = true;
|
||||
|
||||
result.subMenuProps = {
|
||||
items: convertButton(btn.children, backgroundColor),
|
||||
styles: {
|
||||
list: {
|
||||
// TODO Figure out how to do it the proper way with subComponentStyles.
|
||||
// TODO Remove all this crazy styling once we adopt Ui-Fabric Azure themes
|
||||
selectors: {
|
||||
".ms-ContextualMenu-itemText": { fontSize: StyleConstants.mediumFontSize },
|
||||
".ms-ContextualMenu-link:hover": { backgroundColor: StyleConstants.AccentLight },
|
||||
".ms-ContextualMenu-icon": { width: 16, height: 16 }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (isSplit) {
|
||||
// It's a split button
|
||||
result.split = true;
|
||||
|
||||
result.subMenuProps = {
|
||||
items: CommandBarUtil.convertButton(btn.children, backgroundColor),
|
||||
styles: {
|
||||
list: {
|
||||
// TODO Figure out how to do it the proper way with subComponentStyles.
|
||||
// TODO Remove all this crazy styling once we adopt Ui-Fabric Azure themes
|
||||
selectors: {
|
||||
".ms-ContextualMenu-itemText": { fontSize: StyleConstants.mediumFontSize },
|
||||
".ms-ContextualMenu-link:hover": { backgroundColor: StyleConstants.AccentLight },
|
||||
".ms-ContextualMenu-icon": { width: 16, height: 16 }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
result.menuIconProps = {
|
||||
iconType: IconType.image,
|
||||
style: {
|
||||
width: 12,
|
||||
paddingLeft: 1,
|
||||
paddingTop: 6
|
||||
},
|
||||
imageProps: { src: ChevronDownIcon, alt: btn.iconAlt }
|
||||
};
|
||||
}
|
||||
|
||||
if (btn.isDropdown) {
|
||||
const selectedChild = _.find(btn.children, child => child.dropdownItemKey === btn.dropdownSelectedKey);
|
||||
result.name = selectedChild?.commandButtonLabel || btn.dropdownPlaceholder;
|
||||
|
||||
const dropdownStyles: Partial<IDropdownStyles> = {
|
||||
root: { margin: 5 },
|
||||
dropdown: { width: btn.dropdownWidth },
|
||||
title: { fontSize: 12, height: 30, lineHeight: 28 },
|
||||
dropdownItem: { fontSize: 12, lineHeight: 28, minHeight: 30 },
|
||||
dropdownItemSelected: { fontSize: 12, lineHeight: 28, minHeight: 30 }
|
||||
};
|
||||
|
||||
result.commandBarButtonAs = (props: IComponentAsProps<ICommandBarItemProps>) => {
|
||||
return (
|
||||
<Dropdown
|
||||
placeholder={btn.dropdownPlaceholder}
|
||||
defaultSelectedKey={btn.dropdownSelectedKey}
|
||||
onChange={(event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption, index?: number): void =>
|
||||
btn.children[index].onCommandClick(event)
|
||||
}
|
||||
options={btn.children.map((child: CommandButtonComponentProps) => ({
|
||||
key: child.dropdownItemKey,
|
||||
text: child.commandButtonLabel
|
||||
}))}
|
||||
styles={dropdownStyles}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
if (btn.isArcadiaPicker && btn.arcadiaProps) {
|
||||
result.commandBarButtonAs = () => <ArcadiaMenuPicker {...btn.arcadiaProps} />;
|
||||
}
|
||||
|
||||
return result;
|
||||
result.menuIconProps = {
|
||||
iconType: IconType.image,
|
||||
style: {
|
||||
width: 12,
|
||||
paddingLeft: 1,
|
||||
paddingTop: 6
|
||||
},
|
||||
imageProps: { src: ChevronDownIcon, alt: btn.iconAlt }
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static createDivider(key: string): ICommandBarItemProps {
|
||||
return {
|
||||
onRender: () => (
|
||||
<div className="dividerContainer">
|
||||
<span />
|
||||
</div>
|
||||
),
|
||||
iconOnly: true,
|
||||
disabled: true,
|
||||
key: key
|
||||
};
|
||||
}
|
||||
if (btn.isDropdown) {
|
||||
const selectedChild = _.find(btn.children, child => child.dropdownItemKey === btn.dropdownSelectedKey);
|
||||
result.name = selectedChild?.commandButtonLabel || btn.dropdownPlaceholder;
|
||||
|
||||
public static createMemoryTracker(key: string, memoryUsageInfo: Observable<MemoryUsageInfo>): ICommandBarItemProps {
|
||||
return {
|
||||
key,
|
||||
onRender: () => <MemoryTrackerComponent memoryUsageInfo={memoryUsageInfo} />
|
||||
};
|
||||
}
|
||||
}
|
||||
const dropdownStyles: Partial<IDropdownStyles> = {
|
||||
root: { margin: 5 },
|
||||
dropdown: { width: btn.dropdownWidth },
|
||||
title: { fontSize: 12, height: 30, lineHeight: 28 },
|
||||
dropdownItem: { fontSize: 12, lineHeight: 28, minHeight: 30 },
|
||||
dropdownItemSelected: { fontSize: 12, lineHeight: 28, minHeight: 30 }
|
||||
};
|
||||
|
||||
const onDropdownChange = (
|
||||
event: React.FormEvent<HTMLDivElement>,
|
||||
option?: IDropdownOption,
|
||||
index?: number
|
||||
): void => {
|
||||
btn.children[index].onCommandClick(event);
|
||||
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label: option.text });
|
||||
};
|
||||
|
||||
result.commandBarButtonAs = (props: IComponentAsProps<ICommandBarItemProps>) => {
|
||||
return (
|
||||
<Dropdown
|
||||
placeholder={btn.dropdownPlaceholder}
|
||||
defaultSelectedKey={btn.dropdownSelectedKey}
|
||||
onChange={onDropdownChange}
|
||||
options={btn.children.map((child: CommandButtonComponentProps) => ({
|
||||
key: child.dropdownItemKey,
|
||||
text: child.commandButtonLabel
|
||||
}))}
|
||||
styles={dropdownStyles}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
if (btn.isArcadiaPicker && btn.arcadiaProps) {
|
||||
result.commandBarButtonAs = () => <ArcadiaMenuPicker {...btn.arcadiaProps} />;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const createDivider = (key: string): ICommandBarItemProps => {
|
||||
return {
|
||||
onRender: () => (
|
||||
<div className="dividerContainer">
|
||||
<span />
|
||||
</div>
|
||||
),
|
||||
iconOnly: true,
|
||||
disabled: true,
|
||||
key: key
|
||||
};
|
||||
};
|
||||
|
||||
export const createMemoryTracker = (
|
||||
key: string,
|
||||
memoryUsageInfo: Observable<MemoryUsageInfo>
|
||||
): ICommandBarItemProps => {
|
||||
return {
|
||||
key,
|
||||
onRender: () => <MemoryTrackerComponent memoryUsageInfo={memoryUsageInfo} />
|
||||
};
|
||||
};
|
||||
|
||||
@@ -15,7 +15,10 @@ export interface OpenNotebookItem {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export type OpenCollectionItem = string;
|
||||
export interface OpenCollectionItem {
|
||||
databaseId: string;
|
||||
collectionId: string;
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
type: Type;
|
||||
@@ -121,7 +124,11 @@ export class MostRecentActivity {
|
||||
public onItemClicked(item: Item) {
|
||||
switch (item.type) {
|
||||
case Type.OpenCollection: {
|
||||
const collection = this.container.findCollection(item.data as OpenCollectionItem);
|
||||
const openCollectionitem = item.data as OpenCollectionItem;
|
||||
const collection = this.container.findCollection(
|
||||
openCollectionitem.databaseId,
|
||||
openCollectionitem.collectionId
|
||||
);
|
||||
if (collection) {
|
||||
collection.openTab();
|
||||
}
|
||||
|
||||
@@ -32,24 +32,27 @@ import {
|
||||
import { message, JupyterMessage, Channels, createMessage, childOf, ofMessageType } from "@nteract/messaging";
|
||||
import { sessions, kernels } from "rx-jupyter";
|
||||
import { RecordOf } from "immutable";
|
||||
import { AnyAction } from "redux";
|
||||
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import * as CdbActions from "./actions";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action as TelemetryAction } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { CdbAppState } from "./types";
|
||||
import { decryptJWTToken } from "../../../Utils/AuthorizationUtils";
|
||||
import * as TextFile from "./contents/file/text-file";
|
||||
import { NotebookUtil } from "../NotebookUtil";
|
||||
import { FileSystemUtil } from "../FileSystemUtil";
|
||||
import * as cdbActions from "../NotebookComponent/actions";
|
||||
import { Areas } from "../../../Common/Constants";
|
||||
|
||||
interface NotebookServiceConfig extends JupyterServerConfig {
|
||||
userPuid?: string;
|
||||
}
|
||||
|
||||
const logToTelemetry = (state: CdbAppState, title: string, error?: string) => {
|
||||
const logFailureToTelemetry = (state: CdbAppState, title: string, error?: string) => {
|
||||
TelemetryProcessor.traceFailure(TelemetryAction.NotebookErrorNotification, {
|
||||
databaseAccountName: state.cdb.databaseAccountName,
|
||||
defaultExperience: state.cdb.defaultExperience,
|
||||
@@ -311,7 +314,7 @@ export const launchWebSocketKernelEpic = (
|
||||
kernelSpecToLaunch = currentKernelspecs.defaultKernelName;
|
||||
const msg = `No kernelspec name specified to launch, using default kernel: ${kernelSpecToLaunch}`;
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
|
||||
logToTelemetry(state$.value, "Launching alternate kernel", msg);
|
||||
logFailureToTelemetry(state$.value, "Launching alternate kernel", msg);
|
||||
} else {
|
||||
return of(
|
||||
actions.launchKernelFailed({
|
||||
@@ -337,7 +340,7 @@ export const launchWebSocketKernelEpic = (
|
||||
msg += ` Using default kernel: ${kernelSpecToLaunch}`;
|
||||
}
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
|
||||
logToTelemetry(state$.value, "Launching alternate kernel", msg);
|
||||
logFailureToTelemetry(state$.value, "Launching alternate kernel", msg);
|
||||
}
|
||||
|
||||
const sessionPayload = {
|
||||
@@ -634,7 +637,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
|
||||
const title = "Kernel restart";
|
||||
const msg = "Kernel successfully restarted";
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
|
||||
logToTelemetry(state$.value, title, msg);
|
||||
logFailureToTelemetry(state$.value, title, msg);
|
||||
break;
|
||||
}
|
||||
case actions.RESTART_KERNEL_FAILED:
|
||||
@@ -645,7 +648,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
|
||||
const title = "Save failure";
|
||||
const msg = `Failed to save notebook: ${(action as actions.SaveFailed).payload.error}`;
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
||||
logToTelemetry(state$.value, title, msg);
|
||||
logFailureToTelemetry(state$.value, title, msg);
|
||||
break;
|
||||
}
|
||||
case actions.FETCH_CONTENT_FAILED: {
|
||||
@@ -654,7 +657,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
|
||||
const title = "Fetching content failure";
|
||||
const msg = `Failed to fetch notebook content: ${filepath}, error: ${typedAction.payload.error}`;
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
||||
logToTelemetry(state$.value, title, msg);
|
||||
logFailureToTelemetry(state$.value, title, msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -679,7 +682,7 @@ const handleKernelConnectionLostEpic = (
|
||||
|
||||
const msg = "Notebook was disconnected from kernel";
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
||||
logToTelemetry(state, "Error", "Kernel connection error");
|
||||
logFailureToTelemetry(state, "Error", "Kernel connection error");
|
||||
|
||||
const host = selectors.currentHost(state);
|
||||
const serverConfig: NotebookServiceConfig = selectors.serverConfig(host as RecordOf<JupyterHostRecordProps>);
|
||||
@@ -692,7 +695,7 @@ const handleKernelConnectionLostEpic = (
|
||||
const msg =
|
||||
"Restarted kernel too many times. Please reload the page to enable Data Explorer to restart the kernel automatically.";
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
||||
logToTelemetry(state, "Kernel restart error", msg);
|
||||
logFailureToTelemetry(state, "Kernel restart error", msg);
|
||||
|
||||
const explorer = window.dataExplorer;
|
||||
if (explorer) {
|
||||
@@ -844,6 +847,105 @@ const closeContentFailedToFetchEpic = (
|
||||
);
|
||||
};
|
||||
|
||||
const traceNotebookTelemetryEpic = (
|
||||
action$: Observable<cdbActions.TraceNotebookTelemetryAction>,
|
||||
state$: StateObservable<CdbAppState>
|
||||
): Observable<{}> => {
|
||||
return action$.pipe(
|
||||
ofType(cdbActions.TRACE_NOTEBOOK_TELEMETRY),
|
||||
mergeMap((action: cdbActions.TraceNotebookTelemetryAction) => {
|
||||
const state = state$.value;
|
||||
|
||||
TelemetryProcessor.trace(action.payload.action, action.payload.actionModifier, {
|
||||
...action.payload.data,
|
||||
databaseAccountName: state.cdb.databaseAccountName,
|
||||
defaultExperience: state.cdb.defaultExperience,
|
||||
dataExplorerArea: Areas.Notebook
|
||||
});
|
||||
return EMPTY;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Log notebook information to telemetry
|
||||
* # raw cells, # markdown cells, # code cells, total
|
||||
* @param action$
|
||||
* @param state$
|
||||
*/
|
||||
const traceNotebookInfoEpic = (
|
||||
action$: Observable<actions.FetchContentFulfilled>,
|
||||
state$: StateObservable<AppState>
|
||||
): Observable<{} | cdbActions.TraceNotebookTelemetryAction> => {
|
||||
return action$.pipe(
|
||||
ofType(actions.FETCH_CONTENT_FULFILLED),
|
||||
mergeMap((action: { payload: any }) => {
|
||||
const state = state$.value;
|
||||
const contentRef = action.payload.contentRef;
|
||||
const model = selectors.model(state, { contentRef });
|
||||
|
||||
// If it's not a notebook, we shouldn't be here
|
||||
if (!model || model.type !== "notebook") {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
const dataToLog = {
|
||||
nbCodeCells: 0,
|
||||
nbRawCells: 0,
|
||||
nbMarkdownCells: 0,
|
||||
nbCells: 0
|
||||
};
|
||||
for (let [id, cell] of selectors.notebook.cellMap(model)) {
|
||||
switch (cell.cell_type) {
|
||||
case "code":
|
||||
dataToLog.nbCodeCells++;
|
||||
break;
|
||||
case "markdown":
|
||||
dataToLog.nbMarkdownCells++;
|
||||
break;
|
||||
case "raw":
|
||||
dataToLog.nbRawCells++;
|
||||
break;
|
||||
}
|
||||
dataToLog.nbCells++;
|
||||
}
|
||||
|
||||
return of(
|
||||
cdbActions.traceNotebookTelemetry({
|
||||
action: TelemetryAction.NotebooksFetched,
|
||||
actionModifier: ActionModifiers.Mark,
|
||||
data: dataToLog
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Log Kernel spec to start
|
||||
* @param action$
|
||||
* @param state$
|
||||
*/
|
||||
const traceNotebookKernelEpic = (
|
||||
action$: Observable<AnyAction>,
|
||||
state$: StateObservable<AppState>
|
||||
): Observable<cdbActions.TraceNotebookTelemetryAction> => {
|
||||
return action$.pipe(
|
||||
ofType(actions.LAUNCH_KERNEL_SUCCESSFUL),
|
||||
mergeMap((action: { payload: any; type: string }) => {
|
||||
return of(
|
||||
cdbActions.traceNotebookTelemetry({
|
||||
action: TelemetryAction.NotebooksKernelSpecName,
|
||||
actionModifier: ActionModifiers.Mark,
|
||||
data: {
|
||||
kernelSpecName: action.payload.kernel.name
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const allEpics = [
|
||||
addInitialCodeCellEpic,
|
||||
focusInitialCodeCellEpic,
|
||||
@@ -856,5 +958,8 @@ export const allEpics = [
|
||||
executeFocusedCellAndFocusNextEpic,
|
||||
closeUnsupportedMimetypesEpic,
|
||||
closeContentFailedToFetchEpic,
|
||||
restartWebSocketKernelEpic
|
||||
restartWebSocketKernelEpic,
|
||||
traceNotebookTelemetryEpic,
|
||||
traceNotebookInfoEpic,
|
||||
traceNotebookKernelEpic
|
||||
];
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { actions, CoreRecord, reducers as nteractReducers } from "@nteract/core";
|
||||
import { Action } from "redux";
|
||||
import { Areas } from "../../../Common/Constants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import * as cdbActions from "./actions";
|
||||
import { CdbRecord } from "./types";
|
||||
|
||||
@@ -72,17 +70,6 @@ export const cdbReducer = (state: CdbRecord, action: Action) => {
|
||||
return state.set("hoveredCellId", typedAction.payload.cellId);
|
||||
}
|
||||
|
||||
case cdbActions.TRACE_NOTEBOOK_TELEMETRY: {
|
||||
const typedAction = action as cdbActions.TraceNotebookTelemetryAction;
|
||||
TelemetryProcessor.trace(typedAction.payload.action, typedAction.payload.actionModifier, {
|
||||
...typedAction.payload.data,
|
||||
databaseAccountName: state.databaseAccountName,
|
||||
defaultExperience: state.defaultExperience,
|
||||
dataExplorerArea: Areas.Notebook
|
||||
});
|
||||
return state;
|
||||
}
|
||||
|
||||
case cdbActions.UPDATE_NOTEBOOK_PARENT_DOM_ELTS: {
|
||||
const typedAction = action as cdbActions.UpdateNotebookParentDomEltAction;
|
||||
var parentEltsMap = state.get("currentNotebookParentElements");
|
||||
|
||||
@@ -166,7 +166,7 @@ export default class NotebookManager {
|
||||
private promptForCommitMsg = (title: string, primaryButtonLabel: string) => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
let commitMsg = "Committed from Azure Cosmos DB Notebooks";
|
||||
this.params.container.showOkCancelTextFieldModalDialog(
|
||||
this.params.container.showOkCancelModalDialog(
|
||||
title || "Commit",
|
||||
undefined,
|
||||
primaryButtonLabel || "Commit",
|
||||
@@ -181,6 +181,7 @@ export default class NotebookManager {
|
||||
},
|
||||
"Cancel",
|
||||
() => reject(new Error("Commit dialog canceled")),
|
||||
undefined,
|
||||
{
|
||||
label: "Commit message",
|
||||
autoAdjustHeight: true,
|
||||
|
||||
@@ -14,6 +14,8 @@ import { CellToolbarContext } from "@nteract/stateful-components";
|
||||
import { CellType, CellId } from "@nteract/commutable";
|
||||
import * as selectors from "@nteract/selectors";
|
||||
import { RecordOf } from "immutable";
|
||||
import * as cdbActions from "../NotebookComponent/actions";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
|
||||
export interface ComponentProps {
|
||||
contentRef: ContentRef;
|
||||
@@ -29,6 +31,7 @@ interface DispatchProps {
|
||||
moveCell: (destinationId: CellId, above: boolean) => void;
|
||||
clearOutputs: () => void;
|
||||
deleteCell: () => void;
|
||||
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: any) => void;
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
@@ -48,12 +51,18 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
|
||||
{
|
||||
key: "Run",
|
||||
text: "Run",
|
||||
onClick: this.props.executeCell
|
||||
onClick: () => {
|
||||
this.props.executeCell();
|
||||
this.props.traceNotebookTelemetry(Action.NotebooksExecuteCellFromMenu, ActionModifiers.Mark);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "Clear Outputs",
|
||||
text: "Clear Outputs",
|
||||
onClick: this.props.clearOutputs
|
||||
onClick: () => {
|
||||
this.props.clearOutputs();
|
||||
this.props.traceNotebookTelemetry(Action.NotebooksClearOutputsFromMenu, ActionModifiers.Mark);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "Divider",
|
||||
@@ -64,31 +73,43 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
|
||||
|
||||
items = items.concat([
|
||||
{
|
||||
key: "Divider",
|
||||
key: "Divider2",
|
||||
itemType: ContextualMenuItemType.Divider
|
||||
},
|
||||
{
|
||||
key: "Insert Code Cell Above",
|
||||
text: "Insert Code Cell Above",
|
||||
onClick: this.props.insertCodeCellAbove
|
||||
onClick: () => {
|
||||
this.props.insertCodeCellAbove();
|
||||
this.props.traceNotebookTelemetry(Action.NotebooksInsertCodeCellAboveFromMenu, ActionModifiers.Mark);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "Insert Code Cell Below",
|
||||
text: "Insert Code Cell Below",
|
||||
onClick: this.props.insertCodeCellBelow
|
||||
onClick: () => {
|
||||
this.props.insertCodeCellBelow();
|
||||
this.props.traceNotebookTelemetry(Action.NotebooksInsertCodeCellBelowFromMenu, ActionModifiers.Mark);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "Insert Text Cell Above",
|
||||
text: "Insert Text Cell Above",
|
||||
onClick: this.props.insertTextCellAbove
|
||||
onClick: () => {
|
||||
this.props.insertTextCellAbove();
|
||||
this.props.traceNotebookTelemetry(Action.NotebooksInsertTextCellAboveFromMenu, ActionModifiers.Mark);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "Insert Text Cell Below",
|
||||
text: "Insert Text Cell Below",
|
||||
onClick: this.props.insertTextCellBelow
|
||||
onClick: () => {
|
||||
this.props.insertTextCellBelow();
|
||||
this.props.traceNotebookTelemetry(Action.NotebooksInsertTextCellBelowFromMenu, ActionModifiers.Mark);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "Divider",
|
||||
key: "Divider3",
|
||||
itemType: ContextualMenuItemType.Divider
|
||||
}
|
||||
]);
|
||||
@@ -98,7 +119,10 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
|
||||
moveItems.push({
|
||||
key: "Move Cell Up",
|
||||
text: "Move Cell Up",
|
||||
onClick: () => this.props.moveCell(this.props.cellIdAbove, true)
|
||||
onClick: () => {
|
||||
this.props.moveCell(this.props.cellIdAbove, true);
|
||||
this.props.traceNotebookTelemetry(Action.NotebooksMoveCellUpFromMenu, ActionModifiers.Mark);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -106,13 +130,16 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
|
||||
moveItems.push({
|
||||
key: "Move Cell Down",
|
||||
text: "Move Cell Down",
|
||||
onClick: () => this.props.moveCell(this.props.cellIdBelow, false)
|
||||
onClick: () => {
|
||||
this.props.moveCell(this.props.cellIdBelow, false);
|
||||
this.props.traceNotebookTelemetry(Action.NotebooksMoveCellDownFromMenu, ActionModifiers.Mark);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (moveItems.length > 0) {
|
||||
moveItems.push({
|
||||
key: "Divider",
|
||||
key: "Divider4",
|
||||
itemType: ContextualMenuItemType.Divider
|
||||
});
|
||||
items = items.concat(moveItems);
|
||||
@@ -121,7 +148,10 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
|
||||
items.push({
|
||||
key: "Delete Cell",
|
||||
text: "Delete Cell",
|
||||
onClick: this.props.deleteCell
|
||||
onClick: () => {
|
||||
this.props.deleteCell();
|
||||
this.props.traceNotebookTelemetry(Action.DeleteCellFromMenu, ActionModifiers.Mark);
|
||||
}
|
||||
});
|
||||
|
||||
const menuItemLabel = "More";
|
||||
@@ -156,7 +186,9 @@ const mapDispatchToProps = (
|
||||
moveCell: (destinationId: CellId, above: boolean) =>
|
||||
dispatch(actions.moveCell({ id, contentRef, destinationId, above })),
|
||||
clearOutputs: () => dispatch(actions.clearOutputs({ id, contentRef })),
|
||||
deleteCell: () => dispatch(actions.deleteCell({ id, contentRef }))
|
||||
deleteCell: () => dispatch(actions.deleteCell({ id, contentRef })),
|
||||
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: any) =>
|
||||
dispatch(cdbActions.traceNotebookTelemetry({ action, actionModifier, data }))
|
||||
});
|
||||
|
||||
const makeMapStateToProps = (state: AppState, ownProps: ComponentProps): ((state: AppState) => StateProps) => {
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
.nteract-cell-input .nteract-cell-source {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/** Adaptation for the R kernel's inline lists **/
|
||||
|
||||
@@ -276,7 +276,7 @@
|
||||
<!-- Fixed option button - Start -->
|
||||
<div class="tab">
|
||||
<input type="radio" id="tab1" name="storage" value="10" class="radio" data-bind="checked: storage">
|
||||
<label for="tab1">Fixed (10 GB)</label>
|
||||
<label for="tab1">Fixed (20 GB)</label>
|
||||
</div>
|
||||
<!-- Fixed option button - End -->
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
|
||||
import { configContext, Platform } from "../../ConfigContext";
|
||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent";
|
||||
import { HashMap } from "../../Common/HashMap";
|
||||
import { PlatformType } from "../../PlatformType";
|
||||
import { refreshCachedResources } from "../../Common/DocumentClientUtilityBase";
|
||||
import { createCollection } from "../../Common/dataAccess/createCollection";
|
||||
|
||||
@@ -101,12 +99,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,9 +323,9 @@ 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
|
||||
configContext.platform !== Platform.Portal
|
||||
) {
|
||||
const offerThroughput: number = this._getThroughput();
|
||||
return offerThroughput <= 100000;
|
||||
@@ -339,7 +335,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 +477,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 +748,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 {
|
||||
|
||||
@@ -74,8 +74,8 @@
|
||||
|
||||
<input id="database-id" type="text" aria-required="true" autocomplete="off" pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
size="40" class="collid" data-bind="textInput: databaseId, hasFocus: firstFieldHasFocus, attr: { placeholder: databaseIdPlaceHolder }"
|
||||
aria-label="Database id" autofocus>
|
||||
size="40" class="collid" data-bind="textInput: databaseId, hasFocus: firstFieldHasFocus, attr: { 'aria-label': databaseIdLabel, 'placeholder': databaseIdPlaceHolder }"
|
||||
autofocus>
|
||||
|
||||
<!-- Database provisioned throughput - Start -->
|
||||
<!-- ko if: canConfigureThroughput -->
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
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,9 +180,9 @@ 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
|
||||
configContext.platform !== Platform.Portal
|
||||
) {
|
||||
const offerThroughput: number = this.throughput();
|
||||
return offerThroughput <= 100000;
|
||||
@@ -203,7 +203,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
});
|
||||
|
||||
this.costsVisible = ko.pureComputed(() => {
|
||||
return !this.container.isEmulator;
|
||||
return configContext.platform !== Platform.Emulator;
|
||||
});
|
||||
|
||||
this.throughputSpendAckVisible = ko.pureComputed<boolean>(() => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
|
||||
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
|
||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
import { HashMap } from "../../Common/HashMap";
|
||||
import { configContext, Platform } from "../../ConfigContext";
|
||||
|
||||
export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
public createTableQuery: ko.Observable<string>;
|
||||
@@ -231,11 +232,11 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
});
|
||||
|
||||
this.costsVisible = ko.pureComputed(() => {
|
||||
return !this.container.isEmulator;
|
||||
return configContext.platform !== Platform.Emulator;
|
||||
});
|
||||
|
||||
this.canRequestSupport = ko.pureComputed(() => {
|
||||
if (!this.container.isEmulator && !this.container.isTryCosmosDBSubscription()) {
|
||||
if (configContext.platform !== Platform.Emulator && !this.container.isTryCosmosDBSubscription()) {
|
||||
const offerThroughput: number = this.throughput();
|
||||
return offerThroughput <= 100000;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
let explorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
||||
explorer = new Explorer();
|
||||
});
|
||||
|
||||
it("should be true if 1 database and 1 collection", () => {
|
||||
@@ -56,7 +56,7 @@ describe("Delete Collection Confirmation Pane", () => {
|
||||
|
||||
describe("shouldRecordFeedback()", () => {
|
||||
it("should return true if last collection and database does not have shared throughput else false", () => {
|
||||
let fakeExplorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
||||
let fakeExplorer = new Explorer();
|
||||
fakeExplorer.isNotificationConsoleExpanded = ko.observable<boolean>(false);
|
||||
fakeExplorer.refreshAllDatabases = () => Q.resolve();
|
||||
|
||||
|
||||
@@ -66,7 +66,11 @@ export default class DeleteCollectionConfirmationPane extends ContextualPaneBase
|
||||
this.isExecuting(false);
|
||||
this.close();
|
||||
this.container.selectedNode(selectedCollection.database);
|
||||
this.container.tabsManager?.closeTabsByComparator(tab => tab.node && tab.node.rid === selectedCollection.rid);
|
||||
this.container.tabsManager?.closeTabsByComparator(
|
||||
tab =>
|
||||
tab.node?.id() === selectedCollection.id() &&
|
||||
(tab.node as ViewModels.Collection).databaseId === selectedCollection.databaseId
|
||||
);
|
||||
this.container.refreshAllDatabases();
|
||||
this.resetData();
|
||||
TelemetryProcessor.traceSuccess(
|
||||
|
||||
@@ -22,7 +22,7 @@ describe("Delete Database Confirmation Pane", () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
|
||||
explorer = new Explorer();
|
||||
});
|
||||
|
||||
it("should be true if only 1 database", () => {
|
||||
|
||||
@@ -69,12 +69,16 @@ export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
|
||||
this.isExecuting(false);
|
||||
this.close();
|
||||
this.container.refreshAllDatabases();
|
||||
this.container.tabsManager.closeTabsByComparator(tab => tab.node && tab.node.rid === selectedDatabase.rid);
|
||||
this.container.tabsManager.closeTabsByComparator(tab => tab.node?.id() === selectedDatabase.id());
|
||||
this.container.selectedNode(null);
|
||||
selectedDatabase
|
||||
.collections()
|
||||
.forEach((collection: ViewModels.Collection) =>
|
||||
this.container.tabsManager.closeTabsByComparator(tab => tab.node && tab.node.rid === collection.rid)
|
||||
this.container.tabsManager.closeTabsByComparator(
|
||||
tab =>
|
||||
tab.node?.id() === collection.id() &&
|
||||
(tab.node as ViewModels.Collection).databaseId === collection.databaseId
|
||||
)
|
||||
);
|
||||
this.resetData();
|
||||
TelemetryProcessor.traceSuccess(
|
||||
|
||||
@@ -140,10 +140,7 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||
}
|
||||
|
||||
public async submit(): Promise<void> {
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Publishing ${this.name} to gallery`
|
||||
);
|
||||
const clearPublishingMessage = NotificationConsoleUtils.logConsoleProgress(`Publishing ${this.name} to gallery`);
|
||||
this.isExecuting = true;
|
||||
this.triggerRender();
|
||||
|
||||
@@ -161,8 +158,16 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||
this.content,
|
||||
this.isLinkInjectionEnabled
|
||||
);
|
||||
if (response.data) {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Published ${name} to gallery`);
|
||||
|
||||
const data = response.data;
|
||||
if (data) {
|
||||
if (data.pendingScanJobIds?.length > 0) {
|
||||
NotificationConsoleUtils.logConsoleInfo(
|
||||
`Content of ${this.name} is currently being scanned for illegal content. It will not be available in the public gallery until the review is complete (may take a few days).`
|
||||
);
|
||||
} else {
|
||||
NotificationConsoleUtils.logConsoleInfo(`Published ${this.name} to gallery`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.formError = `Failed to publish ${this.name} to gallery`;
|
||||
@@ -170,10 +175,10 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||
|
||||
const message = `${this.formError}: ${this.formErrorDetail}`;
|
||||
Logger.logError(message, "PublishNotebookPaneAdapter/submit");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
NotificationConsoleUtils.logConsoleError(message);
|
||||
return;
|
||||
} finally {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
clearPublishingMessage();
|
||||
this.isExecuting = false;
|
||||
this.triggerRender();
|
||||
}
|
||||
|
||||
@@ -296,7 +296,9 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
||||
downloads: 0,
|
||||
favorites: 0,
|
||||
views: 0,
|
||||
newCellId: undefined
|
||||
newCellId: undefined,
|
||||
policyViolations: undefined,
|
||||
pendingScanJobIds: undefined
|
||||
}}
|
||||
isFavorite={false}
|
||||
showDownload={true}
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -103,6 +103,8 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
|
||||
"isSample": false,
|
||||
"name": "SampleNotebook.ipynb",
|
||||
"newCellId": undefined,
|
||||
"pendingScanJobIds": undefined,
|
||||
"policyViolations": undefined,
|
||||
"tags": Array [
|
||||
"",
|
||||
],
|
||||
|
||||
@@ -6,7 +6,7 @@ import Explorer from "../Explorer";
|
||||
jest.mock("../Explorer");
|
||||
|
||||
const createExplorer = () => {
|
||||
const mock = new Explorer({} as any);
|
||||
const mock = new Explorer();
|
||||
mock.selectedNode = ko.observable();
|
||||
mock.isNotebookEnabled = ko.observable(false);
|
||||
mock.addCollectionText = ko.observable("add collection");
|
||||
|
||||
@@ -71,6 +71,8 @@ export var htmlSelectors = {
|
||||
dataTableScrollContainerSelector: ".dataTables_scroll",
|
||||
dataTableHeaderTypeSelector: "table thead th",
|
||||
dataTablePaginationButtonSelector: ".paginate_button",
|
||||
dataTableHeaderTableSelector: "#storageTable_wrapper .dataTables_scrollHeadInner table",
|
||||
dataTableBodyTableSelector: "#storageTable_wrapper .dataTables_scrollBody table",
|
||||
searchInputField: ".search-input",
|
||||
uploadDropdownSelector: "#upload-dropdown",
|
||||
navigationDropdownSelector: "#navigation-dropdown",
|
||||
|
||||
18
src/Explorer/Tables/CqlUtilities.test.ts
Normal file
18
src/Explorer/Tables/CqlUtilities.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { getQuotedCqlIdentifier } from "./CqlUtilities";
|
||||
|
||||
describe("getQuotedCqlIdentifier", () => {
|
||||
it("undefined id", () => {
|
||||
const result = getQuotedCqlIdentifier(undefined);
|
||||
expect(result).toBe(undefined);
|
||||
});
|
||||
|
||||
it("id with no quotes", () => {
|
||||
const result = getQuotedCqlIdentifier("foo");
|
||||
expect(result).toBe('"foo"');
|
||||
});
|
||||
|
||||
it("id with quotes", () => {
|
||||
const result = getQuotedCqlIdentifier('"foo"');
|
||||
expect(result).toBe('"""foo"""');
|
||||
});
|
||||
});
|
||||
12
src/Explorer/Tables/CqlUtilities.ts
Normal file
12
src/Explorer/Tables/CqlUtilities.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export function getQuotedCqlIdentifier(identifier: string): string {
|
||||
let result = identifier;
|
||||
if (!identifier) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (identifier.includes('"')) {
|
||||
result = identifier.replace(/"/g, '""');
|
||||
}
|
||||
|
||||
return `"${result}"`;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user