mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 19:01:28 +00:00
Compare commits
58 Commits
replace_jq
...
steve-self
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c857b9aab9 | ||
|
|
cd45f2943d | ||
|
|
385c9f216f | ||
|
|
8c40df0fa1 | ||
|
|
d17508cc27 | ||
|
|
2ec2a891b4 | ||
|
|
fcbc9474ea | ||
|
|
b34628e9fc | ||
|
|
3fb4af53c8 | ||
|
|
81f861af39 | ||
|
|
9afa29cdb6 | ||
|
|
9a1e8b2d87 | ||
|
|
41f37055ef | ||
|
|
aa925d8d54 | ||
|
|
c9cea86225 | ||
|
|
318842624f | ||
|
|
babda4d9cb | ||
|
|
9d20a13dd4 | ||
|
|
3effbe6991 | ||
|
|
af53697ff4 | ||
|
|
b1ad80480e | ||
|
|
9247a6c4a2 | ||
|
|
767d46480e | ||
|
|
2d98c5d269 | ||
|
|
6627172a52 | ||
|
|
19fa5e17a5 | ||
|
|
a4a367a212 | ||
|
|
983c9201bb | ||
|
|
76d7f00a90 | ||
|
|
6490597736 | ||
|
|
229119e697 | ||
|
|
ceefd7c615 | ||
|
|
6e619175c6 | ||
|
|
08e8bf4bcf | ||
|
|
89dc0f394b | ||
|
|
30e0001b7f | ||
|
|
4a8f408112 | ||
|
|
e801364800 | ||
|
|
373327dc88 | ||
|
|
8333ee7ec4 | ||
|
|
97116175ab | ||
|
|
a55f2d0de9 | ||
|
|
d40b1aa9b5 | ||
|
|
cc63cdc1fd | ||
|
|
f770bb193e | ||
|
|
8cb8d10bc3 | ||
|
|
b298caf9ff | ||
|
|
a2022fbbac | ||
|
|
b3b57462ef | ||
|
|
95fc75cb23 | ||
|
|
c97eb6018b | ||
|
|
90fb7e7d8f | ||
|
|
2dbde9c31a | ||
|
|
4381ea447c | ||
|
|
69b17f1a00 | ||
|
|
8cf160d818 | ||
|
|
88d71d7070 | ||
|
|
84017660c1 |
@@ -69,6 +69,10 @@ Jest and Puppeteer are used for end to end browser based tests and are contained
|
||||
|
||||
We generally adhere to the release strategy [documented by the Azure SDK Guidelines](https://azure.github.io/azure-sdk/policies_repobranching.html#release-branches). Most releases should happen from the master branch. If master contains commits that cannot be released, you may create a release from a `release/` or `hotfix/` branch. See linked documentation for more details.
|
||||
|
||||
### Architechture
|
||||
|
||||
[](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ3JhcGggTFJcbiAgaG9zdGVkKGh0dHBzOi8vY29zbW9zLmF6dXJlLmNvbSlcbiAgcG9ydGFsKFBvcnRhbClcbiAgZW11bGF0b3IoRW11bGF0b3IpXG4gIGFhZFtBQURdXG4gIHJlc291cmNlVG9rZW5bUmVzb3VyY2UgVG9rZW5dXG4gIGNvbm5lY3Rpb25TdHJpbmdbQ29ubmVjdGlvbiBTdHJpbmddXG4gIHBvcnRhbFRva2VuW0VuY3J5cHRlZCBQb3J0YWwgVG9rZW5dXG4gIG1hc3RlcktleVtNYXN0ZXIgS2V5XVxuICBhcm1bQVJNIFJlc291cmNlIFByb3ZpZGVyXVxuICBkYXRhcGxhbmVbRGF0YSBQbGFuZV1cbiAgcHJveHlbUG9ydGFsIEFQSSBQcm94eV1cbiAgc3FsW1NRTF1cbiAgbW9uZ29bTW9uZ29dXG4gIHRhYmxlc1tUYWJsZXNdXG4gIGNhc3NhbmRyYVtDYXNzYW5kcmFdXG4gIGdyYWZbR3JhcGhdXG5cblxuICBlbXVsYXRvciAtLT4gbWFzdGVyS2V5IC0tLS0-IGRhdGFwbGFuZVxuICBwb3J0YWwgLS0-IGFhZFxuICBob3N0ZWQgLS0-IHBvcnRhbFRva2VuICYgcmVzb3VyY2VUb2tlbiAmIGNvbm5lY3Rpb25TdHJpbmcgJiBhYWRcbiAgYWFkIC0tLT4gYXJtXG4gIGFhZCAtLS0-IGRhdGFwbGFuZVxuICBhYWQgLS0tPiBwcm94eVxuICByZXNvdXJjZVRva2VuIC0tLT4gc3FsIC0tPiBkYXRhcGxhbmVcbiAgcG9ydGFsVG9rZW4gLS0tPiBwcm94eVxuICBwcm94eSAtLT4gZGF0YXBsYW5lXG4gIGNvbm5lY3Rpb25TdHJpbmcgLS0-IHNxbCAmIG1vbmdvICYgY2Fzc2FuZHJhICYgZ3JhZiAmIHRhYmxlc1xuICBzcWwgLS0-IGRhdGFwbGFuZVxuICB0YWJsZXMgLS0-IGRhdGFwbGFuZVxuICBtb25nbyAtLT4gcHJveHlcbiAgY2Fzc2FuZHJhIC0tPiBwcm94eVxuICBncmFmIC0tPiBwcm94eVxuXG5cdFx0IiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQifSwidXBkYXRlRWRpdG9yIjpmYWxzZX0)
|
||||
|
||||
# Contributing
|
||||
|
||||
Please read the [contribution guidelines](./CONTRIBUTING.md).
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
module.exports = {
|
||||
presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-react", "@babel/preset-typescript"]
|
||||
presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-react", "@babel/preset-typescript"],
|
||||
plugins: [["@babel/plugin-proposal-decorators", { legacy: true }]]
|
||||
};
|
||||
|
||||
7
canvas/README.md
Normal file
7
canvas/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Why?
|
||||
|
||||
This adds a mock module for `canvas`. Nteract has a ignored require and undeclared dependency on this module. `cavnas` is a server side node module and is not used in browser side code for nteract.
|
||||
|
||||
Installing it locally (`npm install canvas`) will resolve the problem, but it is a native module so it is flaky depending on the system, node version, processor arch, etc. This module provides a simpler, more robust solution.
|
||||
|
||||
Remove this workaround if [this bug](https://github.com/nteract/any-vega/issues/2) ever gets resolved
|
||||
1
canvas/index.js
Normal file
1
canvas/index.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = {}
|
||||
11
canvas/package.json
Normal file
11
canvas/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "canvas",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
/******************************************************************************/
|
||||
|
||||
@font-face {
|
||||
font-family: wf_segoe-ui_normal;
|
||||
src: url('../../fonts/segoe-ui/west-european/normal/latest.woff');
|
||||
font-family: wf_segoe-ui_normal;
|
||||
src: url("../../fonts/segoe-ui/west-european/normal/latest.woff");
|
||||
}
|
||||
|
||||
@DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
|
||||
@@ -20,26 +20,26 @@
|
||||
COLORS
|
||||
/******************************************************************************/
|
||||
|
||||
@AccentMediumHigh: #0058AD;
|
||||
@AccentMedium: #004E87;
|
||||
@AccentHigh: #1EBAED;
|
||||
@AccentExtraHigh: #55B3FF;
|
||||
@AccentLow: #EDF6FF;
|
||||
@AccentMediumLow: #DDEEFE;
|
||||
@AccentLight: #EEF7FF;
|
||||
@AccentExtra: #DDF0FF;
|
||||
@AccentMediumHigh: #0058ad;
|
||||
@AccentMedium: #004e87;
|
||||
@AccentHigh: #1ebaed;
|
||||
@AccentExtraHigh: #55b3ff;
|
||||
@AccentLow: #edf6ff;
|
||||
@AccentMediumLow: #ddeefe;
|
||||
@AccentLight: #eef7ff;
|
||||
@AccentExtra: #ddf0ff;
|
||||
|
||||
@SelectionHigh: #B91F26;
|
||||
@BaseLight: #FFFFFF;
|
||||
@SelectionHigh: #b91f26;
|
||||
@BaseLight: #ffffff;
|
||||
@BaseDark: #000000;
|
||||
@NotificationLow: #FFF4CE;
|
||||
@NotificationHigh: #F9E9B0;
|
||||
@Purple1: #8A2DA5;
|
||||
@NotificationLow: #fff4ce;
|
||||
@NotificationHigh: #f9e9b0;
|
||||
@Purple1: #8a2da5;
|
||||
@Dirty: #9b4f96;
|
||||
|
||||
@BaseLow: #F2F2F2;
|
||||
@BaseMediumLow: #E6E6E6;
|
||||
@BaseMedium: #CCCCCC;
|
||||
@BaseLow: #f2f2f2;
|
||||
@BaseMediumLow: #e6e6e6;
|
||||
@BaseMedium: #cccccc;
|
||||
@BaseMediumHigh: #767676;
|
||||
@BaseHigh: #393939;
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
@ErrorColor: @SelectionHigh;
|
||||
|
||||
@SelectionColor: #3074B0;
|
||||
@SelectionColor: #3074b0;
|
||||
|
||||
@FocusColor: #605e5c;
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
@ImgWidth: 14px;
|
||||
@ImgHeight: 14px;
|
||||
|
||||
@toggleFontWeight:700;
|
||||
@toggleFontWeight: 700;
|
||||
|
||||
//Resource Tree
|
||||
@TreeLineHeight: 17px;
|
||||
@@ -144,16 +144,16 @@
|
||||
/**********************************************************************************/
|
||||
|
||||
.flex-display(@display: flex) {
|
||||
display: ~"-webkit-@{display}";
|
||||
display: ~"-ms-@{display}box"; // IE10 uses -ms-flexbox
|
||||
display: ~"-ms-@{display}"; // IE11
|
||||
display: @display;
|
||||
display: ~"-webkit-@{display}";
|
||||
display: ~"-ms-@{display}box"; // IE10 uses -ms-flexbox
|
||||
display: ~"-ms-@{display}"; // IE11
|
||||
display: @display;
|
||||
}
|
||||
|
||||
.flex-direction(@direction: column) {
|
||||
-webkit-flex-direction: @direction;
|
||||
-ms-flex-direction: @direction;
|
||||
flex-direction: @direction;
|
||||
-ms-flex-direction: @direction;
|
||||
flex-direction: @direction;
|
||||
}
|
||||
|
||||
/*************************************************************************************
|
||||
@@ -161,32 +161,31 @@
|
||||
**************************************************************************************/
|
||||
|
||||
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
|
||||
.selectedRadio,
|
||||
.selectedRadio:hover,
|
||||
.selectedRadio:active,
|
||||
.selectedRadio.dirty,
|
||||
.tab [type=radio]:checked ~ label,
|
||||
.tab [type=radio]:checked ~ label:hover {
|
||||
-ms-high-contrast-adjust: none;
|
||||
-webkit-text-fill-color: HighlightText;
|
||||
color: HighlightText;
|
||||
border-color: HighlightText;
|
||||
background-color: Highlight;
|
||||
}
|
||||
|
||||
.queryMetricsSummaryTuple {
|
||||
|
||||
th, td {
|
||||
|
||||
&:nth-child(2) {
|
||||
width: @IETableDataWidth;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
.selectedRadio,
|
||||
.selectedRadio:hover,
|
||||
.selectedRadio:active,
|
||||
.selectedRadio.dirty,
|
||||
.tab [type="radio"]:checked ~ label,
|
||||
.tab [type="radio"]:checked ~ label:hover {
|
||||
-ms-high-contrast-adjust: none;
|
||||
-webkit-text-fill-color: HighlightText;
|
||||
color: HighlightText;
|
||||
border-color: HighlightText;
|
||||
background-color: Highlight;
|
||||
}
|
||||
|
||||
.queryMetricsSummaryTuple {
|
||||
th,
|
||||
td {
|
||||
&:nth-child(2) {
|
||||
width: @IETableDataWidth;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/********************************************************************************************
|
||||
@@ -194,15 +193,15 @@
|
||||
*********************************************************************************************/
|
||||
|
||||
.hover() {
|
||||
background-color: @AccentLight;
|
||||
background-color: @AccentLight;
|
||||
}
|
||||
|
||||
.active() {
|
||||
background-color: @AccentExtra;
|
||||
background-color: @AccentExtra;
|
||||
}
|
||||
|
||||
.focus() {
|
||||
outline: 1px dashed @FocusColor;
|
||||
outline: 1px dashed @FocusColor;
|
||||
}
|
||||
|
||||
/************************************************************************************************
|
||||
@@ -212,63 +211,87 @@
|
||||
@ToggleWidth: 180px;
|
||||
|
||||
.toggleSwitch() {
|
||||
max-width: 100%;
|
||||
margin-bottom: @SmallSpace;
|
||||
padding: @SmallSpace;
|
||||
cursor: pointer;
|
||||
color: @BaseHigh;
|
||||
font-weight: 400;
|
||||
font-size: @mediumFontSize;
|
||||
font-family: @DataExplorerFont;
|
||||
max-width: 100%;
|
||||
margin-bottom: @SmallSpace;
|
||||
padding: @SmallSpace;
|
||||
cursor: pointer;
|
||||
color: @BaseHigh;
|
||||
font-weight: 400;
|
||||
font-size: @mediumFontSize;
|
||||
font-family: @DataExplorerFont;
|
||||
}
|
||||
|
||||
.selectedToggle() {
|
||||
border-bottom: 2px solid @BaseHigh;
|
||||
border-bottom: 2px solid @BaseHigh;
|
||||
}
|
||||
|
||||
.unselectedToggle() {
|
||||
color: @AccentMediumHigh;
|
||||
color: @AccentMediumHigh;
|
||||
}
|
||||
|
||||
/********************************************************************************************************
|
||||
Common Data Explorer Icons
|
||||
*********************************************************************************************************/
|
||||
.dataExplorerIcons() {
|
||||
cursor: pointer;
|
||||
width: @ImgWidth;
|
||||
height: @ImgHeight;
|
||||
cursor: pointer;
|
||||
width: @ImgWidth;
|
||||
height: @ImgHeight;
|
||||
}
|
||||
|
||||
/*********************************************************************************************************
|
||||
Info Tooltip
|
||||
**********************************************************************************************************/
|
||||
.infoTooltip() {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tooltipText(@textColor: @BaseLight, @backgroundColor: @BaseHigh) {
|
||||
visibility: hidden;
|
||||
background-color: @backgroundColor;
|
||||
color: @textColor;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
left: @MediumSpace;
|
||||
padding: @MediumSpace;
|
||||
visibility: hidden;
|
||||
background-color: @backgroundColor;
|
||||
color: @textColor;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
left: @MediumSpace;
|
||||
padding: @MediumSpace;
|
||||
}
|
||||
|
||||
.tooltipTextAfter(@color: @BaseDark) {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
border-style: solid;
|
||||
border-color: transparent @color transparent transparent;
|
||||
left: 0px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: @InfoPointerColor transparent;
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
border-style: solid;
|
||||
border-color: transparent @color transparent transparent;
|
||||
left: 0px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: @InfoPointerColor transparent;
|
||||
}
|
||||
|
||||
.tooltipVisible() {
|
||||
visibility: visible;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.inputTooltip() {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inputTooltipText(@textColor: @BaseLight, @backgroundColor: @BaseHigh) {
|
||||
background-color: @backgroundColor;
|
||||
color: @textColor;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
padding: @MediumSpace;
|
||||
}
|
||||
|
||||
.inputTooltipTextAfter(@color: @BaseDark) {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
border-style: solid;
|
||||
border-color: transparent @color transparent transparent;
|
||||
left: 10px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: @InfoPointerColor transparent;
|
||||
}
|
||||
|
||||
3829
less/documentDB.less
3829
less/documentDB.less
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,11 @@
|
||||
@NavMediumSpace: 10px;
|
||||
@NavLargeSpace: 15px;
|
||||
|
||||
.skip-link {
|
||||
position: fixed;
|
||||
top: -200px;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
|
||||
padding: 0px;
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
@import "./Common/Constants";
|
||||
|
||||
.main {
|
||||
width: 100%;
|
||||
float: left;
|
||||
transition: all .0s ease-in-out;
|
||||
-ms-transition: all 0s ease-in-out;
|
||||
-webkit-transition: all 0s ease-in-out;
|
||||
-moz-transition: all .0s ease-in-out;
|
||||
height: 100%;
|
||||
background-color: white;
|
||||
border-left: 0px solid white;
|
||||
}
|
||||
|
||||
.resourceTree {
|
||||
height: 100%;
|
||||
flex: 0 0 auto;
|
||||
.main {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.resourceTreeScroll {
|
||||
|
||||
253
package-lock.json
generated
253
package-lock.json
generated
@@ -393,7 +393,6 @@
|
||||
"version": "7.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz",
|
||||
"integrity": "sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-function-name": "^7.10.4",
|
||||
"@babel/helper-member-expression-to-functions": "^7.12.1",
|
||||
@@ -620,6 +619,25 @@
|
||||
"@babel/plugin-syntax-async-generators": "^7.8.0"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-proposal-class-properties": {
|
||||
"version": "7.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz",
|
||||
"integrity": "sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w==",
|
||||
"requires": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.12.1",
|
||||
"@babel/helper-plugin-utils": "^7.10.4"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-proposal-decorators": {
|
||||
"version": "7.12.12",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.12.12.tgz",
|
||||
"integrity": "sha512-fhkE9lJYpw2mjHelBpM2zCbaA11aov2GJs7q4cFaXNrWx0H3bW58H9Esy2rdtYOghFBEYUDRIpvlgi+ZD+AvvQ==",
|
||||
"requires": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.12.1",
|
||||
"@babel/helper-plugin-utils": "^7.10.4",
|
||||
"@babel/plugin-syntax-decorators": "^7.12.1"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-proposal-dynamic-import": {
|
||||
"version": "7.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.1.tgz",
|
||||
@@ -729,6 +747,14 @@
|
||||
"@babel/helper-plugin-utils": "^7.10.4"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-syntax-decorators": {
|
||||
"version": "7.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.12.1.tgz",
|
||||
"integrity": "sha512-ir9YW5daRrTYiy9UJ2TzdNIJEZu8KclVzDcfSt4iEmOtwQ4llPtWInNKJyKnVXp1vE4bbVd5S31M/im3mYMO1w==",
|
||||
"requires": {
|
||||
"@babel/helper-plugin-utils": "^7.10.4"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-syntax-dynamic-import": {
|
||||
"version": "7.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz",
|
||||
@@ -5393,11 +5419,6 @@
|
||||
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz",
|
||||
"integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q=="
|
||||
},
|
||||
"abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
|
||||
},
|
||||
"abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
@@ -5641,6 +5662,7 @@
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
|
||||
"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"delegates": "^1.0.0",
|
||||
"readable-stream": "^2.0.6"
|
||||
@@ -6883,14 +6905,7 @@
|
||||
"dev": true
|
||||
},
|
||||
"canvas": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/canvas/-/canvas-2.6.1.tgz",
|
||||
"integrity": "sha512-S98rKsPcuhfTcYbtF53UIJhcbgIAK533d1kJKMwsMwAIFgfd58MOyxRud3kktlzWiEkFliaJtvyZCBtud/XVEA==",
|
||||
"requires": {
|
||||
"nan": "^2.14.0",
|
||||
"node-pre-gyp": "^0.11.0",
|
||||
"simple-get": "^3.0.3"
|
||||
}
|
||||
"version": "file:canvas"
|
||||
},
|
||||
"capture-exit": {
|
||||
"version": "2.0.0",
|
||||
@@ -7454,7 +7469,8 @@
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
|
||||
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
|
||||
"optional": true
|
||||
},
|
||||
"constants-browserify": {
|
||||
"version": "1.0.0",
|
||||
@@ -8435,6 +8451,7 @@
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
|
||||
"integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"mimic-response": "^2.0.0"
|
||||
}
|
||||
@@ -8460,7 +8477,8 @@
|
||||
"deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||
"optional": true
|
||||
},
|
||||
"deep-is": {
|
||||
"version": "0.1.3",
|
||||
@@ -8652,7 +8670,8 @@
|
||||
"delegates": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
|
||||
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
|
||||
"optional": true
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.1.2",
|
||||
@@ -8688,7 +8707,8 @@
|
||||
"detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
|
||||
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
|
||||
"optional": true
|
||||
},
|
||||
"detect-newline": {
|
||||
"version": "2.1.0",
|
||||
@@ -10674,14 +10694,6 @@
|
||||
"universalify": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"fs-minipass": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
|
||||
"integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
|
||||
"requires": {
|
||||
"minipass": "^2.6.0"
|
||||
}
|
||||
},
|
||||
"fs-observable": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/fs-observable/-/fs-observable-4.1.14.tgz",
|
||||
@@ -10823,6 +10835,7 @@
|
||||
"version": "2.7.4",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
|
||||
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"aproba": "^1.0.3",
|
||||
"console-control-strings": "^1.0.0",
|
||||
@@ -10837,12 +10850,14 @@
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
|
||||
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
|
||||
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
|
||||
"optional": true
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
|
||||
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@@ -10851,6 +10866,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
|
||||
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@@ -10861,6 +10877,7 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
@@ -11350,7 +11367,8 @@
|
||||
"has-unicode": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
||||
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk="
|
||||
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
|
||||
"optional": true
|
||||
},
|
||||
"has-value": {
|
||||
"version": "1.0.0",
|
||||
@@ -11832,14 +11850,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
|
||||
"integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw=="
|
||||
},
|
||||
"ignore-walk": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz",
|
||||
"integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==",
|
||||
"requires": {
|
||||
"minimatch": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"image-size": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
|
||||
@@ -15544,7 +15554,8 @@
|
||||
"mimic-response": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
|
||||
"integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA=="
|
||||
"integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==",
|
||||
"optional": true
|
||||
},
|
||||
"min-document": {
|
||||
"version": "2.19.0",
|
||||
@@ -15601,15 +15612,6 @@
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
|
||||
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"minipass-collect": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz",
|
||||
@@ -15679,14 +15681,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"minizlib": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
|
||||
"integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
|
||||
"requires": {
|
||||
"minipass": "^2.9.0"
|
||||
}
|
||||
},
|
||||
"mississippi": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz",
|
||||
@@ -15861,7 +15855,8 @@
|
||||
"nan": {
|
||||
"version": "2.14.2",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
|
||||
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ=="
|
||||
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
|
||||
"optional": true
|
||||
},
|
||||
"nanomatch": {
|
||||
"version": "1.2.13",
|
||||
@@ -15911,26 +15906,6 @@
|
||||
"semver": "^5.4.1"
|
||||
}
|
||||
},
|
||||
"needle": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/needle/-/needle-2.5.2.tgz",
|
||||
"integrity": "sha512-LbRIwS9BfkPvNwNHlsA41Q29kL2L/6VaOJ0qisM5lLWsTV3nP15abO5ITL6L81zqFhzjRKDAYjpcBcwM0AVvLQ==",
|
||||
"requires": {
|
||||
"debug": "^3.2.6",
|
||||
"iconv-lite": "^0.4.4",
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"negotiator": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
|
||||
@@ -16099,41 +16074,6 @@
|
||||
"which": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node-pre-gyp": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz",
|
||||
"integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==",
|
||||
"requires": {
|
||||
"detect-libc": "^1.0.2",
|
||||
"mkdirp": "^0.5.1",
|
||||
"needle": "^2.2.1",
|
||||
"nopt": "^4.0.1",
|
||||
"npm-packlist": "^1.1.6",
|
||||
"npmlog": "^4.0.2",
|
||||
"rc": "^1.2.7",
|
||||
"rimraf": "^2.6.1",
|
||||
"semver": "^5.3.0",
|
||||
"tar": "^4"
|
||||
},
|
||||
"dependencies": {
|
||||
"mkdirp": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
||||
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
||||
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"node-releases": {
|
||||
"version": "1.1.66",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.66.tgz",
|
||||
@@ -16146,15 +16086,6 @@
|
||||
"integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=",
|
||||
"optional": true
|
||||
},
|
||||
"nopt": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
|
||||
"integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
|
||||
"requires": {
|
||||
"abbrev": "1",
|
||||
"osenv": "^0.1.4"
|
||||
}
|
||||
},
|
||||
"normalize-package-data": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
||||
@@ -16179,29 +16110,6 @@
|
||||
"resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz",
|
||||
"integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg=="
|
||||
},
|
||||
"npm-bundled": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz",
|
||||
"integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==",
|
||||
"requires": {
|
||||
"npm-normalize-package-bin": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"npm-normalize-package-bin": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz",
|
||||
"integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA=="
|
||||
},
|
||||
"npm-packlist": {
|
||||
"version": "1.4.8",
|
||||
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz",
|
||||
"integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==",
|
||||
"requires": {
|
||||
"ignore-walk": "^3.0.1",
|
||||
"npm-bundled": "^1.0.1",
|
||||
"npm-normalize-package-bin": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"npm-run-path": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
|
||||
@@ -16214,6 +16122,7 @@
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
|
||||
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"are-we-there-yet": "~1.1.2",
|
||||
"console-control-strings": "~1.1.0",
|
||||
@@ -16605,7 +16514,8 @@
|
||||
"os-homedir": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
|
||||
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
|
||||
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
|
||||
"dev": true
|
||||
},
|
||||
"os-locale": {
|
||||
"version": "1.4.0",
|
||||
@@ -16624,20 +16534,6 @@
|
||||
"windows-release": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"os-tmpdir": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
||||
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
|
||||
},
|
||||
"osenv": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
|
||||
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
|
||||
"requires": {
|
||||
"os-homedir": "^1.0.0",
|
||||
"os-tmpdir": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"p-defer": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
|
||||
@@ -17690,6 +17586,7 @@
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"deep-extend": "^0.6.0",
|
||||
"ini": "~1.3.0",
|
||||
@@ -18101,6 +17998,11 @@
|
||||
"resolved": "https://registry.npmjs.org/redux-observable/-/redux-observable-2.0.0-alpha.0.tgz",
|
||||
"integrity": "sha512-w0RsVGprIFiYi1AhFCOATiv3ld2AtuobvbcVsLvX19p8eAwLowWl2OrKYcCq/QEeEpmSHTXutXfVfcBnzaWmdw=="
|
||||
},
|
||||
"reflect-metadata": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
|
||||
"integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg=="
|
||||
},
|
||||
"reflect.ownkeys": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz",
|
||||
@@ -19116,12 +19018,14 @@
|
||||
"simple-concat": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="
|
||||
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
||||
"optional": true
|
||||
},
|
||||
"simple-get": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz",
|
||||
"integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"decompress-response": "^4.2.0",
|
||||
"once": "^1.3.1",
|
||||
@@ -20113,30 +20017,6 @@
|
||||
"integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==",
|
||||
"dev": true
|
||||
},
|
||||
"tar": {
|
||||
"version": "4.4.13",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz",
|
||||
"integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==",
|
||||
"requires": {
|
||||
"chownr": "^1.1.1",
|
||||
"fs-minipass": "^1.2.5",
|
||||
"minipass": "^2.8.6",
|
||||
"minizlib": "^1.2.1",
|
||||
"mkdirp": "^0.5.0",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"mkdirp": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
||||
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tar-fs": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
|
||||
@@ -22192,6 +22072,7 @@
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
|
||||
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"string-width": "^1.0.2 || 2"
|
||||
},
|
||||
@@ -22199,12 +22080,14 @@
|
||||
"ansi-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
|
||||
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
|
||||
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
|
||||
"optional": true
|
||||
},
|
||||
"string-width": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
|
||||
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-fullwidth-code-point": "^2.0.0",
|
||||
"strip-ansi": "^4.0.0"
|
||||
@@ -22214,6 +22097,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
|
||||
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^3.0.0"
|
||||
}
|
||||
@@ -22383,7 +22267,8 @@
|
||||
"yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true
|
||||
},
|
||||
"yargs": {
|
||||
"version": "13.3.2",
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
"dependencies": {
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/cosmos": "3.9.0",
|
||||
"@azure/identity": "1.1.0",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@azure/identity": "1.1.0",
|
||||
"@babel/plugin-proposal-class-properties": "7.12.1",
|
||||
"@babel/plugin-proposal-decorators": "7.12.12",
|
||||
"@jupyterlab/services": "6.0.0-rc.2",
|
||||
"@jupyterlab/terminal": "3.0.0-rc.2",
|
||||
"@microsoft/applicationinsights-web": "2.5.9",
|
||||
@@ -44,7 +46,7 @@
|
||||
"applicationinsights": "1.8.0",
|
||||
"babel-polyfill": "6.26.0",
|
||||
"bootstrap": "3.4.1",
|
||||
"canvas": "2.6.1",
|
||||
"canvas": "file:./canvas",
|
||||
"clean-webpack-plugin": "0.1.19",
|
||||
"copy-webpack-plugin": "6.0.2",
|
||||
"crossroads": "0.12.2",
|
||||
@@ -85,6 +87,7 @@
|
||||
"react-notification-system": "0.2.17",
|
||||
"react-redux": "7.1.3",
|
||||
"redux": "4.0.4",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rx-jupyter": "5.5.12",
|
||||
"rxjs": "6.6.3",
|
||||
"styled-components": "4.3.2",
|
||||
|
||||
@@ -1,434 +1,437 @@
|
||||
import { HashMap } from "./HashMap";
|
||||
|
||||
export class AuthorizationEndpoints {
|
||||
public static arm: string = "https://management.core.windows.net/";
|
||||
public static common: string = "https://login.windows.net/";
|
||||
}
|
||||
|
||||
export class CodeOfConductEndpoints {
|
||||
public static privacyStatement: string = "https://aka.ms/ms-privacy-policy";
|
||||
public static codeOfConduct: string = "https://aka.ms/cosmos-code-of-conduct";
|
||||
public static termsOfUse: string = "https://aka.ms/ms-terms-of-use";
|
||||
}
|
||||
|
||||
export class EndpointsRegex {
|
||||
public static readonly cassandra = [
|
||||
"AccountEndpoint=(.*).cassandra.cosmosdb.azure.com",
|
||||
"HostName=(.*).cassandra.cosmos.azure.com"
|
||||
];
|
||||
public static readonly mongo = "mongodb://.*:(.*)@(.*).documents.azure.com";
|
||||
public static readonly mongoCompute = "mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com";
|
||||
public static readonly sql = "AccountEndpoint=https://(.*).documents.azure.com";
|
||||
public static readonly table = "TableEndpoint=https://(.*).table.cosmosdb.azure.com";
|
||||
}
|
||||
|
||||
export class ApiEndpoints {
|
||||
public static runtimeProxy: string = "/api/RuntimeProxy";
|
||||
public static guestRuntimeProxy: string = "/api/guest/RuntimeProxy";
|
||||
}
|
||||
|
||||
export class ServerIds {
|
||||
public static localhost: string = "localhost";
|
||||
public static blackforest: string = "blackforest";
|
||||
public static fairfax: string = "fairfax";
|
||||
public static mooncake: string = "mooncake";
|
||||
public static productionPortal: string = "prod";
|
||||
public static dev: string = "dev";
|
||||
}
|
||||
|
||||
export class ArmApiVersions {
|
||||
public static readonly documentDB: string = "2015-11-06";
|
||||
public static readonly arcadia: string = "2019-06-01-preview";
|
||||
public static readonly arcadiaLivy: string = "2019-11-01-preview";
|
||||
public static readonly arm: string = "2015-11-01";
|
||||
public static readonly armFeatures: string = "2014-08-01-preview";
|
||||
public static readonly publicVersion = "2020-04-01";
|
||||
}
|
||||
|
||||
export class ArmResourceTypes {
|
||||
public static readonly notebookWorkspaces = "Microsoft.DocumentDB/databaseAccounts/notebookWorkspaces";
|
||||
public static readonly synapseWorkspaces = "Microsoft.Synapse/workspaces";
|
||||
}
|
||||
|
||||
export class BackendDefaults {
|
||||
public static partitionKeyKind: string = "Hash";
|
||||
public static singlePartitionStorageInGb: string = "10";
|
||||
public static multiPartitionStorageInGb: string = "100";
|
||||
public static maxChangeFeedRetentionDuration: number = 10;
|
||||
public static partitionKeyVersion = 2;
|
||||
}
|
||||
|
||||
export class ClientDefaults {
|
||||
public static requestTimeoutMs: number = 60000;
|
||||
public static portalCacheTimeoutMs: number = 10000;
|
||||
public static errorNotificationTimeoutMs: number = 5000;
|
||||
public static copyHelperTimeoutMs: number = 2000;
|
||||
public static waitForDOMElementMs: number = 500;
|
||||
public static cacheBustingTimeoutMs: number =
|
||||
10 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/;
|
||||
public static databaseThroughputIncreaseFactor: number = 100;
|
||||
public static readonly arcadiaTokenRefreshInterval: number =
|
||||
20 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/;
|
||||
public static readonly arcadiaTokenRefreshIntervalPaddingMs: number = 2000;
|
||||
}
|
||||
|
||||
export class AccountKind {
|
||||
public static DocumentDB: string = "DocumentDB";
|
||||
public static MongoDB: string = "MongoDB";
|
||||
public static Parse: string = "Parse";
|
||||
public static GlobalDocumentDB: string = "GlobalDocumentDB";
|
||||
public static Default: string = AccountKind.DocumentDB;
|
||||
}
|
||||
|
||||
export class CorrelationBackend {
|
||||
public static Url: string = "https://aka.ms/cosmosdbanalytics";
|
||||
}
|
||||
|
||||
export class DefaultAccountExperience {
|
||||
public static DocumentDB: string = "DocumentDB";
|
||||
public static Graph: string = "Graph";
|
||||
public static MongoDB: string = "MongoDB";
|
||||
public static ApiForMongoDB: string = "Azure Cosmos DB for MongoDB API";
|
||||
public static Table: string = "Table";
|
||||
public static Cassandra: string = "Cassandra";
|
||||
public static Default: string = DefaultAccountExperience.DocumentDB;
|
||||
}
|
||||
|
||||
export class CapabilityNames {
|
||||
public static EnableTable: string = "EnableTable";
|
||||
public static EnableGremlin: string = "EnableGremlin";
|
||||
public static EnableCassandra: string = "EnableCassandra";
|
||||
public static EnableAutoScale: string = "EnableAutoScale";
|
||||
public static readonly EnableNotebooks: string = "EnableNotebooks";
|
||||
public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics";
|
||||
public static readonly EnableMongo: string = "EnableMongo";
|
||||
public static readonly EnableServerless: string = "EnableServerless";
|
||||
}
|
||||
|
||||
export class Features {
|
||||
public static readonly cosmosdb = "cosmosdb";
|
||||
public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy";
|
||||
public static readonly executeSproc = "dataexplorerexecutesproc";
|
||||
public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
|
||||
public static readonly enableTtl = "enablettl";
|
||||
public static readonly enableNotebooks = "enablenotebooks";
|
||||
public static readonly enableGalleryPublish = "enablegallerypublish";
|
||||
public static readonly enableLinkInjection = "enablelinkinjection";
|
||||
public static readonly enableSpark = "enablespark";
|
||||
public static readonly livyEndpoint = "livyendpoint";
|
||||
public static readonly notebookServerUrl = "notebookserverurl";
|
||||
public static readonly notebookServerToken = "notebookservertoken";
|
||||
public static readonly notebookBasePath = "notebookbasepath";
|
||||
public static readonly canExceedMaximumValue = "canexceedmaximumvalue";
|
||||
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
|
||||
public static readonly ttl90Days = "ttl90days";
|
||||
public static readonly enableRightPanelV2 = "enablerightpanelv2";
|
||||
public static readonly enableSchema = "enableschema";
|
||||
public static readonly enableSDKoperations = "enablesdkoperations";
|
||||
public static readonly showMinRUSurvey = "showminrusurvey";
|
||||
}
|
||||
|
||||
// flight names returned from the portal are always lowercase
|
||||
export class Flights {
|
||||
public static readonly SettingsV2 = "settingsv2";
|
||||
public static readonly MongoIndexEditor = "mongoindexeditor";
|
||||
}
|
||||
|
||||
export class AfecFeatures {
|
||||
public static readonly Spark = "spark-public-preview";
|
||||
public static readonly Notebooks = "sparknotebooks-public-preview";
|
||||
public static readonly StorageAnalytics = "storageanalytics-public-preview";
|
||||
}
|
||||
|
||||
export class Spark {
|
||||
public static readonly MaxWorkerCount = 10;
|
||||
public static readonly SKUs: HashMap<string> = new HashMap({
|
||||
"Cosmos.Spark.D1s": "D1s / 1 core / 4GB RAM",
|
||||
"Cosmos.Spark.D2s": "D2s / 2 cores / 8GB RAM",
|
||||
"Cosmos.Spark.D4s": "D4s / 4 cores / 16GB RAM",
|
||||
"Cosmos.Spark.D8s": "D8s / 8 cores / 32GB RAM",
|
||||
"Cosmos.Spark.D16s": "D16s / 16 cores / 64GB RAM",
|
||||
"Cosmos.Spark.D32s": "D32s / 32 cores / 128GB RAM",
|
||||
"Cosmos.Spark.D64s": "D64s / 64 cores / 256GB RAM"
|
||||
});
|
||||
}
|
||||
|
||||
export class TagNames {
|
||||
public static defaultExperience: string = "defaultExperience";
|
||||
}
|
||||
|
||||
export class MongoDBAccounts {
|
||||
public static protocol: string = "https";
|
||||
public static defaultPort: string = "10255";
|
||||
}
|
||||
|
||||
export enum MongoBackendEndpointType {
|
||||
local,
|
||||
remote
|
||||
}
|
||||
|
||||
// TODO: 435619 Add default endpoints per cloud and use regional only when available
|
||||
export class CassandraBackend {
|
||||
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
||||
public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete";
|
||||
public static readonly queryApi: string = "api/cassandra";
|
||||
public static readonly guestQueryApi: string = "api/guest/cassandra";
|
||||
public static readonly keysApi: string = "api/cassandra/keys";
|
||||
public static readonly guestKeysApi: string = "api/guest/cassandra/keys";
|
||||
public static readonly schemaApi: string = "api/cassandra/schema";
|
||||
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
|
||||
}
|
||||
|
||||
export class Queries {
|
||||
public static CustomPageOption: string = "custom";
|
||||
public static UnlimitedPageOption: string = "unlimited";
|
||||
public static itemsPerPage: number = 100;
|
||||
public static unlimitedItemsPerPage: number = 100; // TODO: Figure out appropriate value so it works for accounts with a large number of partitions
|
||||
|
||||
public static QueryEditorMinHeightRatio: number = 0.1;
|
||||
public static QueryEditorMaxHeightRatio: number = 0.4;
|
||||
public static readonly DefaultMaxDegreeOfParallelism = 6;
|
||||
}
|
||||
|
||||
export class SavedQueries {
|
||||
public static readonly CollectionName: string = "___Query";
|
||||
public static readonly DatabaseName: string = "___Cosmos";
|
||||
public static readonly OfferThroughput: number = 400;
|
||||
public static readonly PartitionKeyProperty: string = "id";
|
||||
}
|
||||
|
||||
export class DocumentsGridMetrics {
|
||||
public static DocumentsPerPage: number = 100;
|
||||
public static IndividualRowHeight: number = 34;
|
||||
public static BufferHeight: number = 28;
|
||||
public static SplitterMinWidth: number = 200;
|
||||
public static SplitterMaxWidth: number = 360;
|
||||
|
||||
public static DocumentEditorMinWidthRatio: number = 0.2;
|
||||
public static DocumentEditorMaxWidthRatio: number = 0.4;
|
||||
}
|
||||
|
||||
export class ExplorerMetrics {
|
||||
public static SplitterMinWidth: number = 240;
|
||||
public static SplitterMaxWidth: number = 400;
|
||||
public static CollapsedResourceTreeWidth: number = 36;
|
||||
}
|
||||
|
||||
export class SplitterMetrics {
|
||||
public static CollapsedPositionLeft: number = ExplorerMetrics.CollapsedResourceTreeWidth;
|
||||
}
|
||||
|
||||
export class Areas {
|
||||
public static ResourceTree: string = "Resource Tree";
|
||||
public static ContextualPane: string = "Contextual Pane";
|
||||
public static Tab: string = "Tab";
|
||||
public static ShareDialog: string = "Share Access Dialog";
|
||||
public static Notebook: string = "Notebook";
|
||||
}
|
||||
|
||||
export class HttpHeaders {
|
||||
public static activityId: string = "x-ms-activity-id";
|
||||
public static apiType: string = "x-ms-cosmos-apitype";
|
||||
public static authorization: string = "authorization";
|
||||
public static collectionIndexTransformationProgress: string =
|
||||
"x-ms-documentdb-collection-index-transformation-progress";
|
||||
public static continuation: string = "x-ms-continuation";
|
||||
public static correlationRequestId: string = "x-ms-correlation-request-id";
|
||||
public static enableScriptLogging: string = "x-ms-documentdb-script-enable-logging";
|
||||
public static guestAccessToken: string = "x-ms-encrypted-auth-token";
|
||||
public static getReadOnlyKey: string = "x-ms-get-read-only-key";
|
||||
public static connectionString: string = "x-ms-connection-string";
|
||||
public static msDate: string = "x-ms-date";
|
||||
public static location: string = "Location";
|
||||
public static contentType: string = "Content-Type";
|
||||
public static offerReplacePending: string = "x-ms-offer-replace-pending";
|
||||
public static user: string = "x-ms-user";
|
||||
public static populatePartitionStatistics: string = "x-ms-documentdb-populatepartitionstatistics";
|
||||
public static queryMetrics: string = "x-ms-documentdb-query-metrics";
|
||||
public static requestCharge: string = "x-ms-request-charge";
|
||||
public static resourceQuota: string = "x-ms-resource-quota";
|
||||
public static resourceUsage: string = "x-ms-resource-usage";
|
||||
public static retryAfterMs: string = "x-ms-retry-after-ms";
|
||||
public static scriptLogResults: string = "x-ms-documentdb-script-log-results";
|
||||
public static populateCollectionThroughputInfo = "x-ms-documentdb-populatecollectionthroughputinfo";
|
||||
public static supportSpatialLegacyCoordinates = "x-ms-documentdb-supportspatiallegacycoordinates";
|
||||
public static usePolygonsSmallerThanAHemisphere = "x-ms-documentdb-usepolygonssmallerthanahemisphere";
|
||||
public static autoPilotThroughput = "autoscaleSettings";
|
||||
public static autoPilotThroughputSDK = "x-ms-cosmos-offer-autopilot-settings";
|
||||
public static partitionKey: string = "x-ms-documentdb-partitionkey";
|
||||
public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput";
|
||||
public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot";
|
||||
}
|
||||
|
||||
export class ApiType {
|
||||
// Mapped to hexadecimal values in the backend
|
||||
public static readonly MongoDB: number = 1;
|
||||
public static readonly Gremlin: number = 2;
|
||||
public static readonly Cassandra: number = 4;
|
||||
public static readonly Table: number = 8;
|
||||
public static readonly SQL: number = 16;
|
||||
}
|
||||
|
||||
export class HttpStatusCodes {
|
||||
public static readonly OK: number = 200;
|
||||
public static readonly Created: number = 201;
|
||||
public static readonly Accepted: number = 202;
|
||||
public static readonly NoContent: number = 204;
|
||||
public static readonly NotModified: number = 304;
|
||||
public static readonly Unauthorized: number = 401;
|
||||
public static readonly Forbidden: number = 403;
|
||||
public static readonly NotFound: number = 404;
|
||||
public static readonly TooManyRequests: number = 429;
|
||||
public static readonly Conflict: number = 409;
|
||||
|
||||
public static readonly InternalServerError: number = 500;
|
||||
public static readonly BadGateway: number = 502;
|
||||
public static readonly ServiceUnavailable: number = 503;
|
||||
public static readonly GatewayTimeout: number = 504;
|
||||
|
||||
public static readonly RetryableStatusCodes: number[] = [
|
||||
HttpStatusCodes.TooManyRequests,
|
||||
HttpStatusCodes.InternalServerError, // TODO: Handle all 500s on Portal backend and remove from retries list
|
||||
HttpStatusCodes.BadGateway,
|
||||
HttpStatusCodes.ServiceUnavailable,
|
||||
HttpStatusCodes.GatewayTimeout
|
||||
];
|
||||
}
|
||||
|
||||
export class Urls {
|
||||
public static feedbackEmail = "https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Data%20Explorer%20Feedback";
|
||||
public static autoscaleMigration = "https://aka.ms/cosmos-autoscale-migration";
|
||||
public static freeTierInformation = "https://aka.ms/cosmos-free-tier";
|
||||
public static cosmosPricing = "https://aka.ms/azure-cosmos-db-pricing";
|
||||
}
|
||||
|
||||
export class HashRoutePrefixes {
|
||||
public static databases: string = "/dbs/{db_id}";
|
||||
public static collections: string = "/dbs/{db_id}/colls/{coll_id}";
|
||||
public static sprocHash: string = "/sprocs/";
|
||||
public static sprocs: string = HashRoutePrefixes.collections + HashRoutePrefixes.sprocHash + "{sproc_id}";
|
||||
public static docs: string = HashRoutePrefixes.collections + "/docs/{doc_id}/";
|
||||
public static conflicts: string = HashRoutePrefixes.collections + "/conflicts";
|
||||
|
||||
public static databasesWithId(databaseId: string): string {
|
||||
return this.databases.replace("{db_id}", databaseId).replace("/", ""); // strip the first slash since hasher adds it
|
||||
}
|
||||
|
||||
public static collectionsWithIds(databaseId: string, collectionId: string): string {
|
||||
const transformedDatabasePrefix: string = this.collections.replace("{db_id}", databaseId);
|
||||
|
||||
return transformedDatabasePrefix.replace("{coll_id}", collectionId).replace("/", ""); // strip the first slash since hasher adds it
|
||||
}
|
||||
|
||||
public static sprocWithIds(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
sprocId: string,
|
||||
stripFirstSlash: boolean = true
|
||||
): string {
|
||||
const transformedDatabasePrefix: string = this.sprocs.replace("{db_id}", databaseId);
|
||||
|
||||
const transformedSprocRoute: string = transformedDatabasePrefix
|
||||
.replace("{coll_id}", collectionId)
|
||||
.replace("{sproc_id}", sprocId);
|
||||
if (!!stripFirstSlash) {
|
||||
return transformedSprocRoute.replace("/", ""); // strip the first slash since hasher adds it
|
||||
}
|
||||
|
||||
return transformedSprocRoute;
|
||||
}
|
||||
|
||||
public static conflictsWithIds(databaseId: string, collectionId: string) {
|
||||
const transformedDatabasePrefix: string = this.conflicts.replace("{db_id}", databaseId);
|
||||
|
||||
return transformedDatabasePrefix.replace("{coll_id}", collectionId).replace("/", ""); // strip the first slash since hasher adds it;
|
||||
}
|
||||
|
||||
public static docsWithIds(databaseId: string, collectionId: string, docId: string) {
|
||||
const transformedDatabasePrefix: string = this.docs.replace("{db_id}", databaseId);
|
||||
|
||||
return transformedDatabasePrefix
|
||||
.replace("{coll_id}", collectionId)
|
||||
.replace("{doc_id}", docId)
|
||||
.replace("/", ""); // strip the first slash since hasher adds it
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfigurationOverridesValues {
|
||||
public static IsBsonSchemaV2: string = "true";
|
||||
}
|
||||
|
||||
export class KeyCodes {
|
||||
public static Space: number = 32;
|
||||
public static Enter: number = 13;
|
||||
public static Escape: number = 27;
|
||||
public static UpArrow: number = 38;
|
||||
public static DownArrow: number = 40;
|
||||
public static LeftArrow: number = 37;
|
||||
public static RightArrow: number = 39;
|
||||
public static Tab: number = 9;
|
||||
}
|
||||
|
||||
// Normalized per: https://www.w3.org/TR/uievents-key/#named-key-attribute-values
|
||||
export class NormalizedEventKey {
|
||||
public static readonly Space = " ";
|
||||
public static readonly Enter = "Enter";
|
||||
public static readonly Escape = "Escape";
|
||||
public static readonly UpArrow = "ArrowUp";
|
||||
public static readonly DownArrow = "ArrowDown";
|
||||
public static readonly LeftArrow = "ArrowLeft";
|
||||
public static readonly RightArrow = "ArrowRight";
|
||||
}
|
||||
|
||||
export class TryCosmosExperience {
|
||||
public static extendUrl: string = "https://trycosmosdb.azure.com/api/resource/extendportal?userId={0}";
|
||||
public static deleteUrl: string = "https://trycosmosdb.azure.com/api/resource/deleteportal?userId={0}";
|
||||
public static collectionsPerAccount: number = 3;
|
||||
public static maxRU: number = 5000;
|
||||
public static defaultRU: number = 3000;
|
||||
}
|
||||
|
||||
export class OfferVersions {
|
||||
public static V1: string = "V1";
|
||||
public static V2: string = "V2";
|
||||
}
|
||||
|
||||
export enum ConflictOperationType {
|
||||
Replace = "replace",
|
||||
Create = "create",
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export const EmulatorMasterKey =
|
||||
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
|
||||
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
|
||||
|
||||
// A variable @MyVariable defined in Constants.less is accessible as StyleConstants.MyVariable
|
||||
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
|
||||
|
||||
export class Notebook {
|
||||
public static readonly defaultBasePath = "./notebooks";
|
||||
public static readonly heartbeatDelayMs = 5000;
|
||||
public static readonly kernelRestartInitialDelayMs = 1000;
|
||||
public static readonly kernelRestartMaxDelayMs = 20000;
|
||||
public static readonly autoSaveIntervalMs = 120000;
|
||||
}
|
||||
|
||||
export class SparkLibrary {
|
||||
public static readonly nameMinLength = 3;
|
||||
public static readonly nameMaxLength = 63;
|
||||
}
|
||||
|
||||
export class AnalyticalStorageTtl {
|
||||
public static readonly Days90: number = 7776000;
|
||||
public static readonly Infinite: number = -1;
|
||||
public static readonly Disabled: number = 0;
|
||||
}
|
||||
|
||||
export class TerminalQueryParams {
|
||||
public static readonly Terminal = "terminal";
|
||||
public static readonly Server = "server";
|
||||
public static readonly Token = "token";
|
||||
public static readonly SubscriptionId = "subscriptionId";
|
||||
public static readonly TerminalEndpoint = "terminalEndpoint";
|
||||
}
|
||||
import { HashMap } from "./HashMap";
|
||||
|
||||
export class AuthorizationEndpoints {
|
||||
public static arm: string = "https://management.core.windows.net/";
|
||||
public static common: string = "https://login.windows.net/";
|
||||
}
|
||||
|
||||
export class CodeOfConductEndpoints {
|
||||
public static privacyStatement: string = "https://aka.ms/ms-privacy-policy";
|
||||
public static codeOfConduct: string = "https://aka.ms/cosmos-code-of-conduct";
|
||||
public static termsOfUse: string = "https://aka.ms/ms-terms-of-use";
|
||||
}
|
||||
|
||||
export class EndpointsRegex {
|
||||
public static readonly cassandra = [
|
||||
"AccountEndpoint=(.*).cassandra.cosmosdb.azure.com",
|
||||
"HostName=(.*).cassandra.cosmos.azure.com"
|
||||
];
|
||||
public static readonly mongo = "mongodb://.*:(.*)@(.*).documents.azure.com";
|
||||
public static readonly mongoCompute = "mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com";
|
||||
public static readonly sql = "AccountEndpoint=https://(.*).documents.azure.com";
|
||||
public static readonly table = "TableEndpoint=https://(.*).table.cosmosdb.azure.com";
|
||||
}
|
||||
|
||||
export class ApiEndpoints {
|
||||
public static runtimeProxy: string = "/api/RuntimeProxy";
|
||||
public static guestRuntimeProxy: string = "/api/guest/RuntimeProxy";
|
||||
}
|
||||
|
||||
export class ServerIds {
|
||||
public static localhost: string = "localhost";
|
||||
public static blackforest: string = "blackforest";
|
||||
public static fairfax: string = "fairfax";
|
||||
public static mooncake: string = "mooncake";
|
||||
public static productionPortal: string = "prod";
|
||||
public static dev: string = "dev";
|
||||
}
|
||||
|
||||
export class ArmApiVersions {
|
||||
public static readonly documentDB: string = "2015-11-06";
|
||||
public static readonly arcadia: string = "2019-06-01-preview";
|
||||
public static readonly arcadiaLivy: string = "2019-11-01-preview";
|
||||
public static readonly arm: string = "2015-11-01";
|
||||
public static readonly armFeatures: string = "2014-08-01-preview";
|
||||
public static readonly publicVersion = "2020-04-01";
|
||||
}
|
||||
|
||||
export class ArmResourceTypes {
|
||||
public static readonly notebookWorkspaces = "Microsoft.DocumentDB/databaseAccounts/notebookWorkspaces";
|
||||
public static readonly synapseWorkspaces = "Microsoft.Synapse/workspaces";
|
||||
}
|
||||
|
||||
export class BackendDefaults {
|
||||
public static partitionKeyKind: string = "Hash";
|
||||
public static singlePartitionStorageInGb: string = "10";
|
||||
public static multiPartitionStorageInGb: string = "100";
|
||||
public static maxChangeFeedRetentionDuration: number = 10;
|
||||
public static partitionKeyVersion = 2;
|
||||
}
|
||||
|
||||
export class ClientDefaults {
|
||||
public static requestTimeoutMs: number = 60000;
|
||||
public static portalCacheTimeoutMs: number = 10000;
|
||||
public static errorNotificationTimeoutMs: number = 5000;
|
||||
public static copyHelperTimeoutMs: number = 2000;
|
||||
public static waitForDOMElementMs: number = 500;
|
||||
public static cacheBustingTimeoutMs: number =
|
||||
10 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/;
|
||||
public static databaseThroughputIncreaseFactor: number = 100;
|
||||
public static readonly arcadiaTokenRefreshInterval: number =
|
||||
20 /** minutes **/ * 60 /** to seconds **/ * 1000 /** to milliseconds **/;
|
||||
public static readonly arcadiaTokenRefreshIntervalPaddingMs: number = 2000;
|
||||
}
|
||||
|
||||
export class AccountKind {
|
||||
public static DocumentDB: string = "DocumentDB";
|
||||
public static MongoDB: string = "MongoDB";
|
||||
public static Parse: string = "Parse";
|
||||
public static GlobalDocumentDB: string = "GlobalDocumentDB";
|
||||
public static Default: string = AccountKind.DocumentDB;
|
||||
}
|
||||
|
||||
export class CorrelationBackend {
|
||||
public static Url: string = "https://aka.ms/cosmosdbanalytics";
|
||||
}
|
||||
|
||||
export class DefaultAccountExperience {
|
||||
public static DocumentDB: string = "DocumentDB";
|
||||
public static Graph: string = "Graph";
|
||||
public static MongoDB: string = "MongoDB";
|
||||
public static ApiForMongoDB: string = "Azure Cosmos DB for MongoDB API";
|
||||
public static Table: string = "Table";
|
||||
public static Cassandra: string = "Cassandra";
|
||||
public static Default: string = DefaultAccountExperience.DocumentDB;
|
||||
}
|
||||
|
||||
export class CapabilityNames {
|
||||
public static EnableTable: string = "EnableTable";
|
||||
public static EnableGremlin: string = "EnableGremlin";
|
||||
public static EnableCassandra: string = "EnableCassandra";
|
||||
public static EnableAutoScale: string = "EnableAutoScale";
|
||||
public static readonly EnableNotebooks: string = "EnableNotebooks";
|
||||
public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics";
|
||||
public static readonly EnableMongo: string = "EnableMongo";
|
||||
public static readonly EnableServerless: string = "EnableServerless";
|
||||
}
|
||||
|
||||
export class Features {
|
||||
public static readonly cosmosdb = "cosmosdb";
|
||||
public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy";
|
||||
public static readonly executeSproc = "dataexplorerexecutesproc";
|
||||
public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
|
||||
public static readonly enableTtl = "enablettl";
|
||||
public static readonly enableNotebooks = "enablenotebooks";
|
||||
public static readonly enableGalleryPublish = "enablegallerypublish";
|
||||
public static readonly enableLinkInjection = "enablelinkinjection";
|
||||
public static readonly enableSpark = "enablespark";
|
||||
public static readonly livyEndpoint = "livyendpoint";
|
||||
public static readonly notebookServerUrl = "notebookserverurl";
|
||||
public static readonly notebookServerToken = "notebookservertoken";
|
||||
public static readonly notebookBasePath = "notebookbasepath";
|
||||
public static readonly canExceedMaximumValue = "canexceedmaximumvalue";
|
||||
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
|
||||
public static readonly ttl90Days = "ttl90days";
|
||||
public static readonly enableRightPanelV2 = "enablerightpanelv2";
|
||||
public static readonly enableSchema = "enableschema";
|
||||
public static readonly enableSDKoperations = "enablesdkoperations";
|
||||
public static readonly showMinRUSurvey = "showminrusurvey";
|
||||
public static readonly selfServeType = "selfservetype";
|
||||
}
|
||||
|
||||
// flight names returned from the portal are always lowercase
|
||||
export class Flights {
|
||||
public static readonly SettingsV2 = "settingsv2";
|
||||
public static readonly MongoIndexEditor = "mongoindexeditor";
|
||||
public static readonly AutoscaleTest = "autoscaletest";
|
||||
public static readonly MongoIndexing = "mongoindexing";
|
||||
}
|
||||
|
||||
export class AfecFeatures {
|
||||
public static readonly Spark = "spark-public-preview";
|
||||
public static readonly Notebooks = "sparknotebooks-public-preview";
|
||||
public static readonly StorageAnalytics = "storageanalytics-public-preview";
|
||||
}
|
||||
|
||||
export class Spark {
|
||||
public static readonly MaxWorkerCount = 10;
|
||||
public static readonly SKUs: HashMap<string> = new HashMap({
|
||||
"Cosmos.Spark.D1s": "D1s / 1 core / 4GB RAM",
|
||||
"Cosmos.Spark.D2s": "D2s / 2 cores / 8GB RAM",
|
||||
"Cosmos.Spark.D4s": "D4s / 4 cores / 16GB RAM",
|
||||
"Cosmos.Spark.D8s": "D8s / 8 cores / 32GB RAM",
|
||||
"Cosmos.Spark.D16s": "D16s / 16 cores / 64GB RAM",
|
||||
"Cosmos.Spark.D32s": "D32s / 32 cores / 128GB RAM",
|
||||
"Cosmos.Spark.D64s": "D64s / 64 cores / 256GB RAM"
|
||||
});
|
||||
}
|
||||
|
||||
export class TagNames {
|
||||
public static defaultExperience: string = "defaultExperience";
|
||||
}
|
||||
|
||||
export class MongoDBAccounts {
|
||||
public static protocol: string = "https";
|
||||
public static defaultPort: string = "10255";
|
||||
}
|
||||
|
||||
export enum MongoBackendEndpointType {
|
||||
local,
|
||||
remote
|
||||
}
|
||||
|
||||
// TODO: 435619 Add default endpoints per cloud and use regional only when available
|
||||
export class CassandraBackend {
|
||||
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
||||
public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete";
|
||||
public static readonly queryApi: string = "api/cassandra";
|
||||
public static readonly guestQueryApi: string = "api/guest/cassandra";
|
||||
public static readonly keysApi: string = "api/cassandra/keys";
|
||||
public static readonly guestKeysApi: string = "api/guest/cassandra/keys";
|
||||
public static readonly schemaApi: string = "api/cassandra/schema";
|
||||
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
|
||||
}
|
||||
|
||||
export class Queries {
|
||||
public static CustomPageOption: string = "custom";
|
||||
public static UnlimitedPageOption: string = "unlimited";
|
||||
public static itemsPerPage: number = 100;
|
||||
public static unlimitedItemsPerPage: number = 100; // TODO: Figure out appropriate value so it works for accounts with a large number of partitions
|
||||
|
||||
public static QueryEditorMinHeightRatio: number = 0.1;
|
||||
public static QueryEditorMaxHeightRatio: number = 0.4;
|
||||
public static readonly DefaultMaxDegreeOfParallelism = 6;
|
||||
}
|
||||
|
||||
export class SavedQueries {
|
||||
public static readonly CollectionName: string = "___Query";
|
||||
public static readonly DatabaseName: string = "___Cosmos";
|
||||
public static readonly OfferThroughput: number = 400;
|
||||
public static readonly PartitionKeyProperty: string = "id";
|
||||
}
|
||||
|
||||
export class DocumentsGridMetrics {
|
||||
public static DocumentsPerPage: number = 100;
|
||||
public static IndividualRowHeight: number = 34;
|
||||
public static BufferHeight: number = 28;
|
||||
public static SplitterMinWidth: number = 200;
|
||||
public static SplitterMaxWidth: number = 360;
|
||||
|
||||
public static DocumentEditorMinWidthRatio: number = 0.2;
|
||||
public static DocumentEditorMaxWidthRatio: number = 0.4;
|
||||
}
|
||||
|
||||
export class ExplorerMetrics {
|
||||
public static SplitterMinWidth: number = 240;
|
||||
public static SplitterMaxWidth: number = 400;
|
||||
public static CollapsedResourceTreeWidth: number = 36;
|
||||
}
|
||||
|
||||
export class SplitterMetrics {
|
||||
public static CollapsedPositionLeft: number = ExplorerMetrics.CollapsedResourceTreeWidth;
|
||||
}
|
||||
|
||||
export class Areas {
|
||||
public static ResourceTree: string = "Resource Tree";
|
||||
public static ContextualPane: string = "Contextual Pane";
|
||||
public static Tab: string = "Tab";
|
||||
public static ShareDialog: string = "Share Access Dialog";
|
||||
public static Notebook: string = "Notebook";
|
||||
}
|
||||
|
||||
export class HttpHeaders {
|
||||
public static activityId: string = "x-ms-activity-id";
|
||||
public static apiType: string = "x-ms-cosmos-apitype";
|
||||
public static authorization: string = "authorization";
|
||||
public static collectionIndexTransformationProgress: string =
|
||||
"x-ms-documentdb-collection-index-transformation-progress";
|
||||
public static continuation: string = "x-ms-continuation";
|
||||
public static correlationRequestId: string = "x-ms-correlation-request-id";
|
||||
public static enableScriptLogging: string = "x-ms-documentdb-script-enable-logging";
|
||||
public static guestAccessToken: string = "x-ms-encrypted-auth-token";
|
||||
public static getReadOnlyKey: string = "x-ms-get-read-only-key";
|
||||
public static connectionString: string = "x-ms-connection-string";
|
||||
public static msDate: string = "x-ms-date";
|
||||
public static location: string = "Location";
|
||||
public static contentType: string = "Content-Type";
|
||||
public static offerReplacePending: string = "x-ms-offer-replace-pending";
|
||||
public static user: string = "x-ms-user";
|
||||
public static populatePartitionStatistics: string = "x-ms-documentdb-populatepartitionstatistics";
|
||||
public static queryMetrics: string = "x-ms-documentdb-query-metrics";
|
||||
public static requestCharge: string = "x-ms-request-charge";
|
||||
public static resourceQuota: string = "x-ms-resource-quota";
|
||||
public static resourceUsage: string = "x-ms-resource-usage";
|
||||
public static retryAfterMs: string = "x-ms-retry-after-ms";
|
||||
public static scriptLogResults: string = "x-ms-documentdb-script-log-results";
|
||||
public static populateCollectionThroughputInfo = "x-ms-documentdb-populatecollectionthroughputinfo";
|
||||
public static supportSpatialLegacyCoordinates = "x-ms-documentdb-supportspatiallegacycoordinates";
|
||||
public static usePolygonsSmallerThanAHemisphere = "x-ms-documentdb-usepolygonssmallerthanahemisphere";
|
||||
public static autoPilotThroughput = "autoscaleSettings";
|
||||
public static autoPilotThroughputSDK = "x-ms-cosmos-offer-autopilot-settings";
|
||||
public static partitionKey: string = "x-ms-documentdb-partitionkey";
|
||||
public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput";
|
||||
public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot";
|
||||
}
|
||||
|
||||
export class ApiType {
|
||||
// Mapped to hexadecimal values in the backend
|
||||
public static readonly MongoDB: number = 1;
|
||||
public static readonly Gremlin: number = 2;
|
||||
public static readonly Cassandra: number = 4;
|
||||
public static readonly Table: number = 8;
|
||||
public static readonly SQL: number = 16;
|
||||
}
|
||||
|
||||
export class HttpStatusCodes {
|
||||
public static readonly OK: number = 200;
|
||||
public static readonly Created: number = 201;
|
||||
public static readonly Accepted: number = 202;
|
||||
public static readonly NoContent: number = 204;
|
||||
public static readonly NotModified: number = 304;
|
||||
public static readonly Unauthorized: number = 401;
|
||||
public static readonly Forbidden: number = 403;
|
||||
public static readonly NotFound: number = 404;
|
||||
public static readonly TooManyRequests: number = 429;
|
||||
public static readonly Conflict: number = 409;
|
||||
|
||||
public static readonly InternalServerError: number = 500;
|
||||
public static readonly BadGateway: number = 502;
|
||||
public static readonly ServiceUnavailable: number = 503;
|
||||
public static readonly GatewayTimeout: number = 504;
|
||||
|
||||
public static readonly RetryableStatusCodes: number[] = [
|
||||
HttpStatusCodes.TooManyRequests,
|
||||
HttpStatusCodes.InternalServerError, // TODO: Handle all 500s on Portal backend and remove from retries list
|
||||
HttpStatusCodes.BadGateway,
|
||||
HttpStatusCodes.ServiceUnavailable,
|
||||
HttpStatusCodes.GatewayTimeout
|
||||
];
|
||||
}
|
||||
|
||||
export class Urls {
|
||||
public static feedbackEmail = "https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Data%20Explorer%20Feedback";
|
||||
public static autoscaleMigration = "https://aka.ms/cosmos-autoscale-migration";
|
||||
public static freeTierInformation = "https://aka.ms/cosmos-free-tier";
|
||||
public static cosmosPricing = "https://aka.ms/azure-cosmos-db-pricing";
|
||||
}
|
||||
|
||||
export class HashRoutePrefixes {
|
||||
public static databases: string = "/dbs/{db_id}";
|
||||
public static collections: string = "/dbs/{db_id}/colls/{coll_id}";
|
||||
public static sprocHash: string = "/sprocs/";
|
||||
public static sprocs: string = HashRoutePrefixes.collections + HashRoutePrefixes.sprocHash + "{sproc_id}";
|
||||
public static docs: string = HashRoutePrefixes.collections + "/docs/{doc_id}/";
|
||||
public static conflicts: string = HashRoutePrefixes.collections + "/conflicts";
|
||||
|
||||
public static databasesWithId(databaseId: string): string {
|
||||
return this.databases.replace("{db_id}", databaseId).replace("/", ""); // strip the first slash since hasher adds it
|
||||
}
|
||||
|
||||
public static collectionsWithIds(databaseId: string, collectionId: string): string {
|
||||
const transformedDatabasePrefix: string = this.collections.replace("{db_id}", databaseId);
|
||||
|
||||
return transformedDatabasePrefix.replace("{coll_id}", collectionId).replace("/", ""); // strip the first slash since hasher adds it
|
||||
}
|
||||
|
||||
public static sprocWithIds(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
sprocId: string,
|
||||
stripFirstSlash: boolean = true
|
||||
): string {
|
||||
const transformedDatabasePrefix: string = this.sprocs.replace("{db_id}", databaseId);
|
||||
|
||||
const transformedSprocRoute: string = transformedDatabasePrefix
|
||||
.replace("{coll_id}", collectionId)
|
||||
.replace("{sproc_id}", sprocId);
|
||||
if (!!stripFirstSlash) {
|
||||
return transformedSprocRoute.replace("/", ""); // strip the first slash since hasher adds it
|
||||
}
|
||||
|
||||
return transformedSprocRoute;
|
||||
}
|
||||
|
||||
public static conflictsWithIds(databaseId: string, collectionId: string) {
|
||||
const transformedDatabasePrefix: string = this.conflicts.replace("{db_id}", databaseId);
|
||||
|
||||
return transformedDatabasePrefix.replace("{coll_id}", collectionId).replace("/", ""); // strip the first slash since hasher adds it;
|
||||
}
|
||||
|
||||
public static docsWithIds(databaseId: string, collectionId: string, docId: string) {
|
||||
const transformedDatabasePrefix: string = this.docs.replace("{db_id}", databaseId);
|
||||
|
||||
return transformedDatabasePrefix
|
||||
.replace("{coll_id}", collectionId)
|
||||
.replace("{doc_id}", docId)
|
||||
.replace("/", ""); // strip the first slash since hasher adds it
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfigurationOverridesValues {
|
||||
public static IsBsonSchemaV2: string = "true";
|
||||
}
|
||||
|
||||
export class KeyCodes {
|
||||
public static Space: number = 32;
|
||||
public static Enter: number = 13;
|
||||
public static Escape: number = 27;
|
||||
public static UpArrow: number = 38;
|
||||
public static DownArrow: number = 40;
|
||||
public static LeftArrow: number = 37;
|
||||
public static RightArrow: number = 39;
|
||||
public static Tab: number = 9;
|
||||
}
|
||||
|
||||
// Normalized per: https://www.w3.org/TR/uievents-key/#named-key-attribute-values
|
||||
export class NormalizedEventKey {
|
||||
public static readonly Space = " ";
|
||||
public static readonly Enter = "Enter";
|
||||
public static readonly Escape = "Escape";
|
||||
public static readonly UpArrow = "ArrowUp";
|
||||
public static readonly DownArrow = "ArrowDown";
|
||||
public static readonly LeftArrow = "ArrowLeft";
|
||||
public static readonly RightArrow = "ArrowRight";
|
||||
}
|
||||
|
||||
export class TryCosmosExperience {
|
||||
public static extendUrl: string = "https://trycosmosdb.azure.com/api/resource/extendportal?userId={0}";
|
||||
public static deleteUrl: string = "https://trycosmosdb.azure.com/api/resource/deleteportal?userId={0}";
|
||||
public static collectionsPerAccount: number = 3;
|
||||
public static maxRU: number = 5000;
|
||||
public static defaultRU: number = 3000;
|
||||
}
|
||||
|
||||
export class OfferVersions {
|
||||
public static V1: string = "V1";
|
||||
public static V2: string = "V2";
|
||||
}
|
||||
|
||||
export enum ConflictOperationType {
|
||||
Replace = "replace",
|
||||
Create = "create",
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export const EmulatorMasterKey =
|
||||
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
|
||||
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
|
||||
|
||||
// A variable @MyVariable defined in Constants.less is accessible as StyleConstants.MyVariable
|
||||
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
|
||||
|
||||
export class Notebook {
|
||||
public static readonly defaultBasePath = "./notebooks";
|
||||
public static readonly heartbeatDelayMs = 5000;
|
||||
public static readonly kernelRestartInitialDelayMs = 1000;
|
||||
public static readonly kernelRestartMaxDelayMs = 20000;
|
||||
public static readonly autoSaveIntervalMs = 120000;
|
||||
}
|
||||
|
||||
export class SparkLibrary {
|
||||
public static readonly nameMinLength = 3;
|
||||
public static readonly nameMaxLength = 63;
|
||||
}
|
||||
|
||||
export class AnalyticalStorageTtl {
|
||||
public static readonly Days90: number = 7776000;
|
||||
public static readonly Infinite: number = -1;
|
||||
public static readonly Disabled: number = 0;
|
||||
}
|
||||
|
||||
export class TerminalQueryParams {
|
||||
public static readonly Terminal = "terminal";
|
||||
public static readonly Server = "server";
|
||||
public static readonly Token = "token";
|
||||
public static readonly SubscriptionId = "subscriptionId";
|
||||
public static readonly TerminalEndpoint = "terminalEndpoint";
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export const handleError = (error: string | ARMError | Error, area: string, cons
|
||||
sendNotificationForError(errorMessage, errorCode);
|
||||
};
|
||||
|
||||
export const getErrorMessage = (error: string | Error): string => {
|
||||
export const getErrorMessage = (error: string | Error = ""): string => {
|
||||
const errorMessage = typeof error === "string" ? error : error.message;
|
||||
return replaceKnownError(errorMessage);
|
||||
};
|
||||
@@ -45,10 +45,10 @@ const sendNotificationForError = (errorMessage: string, errorCode: number | stri
|
||||
const replaceKnownError = (errorMessage: string): string => {
|
||||
if (
|
||||
window.dataExplorer?.subscriptionType() === SubscriptionType.Internal &&
|
||||
errorMessage.indexOf("SharedOffer is Disabled for your account") >= 0
|
||||
errorMessage?.indexOf("SharedOffer is Disabled for your account") >= 0
|
||||
) {
|
||||
return "Database throughput is not supported for internal subscriptions.";
|
||||
} else if (errorMessage.indexOf("Partition key paths must contain only valid") >= 0) {
|
||||
} else if (errorMessage?.indexOf("Partition key paths must contain only valid") >= 0) {
|
||||
return "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character.";
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,11 @@ import { Offer, SDKOfferDefinition } from "../Contracts/DataModels";
|
||||
import { OfferResponse } from "@azure/cosmos";
|
||||
import { HttpHeaders } from "./Constants";
|
||||
|
||||
export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => {
|
||||
const offerDefinition: SDKOfferDefinition = offerResponse?.resource;
|
||||
export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer | undefined => {
|
||||
const offerDefinition: SDKOfferDefinition | undefined = offerResponse?.resource;
|
||||
if (!offerDefinition) {
|
||||
return undefined;
|
||||
}
|
||||
const offerContent = offerDefinition.content;
|
||||
if (!offerContent) {
|
||||
return undefined;
|
||||
@@ -12,7 +15,7 @@ export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => {
|
||||
const minimumThroughput = offerContent.collectionThroughputInfo?.minimumRUForCollection;
|
||||
const autopilotSettings = offerContent.offerAutopilotSettings;
|
||||
|
||||
if (autopilotSettings) {
|
||||
if (autopilotSettings && autopilotSettings.maxThroughput && minimumThroughput) {
|
||||
return {
|
||||
id: offerDefinition.id,
|
||||
autoscaleMaxThroughput: autopilotSettings.maxThroughput,
|
||||
|
||||
@@ -23,10 +23,10 @@ export class Splitter {
|
||||
public splitterId: string;
|
||||
public leftSideId: string;
|
||||
|
||||
public splitter: HTMLElement;
|
||||
public leftSide: HTMLElement;
|
||||
public lastX: number;
|
||||
public lastWidth: number;
|
||||
public splitter!: HTMLElement;
|
||||
public leftSide!: HTMLElement;
|
||||
public lastX!: number;
|
||||
public lastWidth!: number;
|
||||
|
||||
private isCollapsed: ko.Observable<boolean>;
|
||||
private bounds: SplitterBounds;
|
||||
@@ -42,9 +42,10 @@ export class Splitter {
|
||||
}
|
||||
|
||||
public initialize() {
|
||||
this.splitter = document.getElementById(this.splitterId);
|
||||
this.leftSide = document.getElementById(this.leftSideId);
|
||||
|
||||
if (document.getElementById(this.splitterId) !== null && document.getElementById(this.leftSideId) != null) {
|
||||
this.splitter = <HTMLElement>document.getElementById(this.splitterId);
|
||||
this.leftSide = <HTMLElement>document.getElementById(this.leftSideId);
|
||||
}
|
||||
const isVerticalSplitter: boolean = this.direction === SplitterDirection.Vertical;
|
||||
const splitterOptions: JQueryUI.ResizableOptions = {
|
||||
animate: true,
|
||||
|
||||
@@ -210,9 +210,9 @@ export interface QueryMetrics {
|
||||
|
||||
export interface Offer {
|
||||
id: string;
|
||||
autoscaleMaxThroughput: number;
|
||||
manualThroughput: number;
|
||||
minimumThroughput: number;
|
||||
autoscaleMaxThroughput: number | undefined;
|
||||
manualThroughput: number | undefined;
|
||||
minimumThroughput: number | undefined;
|
||||
offerDefinition?: SDKOfferDefinition;
|
||||
offerReplacePending: boolean;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
||||
import Trigger from "../Explorer/Tree/Trigger";
|
||||
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
|
||||
import { SelfServeType } from "../SelfServe/SelfServeUtils";
|
||||
import { UploadDetails } from "../workers/upload/definitions";
|
||||
import * as DataModels from "./DataModels";
|
||||
import { SubscriptionType } from "./SubscriptionType";
|
||||
@@ -395,6 +396,7 @@ export interface DataExplorerInputsFrame {
|
||||
isAuthWithresourceToken?: boolean;
|
||||
defaultCollectionThroughput?: CollectionCreationDefaults;
|
||||
flights?: readonly string[];
|
||||
selfServeType?: SelfServeType;
|
||||
}
|
||||
|
||||
export interface CollectionCreationDefaults {
|
||||
|
||||
@@ -42,7 +42,7 @@ interface CollapsiblePanelParams {
|
||||
* Use the optional "collapseToLeft" parameter to collapse to the left.
|
||||
*/
|
||||
class CollapsiblePanelViewModel {
|
||||
private params: CollapsiblePanelParams;
|
||||
public params: CollapsiblePanelParams;
|
||||
private isCollapsed: ko.Observable<boolean>;
|
||||
|
||||
public constructor(params: CollapsiblePanelParams) {
|
||||
@@ -50,7 +50,7 @@ class CollapsiblePanelViewModel {
|
||||
this.isCollapsed = params.isCollapsed || ko.observable(false);
|
||||
}
|
||||
|
||||
private toggleCollapse(): void {
|
||||
public toggleCollapse(): void {
|
||||
this.isCollapsed(!this.isCollapsed());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
||||
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
|
||||
{ key: "feature.enablettl", label: "Enable TTL", value: "true" },
|
||||
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" },
|
||||
{ key: "feature.selfServeType", label: "Self serve feature", value: "sample" },
|
||||
{
|
||||
key: "feature.enableLinkInjection",
|
||||
label: "Enable Injecting Notebook Viewer Link into the first cell",
|
||||
|
||||
@@ -157,14 +157,14 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enableLinkInjection"
|
||||
label="Enable Injecting Notebook Viewer Link into the first cell"
|
||||
key="feature.selfServeType"
|
||||
label="Self serve feature"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.canexceedmaximumvalue"
|
||||
label="Can exceed max value"
|
||||
key="feature.enableLinkInjection"
|
||||
label="Enable Injecting Notebook Viewer Link into the first cell"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
@@ -172,6 +172,12 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
className="checkboxRow"
|
||||
horizontalAlign="space-between"
|
||||
>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.canexceedmaximumvalue"
|
||||
label="Can exceed max value"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enablefixedcollectionwithsharedthroughput"
|
||||
|
||||
@@ -71,7 +71,7 @@ interface InputTypeaheadParams {
|
||||
/**
|
||||
* This function gets called when pressing ENTER on the input box
|
||||
*/
|
||||
submitFct?: (inputValue: string, selection: Item) => void;
|
||||
submitFct?: (inputValue: string | null, selection: Item | null) => void;
|
||||
|
||||
/**
|
||||
* Typehead comes with a Search button that we normally remove.
|
||||
@@ -88,8 +88,8 @@ interface OnClickItem {
|
||||
}
|
||||
|
||||
interface Cache {
|
||||
inputValue: string;
|
||||
selection: Item;
|
||||
inputValue: string | null;
|
||||
selection: Item | null;
|
||||
}
|
||||
|
||||
class InputTypeaheadViewModel {
|
||||
@@ -98,15 +98,12 @@ class InputTypeaheadViewModel {
|
||||
private params: InputTypeaheadParams;
|
||||
|
||||
private cache: Cache;
|
||||
private inputValue: string;
|
||||
private selection: Item;
|
||||
|
||||
public constructor(params: InputTypeaheadParams) {
|
||||
this.instanceNumber = InputTypeaheadViewModel.instanceCount++;
|
||||
this.params = params;
|
||||
|
||||
this.params.choices.subscribe(this.initializeTypeahead.bind(this));
|
||||
|
||||
this.cache = {
|
||||
inputValue: null,
|
||||
selection: null
|
||||
@@ -161,7 +158,7 @@ class InputTypeaheadViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
$.typeahead(options);
|
||||
($ as any).typeahead(options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -177,11 +174,11 @@ class InputTypeaheadViewModel {
|
||||
* Use ko's "template: afterRender" callback to do that without actually using any template.
|
||||
* Another way is to call it within setTimeout() in constructor.
|
||||
*/
|
||||
private afterRender(): void {
|
||||
public afterRender(): void {
|
||||
this.initializeTypeahead();
|
||||
}
|
||||
|
||||
private submit(): void {
|
||||
public submit(): void {
|
||||
if (this.params.submitFct) {
|
||||
this.params.submitFct(this.cache.inputValue, this.cache.selection);
|
||||
}
|
||||
|
||||
@@ -59,10 +59,12 @@ export class JsonEditorViewModel extends WaitsForTemplateViewModel {
|
||||
this.params = params;
|
||||
|
||||
this.params.content.subscribe((newValue: string) => {
|
||||
if (!!this.editor) {
|
||||
this.editor.getModel().setValue(newValue);
|
||||
} else {
|
||||
this.createEditor(newValue, this.configureEditor.bind(this));
|
||||
if (newValue) {
|
||||
if (!!this.editor) {
|
||||
this.editor.getModel().setValue(newValue);
|
||||
} else {
|
||||
this.createEditor(newValue, this.configureEditor.bind(this));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ describe("SettingsComponent", () => {
|
||||
|
||||
it("getUpdatedConflictResolutionPolicy", () => {
|
||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
||||
const conflictResolutionPolicyPath = "_ts";
|
||||
const conflictResolutionPolicyPath = "/_ts";
|
||||
const conflictResolutionPolicyProcedure = "sample_sproc";
|
||||
const expectSprocPath =
|
||||
"/dbs/" + collection.databaseId + "/colls/" + collection.id() + "/sprocs/" + conflictResolutionPolicyProcedure;
|
||||
|
||||
@@ -138,8 +138,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
|
||||
// Mongo container with system partition key still treat as "Fixed"
|
||||
this.isFixedContainer =
|
||||
!this.collection.partitionKey ||
|
||||
(this.container.isPreferredApiMongoDB() && this.collection.partitionKey.systemKey);
|
||||
this.container.isPreferredApiMongoDB() &&
|
||||
(!this.collection.partitionKey || this.collection.partitionKey.systemKey);
|
||||
|
||||
this.state = {
|
||||
throughput: undefined,
|
||||
@@ -684,7 +684,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
|
||||
if (policy.mode === DataModels.ConflictResolutionMode.LastWriterWins) {
|
||||
policy.conflictResolutionPath = this.state.conflictResolutionPolicyPath;
|
||||
if (policy.conflictResolutionPath?.startsWith("/")) {
|
||||
if (!policy.conflictResolutionPath?.startsWith("/")) {
|
||||
policy.conflictResolutionPath = "/" + policy.conflictResolutionPath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export interface PriceBreakdown {
|
||||
currencySign: string;
|
||||
}
|
||||
|
||||
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } };
|
||||
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14 } };
|
||||
|
||||
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
||||
label: {
|
||||
@@ -166,7 +166,10 @@ export const separatorStyles: Partial<ISeparatorStyles> = {
|
||||
]
|
||||
};
|
||||
|
||||
export const messageBarStyles: Partial<IMessageBarStyles> = { root: { marginTop: "5px" } };
|
||||
export const messageBarStyles: Partial<IMessageBarStyles> = {
|
||||
root: { marginTop: "5px", backgroundColor: "white" },
|
||||
text: { fontSize: 14 }
|
||||
};
|
||||
|
||||
export const throughputUnit = "RU/s";
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ exports[`IndexingPolicyRefreshComponent renders 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from "../SettingsRenderUtils";
|
||||
import { hasDatabaseSharedThroughput } from "../SettingsUtils";
|
||||
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
|
||||
import { Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
|
||||
import { Link, Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
|
||||
import { configContext, Platform } from "../../../../ConfigContext";
|
||||
|
||||
export interface ScaleComponentProps {
|
||||
@@ -165,6 +165,8 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
private getThroughputInputComponent = (): JSX.Element => (
|
||||
<ThroughputInputAutoPilotV3Component
|
||||
databaseAccount={this.props.container.databaseAccount()}
|
||||
databaseName={this.props.collection.databaseId}
|
||||
collectionName={this.props.collection.id()}
|
||||
serverId={this.props.container.serverId()}
|
||||
throughput={this.props.throughput}
|
||||
throughputBaseline={this.props.throughputBaseline}
|
||||
@@ -176,6 +178,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
label={this.getThroughputTitle()}
|
||||
isEmulator={this.isEmulator}
|
||||
isFixed={this.props.isFixedContainer}
|
||||
isFreeTierAccount={this.isFreeTierAccount()}
|
||||
isAutoPilotSelected={this.props.isAutoPilotSelected}
|
||||
onAutoPilotSelected={this.props.onAutoPilotSelected}
|
||||
wasAutopilotOriginallySet={this.props.wasAutopilotOriginallySet}
|
||||
@@ -190,9 +193,37 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
/>
|
||||
);
|
||||
|
||||
private isFreeTierAccount(): boolean {
|
||||
const databaseAccount = this.props.container?.databaseAccount();
|
||||
return databaseAccount?.properties?.enableFreeTier;
|
||||
}
|
||||
|
||||
private getFreeTierInfoMessage(): JSX.Element {
|
||||
return (
|
||||
<Text>
|
||||
With free tier, you will get the first 400 RU/s and 5 GB of storage in this account for free. To keep your
|
||||
account free, keep the total RU/s across all resources in the account to 400 RU/s.
|
||||
<Link
|
||||
href="https://docs.microsoft.com/en-us/azure/cosmos-db/understand-your-bill#billing-examples-with-free-tier-accounts"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more.
|
||||
</Link>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<Stack {...subComponentStackProps}>
|
||||
{this.isFreeTierAccount() && (
|
||||
<MessageBar
|
||||
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
|
||||
styles={{ text: { fontSize: 14 } }}
|
||||
>
|
||||
{this.getFreeTierInfoMessage()}
|
||||
</MessageBar>
|
||||
)}
|
||||
{this.getInitialNotificationElement() && (
|
||||
<MessageBar messageBarType={MessageBarType.warning}>{this.getInitialNotificationElement()}</MessageBar>
|
||||
)}
|
||||
|
||||
@@ -13,16 +13,7 @@ import {
|
||||
} from "../SettingsUtils";
|
||||
import Explorer from "../../../Explorer";
|
||||
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||
import {
|
||||
Label,
|
||||
Text,
|
||||
TextField,
|
||||
Stack,
|
||||
IChoiceGroupOption,
|
||||
ChoiceGroup,
|
||||
MessageBar,
|
||||
MessageBarType
|
||||
} from "office-ui-fabric-react";
|
||||
import { Label, Text, TextField, Stack, IChoiceGroupOption, ChoiceGroup, MessageBar } from "office-ui-fabric-react";
|
||||
import {
|
||||
getTextFieldStyles,
|
||||
changeFeedPolicyToolTip,
|
||||
@@ -190,7 +181,10 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
styles={getChoiceGroupStyles(this.props.timeToLive, this.props.timeToLiveBaseline)}
|
||||
/>
|
||||
{isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && (
|
||||
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
|
||||
<MessageBar
|
||||
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
|
||||
styles={messageBarStyles}
|
||||
>
|
||||
{ttlWarning}
|
||||
</MessageBar>
|
||||
)}
|
||||
|
||||
@@ -9,6 +9,8 @@ import * as DataModels from "../../../../../Contracts/DataModels";
|
||||
describe("ThroughputInputAutoPilotV3Component", () => {
|
||||
const baseProps: ThroughputInputAutoPilotV3Props = {
|
||||
databaseAccount: {} as DataModels.DatabaseAccount,
|
||||
databaseName: "test",
|
||||
collectionName: "test",
|
||||
serverId: undefined,
|
||||
wasAutopilotOriginallySet: false,
|
||||
throughput: 100,
|
||||
@@ -26,6 +28,7 @@ describe("ThroughputInputAutoPilotV3Component", () => {
|
||||
spendAckVisible: false,
|
||||
showAsMandatory: true,
|
||||
isFixed: false,
|
||||
isFreeTierAccount: false,
|
||||
label: "label",
|
||||
infoBubbleText: "infoBubbleText",
|
||||
canExceedMaximumValue: true,
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
Label,
|
||||
Link,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
FontIcon,
|
||||
IColumn
|
||||
} from "office-ui-fabric-react";
|
||||
@@ -42,8 +41,13 @@ import { SubscriptionType } from "../../../../../Contracts/SubscriptionType";
|
||||
import { usageInGB, calculateEstimateNumber } from "../../../../../Utils/PricingUtils";
|
||||
import { Features } from "../../../../../Common/Constants";
|
||||
|
||||
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants";
|
||||
|
||||
export interface ThroughputInputAutoPilotV3Props {
|
||||
databaseAccount: DataModels.DatabaseAccount;
|
||||
databaseName: string;
|
||||
collectionName: string;
|
||||
serverId: string;
|
||||
throughput: number;
|
||||
throughputBaseline: number;
|
||||
@@ -58,6 +62,7 @@ export interface ThroughputInputAutoPilotV3Props {
|
||||
spendAckVisible?: boolean;
|
||||
showAsMandatory?: boolean;
|
||||
isFixed: boolean;
|
||||
isFreeTierAccount: boolean;
|
||||
isEmulator: boolean;
|
||||
label: string;
|
||||
infoBubbleText?: string;
|
||||
@@ -76,6 +81,7 @@ export interface ThroughputInputAutoPilotV3Props {
|
||||
|
||||
interface ThroughputInputAutoPilotV3State {
|
||||
spendAckChecked: boolean;
|
||||
exceedFreeTierThroughput: boolean;
|
||||
}
|
||||
|
||||
export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
@@ -149,7 +155,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
public constructor(props: ThroughputInputAutoPilotV3Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
spendAckChecked: this.props.spendAckChecked
|
||||
spendAckChecked: this.props.spendAckChecked,
|
||||
exceedFreeTierThroughput:
|
||||
this.props.isFreeTierAccount && !this.props.isAutoPilotSelected && this.props.throughput > 400
|
||||
};
|
||||
|
||||
this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep;
|
||||
@@ -424,7 +432,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => {
|
||||
const newThroughput = getSanitizedInputValue(newValue, this.autoPilotInputMaxValue);
|
||||
const newThroughput = getSanitizedInputValue(newValue);
|
||||
this.props.onMaxAutoPilotThroughputChange(newThroughput);
|
||||
};
|
||||
|
||||
@@ -432,10 +440,11 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => {
|
||||
const newThroughput = getSanitizedInputValue(newValue, this.throughputInputMaxValue);
|
||||
const newThroughput = getSanitizedInputValue(newValue);
|
||||
if (this.overrideWithAutoPilotSettings()) {
|
||||
this.props.onMaxAutoPilotThroughputChange(newThroughput);
|
||||
} else {
|
||||
this.setState({ exceedFreeTierThroughput: this.props.isFreeTierAccount && newThroughput > 400 });
|
||||
this.props.onThroughputChange(newThroughput);
|
||||
}
|
||||
};
|
||||
@@ -443,7 +452,19 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
private onChoiceGroupChange = (
|
||||
event?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
): void => this.props.onAutoPilotSelected(option.key === "true");
|
||||
): void => {
|
||||
this.props.onAutoPilotSelected(option.key === "true");
|
||||
TelemetryProcessor.trace(Action.ToggleAutoscaleSetting, ActionModifiers.Mark, {
|
||||
changedSelectedValueTo:
|
||||
option.key === "true" ? ActionModifiers.ToggleAutoscaleOn : ActionModifiers.ToggleAutoscaleOff,
|
||||
subscriptionId: userContext.subscriptionId,
|
||||
databaseAccountName: this.props.databaseAccount?.name,
|
||||
databaseName: this.props.databaseName,
|
||||
collectionName: this.props.collectionName,
|
||||
apiKind: userContext.defaultExperience,
|
||||
dataExplorerArea: "Scale Tab V2"
|
||||
});
|
||||
};
|
||||
|
||||
private minRUperGBSurvey = (): JSX.Element => {
|
||||
const href = `https://ncv.microsoft.com/vRBTO37jmO?ctx={"AzureSubscriptionId":"${userContext.subscriptionId}","CosmosDBAccountName":"${userContext.databaseAccount?.name}"}`;
|
||||
@@ -479,7 +500,10 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
/>
|
||||
</Label>
|
||||
{this.overrideWithProvisionedThroughputSettings() && (
|
||||
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
|
||||
<MessageBar
|
||||
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
|
||||
styles={messageBarStyles}
|
||||
>
|
||||
{manualToAutoscaleDisclaimerElement}
|
||||
</MessageBar>
|
||||
)}
|
||||
@@ -556,8 +580,21 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
}
|
||||
onChange={this.onThroughputChange}
|
||||
/>
|
||||
{this.state.exceedFreeTierThroughput && (
|
||||
<MessageBar
|
||||
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
|
||||
styles={messageBarStyles}
|
||||
>
|
||||
{
|
||||
"Billing will apply if you provision more than 400 RU/s of manual throughput, or if the resource scales beyond 400 RU/s with autoscale."
|
||||
}
|
||||
</MessageBar>
|
||||
)}
|
||||
{this.props.getThroughputWarningMessage() && (
|
||||
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
|
||||
<MessageBar
|
||||
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
|
||||
styles={messageBarStyles}
|
||||
>
|
||||
{this.props.getThroughputWarningMessage()}
|
||||
</MessageBar>
|
||||
)}
|
||||
@@ -583,7 +620,15 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
warningMessage = saveThroughputWarningMessage;
|
||||
}
|
||||
|
||||
return <>{warningMessage && <MessageBar messageBarType={MessageBarType.warning}>{warningMessage}</MessageBar>}</>;
|
||||
return (
|
||||
<>
|
||||
{warningMessage && (
|
||||
<MessageBar messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}>
|
||||
{warningMessage}
|
||||
</MessageBar>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
|
||||
@@ -9,13 +9,18 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
}
|
||||
>
|
||||
<StyledMessageBarBase
|
||||
messageBarType={5}
|
||||
messageBarIconProps={
|
||||
Object {
|
||||
"className": "messageBarWarningIcon",
|
||||
"iconName": "WarningSolid",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -34,7 +39,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -45,12 +50,21 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
/>
|
||||
</StyledLabelBase>
|
||||
<StyledMessageBarBase
|
||||
messageBarType={5}
|
||||
messageBarIconProps={
|
||||
Object {
|
||||
"className": "messageBarInfoIcon",
|
||||
"iconName": "InfoSolid",
|
||||
}
|
||||
}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"backgroundColor": "white",
|
||||
"marginTop": "5px",
|
||||
},
|
||||
"text": Object {
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -59,7 +73,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -171,7 +185,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -444,7 +458,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,8 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||
>
|
||||
<ThroughputInputAutoPilotV3Component
|
||||
canExceedMaximumValue={true}
|
||||
collectionName="test"
|
||||
databaseName="test"
|
||||
getThroughputWarningMessage={[Function]}
|
||||
isAutoPilotSelected={false}
|
||||
isEmulator={false}
|
||||
|
||||
@@ -136,7 +136,7 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -412,7 +412,7 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -952,7 +952,7 @@ exports[`SubSettingsComponent renders 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1228,7 +1228,7 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,13 +101,13 @@ export const parseConflictResolutionProcedure = (procedureFromBackEnd: string):
|
||||
return procedureFromBackEnd;
|
||||
};
|
||||
|
||||
export const getSanitizedInputValue = (newValueString: string, max: number): number => {
|
||||
export const getSanitizedInputValue = (newValueString: string, max?: number): number => {
|
||||
const newValue = parseInt(newValueString);
|
||||
if (isNaN(newValue)) {
|
||||
return zeroValue;
|
||||
}
|
||||
// make sure new value does not exceed the maximum throughput
|
||||
return Math.min(newValue, max);
|
||||
return max ? Math.min(newValue, max) : newValue;
|
||||
};
|
||||
|
||||
export const isDirty = (current: isDirtyTypes, baseline: isDirtyTypes): boolean => {
|
||||
|
||||
@@ -55,6 +55,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"freeTierExceedThroughputTooltip": [Function],
|
||||
"id": "adddatabasepane",
|
||||
"isAutoPilotSelected": [Function],
|
||||
"isExecuting": [Function],
|
||||
@@ -104,6 +105,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"formWarnings": [Function],
|
||||
"freeTierExceedThroughputTooltip": [Function],
|
||||
"id": "addcollectionpane",
|
||||
"isAnalyticalStorageOn": [Function],
|
||||
"isAutoPilotSelected": [Function],
|
||||
@@ -591,6 +593,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"formWarnings": [Function],
|
||||
"freeTierExceedThroughputTooltip": [Function],
|
||||
"id": "addcollectionpane",
|
||||
"isAnalyticalStorageOn": [Function],
|
||||
"isAutoPilotSelected": [Function],
|
||||
@@ -665,6 +668,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"freeTierExceedThroughputTooltip": [Function],
|
||||
"id": "adddatabasepane",
|
||||
"isAutoPilotSelected": [Function],
|
||||
"isExecuting": [Function],
|
||||
@@ -942,6 +946,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"hasWriteAccess": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isAutoscaleDefaultEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
@@ -950,6 +955,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isLeftPaneExpanded": [Function],
|
||||
"isLinkInjectionEnabled": [Function],
|
||||
"isMongoIndexingEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isNotificationConsoleExpanded": [Function],
|
||||
@@ -1114,6 +1120,14 @@ exports[`SettingsComponent renders 1`] = `
|
||||
},
|
||||
"selectedDatabaseId": [Function],
|
||||
"selectedNode": [Function],
|
||||
"selfServeComponentAdapter": SelfServeComponentAdapter {
|
||||
"container": [Circular],
|
||||
"parameters": [Function],
|
||||
},
|
||||
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
|
||||
"parameters": [Function],
|
||||
},
|
||||
"selfServeType": [Function],
|
||||
"serverId": [Function],
|
||||
"settingsPane": SettingsPane {
|
||||
"container": [Circular],
|
||||
@@ -1175,11 +1189,9 @@ exports[`SettingsComponent renders 1`] = `
|
||||
},
|
||||
"direction": "vertical",
|
||||
"isCollapsed": [Function],
|
||||
"leftSide": null,
|
||||
"leftSideId": "resourcetree",
|
||||
"onResizeStart": [Function],
|
||||
"onResizeStop": [Function],
|
||||
"splitter": null,
|
||||
"splitterId": "h_splitter1",
|
||||
},
|
||||
"stringInputPane": StringInputPane {
|
||||
@@ -1326,6 +1338,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"freeTierExceedThroughputTooltip": [Function],
|
||||
"id": "adddatabasepane",
|
||||
"isAutoPilotSelected": [Function],
|
||||
"isExecuting": [Function],
|
||||
@@ -1375,6 +1388,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"formWarnings": [Function],
|
||||
"freeTierExceedThroughputTooltip": [Function],
|
||||
"id": "addcollectionpane",
|
||||
"isAnalyticalStorageOn": [Function],
|
||||
"isAutoPilotSelected": [Function],
|
||||
@@ -1862,6 +1876,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"formWarnings": [Function],
|
||||
"freeTierExceedThroughputTooltip": [Function],
|
||||
"id": "addcollectionpane",
|
||||
"isAnalyticalStorageOn": [Function],
|
||||
"isAutoPilotSelected": [Function],
|
||||
@@ -1936,6 +1951,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"freeTierExceedThroughputTooltip": [Function],
|
||||
"id": "adddatabasepane",
|
||||
"isAutoPilotSelected": [Function],
|
||||
"isExecuting": [Function],
|
||||
@@ -2213,6 +2229,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"hasWriteAccess": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isAutoscaleDefaultEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
@@ -2221,6 +2238,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isLeftPaneExpanded": [Function],
|
||||
"isLinkInjectionEnabled": [Function],
|
||||
"isMongoIndexingEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isNotificationConsoleExpanded": [Function],
|
||||
@@ -2385,6 +2403,14 @@ exports[`SettingsComponent renders 1`] = `
|
||||
},
|
||||
"selectedDatabaseId": [Function],
|
||||
"selectedNode": [Function],
|
||||
"selfServeComponentAdapter": SelfServeComponentAdapter {
|
||||
"container": [Circular],
|
||||
"parameters": [Function],
|
||||
},
|
||||
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
|
||||
"parameters": [Function],
|
||||
},
|
||||
"selfServeType": [Function],
|
||||
"serverId": [Function],
|
||||
"settingsPane": SettingsPane {
|
||||
"container": [Circular],
|
||||
@@ -2446,11 +2472,9 @@ exports[`SettingsComponent renders 1`] = `
|
||||
},
|
||||
"direction": "vertical",
|
||||
"isCollapsed": [Function],
|
||||
"leftSide": null,
|
||||
"leftSideId": "resourcetree",
|
||||
"onResizeStart": [Function],
|
||||
"onResizeStop": [Function],
|
||||
"splitter": null,
|
||||
"splitterId": "h_splitter1",
|
||||
},
|
||||
"stringInputPane": StringInputPane {
|
||||
@@ -2610,6 +2634,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"freeTierExceedThroughputTooltip": [Function],
|
||||
"id": "adddatabasepane",
|
||||
"isAutoPilotSelected": [Function],
|
||||
"isExecuting": [Function],
|
||||
@@ -2659,6 +2684,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"formWarnings": [Function],
|
||||
"freeTierExceedThroughputTooltip": [Function],
|
||||
"id": "addcollectionpane",
|
||||
"isAnalyticalStorageOn": [Function],
|
||||
"isAutoPilotSelected": [Function],
|
||||
@@ -3146,6 +3172,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"formWarnings": [Function],
|
||||
"freeTierExceedThroughputTooltip": [Function],
|
||||
"id": "addcollectionpane",
|
||||
"isAnalyticalStorageOn": [Function],
|
||||
"isAutoPilotSelected": [Function],
|
||||
@@ -3220,6 +3247,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"freeTierExceedThroughputTooltip": [Function],
|
||||
"id": "adddatabasepane",
|
||||
"isAutoPilotSelected": [Function],
|
||||
"isExecuting": [Function],
|
||||
@@ -3497,6 +3525,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"hasWriteAccess": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isAutoscaleDefaultEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
@@ -3505,6 +3534,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isLeftPaneExpanded": [Function],
|
||||
"isLinkInjectionEnabled": [Function],
|
||||
"isMongoIndexingEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isNotificationConsoleExpanded": [Function],
|
||||
@@ -3669,6 +3699,14 @@ exports[`SettingsComponent renders 1`] = `
|
||||
},
|
||||
"selectedDatabaseId": [Function],
|
||||
"selectedNode": [Function],
|
||||
"selfServeComponentAdapter": SelfServeComponentAdapter {
|
||||
"container": [Circular],
|
||||
"parameters": [Function],
|
||||
},
|
||||
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
|
||||
"parameters": [Function],
|
||||
},
|
||||
"selfServeType": [Function],
|
||||
"serverId": [Function],
|
||||
"settingsPane": SettingsPane {
|
||||
"container": [Circular],
|
||||
@@ -3730,11 +3768,9 @@ exports[`SettingsComponent renders 1`] = `
|
||||
},
|
||||
"direction": "vertical",
|
||||
"isCollapsed": [Function],
|
||||
"leftSide": null,
|
||||
"leftSideId": "resourcetree",
|
||||
"onResizeStart": [Function],
|
||||
"onResizeStop": [Function],
|
||||
"splitter": null,
|
||||
"splitterId": "h_splitter1",
|
||||
},
|
||||
"stringInputPane": StringInputPane {
|
||||
@@ -3881,6 +3917,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"freeTierExceedThroughputTooltip": [Function],
|
||||
"id": "adddatabasepane",
|
||||
"isAutoPilotSelected": [Function],
|
||||
"isExecuting": [Function],
|
||||
@@ -3930,6 +3967,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"formWarnings": [Function],
|
||||
"freeTierExceedThroughputTooltip": [Function],
|
||||
"id": "addcollectionpane",
|
||||
"isAnalyticalStorageOn": [Function],
|
||||
"isAutoPilotSelected": [Function],
|
||||
@@ -4417,6 +4455,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"formWarnings": [Function],
|
||||
"freeTierExceedThroughputTooltip": [Function],
|
||||
"id": "addcollectionpane",
|
||||
"isAnalyticalStorageOn": [Function],
|
||||
"isAutoPilotSelected": [Function],
|
||||
@@ -4491,6 +4530,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"freeTierExceedThroughputTooltip": [Function],
|
||||
"id": "adddatabasepane",
|
||||
"isAutoPilotSelected": [Function],
|
||||
"isExecuting": [Function],
|
||||
@@ -4768,6 +4808,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"hasWriteAccess": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isAutoscaleDefaultEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
@@ -4776,6 +4817,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isLeftPaneExpanded": [Function],
|
||||
"isLinkInjectionEnabled": [Function],
|
||||
"isMongoIndexingEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isNotificationConsoleExpanded": [Function],
|
||||
@@ -4940,6 +4982,14 @@ exports[`SettingsComponent renders 1`] = `
|
||||
},
|
||||
"selectedDatabaseId": [Function],
|
||||
"selectedNode": [Function],
|
||||
"selfServeComponentAdapter": SelfServeComponentAdapter {
|
||||
"container": [Circular],
|
||||
"parameters": [Function],
|
||||
},
|
||||
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
|
||||
"parameters": [Function],
|
||||
},
|
||||
"selfServeType": [Function],
|
||||
"serverId": [Function],
|
||||
"settingsPane": SettingsPane {
|
||||
"container": [Circular],
|
||||
@@ -5001,11 +5051,9 @@ exports[`SettingsComponent renders 1`] = `
|
||||
},
|
||||
"direction": "vertical",
|
||||
"isCollapsed": [Function],
|
||||
"leftSide": null,
|
||||
"leftSideId": "resourcetree",
|
||||
"onResizeStart": [Function],
|
||||
"onResizeStop": [Function],
|
||||
"splitter": null,
|
||||
"splitterId": "h_splitter1",
|
||||
},
|
||||
"stringInputPane": StringInputPane {
|
||||
|
||||
@@ -159,7 +159,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -176,7 +176,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -195,7 +195,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -207,7 +207,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -219,7 +219,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -230,7 +230,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -249,7 +249,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -268,7 +268,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -286,7 +286,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -299,7 +299,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -310,7 +310,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -329,7 +329,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -371,7 +371,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -386,7 +386,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -402,7 +402,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { SmartUiComponent, Descriptor, InputType } from "./SmartUiComponent";
|
||||
import { SmartUiComponent, SmartUiDescriptor, UiType } from "./SmartUiComponent";
|
||||
|
||||
describe("SmartUiComponent", () => {
|
||||
const exampleData: Descriptor = {
|
||||
const exampleData: SmartUiDescriptor = {
|
||||
root: {
|
||||
id: "root",
|
||||
info: {
|
||||
@@ -24,7 +24,7 @@ describe("SmartUiComponent", () => {
|
||||
max: 500,
|
||||
step: 10,
|
||||
defaultValue: 400,
|
||||
inputType: "spin"
|
||||
uiType: UiType.Spinner
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -37,7 +37,21 @@ describe("SmartUiComponent", () => {
|
||||
max: 500,
|
||||
step: 10,
|
||||
defaultValue: 400,
|
||||
inputType: "slider"
|
||||
uiType: UiType.Slider
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "throughput3",
|
||||
input: {
|
||||
label: "Throughput (invalid)",
|
||||
dataFieldName: "throughput3",
|
||||
type: "boolean",
|
||||
min: 400,
|
||||
max: 500,
|
||||
step: 10,
|
||||
defaultValue: 400,
|
||||
uiType: UiType.Spinner,
|
||||
errorMessage: "label, truelabel and falselabel are required for boolean input 'throughput3'"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -64,11 +78,11 @@ describe("SmartUiComponent", () => {
|
||||
input: {
|
||||
label: "Database",
|
||||
dataFieldName: "database",
|
||||
type: "enum",
|
||||
type: "object",
|
||||
choices: [
|
||||
{ label: "Database 1", key: "db1", value: "database1" },
|
||||
{ label: "Database 2", key: "db2", value: "database2" },
|
||||
{ label: "Database 3", key: "db3", value: "database3" }
|
||||
{ label: "Database 1", key: "db1" },
|
||||
{ label: "Database 2", key: "db2" },
|
||||
{ label: "Database 3", key: "db3" }
|
||||
],
|
||||
defaultKey: "db2"
|
||||
}
|
||||
@@ -77,12 +91,11 @@ describe("SmartUiComponent", () => {
|
||||
}
|
||||
};
|
||||
|
||||
const exampleCallbacks = (newValues: Map<string, InputType>): void => {
|
||||
console.log("New values:", newValues);
|
||||
};
|
||||
|
||||
it("should render", () => {
|
||||
const wrapper = shallow(<SmartUiComponent descriptor={exampleData} onChange={exampleCallbacks} />);
|
||||
it("should render", async () => {
|
||||
const wrapper = shallow(
|
||||
<SmartUiComponent descriptor={exampleData} currentValues={new Map()} onInputChange={undefined} />
|
||||
);
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,11 +5,9 @@ import { SpinButton } from "office-ui-fabric-react/lib/SpinButton";
|
||||
import { Dropdown, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
|
||||
import { TextField } from "office-ui-fabric-react/lib/TextField";
|
||||
import { Text } from "office-ui-fabric-react/lib/Text";
|
||||
import { InputType } from "../../Tables/Constants";
|
||||
import { RadioSwitchComponent } from "../RadioSwitchComponent/RadioSwitchComponent";
|
||||
import { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack";
|
||||
import { Link, MessageBar, MessageBarType } from "office-ui-fabric-react";
|
||||
|
||||
import * as InputUtils from "./InputUtils";
|
||||
import "./SmartUiComponent.less";
|
||||
|
||||
@@ -21,45 +19,16 @@ import "./SmartUiComponent.less";
|
||||
* - a descriptor of the UX.
|
||||
*/
|
||||
|
||||
export type InputTypeValue = "number" | "string" | "boolean" | "enum";
|
||||
export type InputTypeValue = "number" | "string" | "boolean" | "object";
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
export type EnumItem = { label: string; key: string; value: any };
|
||||
|
||||
export type InputType = number | string | boolean | EnumItem;
|
||||
|
||||
interface BaseInput {
|
||||
label: string;
|
||||
dataFieldName: string;
|
||||
type: InputTypeValue;
|
||||
placeholder?: string;
|
||||
export enum UiType {
|
||||
Spinner = "Spinner",
|
||||
Slider = "Slider"
|
||||
}
|
||||
|
||||
/**
|
||||
* For now, this only supports integers
|
||||
*/
|
||||
export interface NumberInput extends BaseInput {
|
||||
min?: number;
|
||||
max?: number;
|
||||
step: number;
|
||||
defaultValue: number;
|
||||
inputType: "spin" | "slider";
|
||||
}
|
||||
export type ChoiceItem = { label: string; key: string };
|
||||
|
||||
export interface BooleanInput extends BaseInput {
|
||||
trueLabel: string;
|
||||
falseLabel: string;
|
||||
defaultValue: boolean;
|
||||
}
|
||||
|
||||
export interface StringInput extends BaseInput {
|
||||
defaultValue?: string;
|
||||
}
|
||||
|
||||
export interface EnumInput extends BaseInput {
|
||||
choices: EnumItem[];
|
||||
defaultKey: string;
|
||||
}
|
||||
export type InputType = number | string | boolean | ChoiceItem;
|
||||
|
||||
export interface Info {
|
||||
message: string;
|
||||
@@ -69,28 +38,62 @@ export interface Info {
|
||||
};
|
||||
}
|
||||
|
||||
export type AnyInput = NumberInput | BooleanInput | StringInput | EnumInput;
|
||||
interface BaseInput {
|
||||
label: string;
|
||||
dataFieldName: string;
|
||||
type: InputTypeValue;
|
||||
placeholder?: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
/**
|
||||
* For now, this only supports integers
|
||||
*/
|
||||
interface NumberInput extends BaseInput {
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
defaultValue?: number;
|
||||
uiType: UiType;
|
||||
}
|
||||
|
||||
interface BooleanInput extends BaseInput {
|
||||
trueLabel: string;
|
||||
falseLabel: string;
|
||||
defaultValue?: boolean;
|
||||
}
|
||||
|
||||
interface StringInput extends BaseInput {
|
||||
defaultValue?: string;
|
||||
}
|
||||
|
||||
interface ChoiceInput extends BaseInput {
|
||||
choices: ChoiceItem[];
|
||||
defaultKey?: string;
|
||||
}
|
||||
|
||||
type AnyInput = NumberInput | BooleanInput | StringInput | ChoiceInput;
|
||||
|
||||
interface Node {
|
||||
id: string;
|
||||
info?: Info;
|
||||
input?: AnyInput;
|
||||
children?: Node[];
|
||||
}
|
||||
|
||||
export interface Descriptor {
|
||||
export interface SmartUiDescriptor {
|
||||
root: Node;
|
||||
}
|
||||
|
||||
/************************** Component implementation starts here ************************************* */
|
||||
|
||||
export interface SmartUiComponentProps {
|
||||
descriptor: Descriptor;
|
||||
onChange: (newValues: Map<string, InputType>) => void;
|
||||
descriptor: SmartUiDescriptor;
|
||||
currentValues: Map<string, InputType>;
|
||||
onInputChange: (input: AnyInput, newValue: InputType) => void;
|
||||
}
|
||||
|
||||
interface SmartUiComponentState {
|
||||
currentValues: Map<string, InputType>;
|
||||
errors: Map<string, string>;
|
||||
}
|
||||
|
||||
@@ -104,7 +107,6 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
constructor(props: SmartUiComponentProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
currentValues: new Map(),
|
||||
errors: new Map()
|
||||
};
|
||||
}
|
||||
@@ -113,42 +115,37 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
return (
|
||||
<MessageBar>
|
||||
{info.message}
|
||||
<Link href={info.link.href} target="_blank">
|
||||
{info.link.text}
|
||||
</Link>
|
||||
{info.link && (
|
||||
<Link href={info.link.href} target="_blank">
|
||||
{info.link.text}
|
||||
</Link>
|
||||
)}
|
||||
</MessageBar>
|
||||
);
|
||||
}
|
||||
|
||||
private onInputChange = (newValue: string | number | boolean, dataFieldName: string) => {
|
||||
const { currentValues } = this.state;
|
||||
currentValues.set(dataFieldName, newValue);
|
||||
this.setState({ currentValues }, () => this.props.onChange(this.state.currentValues));
|
||||
};
|
||||
|
||||
private renderStringInput(input: StringInput): JSX.Element {
|
||||
private renderTextInput(input: StringInput): JSX.Element {
|
||||
const value = this.props.currentValues.get(input.dataFieldName) as string;
|
||||
return (
|
||||
<div className="stringInputContainer">
|
||||
<div>
|
||||
<TextField
|
||||
id={`${input.dataFieldName}-input`}
|
||||
label={input.label}
|
||||
type="text"
|
||||
value={input.defaultValue}
|
||||
placeholder={input.placeholder}
|
||||
onChange={(_, newValue) => this.onInputChange(newValue, input.dataFieldName)}
|
||||
styles={{
|
||||
subComponentStyles: {
|
||||
label: {
|
||||
root: {
|
||||
...SmartUiComponent.labelStyle,
|
||||
fontWeight: 600
|
||||
}
|
||||
<TextField
|
||||
id={`${input.dataFieldName}-textBox-input`}
|
||||
label={input.label}
|
||||
type="text"
|
||||
value={value}
|
||||
placeholder={input.placeholder}
|
||||
onChange={(_, newValue) => this.props.onInputChange(input, newValue)}
|
||||
styles={{
|
||||
subComponentStyles: {
|
||||
label: {
|
||||
root: {
|
||||
...SmartUiComponent.labelStyle,
|
||||
fontWeight: 600
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -159,10 +156,11 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
this.setState({ errors });
|
||||
}
|
||||
|
||||
private onValidate = (value: string, min: number, max: number, dataFieldName: string): string => {
|
||||
private onValidate = (input: AnyInput, value: string, min: number, max: number): string => {
|
||||
const newValue = InputUtils.onValidateValueChange(value, min, max);
|
||||
const dataFieldName = input.dataFieldName;
|
||||
if (newValue) {
|
||||
this.onInputChange(newValue, dataFieldName);
|
||||
this.props.onInputChange(input, newValue);
|
||||
this.clearError(dataFieldName);
|
||||
return newValue.toString();
|
||||
} else {
|
||||
@@ -173,20 +171,22 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
return undefined;
|
||||
};
|
||||
|
||||
private onIncrement = (value: string, step: number, max: number, dataFieldName: string): string => {
|
||||
private onIncrement = (input: AnyInput, value: string, step: number, max: number): string => {
|
||||
const newValue = InputUtils.onIncrementValue(value, step, max);
|
||||
const dataFieldName = input.dataFieldName;
|
||||
if (newValue) {
|
||||
this.onInputChange(newValue, dataFieldName);
|
||||
this.props.onInputChange(input, newValue);
|
||||
this.clearError(dataFieldName);
|
||||
return newValue.toString();
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
private onDecrement = (value: string, step: number, min: number, dataFieldName: string): string => {
|
||||
private onDecrement = (input: AnyInput, value: string, step: number, min: number): string => {
|
||||
const newValue = InputUtils.onDecrementValue(value, step, min);
|
||||
const dataFieldName = input.dataFieldName;
|
||||
if (newValue) {
|
||||
this.onInputChange(newValue, dataFieldName);
|
||||
this.props.onInputChange(input, newValue);
|
||||
this.clearError(dataFieldName);
|
||||
return newValue.toString();
|
||||
}
|
||||
@@ -194,18 +194,26 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
};
|
||||
|
||||
private renderNumberInput(input: NumberInput): JSX.Element {
|
||||
const { label, min, max, defaultValue, dataFieldName, step } = input;
|
||||
const props = { label, min, max, ariaLabel: label, step };
|
||||
const { label, min, max, dataFieldName, step } = input;
|
||||
const props = {
|
||||
label: label,
|
||||
min: min,
|
||||
max: max,
|
||||
ariaLabel: label,
|
||||
step: step
|
||||
};
|
||||
|
||||
if (input.inputType === "spin") {
|
||||
const value = this.props.currentValues.get(dataFieldName) as number;
|
||||
if (input.uiType === UiType.Spinner) {
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<SpinButton
|
||||
{...props}
|
||||
defaultValue={defaultValue.toString()}
|
||||
onValidate={newValue => this.onValidate(newValue, min, max, dataFieldName)}
|
||||
onIncrement={newValue => this.onIncrement(newValue, step, max, dataFieldName)}
|
||||
onDecrement={newValue => this.onDecrement(newValue, step, min, dataFieldName)}
|
||||
id={`${input.dataFieldName}-spinner-input`}
|
||||
value={value?.toString()}
|
||||
onValidate={newValue => this.onValidate(input, newValue, props.min, props.max)}
|
||||
onIncrement={newValue => this.onIncrement(input, newValue, props.step, props.max)}
|
||||
onDecrement={newValue => this.onDecrement(input, newValue, props.step, props.min)}
|
||||
labelPosition={Position.top}
|
||||
styles={{
|
||||
label: {
|
||||
@@ -217,34 +225,35 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
{this.state.errors.has(dataFieldName) && (
|
||||
<MessageBar messageBarType={MessageBarType.error}>Error: {this.state.errors.get(dataFieldName)}</MessageBar>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
} else if (input.uiType === UiType.Slider) {
|
||||
return (
|
||||
<div id={`${input.dataFieldName}-slider-input`}>
|
||||
<Slider
|
||||
{...props}
|
||||
value={value}
|
||||
onChange={newValue => this.props.onInputChange(input, newValue)}
|
||||
styles={{
|
||||
titleLabel: {
|
||||
...SmartUiComponent.labelStyle,
|
||||
fontWeight: 600
|
||||
},
|
||||
valueLabel: SmartUiComponent.labelStyle
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (input.inputType === "slider") {
|
||||
return (
|
||||
<Slider
|
||||
// showValue={true}
|
||||
// valueFormat={}
|
||||
{...props}
|
||||
defaultValue={defaultValue}
|
||||
onChange={newValue => this.onInputChange(newValue, dataFieldName)}
|
||||
styles={{
|
||||
titleLabel: {
|
||||
...SmartUiComponent.labelStyle,
|
||||
fontWeight: 600
|
||||
},
|
||||
valueLabel: SmartUiComponent.labelStyle
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <>Unsupported number input type {input.inputType}</>;
|
||||
return <>Unsupported number UI type {input.uiType}</>;
|
||||
}
|
||||
}
|
||||
|
||||
private renderBooleanInput(input: BooleanInput): JSX.Element {
|
||||
const { dataFieldName } = input;
|
||||
const value = this.props.currentValues.get(input.dataFieldName) as boolean;
|
||||
const selectedKey = value || input.defaultValue ? "true" : "false";
|
||||
return (
|
||||
<div>
|
||||
<div id={`${input.dataFieldName}-radioSwitch-input`}>
|
||||
<div className="inputLabelContainer">
|
||||
<Text variant="small" nowrap className="inputLabel">
|
||||
{input.label}
|
||||
@@ -255,41 +264,33 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
{
|
||||
label: input.falseLabel,
|
||||
key: "false",
|
||||
onSelect: () => this.onInputChange(false, dataFieldName)
|
||||
onSelect: () => this.props.onInputChange(input, false)
|
||||
},
|
||||
{
|
||||
label: input.trueLabel,
|
||||
key: "true",
|
||||
onSelect: () => this.onInputChange(true, dataFieldName)
|
||||
onSelect: () => this.props.onInputChange(input, true)
|
||||
}
|
||||
]}
|
||||
selectedKey={
|
||||
(this.state.currentValues.has(dataFieldName)
|
||||
? (this.state.currentValues.get(dataFieldName) as boolean)
|
||||
: input.defaultValue)
|
||||
? "true"
|
||||
: "false"
|
||||
}
|
||||
selectedKey={selectedKey}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderEnumInput(input: EnumInput): JSX.Element {
|
||||
const { label, defaultKey, dataFieldName, choices, placeholder } = input;
|
||||
private renderChoiceInput(input: ChoiceInput): JSX.Element {
|
||||
const { label, defaultKey: defaultKey, dataFieldName, choices, placeholder } = input;
|
||||
const value = this.props.currentValues.get(dataFieldName) as string;
|
||||
return (
|
||||
<Dropdown
|
||||
id={`${input.dataFieldName}-dropown-input`}
|
||||
label={label}
|
||||
selectedKey={
|
||||
this.state.currentValues.has(dataFieldName)
|
||||
? (this.state.currentValues.get(dataFieldName) as string)
|
||||
: defaultKey
|
||||
}
|
||||
onChange={(_, item: IDropdownOption) => this.onInputChange(item.key.toString(), dataFieldName)}
|
||||
selectedKey={value ? value : defaultKey}
|
||||
onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())}
|
||||
placeholder={placeholder}
|
||||
options={choices.map(c => ({
|
||||
key: c.key,
|
||||
text: c.value
|
||||
text: c.label
|
||||
}))}
|
||||
styles={{
|
||||
label: {
|
||||
@@ -302,34 +303,48 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
);
|
||||
}
|
||||
|
||||
private renderError(input: AnyInput): JSX.Element {
|
||||
return <MessageBar messageBarType={MessageBarType.error}>Error: {input.errorMessage}</MessageBar>;
|
||||
}
|
||||
|
||||
private renderInput(input: AnyInput): JSX.Element {
|
||||
if (input.errorMessage) {
|
||||
return this.renderError(input);
|
||||
}
|
||||
switch (input.type) {
|
||||
case "string":
|
||||
return this.renderStringInput(input as StringInput);
|
||||
return this.renderTextInput(input as StringInput);
|
||||
case "number":
|
||||
return this.renderNumberInput(input as NumberInput);
|
||||
case "boolean":
|
||||
return this.renderBooleanInput(input as BooleanInput);
|
||||
case "enum":
|
||||
return this.renderEnumInput(input as EnumInput);
|
||||
case "object":
|
||||
return this.renderChoiceInput(input as ChoiceInput);
|
||||
default:
|
||||
throw new Error(`Unknown input type: ${input.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
private renderNode(node: Node): JSX.Element {
|
||||
const containerStackTokens: IStackTokens = { childrenGap: 10 };
|
||||
const containerStackTokens: IStackTokens = { childrenGap: 15 };
|
||||
|
||||
return (
|
||||
<Stack tokens={containerStackTokens} className="widgetRendererContainer">
|
||||
{node.info && this.renderInfo(node.info)}
|
||||
{node.input && this.renderInput(node.input)}
|
||||
<Stack.Item>
|
||||
{node.info && this.renderInfo(node.info as Info)}
|
||||
{node.input && this.renderInput(node.input)}
|
||||
</Stack.Item>
|
||||
{node.children && node.children.map(child => <div key={child.id}>{this.renderNode(child)}</div>)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
return <>{this.renderNode(this.props.descriptor.root)}</>;
|
||||
const containerStackTokens: IStackTokens = { childrenGap: 20 };
|
||||
return (
|
||||
<Stack tokens={containerStackTokens} styles={{ root: { width: 400, padding: 10 } }}>
|
||||
{this.renderNode(this.props.descriptor.root)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,40 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SmartUiComponent should render 1`] = `
|
||||
<Fragment>
|
||||
<Stack
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"padding": 10,
|
||||
"width": 400,
|
||||
},
|
||||
}
|
||||
}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
className="widgetRendererContainer"
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
"childrenGap": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledMessageBarBase>
|
||||
Start at $24/mo per database
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/azure-cosmos-db-pricing"
|
||||
target="_blank"
|
||||
>
|
||||
More Details
|
||||
</StyledLinkBase>
|
||||
</StyledMessageBarBase>
|
||||
<StackItem>
|
||||
<StyledMessageBarBase>
|
||||
Start at $24/mo per database
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/azure-cosmos-db-pricing"
|
||||
target="_blank"
|
||||
>
|
||||
More Details
|
||||
</StyledLinkBase>
|
||||
</StyledMessageBarBase>
|
||||
</StackItem>
|
||||
<div
|
||||
key="throughput"
|
||||
>
|
||||
@@ -26,11 +42,11 @@ exports[`SmartUiComponent should render 1`] = `
|
||||
className="widgetRendererContainer"
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
"childrenGap": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<StackItem>
|
||||
<CustomizedSpinButton
|
||||
ariaLabel="Throughput (input)"
|
||||
decrementButtonIcon={
|
||||
@@ -38,8 +54,8 @@ exports[`SmartUiComponent should render 1`] = `
|
||||
"iconName": "ChevronDownSmall",
|
||||
}
|
||||
}
|
||||
defaultValue="400"
|
||||
disabled={false}
|
||||
id="throughput-spinner-input"
|
||||
incrementButtonIcon={
|
||||
Object {
|
||||
"iconName": "ChevronUpSmall",
|
||||
@@ -64,7 +80,7 @@ exports[`SmartUiComponent should render 1`] = `
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</div>
|
||||
<div
|
||||
@@ -74,34 +90,60 @@ exports[`SmartUiComponent should render 1`] = `
|
||||
className="widgetRendererContainer"
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
"childrenGap": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledSliderBase
|
||||
ariaLabel="Throughput (Slider)"
|
||||
defaultValue={400}
|
||||
label="Throughput (Slider)"
|
||||
max={500}
|
||||
min={400}
|
||||
onChange={[Function]}
|
||||
step={10}
|
||||
styles={
|
||||
Object {
|
||||
"titleLabel": Object {
|
||||
"color": "#393939",
|
||||
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||
"fontSize": 12,
|
||||
"fontWeight": 600,
|
||||
},
|
||||
"valueLabel": Object {
|
||||
"color": "#393939",
|
||||
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
<StackItem>
|
||||
<div
|
||||
id="throughput2-slider-input"
|
||||
>
|
||||
<StyledSliderBase
|
||||
ariaLabel="Throughput (Slider)"
|
||||
label="Throughput (Slider)"
|
||||
max={500}
|
||||
min={400}
|
||||
onChange={[Function]}
|
||||
step={10}
|
||||
styles={
|
||||
Object {
|
||||
"titleLabel": Object {
|
||||
"color": "#393939",
|
||||
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||
"fontSize": 12,
|
||||
"fontWeight": 600,
|
||||
},
|
||||
"valueLabel": Object {
|
||||
"color": "#393939",
|
||||
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</div>
|
||||
<div
|
||||
key="throughput3"
|
||||
>
|
||||
<Stack
|
||||
className="widgetRendererContainer"
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 15,
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<StackItem>
|
||||
<StyledMessageBarBase
|
||||
messageBarType={1}
|
||||
>
|
||||
Error:
|
||||
label, truelabel and falselabel are required for boolean input 'throughput3'
|
||||
</StyledMessageBarBase>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</div>
|
||||
<div
|
||||
@@ -111,16 +153,16 @@ exports[`SmartUiComponent should render 1`] = `
|
||||
className="widgetRendererContainer"
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
"childrenGap": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="stringInputContainer"
|
||||
>
|
||||
<div>
|
||||
<StackItem>
|
||||
<div
|
||||
className="stringInputContainer"
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
id="containerId-input"
|
||||
id="containerId-textBox-input"
|
||||
label="Container id"
|
||||
onChange={[Function]}
|
||||
styles={
|
||||
@@ -140,7 +182,7 @@ exports[`SmartUiComponent should render 1`] = `
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</div>
|
||||
<div
|
||||
@@ -150,40 +192,44 @@ exports[`SmartUiComponent should render 1`] = `
|
||||
className="widgetRendererContainer"
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
"childrenGap": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<StackItem>
|
||||
<div
|
||||
className="inputLabelContainer"
|
||||
id="analyticalStore-radioSwitch-input"
|
||||
>
|
||||
<Text
|
||||
className="inputLabel"
|
||||
nowrap={true}
|
||||
variant="small"
|
||||
<div
|
||||
className="inputLabelContainer"
|
||||
>
|
||||
Analytical Store
|
||||
</Text>
|
||||
<Text
|
||||
className="inputLabel"
|
||||
nowrap={true}
|
||||
variant="small"
|
||||
>
|
||||
Analytical Store
|
||||
</Text>
|
||||
</div>
|
||||
<RadioSwitchComponent
|
||||
choices={
|
||||
Array [
|
||||
Object {
|
||||
"key": "false",
|
||||
"label": "Disabled",
|
||||
"onSelect": [Function],
|
||||
},
|
||||
Object {
|
||||
"key": "true",
|
||||
"label": "Enabled",
|
||||
"onSelect": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
selectedKey="true"
|
||||
/>
|
||||
</div>
|
||||
<RadioSwitchComponent
|
||||
choices={
|
||||
Array [
|
||||
Object {
|
||||
"key": "false",
|
||||
"label": "Disabled",
|
||||
"onSelect": [Function],
|
||||
},
|
||||
Object {
|
||||
"key": "true",
|
||||
"label": "Enabled",
|
||||
"onSelect": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
selectedKey="true"
|
||||
/>
|
||||
</div>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</div>
|
||||
<div
|
||||
@@ -193,48 +239,51 @@ exports[`SmartUiComponent should render 1`] = `
|
||||
className="widgetRendererContainer"
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
"childrenGap": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledWithResponsiveMode
|
||||
label="Database"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": "db1",
|
||||
"text": "database1",
|
||||
},
|
||||
Object {
|
||||
"key": "db2",
|
||||
"text": "database2",
|
||||
},
|
||||
Object {
|
||||
"key": "db3",
|
||||
"text": "database3",
|
||||
},
|
||||
]
|
||||
}
|
||||
selectedKey="db2"
|
||||
styles={
|
||||
Object {
|
||||
"dropdown": Object {
|
||||
"color": "#393939",
|
||||
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||
"fontSize": 12,
|
||||
},
|
||||
"label": Object {
|
||||
"color": "#393939",
|
||||
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||
"fontSize": 12,
|
||||
"fontWeight": 600,
|
||||
},
|
||||
<StackItem>
|
||||
<StyledWithResponsiveMode
|
||||
id="database-dropown-input"
|
||||
label="Database"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": "db1",
|
||||
"text": "Database 1",
|
||||
},
|
||||
Object {
|
||||
"key": "db2",
|
||||
"text": "Database 2",
|
||||
},
|
||||
Object {
|
||||
"key": "db3",
|
||||
"text": "Database 3",
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
/>
|
||||
selectedKey="db2"
|
||||
styles={
|
||||
Object {
|
||||
"dropdown": Object {
|
||||
"color": "#393939",
|
||||
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||
"fontSize": 12,
|
||||
},
|
||||
"label": Object {
|
||||
"color": "#393939",
|
||||
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||
"fontSize": 12,
|
||||
"fontWeight": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</div>
|
||||
</Stack>
|
||||
</Fragment>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
@@ -5,6 +5,9 @@ import ThroughputInputComponentAutoscaleV3 from "./ThroughputInputComponentAutos
|
||||
import { KeyCodes } from "../../../Common/Constants";
|
||||
import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel";
|
||||
|
||||
import { userContext } from "../../../UserContext";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
/**
|
||||
* Throughput Input:
|
||||
*
|
||||
@@ -129,6 +132,8 @@ export interface ThroughputInputParams {
|
||||
showAutoPilot?: ko.Observable<boolean>;
|
||||
overrideWithAutoPilotSettings: ko.Observable<boolean>;
|
||||
overrideWithProvisionedThroughputSettings: ko.Observable<boolean>;
|
||||
freeTierExceedThroughputTooltip?: ko.Observable<string>;
|
||||
freeTierExceedThroughputWarning?: ko.Observable<string>;
|
||||
}
|
||||
|
||||
export class ThroughputInputViewModel extends WaitsForTemplateViewModel {
|
||||
@@ -165,6 +170,10 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel {
|
||||
public overrideWithProvisionedThroughputSettings: ko.Observable<boolean>;
|
||||
public isManualThroughputInputFieldRequired: ko.Computed<boolean>;
|
||||
public isAutoscaleThroughputInputFieldRequired: ko.Computed<boolean>;
|
||||
public freeTierExceedThroughputTooltip: ko.Observable<string>;
|
||||
public freeTierExceedThroughputWarning: ko.Observable<string>;
|
||||
public showFreeTierExceedThroughputTooltip: ko.Computed<boolean>;
|
||||
public showFreeTierExceedThroughputWarning: ko.Computed<boolean>;
|
||||
|
||||
public constructor(options: ThroughputInputParams) {
|
||||
super();
|
||||
@@ -195,6 +204,16 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel {
|
||||
this.label = options.label || ko.observable<string>();
|
||||
this.showAutoPilot = options.showAutoPilot !== undefined ? options.showAutoPilot : ko.observable<boolean>(true);
|
||||
this.isAutoPilotSelected = options.isAutoPilotSelected || ko.observable<boolean>(false);
|
||||
this.isAutoPilotSelected.subscribe(value => {
|
||||
TelemetryProcessor.trace(Action.ToggleAutoscaleSetting, ActionModifiers.Mark, {
|
||||
changedSelectedValueTo: value ? ActionModifiers.ToggleAutoscaleOn : ActionModifiers.ToggleAutoscaleOff,
|
||||
databaseAccountName: userContext.databaseAccount?.name,
|
||||
subscriptionId: userContext.subscriptionId,
|
||||
apiKind: userContext.defaultExperience,
|
||||
dataExplorerArea: "Scale Tab V1"
|
||||
});
|
||||
});
|
||||
|
||||
this.throughputAutoPilotRadioId = options.throughputAutoPilotRadioId;
|
||||
this.throughputProvisionedRadioId = options.throughputProvisionedRadioId;
|
||||
this.throughputModeRadioName = options.throughputModeRadioName;
|
||||
@@ -219,6 +238,16 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel {
|
||||
this.isAutoscaleThroughputInputFieldRequired = ko.pureComputed(
|
||||
() => this.isEnabled() && this.isAutoPilotSelected()
|
||||
);
|
||||
|
||||
this.freeTierExceedThroughputTooltip = options.freeTierExceedThroughputTooltip || ko.observable<string>();
|
||||
this.freeTierExceedThroughputWarning = options.freeTierExceedThroughputWarning || ko.observable<string>();
|
||||
this.showFreeTierExceedThroughputTooltip = ko.pureComputed<boolean>(
|
||||
() => !!this.freeTierExceedThroughputTooltip() && this.value() > 400
|
||||
);
|
||||
|
||||
this.showFreeTierExceedThroughputWarning = ko.pureComputed<boolean>(
|
||||
() => !!this.freeTierExceedThroughputWarning() && this.value() > 400
|
||||
);
|
||||
}
|
||||
|
||||
public decreaseThroughput() {
|
||||
|
||||
@@ -132,6 +132,14 @@
|
||||
<a target="_blank" href="https://cosmos.azure.com/capacitycalculator/">capacity calculator</a></span
|
||||
>
|
||||
</p>
|
||||
|
||||
<div class="inputTooltip">
|
||||
<span
|
||||
data-bind="text: freeTierExceedThroughputTooltip, visible: showFreeTierExceedThroughputTooltip"
|
||||
class="inputTooltipText"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div data-bind="setTemplateReady: true">
|
||||
<input
|
||||
data-bind="
|
||||
@@ -154,6 +162,11 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="freeTierInlineWarning" data-bind="visible: showFreeTierExceedThroughputWarning">
|
||||
<span class="freeTierWarningIcon"><img src="/warning.svg" alt="Warning"/></span>
|
||||
<span class="freeTierWarningMessage" data-bind="text: freeTierExceedThroughputWarning"></span>
|
||||
</div>
|
||||
|
||||
<p data-bind="visible: costsVisible">
|
||||
<span data-bind="html: requestUnitsUsageCost"></span>
|
||||
</p>
|
||||
|
||||
@@ -88,6 +88,9 @@ import { stringToBlob } from "../Utils/BlobUtils";
|
||||
import { IChoiceGroupProps } from "office-ui-fabric-react";
|
||||
import { getErrorMessage, handleError, getErrorStack } from "../Common/ErrorHandlingUtils";
|
||||
import { SubscriptionType } from "../Contracts/SubscriptionType";
|
||||
import { SelfServeLoadingComponentAdapter } from "../SelfServe/SelfServeLoadingComponentAdapter";
|
||||
import { SelfServeType } from "../SelfServe/SelfServeUtils";
|
||||
import { SelfServeComponentAdapter } from "../SelfServe/SelfServeComponentAdapter";
|
||||
|
||||
BindingHandlersRegisterer.registerBindingHandlers();
|
||||
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
||||
@@ -131,6 +134,7 @@ export default class Explorer {
|
||||
public isEnableMongoCapabilityPresent: ko.Computed<boolean>;
|
||||
public isServerlessEnabled: ko.Computed<boolean>;
|
||||
public isAccountReady: ko.Observable<boolean>;
|
||||
public selfServeType: ko.Observable<SelfServeType>;
|
||||
public canSaveQueries: ko.Computed<boolean>;
|
||||
public features: ko.Observable<any>;
|
||||
public serverId: ko.Observable<string>;
|
||||
@@ -156,6 +160,7 @@ export default class Explorer {
|
||||
public selectedNode: ko.Observable<ViewModels.TreeNode>;
|
||||
public isRefreshingExplorer: ko.Observable<boolean>;
|
||||
private resourceTree: ResourceTreeAdapter;
|
||||
private selfServeComponentAdapter: SelfServeComponentAdapter;
|
||||
|
||||
// Resource Token
|
||||
public resourceTokenDatabaseId: ko.Observable<string>;
|
||||
@@ -207,7 +212,9 @@ export default class Explorer {
|
||||
public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
|
||||
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
||||
public isRightPanelV2Enabled: ko.Computed<boolean>;
|
||||
public isMongoIndexingEnabled: ko.Observable<boolean>;
|
||||
public canExceedMaximumValue: ko.Computed<boolean>;
|
||||
public isAutoscaleDefaultEnabled: ko.Observable<boolean>;
|
||||
|
||||
public shouldShowShareDialogContents: ko.Observable<boolean>;
|
||||
public shareAccessData: ko.Observable<AdHocAccessData>;
|
||||
@@ -257,6 +264,7 @@ export default class Explorer {
|
||||
private _dialogProps: ko.Observable<DialogProps>;
|
||||
private addSynapseLinkDialog: DialogComponentAdapter;
|
||||
private _addSynapseLinkDialogProps: ko.Observable<DialogProps>;
|
||||
private selfServeLoadingComponentAdapter: SelfServeLoadingComponentAdapter;
|
||||
|
||||
private static readonly MaxNbDatabasesToAutoExpand = 5;
|
||||
|
||||
@@ -292,6 +300,7 @@ export default class Explorer {
|
||||
}
|
||||
});
|
||||
this.isAccountReady = ko.observable<boolean>(false);
|
||||
this.selfServeType = ko.observable<SelfServeType>(undefined);
|
||||
this._isInitializingNotebooks = false;
|
||||
this._isInitializingSparkConnectionInfo = false;
|
||||
this.arcadiaToken = ko.observable<string>();
|
||||
@@ -402,6 +411,7 @@ export default class Explorer {
|
||||
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
|
||||
);
|
||||
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
|
||||
this.isMongoIndexingEnabled = ko.observable<boolean>(false);
|
||||
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||
|
||||
@@ -412,6 +422,8 @@ export default class Explorer {
|
||||
this.isSchemaEnabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSchema));
|
||||
this.isNotificationConsoleExpanded = ko.observable<boolean>(false);
|
||||
|
||||
this.isAutoscaleDefaultEnabled = ko.observable<boolean>(false);
|
||||
|
||||
this.databases = ko.observableArray<ViewModels.Database>();
|
||||
this.canSaveQueries = ko.computed<boolean>(() => {
|
||||
const savedQueriesDatabase: ViewModels.Database = _.find(
|
||||
@@ -693,6 +705,7 @@ export default class Explorer {
|
||||
});
|
||||
|
||||
this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this);
|
||||
this.selfServeComponentAdapter = new SelfServeComponentAdapter(this);
|
||||
|
||||
this.loadQueryPane = new LoadQueryPane({
|
||||
id: "loadquerypane",
|
||||
@@ -868,6 +881,7 @@ export default class Explorer {
|
||||
});
|
||||
|
||||
this.commandBarComponentAdapter = new CommandBarComponentAdapter(this);
|
||||
this.selfServeLoadingComponentAdapter = new SelfServeLoadingComponentAdapter();
|
||||
this.notificationConsoleComponentAdapter = new NotificationConsoleComponentAdapter(this);
|
||||
|
||||
this._initSettings();
|
||||
@@ -1839,6 +1853,20 @@ export default class Explorer {
|
||||
return false;
|
||||
}
|
||||
|
||||
public setSelfServeType(inputs: ViewModels.DataExplorerInputsFrame): void {
|
||||
const selfServeFeature = inputs.features[Constants.Features.selfServeType];
|
||||
if (selfServeFeature) {
|
||||
// self serve type received from query string
|
||||
const selfServeType = SelfServeType[selfServeFeature?.toLowerCase() as keyof typeof SelfServeType];
|
||||
this.selfServeType(selfServeType ? selfServeType : SelfServeType.invalid);
|
||||
} else if (inputs.selfServeType) {
|
||||
// self serve type received from portal
|
||||
this.selfServeType(inputs.selfServeType);
|
||||
} else {
|
||||
this.selfServeType(SelfServeType.none);
|
||||
}
|
||||
}
|
||||
|
||||
public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): void {
|
||||
if (inputs != null) {
|
||||
// In development mode, save the iframe message from the portal in session storage.
|
||||
@@ -1862,6 +1890,7 @@ export default class Explorer {
|
||||
this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription);
|
||||
this.isAuthWithResourceToken(inputs.isAuthWithresourceToken);
|
||||
this.setFeatureFlagsFromFlights(inputs.flights);
|
||||
this.setSelfServeType(inputs);
|
||||
this._importExplorerConfigComplete = true;
|
||||
|
||||
updateConfigContext({
|
||||
@@ -1896,6 +1925,12 @@ export default class Explorer {
|
||||
if (!flights) {
|
||||
return;
|
||||
}
|
||||
if (flights.indexOf(Constants.Flights.AutoscaleTest) !== -1) {
|
||||
this.isAutoscaleDefaultEnabled(true);
|
||||
}
|
||||
if (flights.indexOf(Constants.Flights.MongoIndexing) !== -1) {
|
||||
this.isMongoIndexingEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
public findSelectedCollection(): ViewModels.Collection {
|
||||
@@ -3014,4 +3049,25 @@ export default class Explorer {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public isFirstResourceCreated(): boolean {
|
||||
const databases: ViewModels.Database[] = this.databases();
|
||||
|
||||
if (!databases || databases.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return databases.some(database => {
|
||||
// user has created at least one collection
|
||||
if (database.collections()?.length > 0) {
|
||||
return true;
|
||||
}
|
||||
// user has created a database with shared throughput
|
||||
if (database.offer()) {
|
||||
return true;
|
||||
}
|
||||
// use has created an empty database without shared throughput
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ export class ArraysByKeyCache<T> {
|
||||
|
||||
public constructor(maxNbElements: number) {
|
||||
this.maxNbElements = maxNbElements;
|
||||
this.clear();
|
||||
this.keyQueue = [];
|
||||
this.cache = {};
|
||||
this.totalElements = 0;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
@@ -58,7 +60,7 @@ export class ArraysByKeyCache<T> {
|
||||
* @param startIndex
|
||||
* @param pageSize
|
||||
*/
|
||||
public retrieve(key: string, startIndex: number, pageSize: number): T[] {
|
||||
public retrieve(key: string, startIndex: number, pageSize: number): T[] | null {
|
||||
if (!this.cache.hasOwnProperty(key)) {
|
||||
return null;
|
||||
}
|
||||
@@ -77,8 +79,10 @@ export class ArraysByKeyCache<T> {
|
||||
private reduceCacheSize(): void {
|
||||
// remove an key and its array
|
||||
const oldKey = this.keyQueue.shift();
|
||||
this.totalElements -= this.cache[oldKey].length;
|
||||
delete this.cache[oldKey];
|
||||
if (oldKey) {
|
||||
this.totalElements -= this.cache[oldKey].length;
|
||||
delete this.cache[oldKey];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -413,13 +413,13 @@ export class GraphData<V extends GremlinVertex, E extends GremlinEdge> {
|
||||
* @param node
|
||||
* @param prop
|
||||
*/
|
||||
public static getNodePropValue(node: D3Node, prop: string): string | number | boolean {
|
||||
public static getNodePropValue(node: D3Node, prop: string): undefined | string | number | boolean {
|
||||
if (node.hasOwnProperty(prop)) {
|
||||
return (node as any)[prop];
|
||||
}
|
||||
|
||||
// This is DocDB specific
|
||||
if (node.hasOwnProperty("properties") && node.properties.hasOwnProperty(prop)) {
|
||||
if (node.properties && node.properties.hasOwnProperty(prop)) {
|
||||
return node.properties[prop][0]["value"];
|
||||
}
|
||||
|
||||
@@ -496,7 +496,7 @@ export class GraphData<V extends GremlinVertex, E extends GremlinEdge> {
|
||||
* Get list of children ids of a given vertex
|
||||
* @param vertex
|
||||
*/
|
||||
private static getChildrenId(vertex: GremlinVertex): string[] {
|
||||
public static getChildrenId(vertex: GremlinVertex): string[] {
|
||||
const ids = <any>{}; // HashSet
|
||||
if (vertex.hasOwnProperty("outE")) {
|
||||
let outE = vertex.outE;
|
||||
|
||||
@@ -8,7 +8,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
let mockExplorer: Explorer;
|
||||
|
||||
describe("Enable Azure Synapse Link Button", () => {
|
||||
const enableAzureSynapseLinkBtnLabel = "Enable Azure Synapse Link (Preview)";
|
||||
const enableAzureSynapseLinkBtnLabel = "Enable Azure Synapse Link";
|
||||
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
|
||||
@@ -269,7 +269,7 @@ export class CommandBarComponentButtonFactory {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = "Enable Azure Synapse Link (Preview)";
|
||||
const label = "Enable Azure Synapse Link";
|
||||
return {
|
||||
iconSrc: SynapseIcon,
|
||||
iconAlt: label,
|
||||
|
||||
@@ -11,8 +11,8 @@ import ErrorBlackIcon from "../../../../images/error_black.svg";
|
||||
import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg";
|
||||
import InfoIcon from "../../../../images/info_color.svg";
|
||||
import ErrorRedIcon from "../../../../images/error_red.svg";
|
||||
import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif";
|
||||
import ClearIcon from "../../../../images/Clear.svg";
|
||||
import LoaderIcon from "../../../../images/circular_loader_black_16x16.gif";
|
||||
import ChevronUpIcon from "../../../../images/QueryBuilder/CollapseChevronUp_16x.png";
|
||||
import ChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
|
||||
|
||||
@@ -59,9 +59,9 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
{ key: "Info", text: "Info" },
|
||||
{ key: "Error", text: "Error" }
|
||||
];
|
||||
private headerTimeoutId: number;
|
||||
private prevHeaderStatus: string;
|
||||
private consoleHeaderElement: HTMLElement;
|
||||
private headerTimeoutId?: number;
|
||||
private prevHeaderStatus: string | null;
|
||||
private consoleHeaderElement?: HTMLElement;
|
||||
|
||||
constructor(props: NotificationConsoleComponentProps) {
|
||||
super(props);
|
||||
@@ -99,6 +99,10 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
}
|
||||
}
|
||||
|
||||
public setElememntRef = (element: HTMLElement) => {
|
||||
this.consoleHeaderElement = element;
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const numInProgress = this.props.consoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.InProgress)
|
||||
.length;
|
||||
@@ -110,7 +114,7 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
<div className="notificationConsoleContainer">
|
||||
<div
|
||||
className="notificationConsoleHeader"
|
||||
ref={(element: HTMLElement) => (this.consoleHeaderElement = element)}
|
||||
ref={this.setElememntRef}
|
||||
onClick={(event: React.MouseEvent<HTMLDivElement>) => this.expandCollapseConsole()}
|
||||
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
|
||||
tabIndex={0}
|
||||
@@ -220,12 +224,12 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
));
|
||||
}
|
||||
|
||||
private onFilterSelected(event: React.ChangeEvent<HTMLSelectElement>, option: IDropdownOption): void {
|
||||
private onFilterSelected = (event: React.ChangeEvent<HTMLSelectElement>, option: IDropdownOption): void => {
|
||||
this.setState({ selectedFilter: String(option.key) });
|
||||
}
|
||||
};
|
||||
|
||||
private getFilteredConsoleData(): ConsoleData[] {
|
||||
let filterType: ConsoleDataType = null;
|
||||
let filterType: ConsoleDataType | null = null;
|
||||
|
||||
switch (this.state.selectedFilter) {
|
||||
case "All":
|
||||
@@ -272,7 +276,7 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
|
||||
private onConsoleWasExpanded = (): void => {
|
||||
this.props.onConsoleExpandedChange(this.state.isExpanded);
|
||||
if (this.state.isExpanded) {
|
||||
if (this.state.isExpanded && this.consoleHeaderElement) {
|
||||
this.consoleHeaderElement.focus();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { Observable, of } from "rxjs";
|
||||
import { AjaxResponse } from "rxjs/ajax";
|
||||
import { ServerConfig } from "rx-jupyter";
|
||||
import { AjaxRequest, AjaxResponse } from "rxjs/ajax";
|
||||
|
||||
let fakeAjaxResponse: AjaxResponse = {
|
||||
originalEvent: undefined,
|
||||
originalEvent: <Event>(<unknown>undefined),
|
||||
xhr: new XMLHttpRequest(),
|
||||
request: null,
|
||||
request: <AjaxRequest>(<unknown>null),
|
||||
status: 200,
|
||||
response: {},
|
||||
responseText: null,
|
||||
responseText: "",
|
||||
responseType: "json"
|
||||
};
|
||||
export const sessions = {
|
||||
create: (serverConfig: ServerConfig, body: object): Observable<AjaxResponse> => of(fakeAjaxResponse),
|
||||
create: (serverConfig: unknown, body: object): Observable<AjaxResponse> => of(fakeAjaxResponse),
|
||||
__setResponse: (response: AjaxResponse) => {
|
||||
fakeAjaxResponse = response;
|
||||
},
|
||||
|
||||
@@ -808,7 +808,7 @@ const closeUnsupportedMimetypesEpic = (
|
||||
const filepath = action.payload.filepath;
|
||||
// Close tab and show error message
|
||||
explorer.tabsManager.closeTabsByComparator(
|
||||
tab => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
|
||||
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
|
||||
);
|
||||
const msg = `${filepath} cannot be rendered. Please download the file, in order to view it outside of Data Explorer.`;
|
||||
explorer.showOkModalDialog("File cannot be rendered", msg);
|
||||
@@ -836,7 +836,7 @@ const closeContentFailedToFetchEpic = (
|
||||
const filepath = action.payload.filepath;
|
||||
// Close tab and show error message
|
||||
explorer.tabsManager.closeTabsByComparator(
|
||||
tab => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
|
||||
(tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
|
||||
);
|
||||
const msg = `Failed to load file: ${filepath}.`;
|
||||
explorer.showOkModalDialog("Failure to load", msg);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { stringifyNotebook } from "@nteract/commutable";
|
||||
export class NotebookContentClient {
|
||||
constructor(
|
||||
private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>,
|
||||
private notebookBasePath: ko.Observable<string>,
|
||||
public notebookBasePath: ko.Observable<string>,
|
||||
private contentProvider: IContentProvider
|
||||
) {}
|
||||
|
||||
@@ -117,8 +117,11 @@ export class NotebookContentClient {
|
||||
|
||||
private async checkIfFilepathExists(filepath: string): Promise<boolean> {
|
||||
const parentDirPath = NotebookUtil.getParentPath(filepath);
|
||||
const items = await this.fetchNotebookFiles(parentDirPath);
|
||||
return items.some(value => FileSystemUtil.isPathEqual(value.path, filepath));
|
||||
if (parentDirPath) {
|
||||
const items = await this.fetchNotebookFiles(parentDirPath);
|
||||
return items.some(value => FileSystemUtil.isPathEqual(value.path, filepath));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,7 +192,7 @@ export class NotebookContentClient {
|
||||
const dir = xhr.response;
|
||||
const item = NotebookUtil.createNotebookContentItem(dir.name, dir.path, dir.type);
|
||||
item.parent = parent;
|
||||
parent.children.push(item);
|
||||
parent.children?.push(item);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
@@ -225,7 +228,7 @@ export class NotebookContentClient {
|
||||
* Convert rx-jupyter type to our type
|
||||
* @param type
|
||||
*/
|
||||
private static getType(type: FileType): NotebookContentItemType {
|
||||
public static getType(type: FileType): NotebookContentItemType {
|
||||
switch (type) {
|
||||
case "directory":
|
||||
return NotebookContentItemType.Directory;
|
||||
|
||||
@@ -152,7 +152,8 @@
|
||||
maxAutoPilotThroughputSet: sharedAutoPilotThroughput,
|
||||
autoPilotUsageCost: autoPilotUsageCost,
|
||||
canExceedMaximumValue: canExceedMaximumValue,
|
||||
showAutoPilot: !isFreeTierAccount()
|
||||
showAutoPilot: !isFreeTierAccount(),
|
||||
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
|
||||
}">
|
||||
</throughput-input-autopilot-v3>
|
||||
</div>
|
||||
@@ -256,7 +257,7 @@
|
||||
range of values and is likely to have evenly distributed access patterns.</span>
|
||||
</span>
|
||||
</p>
|
||||
<input type="text" id="partitionKeyValue" data-test="addCollection-partitionKeyValue" aria-required="true" size="40"
|
||||
<input type="text" id="addCollection-partitionKeyValue" data-test="addCollection-partitionKeyValue" aria-required="true" size="40"
|
||||
class="textfontclr collid" data-bind="textInput: partitionKey,
|
||||
attr: {
|
||||
placeholder: partitionKeyPlaceholder,
|
||||
@@ -333,7 +334,8 @@
|
||||
maxAutoPilotThroughputSet: autoPilotThroughput,
|
||||
autoPilotUsageCost: autoPilotUsageCost,
|
||||
canExceedMaximumValue: canExceedMaximumValue,
|
||||
showAutoPilot: !isFixedStorageSelected()
|
||||
showAutoPilot: !isFixedStorageSelected(),
|
||||
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
|
||||
}">
|
||||
</throughput-input-autopilot-v3>
|
||||
</div>
|
||||
|
||||
@@ -74,7 +74,7 @@ describe("Add Collection Pane", () => {
|
||||
explorer.databaseAccount(mockFreeTierDatabaseAccount);
|
||||
const addCollectionPane = explorer.addCollectionPane as AddCollectionPane;
|
||||
expect(addCollectionPane.isFreeTierAccount()).toBe(true);
|
||||
expect(addCollectionPane.upsellMessage()).toContain("With free tier discount");
|
||||
expect(addCollectionPane.upsellMessage()).toContain("With free tier");
|
||||
expect(addCollectionPane.upsellAnchorUrl()).toBe(Constants.Urls.freeTierInformation);
|
||||
expect(addCollectionPane.upsellAnchorText()).toBe("Learn more");
|
||||
});
|
||||
|
||||
@@ -89,6 +89,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
public isSynapseLinkUpdating: ko.Computed<boolean>;
|
||||
public canExceedMaximumValue: ko.PureComputed<boolean>;
|
||||
public ruToolTipText: ko.Computed<string>;
|
||||
public freeTierExceedThroughputTooltip: ko.Computed<string>;
|
||||
public canConfigureThroughput: ko.PureComputed<boolean>;
|
||||
public showUpsellMessage: ko.PureComputed<boolean>;
|
||||
public shouldCreateMongoWildcardIndex: ko.Observable<boolean>;
|
||||
@@ -99,7 +100,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
super(options);
|
||||
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText());
|
||||
this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled());
|
||||
this.showUpsellMessage = ko.pureComputed(() => !this.container.isServerlessEnabled());
|
||||
this.formWarnings = ko.observable<string>();
|
||||
this.collectionId = ko.observable<string>();
|
||||
this.databaseId = ko.observable<string>();
|
||||
@@ -481,8 +481,20 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
this.resetData();
|
||||
});
|
||||
|
||||
this.freeTierExceedThroughputTooltip = ko.pureComputed<string>(() =>
|
||||
this.isFreeTierAccount() && !this.container.isFirstResourceCreated()
|
||||
? "The first 400 RU/s in this account are free. Billing will apply to any throughput beyond 400 RU/s."
|
||||
: ""
|
||||
);
|
||||
|
||||
this.upsellMessage = ko.pureComputed<string>(() => {
|
||||
return PricingUtils.getUpsellMessage(this.container.serverId(), this.isFreeTierAccount());
|
||||
return PricingUtils.getUpsellMessage(
|
||||
this.container.serverId(),
|
||||
this.isFreeTierAccount(),
|
||||
this.container.isFirstResourceCreated(),
|
||||
this.container.defaultExperience(),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
this.upsellMessageAriaLabel = ko.pureComputed<string>(() => {
|
||||
@@ -534,6 +546,23 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
return isFreeTierAccount;
|
||||
});
|
||||
|
||||
this.showUpsellMessage = ko.pureComputed(() => {
|
||||
if (this.container.isServerlessEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
this.isFreeTierAccount() &&
|
||||
!this.databaseCreateNew() &&
|
||||
this.databaseHasSharedOffer() &&
|
||||
!this.collectionWithThroughputInShared()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
this.showIndexingOptionsForSharedThroughput = ko.computed<boolean>(() => {
|
||||
const newDatabaseWithSharedOffer = this.databaseCreateNew() && this.databaseCreateNewShared();
|
||||
const existingDatabaseWithSharedOffer = !this.databaseCreateNew() && this.databaseHasSharedOffer();
|
||||
@@ -625,7 +654,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
});
|
||||
});
|
||||
|
||||
this.shouldCreateMongoWildcardIndex = ko.observable(false);
|
||||
this.shouldCreateMongoWildcardIndex = ko.observable(this.container.isMongoIndexingEnabled());
|
||||
}
|
||||
|
||||
public getSharedThroughputDefault(): boolean {
|
||||
@@ -650,6 +679,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
// TODO: Figure out if a database level partition split is about to happen once shared throughput read is available
|
||||
this.formWarnings("");
|
||||
this.databaseCreateNewShared(this.getSharedThroughputDefault());
|
||||
this.shouldCreateMongoWildcardIndex(this.container.isMongoIndexingEnabled());
|
||||
if (this.isPreferredApiTable() && !databaseId) {
|
||||
databaseId = SharedConstants.CollectionCreation.TablesAPIDefaultDatabase;
|
||||
}
|
||||
@@ -899,11 +929,13 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
this.databaseId("");
|
||||
this.partitionKey("");
|
||||
this.throughputSpendAck(false);
|
||||
this.isAutoPilotSelected(false);
|
||||
this.isSharedAutoPilotSelected(false);
|
||||
this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled());
|
||||
this.isSharedAutoPilotSelected(this.container.isAutoscaleDefaultEnabled());
|
||||
this.autoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
|
||||
this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
|
||||
|
||||
this.shouldCreateMongoWildcardIndex = ko.observable(this.container.isMongoIndexingEnabled());
|
||||
|
||||
this.uniqueKeys([]);
|
||||
this.useIndexingForSharedThroughput(true);
|
||||
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
maxAutoPilotThroughputSet: maxAutoPilotThroughputSet,
|
||||
autoPilotUsageCost: autoPilotUsageCost,
|
||||
canExceedMaximumValue: canExceedMaximumValue,
|
||||
showAutoPilot: !isFreeTierAccount()
|
||||
showAutoPilot: !isFreeTierAccount(),
|
||||
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
|
||||
}">
|
||||
</throughput-input-autopilot-v3>
|
||||
<p data-bind="visible: canRequestSupport">
|
||||
|
||||
@@ -77,7 +77,7 @@ describe("Add Database Pane", () => {
|
||||
explorer.databaseAccount(mockFreeTierDatabaseAccount);
|
||||
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
|
||||
expect(addDatabasePane.isFreeTierAccount()).toBe(true);
|
||||
expect(addDatabasePane.upsellMessage()).toContain("With free tier discount");
|
||||
expect(addDatabasePane.upsellMessage()).toContain("With free tier");
|
||||
expect(addDatabasePane.upsellAnchorUrl()).toBe(Constants.Urls.freeTierInformation);
|
||||
expect(addDatabasePane.upsellAnchorText()).toBe("Learn more");
|
||||
});
|
||||
|
||||
@@ -44,6 +44,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
public autoPilotUsageCost: ko.Computed<string>;
|
||||
public canExceedMaximumValue: ko.PureComputed<boolean>;
|
||||
public ruToolTipText: ko.Computed<string>;
|
||||
public freeTierExceedThroughputTooltip: ko.Computed<string>;
|
||||
public isFreeTierAccount: ko.Computed<boolean>;
|
||||
public canConfigureThroughput: ko.PureComputed<boolean>;
|
||||
public showUpsellMessage: ko.PureComputed<boolean>;
|
||||
@@ -54,7 +55,6 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
this.databaseId = ko.observable<string>();
|
||||
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText());
|
||||
this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled());
|
||||
this.showUpsellMessage = ko.pureComputed(() => !this.container.isServerlessEnabled());
|
||||
|
||||
this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue());
|
||||
|
||||
@@ -182,6 +182,18 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
return isFreeTierAccount;
|
||||
});
|
||||
|
||||
this.showUpsellMessage = ko.pureComputed(() => {
|
||||
if (this.container.isServerlessEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isFreeTierAccount()) {
|
||||
return this.databaseCreateNewShared();
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
this.maxThroughputRUText = ko.pureComputed(() => {
|
||||
return this.maxThroughputRU().toLocaleString();
|
||||
});
|
||||
@@ -219,8 +231,20 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
this.resetData();
|
||||
});
|
||||
|
||||
this.freeTierExceedThroughputTooltip = ko.pureComputed<string>(() =>
|
||||
this.isFreeTierAccount() && !this.container.isFirstResourceCreated()
|
||||
? "The first 400 RU/s in this account are free. Billing will apply to any throughput beyond 400 RU/s."
|
||||
: ""
|
||||
);
|
||||
|
||||
this.upsellMessage = ko.pureComputed<string>(() => {
|
||||
return PricingUtils.getUpsellMessage(this.container.serverId(), this.isFreeTierAccount());
|
||||
return PricingUtils.getUpsellMessage(
|
||||
this.container.serverId(),
|
||||
this.isFreeTierAccount(),
|
||||
this.container.isFirstResourceCreated(),
|
||||
this.container.defaultExperience(),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
this.upsellMessageAriaLabel = ko.pureComputed<string>(() => {
|
||||
@@ -313,7 +337,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
public resetData() {
|
||||
this.databaseId("");
|
||||
this.databaseCreateNewShared(this.getSharedThroughputDefault());
|
||||
this.isAutoPilotSelected(false);
|
||||
this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled());
|
||||
this.maxAutoPilotThroughputSet(AutoPilotUtils.minAutoPilotThroughput);
|
||||
this._updateThroughputLimitByDatabase();
|
||||
this.throughputSpendAck(false);
|
||||
|
||||
@@ -451,8 +451,8 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
public resetData() {
|
||||
super.resetData();
|
||||
const throughputDefaults = this.container.collectionCreationDefaults.throughput;
|
||||
this.isAutoPilotSelected(false);
|
||||
this.isSharedAutoPilotSelected(false);
|
||||
this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled());
|
||||
this.isSharedAutoPilotSelected(this.container.isAutoscaleDefaultEnabled());
|
||||
this.selectedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
|
||||
this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
|
||||
this.throughput(AddCollectionUtility.getMaxThroughput(this.container.collectionCreationDefaults, this.container));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
abstract class CacheBase<T> {
|
||||
public data: T[];
|
||||
public data: T[] | null;
|
||||
public sortOrder: any;
|
||||
public serverCallInProgress: boolean;
|
||||
|
||||
|
||||
@@ -16,14 +16,75 @@ import * as Entities from "../Entities";
|
||||
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
||||
import * as TableEntityProcessor from "../TableEntityProcessor";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
|
||||
interface IListTableEntitiesSegmentedResult extends Entities.IListTableEntitiesResult {
|
||||
ExceedMaximumRetries?: boolean;
|
||||
}
|
||||
|
||||
export interface ErrorDataModel {
|
||||
message: string;
|
||||
severity?: string;
|
||||
location?: {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
code?: string;
|
||||
}
|
||||
|
||||
function parseError(err: any): ErrorDataModel[] {
|
||||
try {
|
||||
return _parse(err);
|
||||
} catch (e) {
|
||||
return [<ErrorDataModel>{ message: JSON.stringify(err) }];
|
||||
}
|
||||
}
|
||||
|
||||
function _parse(err: any): ErrorDataModel[] {
|
||||
var normalizedErrors: ErrorDataModel[] = [];
|
||||
if (err.message && !err.code) {
|
||||
normalizedErrors.push(err);
|
||||
} else {
|
||||
const innerErrors: any[] = _getInnerErrors(err.message);
|
||||
normalizedErrors = innerErrors.map(innerError =>
|
||||
typeof innerError === "string" ? { message: innerError } : innerError
|
||||
);
|
||||
}
|
||||
|
||||
return normalizedErrors;
|
||||
}
|
||||
|
||||
function _getInnerErrors(message: string): any[] {
|
||||
/*
|
||||
The backend error message has an inner-message which is a stringified object.
|
||||
For SQL errors, the "errors" property is an array of SqlErrorDataModel.
|
||||
Example:
|
||||
"Message: {"Errors":["Resource with specified id or name already exists"]}\r\nActivityId: 80005000008d40b6a, Request URI: /apps/19000c000c0a0005/services/mctestdocdbprod-MasterService-0-00066ab9937/partitions/900005f9000e676fb8/replicas/13000000000955p"
|
||||
For non-SQL errors the "Errors" propery is an array of string.
|
||||
Example:
|
||||
"Message: {"errors":[{"severity":"Error","location":{"start":7,"end":8},"code":"SC1001","message":"Syntax error, incorrect syntax near '.'."}]}\r\nActivityId: d3300016d4084e310a, Request URI: /apps/12401f9e1df77/services/dc100232b1f44545/partitions/f86f3bc0001a2f78/replicas/13085003638s"
|
||||
*/
|
||||
|
||||
let innerMessage: any = null;
|
||||
|
||||
const singleLineMessage = message.replace(/[\r\n]|\r|\n/g, "");
|
||||
try {
|
||||
// Multi-Partition error flavor
|
||||
const regExp = /^(.*)ActivityId: (.*)/g;
|
||||
const regString = regExp.exec(singleLineMessage);
|
||||
const innerMessageString = regString[1];
|
||||
innerMessage = JSON.parse(innerMessageString);
|
||||
} catch (e) {
|
||||
// Single-partition error flavor
|
||||
const regExp = /^Message: (.*)ActivityId: (.*), Request URI: (.*)/g;
|
||||
const regString = regExp.exec(singleLineMessage);
|
||||
const innerMessageString = regString[1];
|
||||
innerMessage = JSON.parse(innerMessageString);
|
||||
}
|
||||
|
||||
return innerMessage.errors ? innerMessage.errors : innerMessage.Errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage Table Entity List ViewModel
|
||||
*/
|
||||
@@ -387,8 +448,17 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
||||
}
|
||||
})
|
||||
.catch((error: any) => {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
this.queryErrorMessage(errorMessage);
|
||||
const parsedErrors = parseError(error);
|
||||
var errors = parsedErrors.map(error => {
|
||||
return <ViewModels.QueryError>{
|
||||
message: error.message,
|
||||
start: error.location ? error.location.start : undefined,
|
||||
end: error.location ? error.location.end : undefined,
|
||||
code: error.code,
|
||||
severity: error.severity
|
||||
};
|
||||
});
|
||||
this.queryErrorMessage(errors[0].message);
|
||||
if (this.queryTablesTab.onLoadStartKey != null && this.queryTablesTab.onLoadStartKey != undefined) {
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.Tab,
|
||||
@@ -399,8 +469,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
||||
defaultExperience: this.queryTablesTab.collection.container.defaultExperience(),
|
||||
dataExplorerArea: Areas.Tab,
|
||||
tabTitle: this.queryTablesTab.tabTitle(),
|
||||
error: errorMessage,
|
||||
errorStack: getErrorStack(error)
|
||||
error: error
|
||||
},
|
||||
this.queryTablesTab.onLoadStartKey
|
||||
);
|
||||
@@ -421,47 +490,53 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
||||
* Note that this also means that we can get less entities than the requested download size in a successful call.
|
||||
* See Microsoft Azure API Documentation at: https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx
|
||||
*/
|
||||
private async prefetchData(
|
||||
private prefetchData(
|
||||
tableQuery: Entities.ITableQuery,
|
||||
downloadSize: number,
|
||||
currentRetry: number = 0
|
||||
): Promise<IListTableEntitiesSegmentedResult> {
|
||||
): Q.Promise<any> {
|
||||
if (!this.cache.serverCallInProgress) {
|
||||
this.cache.serverCallInProgress = true;
|
||||
this.allDownloaded = false;
|
||||
this.lastPrefetchTime = new Date().getTime();
|
||||
const time = this.lastPrefetchTime;
|
||||
var time = this.lastPrefetchTime;
|
||||
|
||||
var promise: Q.Promise<IListTableEntitiesSegmentedResult>;
|
||||
if (this._documentIterator && this.continuationToken) {
|
||||
// TODO handle Cassandra case
|
||||
const response = await this._documentIterator.fetchNext();
|
||||
const entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(response?.resources);
|
||||
|
||||
return {
|
||||
Results: entities,
|
||||
ContinuationToken: this._documentIterator.hasMoreResults()
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
let documents: IListTableEntitiesSegmentedResult;
|
||||
if (this.continuationToken && this.queryTablesTab.container.isPreferredApiCassandra()) {
|
||||
documents = await this.queryTablesTab.container.tableDataClient.queryDocuments(
|
||||
promise = Q(this._documentIterator.fetchNext().then(response => response.resources)).then(
|
||||
(documents: any[]) => {
|
||||
let entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(documents);
|
||||
let finalEntities: IListTableEntitiesSegmentedResult = <IListTableEntitiesSegmentedResult>{
|
||||
Results: entities,
|
||||
ContinuationToken: this._documentIterator.hasMoreResults()
|
||||
};
|
||||
return Q.resolve(finalEntities);
|
||||
}
|
||||
);
|
||||
} else if (this.continuationToken && this.queryTablesTab.container.isPreferredApiCassandra()) {
|
||||
promise = Q(
|
||||
this.queryTablesTab.container.tableDataClient.queryDocuments(
|
||||
this.queryTablesTab.collection,
|
||||
this.cqlQuery(),
|
||||
true,
|
||||
this.continuationToken
|
||||
);
|
||||
} else {
|
||||
const query = this.queryTablesTab.container.isPreferredApiCassandra() ? this.cqlQuery() : this.sqlQuery();
|
||||
documents = await this.queryTablesTab.container.tableDataClient.queryDocuments(
|
||||
this.queryTablesTab.collection,
|
||||
query,
|
||||
true
|
||||
);
|
||||
|
||||
)
|
||||
);
|
||||
} else {
|
||||
let query = this.sqlQuery();
|
||||
if (this.queryTablesTab.container.isPreferredApiCassandra()) {
|
||||
query = this.cqlQuery();
|
||||
}
|
||||
promise = Q(
|
||||
this.queryTablesTab.container.tableDataClient.queryDocuments(this.queryTablesTab.collection, query, true)
|
||||
);
|
||||
}
|
||||
return promise
|
||||
.then((result: IListTableEntitiesSegmentedResult) => {
|
||||
if (!this._documentIterator) {
|
||||
this._documentIterator = documents.iterator;
|
||||
this._documentIterator = result.iterator;
|
||||
}
|
||||
var actualDownloadSize: number = 0;
|
||||
|
||||
@@ -472,11 +547,11 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
||||
return Q.resolve(null);
|
||||
}
|
||||
|
||||
var entities = documents.Results;
|
||||
var entities = result.Results;
|
||||
actualDownloadSize = entities.length;
|
||||
|
||||
// Queries can fetch no results and still return a continuation header. See prefetchAndRender() method.
|
||||
this.continuationToken = this.isCancelled ? null : documents.ContinuationToken;
|
||||
this.continuationToken = this.isCancelled ? null : result.ContinuationToken;
|
||||
|
||||
if (!this.continuationToken) {
|
||||
this.allDownloaded = true;
|
||||
@@ -508,22 +583,20 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
||||
// For #2.1, set prefetch exceeds maximum retry number and end prefetch.
|
||||
// For #2.2, go to next round prefetch.
|
||||
if (this.allDownloaded || nextDownloadSize === 0) {
|
||||
return documents;
|
||||
return Q.resolve(result);
|
||||
}
|
||||
|
||||
if (currentRetry >= TableEntityListViewModel._maximumNumberOfPrefetchRetries) {
|
||||
documents.ExceedMaximumRetries = true;
|
||||
return documents;
|
||||
result.ExceedMaximumRetries = true;
|
||||
return Q.resolve(result);
|
||||
}
|
||||
|
||||
return await this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1);
|
||||
}
|
||||
} catch (error) {
|
||||
this.cache.serverCallInProgress = false;
|
||||
throw error;
|
||||
}
|
||||
return this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1);
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
this.cache.serverCallInProgress = false;
|
||||
return Q.reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface ITableEntity {
|
||||
export interface ITableEntityForTablesAPI extends ITableEntity {
|
||||
PartitionKey: ITableEntityAttribute;
|
||||
RowKey: ITableEntityAttribute;
|
||||
Timestamp?: ITableEntityAttribute;
|
||||
Timestamp: ITableEntityAttribute;
|
||||
}
|
||||
|
||||
export interface ITableEntityAttribute {
|
||||
|
||||
@@ -265,11 +265,9 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
authType === AuthType.EncryptedToken
|
||||
? Constants.CassandraBackend.guestQueryApi
|
||||
: Constants.CassandraBackend.queryApi;
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
|
||||
const response = await fetch(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
const data: any = await $.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
|
||||
type: "POST",
|
||||
data: {
|
||||
accountName:
|
||||
collection && collection.container.databaseAccount && collection.container.databaseAccount().name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(
|
||||
@@ -280,19 +278,11 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
tableId: collection.id(),
|
||||
query,
|
||||
paginationToken
|
||||
}),
|
||||
headers: {
|
||||
[authorizationHeader.header]: authorizationHeader.token,
|
||||
[Constants.HttpHeaders.contentType]: "application/json"
|
||||
}
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader,
|
||||
error: this.handleAjaxError,
|
||||
cache: false
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
displayTokenRenewalPromptForStatus(response.status);
|
||||
throw Error(`Failed to query rows for table ${collection.id()}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
shouldNotify &&
|
||||
NotificationConsoleUtils.logConsoleInfo(
|
||||
`Successfully fetched ${data.result.length} rows for table ${collection.id()}`
|
||||
@@ -322,9 +312,10 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting row ${currEntityToDelete.RowKey._}`);
|
||||
const partitionKeyValue = currEntityToDelete[partitionKeyProperty];
|
||||
const currQuery =
|
||||
query + this.isStringType(partitionKeyValue.$)
|
||||
query +
|
||||
(this.isStringType(partitionKeyValue.$)
|
||||
? `${partitionKeyProperty} = '${partitionKeyValue._}'`
|
||||
: `${partitionKeyProperty} = ${partitionKeyValue._}`;
|
||||
: `${partitionKeyProperty} = ${partitionKeyValue._}`);
|
||||
|
||||
try {
|
||||
await this.queryDocuments(collection, currQuery);
|
||||
@@ -460,9 +451,9 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public async getTableKeys(collection: ViewModels.Collection): Promise<CassandraTableKeys> {
|
||||
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
||||
if (!!collection.cassandraKeys) {
|
||||
return collection.cassandraKeys;
|
||||
return Q.resolve(collection.cassandraKeys);
|
||||
}
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
@@ -474,51 +465,45 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
? Constants.CassandraBackend.guestKeysApi
|
||||
: Constants.CassandraBackend.keysApi;
|
||||
let endpoint = `${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`;
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
accountName:
|
||||
collection && collection.container.databaseAccount && collection.container.databaseAccount().name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(
|
||||
collection.container.databaseAccount().properties.cassandraEndpoint
|
||||
),
|
||||
resourceId: collection.container.databaseAccount().id,
|
||||
keyspaceId: collection.databaseId,
|
||||
tableId: collection.id()
|
||||
}),
|
||||
headers: {
|
||||
[authorizationHeader.header]: authorizationHeader.token,
|
||||
[Constants.HttpHeaders.contentType]: "application/json"
|
||||
const deferred = Q.defer<CassandraTableKeys>();
|
||||
$.ajax(endpoint, {
|
||||
type: "POST",
|
||||
data: {
|
||||
accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(
|
||||
collection.container.databaseAccount().properties.cassandraEndpoint
|
||||
),
|
||||
resourceId: collection.container.databaseAccount().id,
|
||||
keyspaceId: collection.databaseId,
|
||||
tableId: collection.id()
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader,
|
||||
error: this.handleAjaxError,
|
||||
cache: false
|
||||
})
|
||||
.then(
|
||||
(data: CassandraTableKeys) => {
|
||||
collection.cassandraKeys = data;
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully fetched keys for table ${collection.id()}`
|
||||
);
|
||||
deferred.resolve(data);
|
||||
},
|
||||
(error: any) => {
|
||||
handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.done(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
displayTokenRenewalPromptForStatus(response.status);
|
||||
throw Error(`Fetching keys for table ${collection.id()} failed`);
|
||||
}
|
||||
|
||||
const data: CassandraTableKeys = await response.json();
|
||||
collection.cassandraKeys = data;
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully fetched keys for table ${collection.id()}`
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
|
||||
throw error;
|
||||
} finally {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
}
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public async getTableSchema(collection: ViewModels.Collection): Promise<CassandraTableKey[]> {
|
||||
public getTableSchema(collection: ViewModels.Collection): Q.Promise<CassandraTableKey[]> {
|
||||
if (!!collection.cassandraSchema) {
|
||||
return collection.cassandraSchema;
|
||||
return Q.resolve(collection.cassandraSchema);
|
||||
}
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
@@ -530,79 +515,74 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
? Constants.CassandraBackend.guestSchemaApi
|
||||
: Constants.CassandraBackend.schemaApi;
|
||||
let endpoint = `${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`;
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
accountName:
|
||||
collection && collection.container.databaseAccount && collection.container.databaseAccount().name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(
|
||||
collection.container.databaseAccount().properties.cassandraEndpoint
|
||||
),
|
||||
resourceId: collection.container.databaseAccount().id,
|
||||
keyspaceId: collection.databaseId,
|
||||
tableId: collection.id()
|
||||
}),
|
||||
headers: {
|
||||
[authorizationHeader.header]: authorizationHeader.token,
|
||||
[Constants.HttpHeaders.contentType]: "application/json"
|
||||
const deferred = Q.defer<CassandraTableKey[]>();
|
||||
$.ajax(endpoint, {
|
||||
type: "POST",
|
||||
data: {
|
||||
accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(
|
||||
collection.container.databaseAccount().properties.cassandraEndpoint
|
||||
),
|
||||
resourceId: collection.container.databaseAccount().id,
|
||||
keyspaceId: collection.databaseId,
|
||||
tableId: collection.id()
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader,
|
||||
error: this.handleAjaxError,
|
||||
cache: false
|
||||
})
|
||||
.then(
|
||||
(data: any) => {
|
||||
collection.cassandraSchema = data.columns;
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully fetched schema for table ${collection.id()}`
|
||||
);
|
||||
deferred.resolve(data.columns);
|
||||
},
|
||||
(error: any) => {
|
||||
handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.done(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
displayTokenRenewalPromptForStatus(response.status);
|
||||
throw Error(`Failed to fetch schema for table ${collection.id()}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
collection.cassandraSchema = data.columns;
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully fetched schema for table ${collection.id()}`
|
||||
);
|
||||
|
||||
return data.columns;
|
||||
} catch (error) {
|
||||
handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
|
||||
throw error;
|
||||
} finally {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
}
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
private async createOrDeleteQuery(
|
||||
private createOrDeleteQuery(
|
||||
cassandraEndpoint: string,
|
||||
resourceId: string,
|
||||
query: string,
|
||||
explorer: Explorer
|
||||
): Promise<void> {
|
||||
): Q.Promise<any> {
|
||||
const deferred = Q.defer();
|
||||
const authType = window.authType;
|
||||
const apiEndpoint: string =
|
||||
authType === AuthType.EncryptedToken
|
||||
? Constants.CassandraBackend.guestCreateOrDeleteApi
|
||||
: Constants.CassandraBackend.createOrDeleteApi;
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
|
||||
const response = await fetch(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
$.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
|
||||
type: "POST",
|
||||
data: {
|
||||
accountName: explorer.databaseAccount() && explorer.databaseAccount().name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(cassandraEndpoint),
|
||||
resourceId,
|
||||
query
|
||||
}),
|
||||
headers: {
|
||||
[authorizationHeader.header]: authorizationHeader.token,
|
||||
[Constants.HttpHeaders.contentType]: "application/json"
|
||||
resourceId: resourceId,
|
||||
query: query
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader,
|
||||
error: this.handleAjaxError,
|
||||
cache: false
|
||||
}).then(
|
||||
(data: any) => {
|
||||
deferred.resolve();
|
||||
},
|
||||
reason => {
|
||||
deferred.reject(reason);
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
displayTokenRenewalPromptForStatus(response.status);
|
||||
throw Error(`Failed to create or delete keyspace/table`);
|
||||
}
|
||||
);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
private trimCassandraEndpoint(cassandraEndpoint: string): string {
|
||||
@@ -621,6 +601,13 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
return cassandraEndpoint;
|
||||
}
|
||||
|
||||
private setAuthorizationHeader: (xhr: XMLHttpRequest) => boolean = (xhr: XMLHttpRequest): boolean => {
|
||||
const authorizationHeaderMetadata: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||
xhr.setRequestHeader(authorizationHeaderMetadata.header, authorizationHeaderMetadata.token);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
private isStringType(dataType: string): boolean {
|
||||
// TODO figure out rest of types that are considered strings by Cassandra (if any have been missed)
|
||||
return (
|
||||
@@ -634,4 +621,12 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
private getCassandraPartitionKeyProperty(collection: ViewModels.Collection): string {
|
||||
return collection.cassandraKeys.partitionKeys[0].property;
|
||||
}
|
||||
|
||||
private handleAjaxError = (xhrObj: XMLHttpRequest, textStatus: string, errorThrown: string): void => {
|
||||
if (!xhrObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
displayTokenRenewalPromptForStatus(xhrObj.status);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,6 +23,19 @@
|
||||
<div class="scaleDivison" aria-label="Scale" aria-controls="scaleRegion">
|
||||
<span class="scaleSettingTitle">Scale</span>
|
||||
</div>
|
||||
<div class="freeTierInfoBanner" data-bind="visible: isFreeTierAccount">
|
||||
<span class="freeTierInfoIcon"><img src="/info_color.svg" alt="Info"/></span>
|
||||
<span class="freeTierInfoMessage"
|
||||
>With free tier, you'll get the first 400 RU/s and 5 GB of storage in this account for free. To keep your
|
||||
account free, keep the total RU/s across all resources in the account to 400 RU/s.
|
||||
<a
|
||||
href="https://docs.microsoft.com/en-us/azure/cosmos-db/understand-your-bill#billing-examples-with-free-tier-accounts"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more.</a
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<div class="ssTextAllignment" id="scaleRegion">
|
||||
<throughput-input-autopilot-v3
|
||||
params="{
|
||||
@@ -46,7 +59,8 @@
|
||||
autoPilotUsageCost: autoPilotUsageCost,
|
||||
canExceedMaximumValue: canExceedMaximumValue,
|
||||
overrideWithAutoPilotSettings: overrideWithAutoPilotSettings,
|
||||
overrideWithProvisionedThroughputSettings: overrideWithProvisionedThroughputSettings
|
||||
overrideWithProvisionedThroughputSettings: overrideWithProvisionedThroughputSettings,
|
||||
freeTierExceedThroughputWarning: freeTierExceedThroughputWarning
|
||||
}"
|
||||
>
|
||||
</throughput-input-autopilot-v3>
|
||||
|
||||
@@ -57,6 +57,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
public canThroughputExceedMaximumValue: ko.Computed<boolean>;
|
||||
public costsVisible: ko.Computed<boolean>;
|
||||
public displayedError: ko.Observable<string>;
|
||||
public isFreeTierAccount: ko.Computed<boolean>;
|
||||
public isTemplateReady: ko.Observable<boolean>;
|
||||
public minRUAnotationVisible: ko.Computed<boolean>;
|
||||
public minRUs: ko.Observable<number>;
|
||||
@@ -82,6 +83,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
public throughputAutoPilotRadioId: string;
|
||||
public throughputProvisionedRadioId: string;
|
||||
public throughputModeRadioName: string;
|
||||
public freeTierExceedThroughputWarning: ko.Computed<string>;
|
||||
|
||||
private _hasProvisioningTypeChanged: ko.Computed<boolean>;
|
||||
private _wasAutopilotOriginallySet: ko.Observable<boolean>;
|
||||
@@ -359,6 +361,17 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
|
||||
this.isTemplateReady = ko.observable<boolean>(false);
|
||||
|
||||
this.isFreeTierAccount = ko.computed<boolean>(() => {
|
||||
const databaseAccount = this.container?.databaseAccount();
|
||||
return databaseAccount?.properties?.enableFreeTier;
|
||||
});
|
||||
|
||||
this.freeTierExceedThroughputWarning = ko.computed<string>(() =>
|
||||
this.isFreeTierAccount()
|
||||
? "Billing will apply if you provision more than 400 RU/s of manual throughput, or if the resource scales beyond 400 RU/s with autoscale."
|
||||
: ""
|
||||
);
|
||||
|
||||
this._buildCommandBarOptions();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
Start by writing a Mongo query, for example: <strong>{'id':'foo'}</strong> or <strong>{ }</strong> to get all the
|
||||
documents.
|
||||
</div>
|
||||
<div class="warningErrorContainer" aria-live="assertive" data-bind="visible: maybeSubQuery">
|
||||
<div class="warningErrorContent">
|
||||
<span><img class="paneErrorIcon" src="/info_color.svg" alt="Error"/></span>
|
||||
<span class="warningErrorDetailsLinkContainer">
|
||||
We have detected you may be using a subquery. Non-correlated subqueries are not currently supported.
|
||||
<a href="https://docs.microsoft.com/en-us/azure/cosmos-db/sql-query-subquery"
|
||||
>Please see Cosmos sub query documentation for further information</a
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="queryEditorWithSplitter" data-bind="attr: { id: queryEditorId }">
|
||||
<editor
|
||||
class="queryEditor"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as ko from "knockout";
|
||||
import Q from "q";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
@@ -7,7 +6,6 @@ import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import TabsBase from "./TabsBase";
|
||||
import { HashMap } from "../../Common/HashMap";
|
||||
import * as HeadersUtility from "../../Common/HeadersUtility";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
||||
@@ -31,6 +29,7 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
|
||||
public fetchNextPageButton: ViewModels.Button;
|
||||
public saveQueryButton: ViewModels.Button;
|
||||
public initialEditorContent: ko.Observable<string>;
|
||||
public maybeSubQuery: ko.Computed<boolean>;
|
||||
public sqlQueryEditorContent: ko.Observable<string>;
|
||||
public selectedContent: ko.Observable<string>;
|
||||
public sqlStatementToExecute: ko.Observable<string>;
|
||||
@@ -120,6 +119,11 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
|
||||
return (container && (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph())) || false;
|
||||
});
|
||||
|
||||
this.maybeSubQuery = ko.computed<boolean>(function() {
|
||||
const sql = this.sqlQueryEditorContent();
|
||||
return sql && /.*\(.*SELECT.*\)/i.test(sql);
|
||||
}, this);
|
||||
|
||||
this.saveQueryButton = {
|
||||
enabled: this._isSaveQueriesEnabled,
|
||||
visible: this._isSaveQueriesEnabled
|
||||
@@ -324,21 +328,6 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
|
||||
queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`;
|
||||
this.showingDocumentsDisplayText(resultsDisplay);
|
||||
this.requestChargeDisplayText(`${queryResults.requestCharge} RUs`);
|
||||
|
||||
if (!this.queryResults() && !results) {
|
||||
const errorMessage: string = JSON.stringify({
|
||||
error: `Returned no results after query execution`,
|
||||
accountName: this.collection && this.collection.container.databaseAccount(),
|
||||
databaseName: this.collection && this.collection.databaseId,
|
||||
collectionName: this.collection && this.collection.id(),
|
||||
sqlQuery: this.sqlStatementToExecute(),
|
||||
hasMoreResults: resultsMetadata.hasMoreResults,
|
||||
itemCount: resultsMetadata.itemCount,
|
||||
responseHeaders: queryResults && queryResults.headers
|
||||
});
|
||||
Logger.logError(errorMessage, "QueryTab");
|
||||
}
|
||||
|
||||
this.queryResults(results);
|
||||
|
||||
TelemetryProcessor.traceSuccess(
|
||||
|
||||
@@ -8,7 +8,7 @@ enum ScrollPosition {
|
||||
|
||||
export class AccessibleVerticalList {
|
||||
private items: any[] = [];
|
||||
private onSelect: (item: any) => void;
|
||||
private onSelect?: (item: any) => void;
|
||||
|
||||
public currentItemIndex: ko.Observable<number>;
|
||||
public currentItem: ko.Computed<any>;
|
||||
@@ -42,7 +42,9 @@ export class AccessibleVerticalList {
|
||||
const targetElement = targetContainer
|
||||
.getElementsByClassName("accessibleListElement")
|
||||
.item(this.currentItemIndex());
|
||||
this.scrollElementIntoContainerViewIfNeeded(targetElement, targetContainer, ScrollPosition.Top);
|
||||
if (targetElement) {
|
||||
this.scrollElementIntoContainerViewIfNeeded(targetElement, targetContainer, ScrollPosition.Top);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (event.keyCode === 40) {
|
||||
@@ -52,7 +54,9 @@ export class AccessibleVerticalList {
|
||||
const targetElement = targetContainer
|
||||
.getElementsByClassName("accessibleListElement")
|
||||
.item(this.currentItemIndex());
|
||||
this.scrollElementIntoContainerViewIfNeeded(targetElement, targetContainer, ScrollPosition.Bottom);
|
||||
if (targetElement) {
|
||||
this.scrollElementIntoContainerViewIfNeeded(targetElement, targetContainer, ScrollPosition.Top);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
34
src/Main.tsx
34
src/Main.tsx
@@ -126,7 +126,17 @@ const App: React.FunctionComponent = () => {
|
||||
|
||||
return (
|
||||
<div className="flexContainer">
|
||||
<div id="divExplorer" className="flexContainer hideOverflows" style={{ display: "none" }}>
|
||||
<div
|
||||
id="divSelfServe"
|
||||
className="flexContainer"
|
||||
data-bind="visible: selfServeType() && selfServeType() !== 'none', react: selfServeComponentAdapter"
|
||||
></div>
|
||||
<div
|
||||
id="divExplorer"
|
||||
data-bind="if: selfServeType() === 'none'"
|
||||
className="flexContainer hideOverflows"
|
||||
style={{ display: "none" }}
|
||||
>
|
||||
{/* Main Command Bar - Start */}
|
||||
<div data-bind="react: commandBarComponentAdapter" />
|
||||
{/* Main Command Bar - End */}
|
||||
@@ -371,17 +381,21 @@ const App: React.FunctionComponent = () => {
|
||||
</div>
|
||||
{/* Explorer Connection - Encryption Token / AAD - End */}
|
||||
{/* Global loader - Start */}
|
||||
|
||||
<div className="splashLoaderContainer" data-bind="visible: isRefreshingExplorer">
|
||||
<div className="splashLoaderContentContainer">
|
||||
<p className="connectExplorerContent">
|
||||
<img src={hdeConnectImage} alt="Azure Cosmos DB" />
|
||||
</p>
|
||||
<p className="splashLoaderTitle" id="explorerLoadingStatusTitle">
|
||||
Welcome to Azure Cosmos DB
|
||||
</p>
|
||||
<p className="splashLoaderText" id="explorerLoadingStatusText" role="alert">
|
||||
Connecting...
|
||||
</p>
|
||||
<div data-bind="visible: selfServeType() === undefined, react: selfServeLoadingComponentAdapter"></div>
|
||||
<div data-bind="if: selfServeType() === 'none'" style={{ display: "none" }}>
|
||||
<p className="connectExplorerContent">
|
||||
<img src={hdeConnectImage} alt="Azure Cosmos DB" />
|
||||
</p>
|
||||
<p className="splashLoaderTitle" id="explorerLoadingStatusTitle">
|
||||
Welcome to Azure Cosmos DB
|
||||
</p>
|
||||
<p className="splashLoaderText" id="explorerLoadingStatusText" role="alert">
|
||||
Connecting...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Global loader - End */}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility"
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||
|
||||
export default class AuthHeadersUtil {
|
||||
public static serverId: string = Constants.ServerIds.productionPortal;
|
||||
@@ -38,7 +37,8 @@ export default class AuthHeadersUtil {
|
||||
cacheLocation: window.navigator.userAgent.indexOf("Edge") > -1 ? "localStorage" : undefined
|
||||
});
|
||||
|
||||
public static async getAccessInputMetadata(accessInput: string): Promise<DataModels.AccessInputMetadata> {
|
||||
public static getAccessInputMetadata(accessInput: string): Q.Promise<DataModels.AccessInputMetadata> {
|
||||
const deferred: Q.Deferred<DataModels.AccessInputMetadata> = Q.defer<DataModels.AccessInputMetadata>();
|
||||
const url = `${configContext.BACKEND_ENDPOINT}${Constants.ApiEndpoints.guestRuntimeProxy}/accessinputmetadata`;
|
||||
const authType: string = (<any>window).authType;
|
||||
const headers: { [headerName: string]: string } = {};
|
||||
@@ -49,55 +49,58 @@ export default class AuthHeadersUtil {
|
||||
headers[Constants.HttpHeaders.connectionString] = accessInput;
|
||||
}
|
||||
|
||||
let responseText: string;
|
||||
try {
|
||||
const timeout = setTimeout(() => {
|
||||
throw Error("Request timed out while fetching access input metadata");
|
||||
}, Constants.ClientDefaults.requestTimeoutMs);
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "GET",
|
||||
headers: headers,
|
||||
cache: false,
|
||||
dataType: "text"
|
||||
}).then(
|
||||
(data: string, textStatus: string, xhr: JQueryXHR<any>) => {
|
||||
if (!data) {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to get access input metadata`);
|
||||
deferred.reject(`Failed to get access input metadata`);
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers,
|
||||
method: "GET"
|
||||
});
|
||||
|
||||
clearTimeout(timeout);
|
||||
|
||||
responseText = await response.text();
|
||||
if (!response.ok || !responseText) {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to get access input metadata`);
|
||||
throw Error("Failed to get access input metadata");
|
||||
try {
|
||||
const metadata: DataModels.AccessInputMetadata = JSON.parse(JSON.parse(data));
|
||||
deferred.resolve(metadata); // TODO: update to a single JSON parse once backend response is stringified exactly once
|
||||
} catch (error) {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, "Failed to parse access input metadata");
|
||||
deferred.reject("Failed to parse access input metadata");
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
(xhr: JQueryXHR<any>, textStatus: string, error: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while fetching access input metadata: ${JSON.stringify(xhr.responseText)}`
|
||||
);
|
||||
deferred.reject(xhr.responseText);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage: string = getErrorMessage(error);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while fetching access input metadata: ${errorMessage}`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
const metadata: DataModels.AccessInputMetadata = JSON.parse(JSON.parse(responseText));
|
||||
return metadata; // TODO: update to a single JSON parse once backend response is stringified exactly once
|
||||
} catch (error) {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, "Failed to parse access input metadata");
|
||||
throw error;
|
||||
}
|
||||
return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs);
|
||||
}
|
||||
|
||||
public static async generateEncryptedToken(): Promise<DataModels.GenerateTokenResponse> {
|
||||
public static generateEncryptedToken(): Q.Promise<DataModels.GenerateTokenResponse> {
|
||||
const url = configContext.BACKEND_ENDPOINT + "/api/tokens/generateToken" + AuthHeadersUtil._generateResourceUrl();
|
||||
const explorer = window.dataExplorer;
|
||||
const headers: any = { authorization: userContext.authorizationToken };
|
||||
headers[Constants.HttpHeaders.getReadOnlyKey] = !explorer.hasWriteAccess();
|
||||
headers[Constants.HttpHeaders.contentType] = "application/json";
|
||||
|
||||
return await AuthHeadersUtil._initiateGenerateTokenRequest(url, "POST", headers);
|
||||
return AuthHeadersUtil._initiateGenerateTokenRequest({
|
||||
url: url,
|
||||
type: "POST",
|
||||
headers: headers,
|
||||
contentType: "application/json",
|
||||
cache: false
|
||||
});
|
||||
}
|
||||
|
||||
public static async generateUnauthenticatedEncryptedTokenForConnectionString(
|
||||
public static generateUnauthenticatedEncryptedTokenForConnectionString(
|
||||
connectionString: string
|
||||
): Promise<DataModels.GenerateTokenResponse> {
|
||||
): Q.Promise<DataModels.GenerateTokenResponse> {
|
||||
if (!connectionString) {
|
||||
return Q.reject("None or empty connection string specified");
|
||||
}
|
||||
@@ -105,9 +108,14 @@ export default class AuthHeadersUtil {
|
||||
const url = configContext.BACKEND_ENDPOINT + "/api/guest/tokens/generateToken";
|
||||
const headers: any = {};
|
||||
headers[Constants.HttpHeaders.connectionString] = connectionString;
|
||||
headers[Constants.HttpHeaders.contentType] = "application/json";
|
||||
|
||||
return await AuthHeadersUtil._initiateGenerateTokenRequest(url, "POST", headers);
|
||||
return AuthHeadersUtil._initiateGenerateTokenRequest({
|
||||
url: url,
|
||||
type: "POST",
|
||||
headers: headers,
|
||||
contentType: "application/json",
|
||||
cache: false
|
||||
});
|
||||
}
|
||||
|
||||
public static isUserSignedIn(): boolean {
|
||||
@@ -274,27 +282,24 @@ export default class AuthHeadersUtil {
|
||||
return `?resourceUrl=${resourceUrl}&rid=${rid}&rtype=${rtype}&sid=${sid}&rg=${rg}&dba=${dba}&api=${apiKind}`;
|
||||
}
|
||||
|
||||
private static async _initiateGenerateTokenRequest(
|
||||
url: string,
|
||||
method: string,
|
||||
headers: any
|
||||
): Promise<DataModels.GenerateTokenResponse> {
|
||||
const timeout = setTimeout(() => {
|
||||
throw Error("Request timed out while generating token");
|
||||
}, Constants.ClientDefaults.requestTimeoutMs);
|
||||
private static _initiateGenerateTokenRequest(
|
||||
requestSettings: JQueryAjaxSettings<any>
|
||||
): Q.Promise<DataModels.GenerateTokenResponse> {
|
||||
const deferred: Q.Deferred<DataModels.GenerateTokenResponse> = Q.defer<DataModels.GenerateTokenResponse>();
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers,
|
||||
method
|
||||
});
|
||||
$.ajax(requestSettings).then(
|
||||
(data: string, textStatus: string, xhr: JQueryXHR<any>) => {
|
||||
if (!data) {
|
||||
deferred.reject("No token generated");
|
||||
}
|
||||
|
||||
clearTimeout(timeout);
|
||||
deferred.resolve(JSON.parse(data));
|
||||
},
|
||||
(xhr: JQueryXHR<any>, textStatus: string, error: any) => {
|
||||
deferred.reject(xhr.responseText);
|
||||
}
|
||||
);
|
||||
|
||||
const token: string = await response.json();
|
||||
if (response.ok && token) {
|
||||
return JSON.parse(token);
|
||||
}
|
||||
|
||||
throw Error("No token generated");
|
||||
return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs);
|
||||
}
|
||||
}
|
||||
|
||||
14
src/SelfServe/ClassDecorators.tsx
Normal file
14
src/SelfServe/ClassDecorators.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Info } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
||||
import { addPropertyToMap, buildSmartUiDescriptor } from "./SelfServeUtils";
|
||||
|
||||
export const IsDisplayable = (): ClassDecorator => {
|
||||
return target => {
|
||||
buildSmartUiDescriptor(target.name, target.prototype);
|
||||
};
|
||||
};
|
||||
|
||||
export const ClassInfo = (info: (() => Promise<Info>) | Info): ClassDecorator => {
|
||||
return target => {
|
||||
addPropertyToMap(target.prototype, "root", target.name, "info", info);
|
||||
};
|
||||
};
|
||||
167
src/SelfServe/Example/SelfServeExample.tsx
Normal file
167
src/SelfServe/Example/SelfServeExample.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import { PropertyInfo, OnChange, Values } from "../PropertyDecorators";
|
||||
import { ClassInfo, IsDisplayable } from "../ClassDecorators";
|
||||
import { SelfServeBaseClass } from "../SelfServeUtils";
|
||||
import { ChoiceItem, Info, InputType, UiType } from "../../Explorer/Controls/SmartUi/SmartUiComponent";
|
||||
import { SessionStorageUtility } from "../../Shared/StorageUtility";
|
||||
|
||||
export enum Regions {
|
||||
NorthCentralUS = "NCUS",
|
||||
WestUS = "WUS",
|
||||
EastUS2 = "EUS2"
|
||||
}
|
||||
|
||||
export const regionDropdownItems: ChoiceItem[] = [
|
||||
{ label: "North Central US", key: Regions.NorthCentralUS },
|
||||
{ label: "West US", key: Regions.WestUS },
|
||||
{ label: "East US 2", key: Regions.EastUS2 }
|
||||
];
|
||||
|
||||
export const selfServeExampleInfo: Info = {
|
||||
message: "This is a self serve class"
|
||||
};
|
||||
|
||||
export const regionDropdownInfo: Info = {
|
||||
message: "More regions can be added in the future."
|
||||
};
|
||||
|
||||
const onDbThroughputChange = (currentState: Map<string, InputType>, newValue: InputType): Map<string, InputType> => {
|
||||
currentState.set("dbThroughput", newValue);
|
||||
currentState.set("collectionThroughput", newValue);
|
||||
return currentState;
|
||||
};
|
||||
|
||||
const initializeMaxThroughput = async (): Promise<number> => {
|
||||
return 10000;
|
||||
};
|
||||
|
||||
/*
|
||||
This is an example self serve class that auto generates UI components for your feature.
|
||||
|
||||
Each self serve class
|
||||
- Needs to extends the SelfServeBase class.
|
||||
- Needs to have the @IsDisplayable() decorator to tell the compiler that UI needs to be generated from this class.
|
||||
- Needs to define an onSubmit() function, a callback for when the submit button is clicked.
|
||||
- Needs to define an initialize() function, to set default values for the inputs.
|
||||
|
||||
You can test this self serve UI by using the featureflag '?feature.selfServeType=example'
|
||||
and plumb in similar feature flags for your own self serve class.
|
||||
*/
|
||||
|
||||
/*
|
||||
@IsDisplayable()
|
||||
- role: Indicates to the compiler that UI should be generated from this class.
|
||||
*/
|
||||
@IsDisplayable()
|
||||
/*
|
||||
@ClassInfo()
|
||||
- optional
|
||||
- input: Info | () => Promise<Info>
|
||||
- role: Display an Info bar as the first element of the UI.
|
||||
*/
|
||||
@ClassInfo(selfServeExampleInfo)
|
||||
export default class SelfServeExample extends SelfServeBaseClass {
|
||||
/*
|
||||
onSubmit()
|
||||
- input: (currentValues: Map<string, InputType>) => Promise<void>
|
||||
- role: Callback that is triggerred when the submit button is clicked. You should perform your rest API
|
||||
calls here using the data from the different inputs passed as a Map to this callback function.
|
||||
|
||||
In this example, the onSubmit callback simply sets the value for keys corresponding to the field name
|
||||
in the SessionStorage.
|
||||
*/
|
||||
public onSubmit = async (currentValues: Map<string, InputType>): Promise<void> => {
|
||||
SessionStorageUtility.setEntry("regions", currentValues.get("regions")?.toString());
|
||||
SessionStorageUtility.setEntry("enableLogging", currentValues.get("enableLogging")?.toString());
|
||||
SessionStorageUtility.setEntry("accountName", currentValues.get("accountName")?.toString());
|
||||
SessionStorageUtility.setEntry("dbThroughput", currentValues.get("dbThroughput")?.toString());
|
||||
SessionStorageUtility.setEntry("collectionThroughput", currentValues.get("collectionThroughput")?.toString());
|
||||
};
|
||||
|
||||
/*
|
||||
initialize()
|
||||
- input: () => Promise<Map<string, InputType>>
|
||||
- role: Set default values for the properties of this class.
|
||||
|
||||
The properties of this class (namely regions, enableLogging, accountName, dbThroughput, collectionThroughput),
|
||||
having the @Values decorator, will each correspond to an UI element. Their values can be of 'InputType'. Their
|
||||
defaults can be set by setting values in a Map corresponding to the field's name.
|
||||
|
||||
Typically, you can make rest calls in the async initialize function, to fetch the initial values for
|
||||
these fields. This is called after the onSubmit callback, to reinitialize the defaults.
|
||||
|
||||
In this example, the initialize function simply reads the SessionStorage to fetch the default values
|
||||
for these fields. These are then set when the changes are submitted.
|
||||
*/
|
||||
public initialize = async (): Promise<Map<string, InputType>> => {
|
||||
const defaults = new Map<string, InputType>();
|
||||
defaults.set("regions", SessionStorageUtility.getEntry("regions"));
|
||||
defaults.set("enableLogging", SessionStorageUtility.getEntry("enableLogging") === "true");
|
||||
const stringInput = SessionStorageUtility.getEntry("accountName");
|
||||
defaults.set("accountName", stringInput ? stringInput : "");
|
||||
const numberSliderInput = parseInt(SessionStorageUtility.getEntry("dbThroughput"));
|
||||
defaults.set("dbThroughput", isNaN(numberSliderInput) ? 1 : numberSliderInput);
|
||||
const numberSpinnerInput = parseInt(SessionStorageUtility.getEntry("collectionThroughput"));
|
||||
defaults.set("collectionThroughput", isNaN(numberSpinnerInput) ? 1 : numberSpinnerInput);
|
||||
return defaults;
|
||||
};
|
||||
|
||||
/*
|
||||
@PropertyInfo()
|
||||
- optional
|
||||
- input: Info | () => Promise<Info>
|
||||
- role: Display an Info bar above the UI element for this property.
|
||||
*/
|
||||
@PropertyInfo(regionDropdownInfo)
|
||||
|
||||
/*
|
||||
@Values() :
|
||||
- input: NumberInputOptions | StringInputOptions | BooleanInputOptions | ChoiceInputOptions
|
||||
- role: Specifies the required options to display the property as TextBox, Number Spinner/Slider, Radio buton or Dropdown.
|
||||
*/
|
||||
@Values({ label: "Regions", choices: regionDropdownItems })
|
||||
regions: ChoiceItem;
|
||||
|
||||
@Values({
|
||||
label: "Enable Logging",
|
||||
trueLabel: "Enable",
|
||||
falseLabel: "Disable"
|
||||
})
|
||||
enableLogging: boolean;
|
||||
|
||||
@Values({
|
||||
label: "Account Name",
|
||||
placeholder: "Enter the account name"
|
||||
})
|
||||
accountName: string;
|
||||
|
||||
/*
|
||||
@OnChange()
|
||||
- optional
|
||||
- input: (currentValues: Map<string, InputType>, newValue: InputType) => Map<string, InputType>
|
||||
- role: Takes a Map of current values and the newValue for this property as inputs. This is called when a property
|
||||
changes its value in the UI. This can be used to change other input values based on some other input.
|
||||
|
||||
The new Map of propertyName -> value is returned.
|
||||
|
||||
In this example, the onDbThroughputChange function sets the collectionThroughput to the same value as the dbThroughput
|
||||
when the slider in moved in the UI.
|
||||
*/
|
||||
@OnChange(onDbThroughputChange)
|
||||
@Values({
|
||||
label: "Database Throughput",
|
||||
min: 400,
|
||||
max: initializeMaxThroughput,
|
||||
step: 100,
|
||||
uiType: UiType.Slider
|
||||
})
|
||||
dbThroughput: number;
|
||||
|
||||
@Values({
|
||||
label: "Collection Throughput",
|
||||
min: 400,
|
||||
max: initializeMaxThroughput,
|
||||
step: 100,
|
||||
uiType: UiType.Spinner
|
||||
})
|
||||
collectionThroughput: number;
|
||||
}
|
||||
99
src/SelfServe/PropertyDecorators.tsx
Normal file
99
src/SelfServe/PropertyDecorators.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import { ChoiceItem, Info, InputType, UiType } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
||||
import { addPropertyToMap, CommonInputTypes } from "./SelfServeUtils";
|
||||
|
||||
type ValueOf<T> = T[keyof T];
|
||||
interface Decorator {
|
||||
name: keyof CommonInputTypes;
|
||||
value: ValueOf<CommonInputTypes>;
|
||||
}
|
||||
|
||||
interface InputOptionsBase {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface NumberInputOptions extends InputOptionsBase {
|
||||
min: (() => Promise<number>) | number;
|
||||
max: (() => Promise<number>) | number;
|
||||
step: (() => Promise<number>) | number;
|
||||
uiType: UiType;
|
||||
}
|
||||
|
||||
export interface StringInputOptions extends InputOptionsBase {
|
||||
placeholder?: (() => Promise<string>) | string;
|
||||
}
|
||||
|
||||
export interface BooleanInputOptions extends InputOptionsBase {
|
||||
trueLabel: (() => Promise<string>) | string;
|
||||
falseLabel: (() => Promise<string>) | string;
|
||||
}
|
||||
|
||||
export interface ChoiceInputOptions extends InputOptionsBase {
|
||||
choices: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
|
||||
}
|
||||
|
||||
type InputOptions = NumberInputOptions | StringInputOptions | BooleanInputOptions | ChoiceInputOptions;
|
||||
|
||||
const isNumberInputOptions = (inputOptions: InputOptions): inputOptions is NumberInputOptions => {
|
||||
return "min" in inputOptions;
|
||||
};
|
||||
|
||||
const isBooleanInputOptions = (inputOptions: InputOptions): inputOptions is BooleanInputOptions => {
|
||||
return "trueLabel" in inputOptions;
|
||||
};
|
||||
|
||||
const isChoiceInputOptions = (inputOptions: InputOptions): inputOptions is ChoiceInputOptions => {
|
||||
return "choices" in inputOptions;
|
||||
};
|
||||
|
||||
const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
|
||||
return (target, property) => {
|
||||
let className = target.constructor.name;
|
||||
const propertyName = property.toString();
|
||||
if (className === "Function") {
|
||||
//eslint-disable-next-line @typescript-eslint/ban-types
|
||||
className = (target as Function).name;
|
||||
throw new Error(`Property '${propertyName}' in class '${className}'should be not be static.`);
|
||||
}
|
||||
|
||||
const propertyType = (Reflect.getMetadata("design:type", target, property)?.name as string)?.toLowerCase();
|
||||
addPropertyToMap(target, propertyName, className, "type", propertyType);
|
||||
addPropertyToMap(target, propertyName, className, "dataFieldName", propertyName);
|
||||
|
||||
decorators.map(decorator => addPropertyToMap(target, propertyName, className, decorator.name, decorator.value));
|
||||
};
|
||||
};
|
||||
|
||||
export const OnChange = (
|
||||
onChange: (currentState: Map<string, InputType>, newValue: InputType) => Map<string, InputType>
|
||||
): PropertyDecorator => {
|
||||
return addToMap({ name: "onChange", value: onChange });
|
||||
};
|
||||
|
||||
export const PropertyInfo = (info: (() => Promise<Info>) | Info): PropertyDecorator => {
|
||||
return addToMap({ name: "info", value: info });
|
||||
};
|
||||
|
||||
export const Values = (inputOptions: InputOptions): PropertyDecorator => {
|
||||
if (isNumberInputOptions(inputOptions)) {
|
||||
return addToMap(
|
||||
{ name: "label", value: inputOptions.label },
|
||||
{ name: "min", value: inputOptions.min },
|
||||
{ name: "max", value: inputOptions.max },
|
||||
{ name: "step", value: inputOptions.step },
|
||||
{ name: "uiType", value: inputOptions.uiType }
|
||||
);
|
||||
} else if (isBooleanInputOptions(inputOptions)) {
|
||||
return addToMap(
|
||||
{ name: "label", value: inputOptions.label },
|
||||
{ name: "trueLabel", value: inputOptions.trueLabel },
|
||||
{ name: "falseLabel", value: inputOptions.falseLabel }
|
||||
);
|
||||
} else if (isChoiceInputOptions(inputOptions)) {
|
||||
return addToMap({ name: "label", value: inputOptions.label }, { name: "choices", value: inputOptions.choices });
|
||||
} else {
|
||||
return addToMap(
|
||||
{ name: "label", value: inputOptions.label },
|
||||
{ name: "placeholder", value: inputOptions.placeholder }
|
||||
);
|
||||
}
|
||||
};
|
||||
104
src/SelfServe/SelfServeComponent.test.tsx
Normal file
104
src/SelfServe/SelfServeComponent.test.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { SelfServeDescriptor, SelfServeComponent, SelfServeComponentState } from "./SelfServeComponent";
|
||||
import { InputType, UiType } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
||||
|
||||
describe("SelfServeComponent", () => {
|
||||
const defaultValues = new Map<string, InputType>([
|
||||
["throughput", "450"],
|
||||
["analyticalStore", "false"],
|
||||
["database", "db2"]
|
||||
]);
|
||||
const initializeMock = jest.fn(async () => defaultValues);
|
||||
const onSubmitMock = jest.fn(async () => {
|
||||
return;
|
||||
});
|
||||
|
||||
const exampleData: SelfServeDescriptor = {
|
||||
initialize: initializeMock,
|
||||
onSubmit: onSubmitMock,
|
||||
inputNames: ["throughput", "containerId", "analyticalStore", "database"],
|
||||
root: {
|
||||
id: "root",
|
||||
info: {
|
||||
message: "Start at $24/mo per database",
|
||||
link: {
|
||||
href: "https://aka.ms/azure-cosmos-db-pricing",
|
||||
text: "More Details"
|
||||
}
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: "throughput",
|
||||
input: {
|
||||
label: "Throughput (input)",
|
||||
dataFieldName: "throughput",
|
||||
type: "number",
|
||||
min: 400,
|
||||
max: 500,
|
||||
step: 10,
|
||||
defaultValue: 400,
|
||||
uiType: UiType.Spinner
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "containerId",
|
||||
input: {
|
||||
label: "Container id",
|
||||
dataFieldName: "containerId",
|
||||
type: "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "analyticalStore",
|
||||
input: {
|
||||
label: "Analytical Store",
|
||||
trueLabel: "Enabled",
|
||||
falseLabel: "Disabled",
|
||||
defaultValue: true,
|
||||
dataFieldName: "analyticalStore",
|
||||
type: "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "database",
|
||||
input: {
|
||||
label: "Database",
|
||||
dataFieldName: "database",
|
||||
type: "object",
|
||||
choices: [
|
||||
{ label: "Database 1", key: "db1" },
|
||||
{ label: "Database 2", key: "db2" },
|
||||
{ label: "Database 3", key: "db3" }
|
||||
],
|
||||
defaultKey: "db2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const verifyDefaultsSet = (currentValues: Map<string, InputType>): void => {
|
||||
for (const key of currentValues.keys()) {
|
||||
if (defaultValues.has(key)) {
|
||||
expect(defaultValues.get(key)).toEqual(currentValues.get(key));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
it("should render", async () => {
|
||||
const wrapper = shallow(<SelfServeComponent descriptor={exampleData} />);
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// initialize() should be called and defaults should be set when component is mounted
|
||||
expect(initializeMock).toHaveBeenCalled();
|
||||
const state = wrapper.state() as SelfServeComponentState;
|
||||
verifyDefaultsSet(state.currentValues);
|
||||
|
||||
// onSubmit() must be called when submit button is clicked
|
||||
const submitButton = wrapper.find("#submitButton");
|
||||
submitButton.simulate("click");
|
||||
expect(onSubmitMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
218
src/SelfServe/SelfServeComponent.tsx
Normal file
218
src/SelfServe/SelfServeComponent.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import React from "react";
|
||||
import { IStackTokens, PrimaryButton, Spinner, SpinnerSize, Stack } from "office-ui-fabric-react";
|
||||
import {
|
||||
ChoiceItem,
|
||||
InputType,
|
||||
InputTypeValue,
|
||||
SmartUiComponent,
|
||||
UiType,
|
||||
SmartUiDescriptor,
|
||||
Info
|
||||
} from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
||||
|
||||
export interface BaseInput {
|
||||
label: (() => Promise<string>) | string;
|
||||
dataFieldName: string;
|
||||
type: InputTypeValue;
|
||||
onChange?: (currentState: Map<string, InputType>, newValue: InputType) => Map<string, InputType>;
|
||||
placeholder?: (() => Promise<string>) | string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface NumberInput extends BaseInput {
|
||||
min: (() => Promise<number>) | number;
|
||||
max: (() => Promise<number>) | number;
|
||||
step: (() => Promise<number>) | number;
|
||||
defaultValue?: number;
|
||||
uiType: UiType;
|
||||
}
|
||||
|
||||
export interface BooleanInput extends BaseInput {
|
||||
trueLabel: (() => Promise<string>) | string;
|
||||
falseLabel: (() => Promise<string>) | string;
|
||||
defaultValue?: boolean;
|
||||
}
|
||||
|
||||
export interface StringInput extends BaseInput {
|
||||
defaultValue?: string;
|
||||
}
|
||||
|
||||
export interface ChoiceInput extends BaseInput {
|
||||
choices: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
|
||||
defaultKey?: string;
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
id: string;
|
||||
info?: (() => Promise<Info>) | Info;
|
||||
input?: AnyInput;
|
||||
children?: Node[];
|
||||
}
|
||||
|
||||
export interface SelfServeDescriptor {
|
||||
root: Node;
|
||||
initialize?: () => Promise<Map<string, InputType>>;
|
||||
onSubmit?: (currentValues: Map<string, InputType>) => Promise<void>;
|
||||
inputNames?: string[];
|
||||
}
|
||||
|
||||
export type AnyInput = NumberInput | BooleanInput | StringInput | ChoiceInput;
|
||||
|
||||
export interface SelfServeComponentProps {
|
||||
descriptor: SelfServeDescriptor;
|
||||
}
|
||||
|
||||
export interface SelfServeComponentState {
|
||||
root: SelfServeDescriptor;
|
||||
currentValues: Map<string, InputType>;
|
||||
baselineValues: Map<string, InputType>;
|
||||
isRefreshing: boolean;
|
||||
}
|
||||
|
||||
export class SelfServeComponent extends React.Component<SelfServeComponentProps, SelfServeComponentState> {
|
||||
componentDidMount(): void {
|
||||
this.initializeSmartUiComponent();
|
||||
}
|
||||
|
||||
constructor(props: SelfServeComponentProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
root: this.props.descriptor,
|
||||
currentValues: new Map(),
|
||||
baselineValues: new Map(),
|
||||
isRefreshing: false
|
||||
};
|
||||
}
|
||||
|
||||
private initializeSmartUiComponent = async (): Promise<void> => {
|
||||
this.setState({ isRefreshing: true });
|
||||
await this.initializeSmartUiNode(this.props.descriptor.root);
|
||||
await this.setDefaults();
|
||||
this.setState({ isRefreshing: false });
|
||||
};
|
||||
|
||||
private setDefaults = async (): Promise<void> => {
|
||||
this.setState({ isRefreshing: true });
|
||||
let { currentValues, baselineValues } = this.state;
|
||||
|
||||
const initialValues = await this.props.descriptor.initialize();
|
||||
for (const key of initialValues.keys()) {
|
||||
if (this.props.descriptor.inputNames.indexOf(key) === -1) {
|
||||
this.setState({ isRefreshing: false });
|
||||
throw new Error(`${key} is not an input property of this class.`);
|
||||
}
|
||||
|
||||
currentValues = currentValues.set(key, initialValues.get(key));
|
||||
baselineValues = baselineValues.set(key, initialValues.get(key));
|
||||
}
|
||||
this.setState({ currentValues, baselineValues, isRefreshing: false });
|
||||
};
|
||||
|
||||
public discard = (): void => {
|
||||
let { currentValues } = this.state;
|
||||
const { baselineValues } = this.state;
|
||||
for (const key of baselineValues.keys()) {
|
||||
currentValues = currentValues.set(key, baselineValues.get(key));
|
||||
}
|
||||
this.setState({ currentValues });
|
||||
};
|
||||
|
||||
private initializeSmartUiNode = async (currentNode: Node): Promise<void> => {
|
||||
currentNode.info = await this.getResolvedValue(currentNode.info);
|
||||
|
||||
if (currentNode.input) {
|
||||
currentNode.input = await this.getResolvedInput(currentNode.input);
|
||||
}
|
||||
|
||||
const promises = currentNode.children?.map(async (child: Node) => await this.initializeSmartUiNode(child));
|
||||
if (promises) {
|
||||
await Promise.all(promises);
|
||||
}
|
||||
};
|
||||
|
||||
private getResolvedInput = async (input: AnyInput): Promise<AnyInput> => {
|
||||
input.label = await this.getResolvedValue(input.label);
|
||||
input.placeholder = await this.getResolvedValue(input.placeholder);
|
||||
|
||||
switch (input.type) {
|
||||
case "string": {
|
||||
return input as StringInput;
|
||||
}
|
||||
case "number": {
|
||||
const numberInput = input as NumberInput;
|
||||
numberInput.min = await this.getResolvedValue(numberInput.min);
|
||||
numberInput.max = await this.getResolvedValue(numberInput.max);
|
||||
numberInput.step = await this.getResolvedValue(numberInput.step);
|
||||
return numberInput;
|
||||
}
|
||||
case "boolean": {
|
||||
const booleanInput = input as BooleanInput;
|
||||
booleanInput.trueLabel = await this.getResolvedValue(booleanInput.trueLabel);
|
||||
booleanInput.falseLabel = await this.getResolvedValue(booleanInput.falseLabel);
|
||||
return booleanInput;
|
||||
}
|
||||
default: {
|
||||
const choiceInput = input as ChoiceInput;
|
||||
choiceInput.choices = await this.getResolvedValue(choiceInput.choices);
|
||||
return choiceInput;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public async getResolvedValue<T>(value: T | (() => Promise<T>)): Promise<T> {
|
||||
if (value instanceof Function) {
|
||||
return value();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private onInputChange = (input: AnyInput, newValue: InputType) => {
|
||||
if (input.onChange) {
|
||||
const newValues = input.onChange(this.state.currentValues, newValue);
|
||||
this.setState({ currentValues: newValues });
|
||||
} else {
|
||||
const dataFieldName = input.dataFieldName;
|
||||
const { currentValues } = this.state;
|
||||
currentValues.set(dataFieldName, newValue);
|
||||
this.setState({ currentValues });
|
||||
}
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const containerStackTokens: IStackTokens = { childrenGap: 20 };
|
||||
return !this.state.isRefreshing ? (
|
||||
<div style={{ overflowX: "auto" }}>
|
||||
<Stack tokens={containerStackTokens} styles={{ root: { width: 400, padding: 10 } }}>
|
||||
<SmartUiComponent
|
||||
descriptor={this.state.root as SmartUiDescriptor}
|
||||
currentValues={this.state.currentValues}
|
||||
onInputChange={this.onInputChange}
|
||||
/>
|
||||
|
||||
<Stack horizontal tokens={{ childrenGap: 10 }}>
|
||||
<PrimaryButton
|
||||
id="submitButton"
|
||||
styles={{ root: { width: 100 } }}
|
||||
text="submit"
|
||||
onClick={async () => {
|
||||
await this.props.descriptor.onSubmit(this.state.currentValues);
|
||||
this.setDefaults();
|
||||
}}
|
||||
/>
|
||||
<PrimaryButton
|
||||
id="discardButton"
|
||||
styles={{ root: { width: 100 } }}
|
||||
text="discard"
|
||||
onClick={() => this.discard()}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
) : (
|
||||
<Spinner
|
||||
size={SpinnerSize.large}
|
||||
styles={{ root: { textAlign: "center", justifyContent: "center", width: "100%", height: "100%" } }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
51
src/SelfServe/SelfServeComponentAdapter.tsx
Normal file
51
src/SelfServe/SelfServeComponentAdapter.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* This adapter is responsible to render the React component
|
||||
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
|
||||
* and update any knockout observables passed from the parent.
|
||||
*/
|
||||
import * as ko from "knockout";
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import { SelfServeDescriptor, SelfServeComponent } from "./SelfServeComponent";
|
||||
import { SelfServeType } from "./SelfServeUtils";
|
||||
|
||||
export class SelfServeComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<SelfServeDescriptor>;
|
||||
public container: Explorer;
|
||||
|
||||
constructor(container: Explorer) {
|
||||
this.container = container;
|
||||
this.parameters = ko.observable(undefined);
|
||||
this.container.selfServeType.subscribe(() => {
|
||||
this.triggerRender();
|
||||
});
|
||||
}
|
||||
|
||||
public static getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDescriptor> => {
|
||||
switch (selfServeType) {
|
||||
case SelfServeType.example: {
|
||||
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
|
||||
return new SelfServeExample.default().toSelfServeDescriptor();
|
||||
}
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
if (this.container.selfServeType() === SelfServeType.invalid) {
|
||||
return <h1>Invalid self serve type!</h1>;
|
||||
}
|
||||
const smartUiDescriptor = this.parameters();
|
||||
return smartUiDescriptor ? <SelfServeComponent descriptor={smartUiDescriptor} /> : <></>;
|
||||
}
|
||||
|
||||
private triggerRender() {
|
||||
window.requestAnimationFrame(async () => {
|
||||
const selfServeType = this.container.selfServeType();
|
||||
const smartUiDescriptor = await SelfServeComponentAdapter.getDescriptor(selfServeType);
|
||||
this.parameters(smartUiDescriptor);
|
||||
});
|
||||
}
|
||||
}
|
||||
25
src/SelfServe/SelfServeLoadingComponentAdapter.tsx
Normal file
25
src/SelfServe/SelfServeLoadingComponentAdapter.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* This adapter is responsible to render the React component
|
||||
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
|
||||
* and update any knockout observables passed from the parent.
|
||||
*/
|
||||
import * as ko from "knockout";
|
||||
import { Spinner, SpinnerSize } from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
|
||||
|
||||
export class SelfServeLoadingComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<number>;
|
||||
|
||||
constructor() {
|
||||
this.parameters = ko.observable(Date.now());
|
||||
}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
return <Spinner size={SpinnerSize.large} />;
|
||||
}
|
||||
|
||||
private triggerRender() {
|
||||
window.requestAnimationFrame(() => this.renderComponent());
|
||||
}
|
||||
}
|
||||
277
src/SelfServe/SelfServeUtils.test.tsx
Normal file
277
src/SelfServe/SelfServeUtils.test.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
import {
|
||||
CommonInputTypes,
|
||||
mapToSmartUiDescriptor,
|
||||
SelfServeBaseClass,
|
||||
updateContextWithDecorator
|
||||
} from "./SelfServeUtils";
|
||||
import { InputType, UiType } from "./../Explorer/Controls/SmartUi/SmartUiComponent";
|
||||
|
||||
describe("SelfServeUtils", () => {
|
||||
it("initialize should be declared for self serve classes", () => {
|
||||
class Test extends SelfServeBaseClass {
|
||||
public onSubmit = async (): Promise<void> => {
|
||||
return;
|
||||
};
|
||||
public initialize: () => Promise<Map<string, InputType>>;
|
||||
}
|
||||
expect(() => new Test().toSelfServeDescriptor()).toThrow("initialize() was not declared for the class 'Test'");
|
||||
});
|
||||
|
||||
it("onSubmit should be declared for self serve classes", () => {
|
||||
class Test extends SelfServeBaseClass {
|
||||
public onSubmit: () => Promise<void>;
|
||||
public initialize = async (): Promise<Map<string, InputType>> => {
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
expect(() => new Test().toSelfServeDescriptor()).toThrow("onSubmit() was not declared for the class 'Test'");
|
||||
});
|
||||
|
||||
it("@SmartUi decorator must be present for self serve classes", () => {
|
||||
class Test extends SelfServeBaseClass {
|
||||
public onSubmit = async (): Promise<void> => {
|
||||
return;
|
||||
};
|
||||
public initialize = async (): Promise<Map<string, InputType>> => {
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
expect(() => new Test().toSelfServeDescriptor()).toThrow(
|
||||
"@SmartUi decorator was not declared for the class 'Test'"
|
||||
);
|
||||
});
|
||||
|
||||
it("updateContextWithDecorator", () => {
|
||||
const context = new Map<string, CommonInputTypes>();
|
||||
updateContextWithDecorator(context, "dbThroughput", "testClass", "max", 1);
|
||||
updateContextWithDecorator(context, "dbThroughput", "testClass", "min", 2);
|
||||
updateContextWithDecorator(context, "collThroughput", "testClass", "max", 5);
|
||||
expect(context.size).toEqual(2);
|
||||
expect(context.get("dbThroughput")).toEqual({ id: "dbThroughput", max: 1, min: 2 });
|
||||
expect(context.get("collThroughput")).toEqual({ id: "collThroughput", max: 5 });
|
||||
});
|
||||
|
||||
it("mapToSmartUiDescriptor", () => {
|
||||
const context: Map<string, CommonInputTypes> = new Map([
|
||||
[
|
||||
"dbThroughput",
|
||||
{
|
||||
id: "dbThroughput",
|
||||
dataFieldName: "dbThroughput",
|
||||
type: "number",
|
||||
label: "Database Throughput",
|
||||
min: 1,
|
||||
max: 5,
|
||||
step: 1,
|
||||
uiType: UiType.Slider
|
||||
}
|
||||
],
|
||||
[
|
||||
"collThroughput",
|
||||
{
|
||||
id: "collThroughput",
|
||||
dataFieldName: "collThroughput",
|
||||
type: "number",
|
||||
label: "Coll Throughput",
|
||||
min: 1,
|
||||
max: 5,
|
||||
step: 1,
|
||||
uiType: UiType.Spinner
|
||||
}
|
||||
],
|
||||
[
|
||||
"invalidThroughput",
|
||||
{
|
||||
id: "invalidThroughput",
|
||||
dataFieldName: "invalidThroughput",
|
||||
type: "boolean",
|
||||
label: "Invalid Coll Throughput",
|
||||
min: 1,
|
||||
max: 5,
|
||||
step: 1,
|
||||
uiType: UiType.Spinner,
|
||||
errorMessage: "label, truelabel and falselabel are required for boolean input"
|
||||
}
|
||||
],
|
||||
[
|
||||
"collName",
|
||||
{
|
||||
id: "collName",
|
||||
dataFieldName: "collName",
|
||||
type: "string",
|
||||
label: "Coll Name",
|
||||
placeholder: "placeholder text"
|
||||
}
|
||||
],
|
||||
[
|
||||
"enableLogging",
|
||||
{
|
||||
id: "enableLogging",
|
||||
dataFieldName: "enableLogging",
|
||||
type: "boolean",
|
||||
label: "Enable Logging",
|
||||
trueLabel: "Enable",
|
||||
falseLabel: "Disable"
|
||||
}
|
||||
],
|
||||
[
|
||||
"invalidEnableLogging",
|
||||
{
|
||||
id: "invalidEnableLogging",
|
||||
dataFieldName: "invalidEnableLogging",
|
||||
type: "boolean",
|
||||
label: "Invalid Enable Logging",
|
||||
placeholder: "placeholder text"
|
||||
}
|
||||
],
|
||||
[
|
||||
"regions",
|
||||
{
|
||||
id: "regions",
|
||||
dataFieldName: "regions",
|
||||
type: "object",
|
||||
label: "Regions",
|
||||
choices: [
|
||||
{ label: "South West US", key: "SWUS" },
|
||||
{ label: "North Central US", key: "NCUS" },
|
||||
{ label: "East US 2", key: "EUS2" }
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
"invalidRegions",
|
||||
{
|
||||
id: "invalidRegions",
|
||||
dataFieldName: "invalidRegions",
|
||||
type: "object",
|
||||
label: "Invalid Regions",
|
||||
placeholder: "placeholder text"
|
||||
}
|
||||
]
|
||||
]);
|
||||
const expectedDescriptor = {
|
||||
root: {
|
||||
id: "root",
|
||||
children: [
|
||||
{
|
||||
id: "dbThroughput",
|
||||
input: {
|
||||
id: "dbThroughput",
|
||||
dataFieldName: "dbThroughput",
|
||||
type: "number",
|
||||
label: "Database Throughput",
|
||||
min: 1,
|
||||
max: 5,
|
||||
step: 1,
|
||||
uiType: "Slider"
|
||||
},
|
||||
children: [] as Node[]
|
||||
},
|
||||
{
|
||||
id: "collThroughput",
|
||||
input: {
|
||||
id: "collThroughput",
|
||||
dataFieldName: "collThroughput",
|
||||
type: "number",
|
||||
label: "Coll Throughput",
|
||||
min: 1,
|
||||
max: 5,
|
||||
step: 1,
|
||||
uiType: "Spinner"
|
||||
},
|
||||
children: [] as Node[]
|
||||
},
|
||||
{
|
||||
id: "invalidThroughput",
|
||||
input: {
|
||||
id: "invalidThroughput",
|
||||
dataFieldName: "invalidThroughput",
|
||||
type: "boolean",
|
||||
label: "Invalid Coll Throughput",
|
||||
min: 1,
|
||||
max: 5,
|
||||
step: 1,
|
||||
uiType: "Spinner",
|
||||
errorMessage: "label, truelabel and falselabel are required for boolean input 'invalidThroughput'."
|
||||
},
|
||||
children: [] as Node[]
|
||||
},
|
||||
{
|
||||
id: "collName",
|
||||
input: {
|
||||
id: "collName",
|
||||
dataFieldName: "collName",
|
||||
type: "string",
|
||||
label: "Coll Name",
|
||||
placeholder: "placeholder text"
|
||||
},
|
||||
children: [] as Node[]
|
||||
},
|
||||
{
|
||||
id: "enableLogging",
|
||||
input: {
|
||||
id: "enableLogging",
|
||||
dataFieldName: "enableLogging",
|
||||
type: "boolean",
|
||||
label: "Enable Logging",
|
||||
trueLabel: "Enable",
|
||||
falseLabel: "Disable"
|
||||
},
|
||||
children: [] as Node[]
|
||||
},
|
||||
{
|
||||
id: "invalidEnableLogging",
|
||||
input: {
|
||||
id: "invalidEnableLogging",
|
||||
dataFieldName: "invalidEnableLogging",
|
||||
type: "boolean",
|
||||
label: "Invalid Enable Logging",
|
||||
placeholder: "placeholder text",
|
||||
errorMessage: "label, truelabel and falselabel are required for boolean input 'invalidEnableLogging'."
|
||||
},
|
||||
children: [] as Node[]
|
||||
},
|
||||
{
|
||||
id: "regions",
|
||||
input: {
|
||||
id: "regions",
|
||||
dataFieldName: "regions",
|
||||
type: "object",
|
||||
label: "Regions",
|
||||
choices: [
|
||||
{ label: "South West US", key: "SWUS" },
|
||||
{ label: "North Central US", key: "NCUS" },
|
||||
{ label: "East US 2", key: "EUS2" }
|
||||
]
|
||||
},
|
||||
children: [] as Node[]
|
||||
},
|
||||
{
|
||||
id: "invalidRegions",
|
||||
input: {
|
||||
id: "invalidRegions",
|
||||
dataFieldName: "invalidRegions",
|
||||
type: "object",
|
||||
label: "Invalid Regions",
|
||||
placeholder: "placeholder text",
|
||||
errorMessage: "label and choices are required for Choice input 'invalidRegions'."
|
||||
},
|
||||
children: [] as Node[]
|
||||
}
|
||||
]
|
||||
},
|
||||
inputNames: [
|
||||
"dbThroughput",
|
||||
"collThroughput",
|
||||
"invalidThroughput",
|
||||
"collName",
|
||||
"enableLogging",
|
||||
"invalidEnableLogging",
|
||||
"regions",
|
||||
"invalidRegions"
|
||||
]
|
||||
};
|
||||
const descriptor = mapToSmartUiDescriptor(context);
|
||||
expect(descriptor).toEqual(expectedDescriptor);
|
||||
});
|
||||
});
|
||||
184
src/SelfServe/SelfServeUtils.tsx
Normal file
184
src/SelfServe/SelfServeUtils.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import "reflect-metadata";
|
||||
import { ChoiceItem, Info, InputTypeValue, InputType } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
||||
import {
|
||||
BooleanInput,
|
||||
ChoiceInput,
|
||||
SelfServeDescriptor,
|
||||
NumberInput,
|
||||
StringInput,
|
||||
Node,
|
||||
AnyInput
|
||||
} from "./SelfServeComponent";
|
||||
|
||||
export enum SelfServeType {
|
||||
// No self serve type passed, launch explorer
|
||||
none = "none",
|
||||
// Unsupported self serve type passed as feature flag
|
||||
invalid = "invalid",
|
||||
// Add your self serve types here
|
||||
example = "example"
|
||||
}
|
||||
|
||||
export abstract class SelfServeBaseClass {
|
||||
public abstract onSubmit: (currentValues: Map<string, InputType>) => Promise<void>;
|
||||
public abstract initialize: () => Promise<Map<string, InputType>>;
|
||||
|
||||
public toSelfServeDescriptor(): SelfServeDescriptor {
|
||||
const className = this.constructor.name;
|
||||
const smartUiDescriptor = Reflect.getMetadata(className, this) as SelfServeDescriptor;
|
||||
|
||||
if (!this.initialize) {
|
||||
throw new Error(`initialize() was not declared for the class '${className}'`);
|
||||
}
|
||||
if (!this.onSubmit) {
|
||||
throw new Error(`onSubmit() was not declared for the class '${className}'`);
|
||||
}
|
||||
if (!smartUiDescriptor?.root) {
|
||||
throw new Error(`@SmartUi decorator was not declared for the class '${className}'`);
|
||||
}
|
||||
|
||||
smartUiDescriptor.initialize = this.initialize;
|
||||
smartUiDescriptor.onSubmit = this.onSubmit;
|
||||
return smartUiDescriptor;
|
||||
}
|
||||
}
|
||||
|
||||
export interface CommonInputTypes {
|
||||
id: string;
|
||||
info?: (() => Promise<Info>) | Info;
|
||||
type?: InputTypeValue;
|
||||
label?: (() => Promise<string>) | string;
|
||||
placeholder?: (() => Promise<string>) | string;
|
||||
dataFieldName?: string;
|
||||
min?: (() => Promise<number>) | number;
|
||||
max?: (() => Promise<number>) | number;
|
||||
step?: (() => Promise<number>) | number;
|
||||
trueLabel?: (() => Promise<string>) | string;
|
||||
falseLabel?: (() => Promise<string>) | string;
|
||||
choices?: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
|
||||
uiType?: string;
|
||||
errorMessage?: string;
|
||||
onChange?: (currentState: Map<string, InputType>, newValue: InputType) => Map<string, InputType>;
|
||||
onSubmit?: (currentValues: Map<string, InputType>) => Promise<void>;
|
||||
initialize?: () => Promise<Map<string, InputType>>;
|
||||
}
|
||||
|
||||
const setValue = <T extends keyof CommonInputTypes, K extends CommonInputTypes[T]>(
|
||||
name: T,
|
||||
value: K,
|
||||
fieldObject: CommonInputTypes
|
||||
): void => {
|
||||
fieldObject[name] = value;
|
||||
};
|
||||
|
||||
const getValue = <T extends keyof CommonInputTypes>(name: T, fieldObject: CommonInputTypes): unknown => {
|
||||
return fieldObject[name];
|
||||
};
|
||||
|
||||
export const addPropertyToMap = <T extends keyof CommonInputTypes, K extends CommonInputTypes[T]>(
|
||||
target: unknown,
|
||||
propertyName: string,
|
||||
className: string,
|
||||
descriptorName: keyof CommonInputTypes,
|
||||
descriptorValue: K
|
||||
): void => {
|
||||
const context =
|
||||
(Reflect.getMetadata(className, target) as Map<string, CommonInputTypes>) ?? new Map<string, CommonInputTypes>();
|
||||
updateContextWithDecorator(context, propertyName, className, descriptorName, descriptorValue);
|
||||
Reflect.defineMetadata(className, context, target);
|
||||
};
|
||||
|
||||
export const updateContextWithDecorator = <T extends keyof CommonInputTypes, K extends CommonInputTypes[T]>(
|
||||
context: Map<string, CommonInputTypes>,
|
||||
propertyName: string,
|
||||
className: string,
|
||||
descriptorName: keyof CommonInputTypes,
|
||||
descriptorValue: K
|
||||
): void => {
|
||||
if (!(context instanceof Map)) {
|
||||
console.log(context);
|
||||
throw new Error(`@SmartUi should be the first decorator for the class '${className}'.`);
|
||||
}
|
||||
|
||||
const propertyObject = context.get(propertyName) ?? { id: propertyName };
|
||||
|
||||
if (getValue(descriptorName, propertyObject) && descriptorName !== "type" && descriptorName !== "dataFieldName") {
|
||||
throw new Error(
|
||||
`Duplicate value passed for '${descriptorName}' on property '${propertyName}' of class '${className}'`
|
||||
);
|
||||
}
|
||||
|
||||
setValue(descriptorName, descriptorValue, propertyObject);
|
||||
context.set(propertyName, propertyObject);
|
||||
};
|
||||
|
||||
export const buildSmartUiDescriptor = (className: string, target: unknown): void => {
|
||||
const context = Reflect.getMetadata(className, target) as Map<string, CommonInputTypes>;
|
||||
const smartUiDescriptor = mapToSmartUiDescriptor(context);
|
||||
Reflect.defineMetadata(className, smartUiDescriptor, target);
|
||||
};
|
||||
|
||||
export const mapToSmartUiDescriptor = (context: Map<string, CommonInputTypes>): SelfServeDescriptor => {
|
||||
const root = context.get("root");
|
||||
context.delete("root");
|
||||
const inputNames: string[] = [];
|
||||
|
||||
const smartUiDescriptor: SelfServeDescriptor = {
|
||||
root: {
|
||||
id: "root",
|
||||
info: root?.info,
|
||||
children: []
|
||||
}
|
||||
};
|
||||
|
||||
while (context.size > 0) {
|
||||
const key = context.keys().next().value;
|
||||
addToDescriptor(context, smartUiDescriptor.root, key, inputNames);
|
||||
}
|
||||
smartUiDescriptor.inputNames = inputNames;
|
||||
|
||||
return smartUiDescriptor;
|
||||
};
|
||||
|
||||
const addToDescriptor = (
|
||||
context: Map<string, CommonInputTypes>,
|
||||
root: Node,
|
||||
key: string,
|
||||
inputNames: string[]
|
||||
): void => {
|
||||
const value = context.get(key);
|
||||
inputNames.push(value.id);
|
||||
const element = {
|
||||
id: value.id,
|
||||
info: value.info,
|
||||
input: getInput(value),
|
||||
children: []
|
||||
} as Node;
|
||||
context.delete(key);
|
||||
root.children.push(element);
|
||||
};
|
||||
|
||||
const getInput = (value: CommonInputTypes): AnyInput => {
|
||||
switch (value.type) {
|
||||
case "number":
|
||||
if (!value.label || !value.step || !value.uiType || !value.min || !value.max) {
|
||||
value.errorMessage = `label, step, min, max and uiType are required for number input '${value.id}'.`;
|
||||
}
|
||||
return value as NumberInput;
|
||||
case "string":
|
||||
if (!value.label) {
|
||||
value.errorMessage = `label is required for string input '${value.id}'.`;
|
||||
}
|
||||
return value as StringInput;
|
||||
case "boolean":
|
||||
if (!value.label || !value.trueLabel || !value.falseLabel) {
|
||||
value.errorMessage = `label, truelabel and falselabel are required for boolean input '${value.id}'.`;
|
||||
}
|
||||
return value as BooleanInput;
|
||||
default:
|
||||
if (!value.label || !value.choices) {
|
||||
value.errorMessage = `label and choices are required for Choice input '${value.id}'.`;
|
||||
}
|
||||
return value as ChoiceInput;
|
||||
}
|
||||
};
|
||||
168
src/SelfServe/__snapshots__/SelfServeComponent.test.tsx.snap
Normal file
168
src/SelfServe/__snapshots__/SelfServeComponent.test.tsx.snap
Normal file
@@ -0,0 +1,168 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SelfServeComponent should render 1`] = `
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"overflowX": "auto",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"padding": 10,
|
||||
"width": 400,
|
||||
},
|
||||
}
|
||||
}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<SmartUiComponent
|
||||
currentValues={
|
||||
Map {
|
||||
"throughput" => "450",
|
||||
"analyticalStore" => "false",
|
||||
"database" => "db2",
|
||||
}
|
||||
}
|
||||
descriptor={
|
||||
Object {
|
||||
"initialize": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
},
|
||||
"inputNames": Array [
|
||||
"throughput",
|
||||
"containerId",
|
||||
"analyticalStore",
|
||||
"database",
|
||||
],
|
||||
"onSubmit": [MockFunction],
|
||||
"root": Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"id": "throughput",
|
||||
"info": undefined,
|
||||
"input": Object {
|
||||
"dataFieldName": "throughput",
|
||||
"defaultValue": 400,
|
||||
"label": "Throughput (input)",
|
||||
"max": 500,
|
||||
"min": 400,
|
||||
"placeholder": undefined,
|
||||
"step": 10,
|
||||
"type": "number",
|
||||
"uiType": "Spinner",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "containerId",
|
||||
"info": undefined,
|
||||
"input": Object {
|
||||
"dataFieldName": "containerId",
|
||||
"label": "Container id",
|
||||
"placeholder": undefined,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "analyticalStore",
|
||||
"info": undefined,
|
||||
"input": Object {
|
||||
"dataFieldName": "analyticalStore",
|
||||
"defaultValue": true,
|
||||
"falseLabel": "Disabled",
|
||||
"label": "Analytical Store",
|
||||
"placeholder": undefined,
|
||||
"trueLabel": "Enabled",
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "database",
|
||||
"info": undefined,
|
||||
"input": Object {
|
||||
"choices": Array [
|
||||
Object {
|
||||
"key": "db1",
|
||||
"label": "Database 1",
|
||||
},
|
||||
Object {
|
||||
"key": "db2",
|
||||
"label": "Database 2",
|
||||
},
|
||||
Object {
|
||||
"key": "db3",
|
||||
"label": "Database 3",
|
||||
},
|
||||
],
|
||||
"dataFieldName": "database",
|
||||
"defaultKey": "db2",
|
||||
"label": "Database",
|
||||
"placeholder": undefined,
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
],
|
||||
"id": "root",
|
||||
"info": Object {
|
||||
"link": Object {
|
||||
"href": "https://aka.ms/azure-cosmos-db-pricing",
|
||||
"text": "More Details",
|
||||
},
|
||||
"message": "Start at $24/mo per database",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
onInputChange={[Function]}
|
||||
/>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedPrimaryButton
|
||||
id="submitButton"
|
||||
onClick={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"width": 100,
|
||||
},
|
||||
}
|
||||
}
|
||||
text="submit"
|
||||
/>
|
||||
<CustomizedPrimaryButton
|
||||
id="discardButton"
|
||||
onClick={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"width": 100,
|
||||
},
|
||||
}
|
||||
}
|
||||
text="discard"
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
`;
|
||||
@@ -4,7 +4,7 @@ import * as DataModels from "../Contracts/DataModels";
|
||||
import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType";
|
||||
|
||||
export class DefaultExperienceUtility {
|
||||
public static getDefaultExperienceFromDatabaseAccount(databaseAccount: DataModels.DatabaseAccount): string {
|
||||
public static getDefaultExperienceFromDatabaseAccount(databaseAccount: DataModels.DatabaseAccount): string | null {
|
||||
if (!databaseAccount) {
|
||||
return null;
|
||||
}
|
||||
@@ -81,11 +81,9 @@ export class DefaultExperienceUtility {
|
||||
|
||||
private static _getDefaultExperience(kind: string, capabilities: DataModels.Capability[]): string {
|
||||
const defaultDefaultExperience: string = Constants.DefaultAccountExperience.DocumentDB;
|
||||
const defaultExperienceFromKind: string = DefaultExperienceUtility._getDefaultExperienceFromAccountKind(kind);
|
||||
const defaultExperienceFromCapabilities: string = DefaultExperienceUtility._getDefaultExperienceFromAccountCapabilities(
|
||||
capabilities
|
||||
);
|
||||
|
||||
const defaultExperienceFromKind: string = DefaultExperienceUtility._getDefaultExperienceFromAccountKind(kind) || "";
|
||||
const defaultExperienceFromCapabilities: string =
|
||||
DefaultExperienceUtility._getDefaultExperienceFromAccountCapabilities(capabilities) || "";
|
||||
if (!!defaultExperienceFromKind) {
|
||||
return defaultExperienceFromKind;
|
||||
}
|
||||
@@ -97,7 +95,7 @@ export class DefaultExperienceUtility {
|
||||
return defaultDefaultExperience;
|
||||
}
|
||||
|
||||
private static _getDefaultExperienceFromAccountKind(kind: string): string {
|
||||
private static _getDefaultExperienceFromAccountKind(kind: string): string | null {
|
||||
if (!kind) {
|
||||
return null;
|
||||
}
|
||||
@@ -113,7 +111,7 @@ export class DefaultExperienceUtility {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static _getDefaultExperienceFromAccountCapabilities(capabilities: DataModels.Capability[]): string {
|
||||
private static _getDefaultExperienceFromAccountCapabilities(capabilities: DataModels.Capability[]): string | null {
|
||||
if (!capabilities) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export enum Action {
|
||||
MongoShell,
|
||||
ContextualPane,
|
||||
ScaleThroughput,
|
||||
ToggleAutoscaleSetting,
|
||||
SelectItem,
|
||||
Tab,
|
||||
UpdateDocument,
|
||||
@@ -104,7 +105,9 @@ export const ActionModifiers = {
|
||||
Submit: "submit",
|
||||
IndexAll: "index all properties",
|
||||
NoIndex: "no indexing",
|
||||
Cancel: "cancel"
|
||||
Cancel: "cancel",
|
||||
ToggleAutoscaleOn: "autoscale on",
|
||||
ToggleAutoscaleOff: "autoscale off"
|
||||
} as const;
|
||||
|
||||
export enum SourceBlade {
|
||||
|
||||
@@ -19,8 +19,8 @@ const getUrlVars = (): { [key: string]: string } => {
|
||||
};
|
||||
|
||||
const createServerSettings = (urlVars: { [key: string]: string }): ServerConnection.ISettings => {
|
||||
let body: BodyInit;
|
||||
let headers: HeadersInit;
|
||||
let body: BodyInit | undefined;
|
||||
let headers: HeadersInit | undefined;
|
||||
if (urlVars.hasOwnProperty(TerminalQueryParams.TerminalEndpoint)) {
|
||||
body = JSON.stringify({
|
||||
endpoint: urlVars[TerminalQueryParams.TerminalEndpoint]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as Constants from "../Common/Constants";
|
||||
import { AutoPilotOfferSettings, Offer } from "../Contracts/DataModels";
|
||||
|
||||
export const manualToAutoscaleDisclaimer = `The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings and storage of your resource. After autoscale has been enabled, you can change the max RU/s. <a href="${Constants.Urls.autoscaleMigration}">Learn more</a>.`;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export function logConsoleMessage(type: ConsoleDataType, message: string, id?: s
|
||||
}
|
||||
dataExplorer.logConsoleData({ type: type, date: formattedDate, message: message, id: id });
|
||||
}
|
||||
return id;
|
||||
return id || "";
|
||||
}
|
||||
|
||||
export function clearInProgressMessageWithId(id: string): void {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as AutoPilotUtils from "../Utils/AutoPilotUtils";
|
||||
import * as Constants from "../Shared/Constants";
|
||||
import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType";
|
||||
|
||||
interface ComputeRUUsagePriceHourlyArgs {
|
||||
serverId: string;
|
||||
@@ -256,9 +257,19 @@ export function getEstimatedSpendAcknowledgeString(
|
||||
)} - ${currencySign}${calculateEstimateNumber(monthlyPrice)} monthly cost for the throughput above.`;
|
||||
}
|
||||
|
||||
export function getUpsellMessage(serverId = "default", isFreeTier = false): string {
|
||||
export function getUpsellMessage(
|
||||
serverId = "default",
|
||||
isFreeTier = false,
|
||||
isFirstResourceCreated = false,
|
||||
defaultExperience: string,
|
||||
isCollection: boolean
|
||||
): string {
|
||||
if (isFreeTier) {
|
||||
return "With free tier discount, you'll get the first 400 RU/s and 5 GB of storage in this account for free. Charges will apply if your resource throughput exceeds 400 RU/s.";
|
||||
const collectionName = getCollectionName(defaultExperience);
|
||||
const resourceType = isCollection ? collectionName : "database";
|
||||
return isFirstResourceCreated
|
||||
? `The free tier discount of 400 RU/s has already been applied to a database or ${collectionName} in this account. Billing will apply to this ${resourceType} after it is created.`
|
||||
: `With free tier, you'll get the first 400 RU/s and 5 GB of storage in this account for free. Billing will apply if you provision more than 400 RU/s of manual throughput, or if the ${resourceType} scales beyond 400 RU/s with autoscale.`;
|
||||
} else {
|
||||
let price: number = Constants.OfferPricing.MonthlyPricing.default.Standard.StartingPrice;
|
||||
|
||||
@@ -269,3 +280,19 @@ export function getUpsellMessage(serverId = "default", isFreeTier = false): stri
|
||||
return `Start at ${getCurrencySign(serverId)}${price}/mo per database, multiple containers included`;
|
||||
}
|
||||
}
|
||||
|
||||
function getCollectionName(defaultExperience: string): string {
|
||||
switch (defaultExperience) {
|
||||
case DefaultAccountExperienceType.DocumentDB:
|
||||
return "container";
|
||||
case DefaultAccountExperienceType.MongoDB:
|
||||
return "collection";
|
||||
case DefaultAccountExperienceType.Table:
|
||||
case DefaultAccountExperienceType.Cassandra:
|
||||
return "table";
|
||||
case DefaultAccountExperienceType.Graph:
|
||||
return "graph";
|
||||
default:
|
||||
throw Error("unknown API type");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { armRequest } from "./request";
|
||||
import fetch from "node-fetch";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
import { AuthType } from "../../AuthType";
|
||||
|
||||
interface Global {
|
||||
Headers: unknown;
|
||||
@@ -8,6 +10,11 @@ interface Global {
|
||||
((global as unknown) as Global).Headers = ((fetch as unknown) as Global).Headers;
|
||||
|
||||
describe("ARM request", () => {
|
||||
window.authType = AuthType.AAD;
|
||||
updateUserContext({
|
||||
authorizationToken: "some-token"
|
||||
});
|
||||
|
||||
it("should call window.fetch", async () => {
|
||||
window.fetch = jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
@@ -48,4 +55,24 @@ describe("ARM request", () => {
|
||||
).rejects.toThrow();
|
||||
expect(window.fetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("should throw token error", async () => {
|
||||
window.authType = AuthType.AAD;
|
||||
updateUserContext({
|
||||
authorizationToken: undefined
|
||||
});
|
||||
const headers = new Headers();
|
||||
headers.set("location", "https://foo.com/operationStatus");
|
||||
window.fetch = jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
headers,
|
||||
status: 200,
|
||||
json: async () => {
|
||||
return { status: "Failed" };
|
||||
}
|
||||
});
|
||||
await expect(() =>
|
||||
armRequest({ apiVersion: "2001-01-01", host: "https://foo.com", path: "foo", method: "GET" })
|
||||
).rejects.toThrow("No authority token provided");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ export class ARMError extends Error {
|
||||
Object.setPrototypeOf(this, ARMError.prototype);
|
||||
}
|
||||
|
||||
public code: string | number;
|
||||
public code?: string | number;
|
||||
}
|
||||
|
||||
interface ARMQueryParams {
|
||||
@@ -63,6 +63,10 @@ export async function armRequest<T>({
|
||||
queryParams.metricNames && url.searchParams.append("metricnames", queryParams.metricNames);
|
||||
}
|
||||
|
||||
if (!userContext.authorizationToken) {
|
||||
throw new Error("No authority token provided");
|
||||
}
|
||||
|
||||
const response = await window.fetch(url.href, {
|
||||
method,
|
||||
headers: {
|
||||
@@ -98,6 +102,10 @@ export async function armRequest<T>({
|
||||
}
|
||||
|
||||
async function getOperationStatus(operationStatusUrl: string) {
|
||||
if (!userContext.authorizationToken) {
|
||||
throw new Error("No authority token provided");
|
||||
}
|
||||
|
||||
const response = await window.fetch(operationStatusUrl, {
|
||||
headers: {
|
||||
Authorization: userContext.authorizationToken
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<a class="skip-link" href="#data-explorer-content">Skip to content</a>
|
||||
<header>
|
||||
<div class="items" role="menubar">
|
||||
<div class="cosmosDBTitle">
|
||||
@@ -48,16 +49,18 @@
|
||||
|
||||
<switch-directory-pane params="{data: switchDirectoryPane}"></switch-directory-pane>
|
||||
|
||||
<!-- TODO generate version number dynamically -->
|
||||
<iframe
|
||||
id="explorerMenu"
|
||||
name="explorer"
|
||||
class="iframe"
|
||||
title="explorer"
|
||||
src="explorer.html?v=1.0.1&platform=Hosted"
|
||||
data-bind="visible: navigationSelection() === 'explorer'"
|
||||
>
|
||||
</iframe>
|
||||
<div id="data-explorer-content">
|
||||
<!-- TODO generate version number dynamically -->
|
||||
<iframe
|
||||
id="explorerMenu"
|
||||
name="explorer"
|
||||
class="iframe"
|
||||
title="explorer"
|
||||
src="explorer.html?v=1.0.1&platform=Hosted"
|
||||
data-bind="visible: navigationSelection() === 'explorer'"
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
<div data-bind="react: firewallWarningComponentAdapter"></div>
|
||||
<div data-bind="react: dialogComponentAdapter"></div>
|
||||
|
||||
@@ -1,59 +1,9 @@
|
||||
import { ElementHandle, Frame } from "puppeteer";
|
||||
import { TestExplorerParams } from "./testExplorer/TestExplorerParams";
|
||||
import * as path from "path";
|
||||
|
||||
export const NOTEBOOK_OPERATION_DELAY = 5000;
|
||||
export const RENDER_DELAY = 2500;
|
||||
|
||||
let testExplorerFrame: Frame;
|
||||
export const getTestExplorerFrame = async (): Promise<Frame> => {
|
||||
if (testExplorerFrame) {
|
||||
return testExplorerFrame;
|
||||
}
|
||||
|
||||
const notebooksTestRunnerTenantId = process.env.NOTEBOOKS_TEST_RUNNER_TENANT_ID;
|
||||
const notebooksTestRunnerClientId = process.env.NOTEBOOKS_TEST_RUNNER_CLIENT_ID;
|
||||
const notebooksTestRunnerClientSecret = process.env.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET;
|
||||
const portalRunnerDatabaseAccount = process.env.PORTAL_RUNNER_DATABASE_ACCOUNT;
|
||||
const portalRunnerDatabaseAccountKey = process.env.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY;
|
||||
const portalRunnerSubscripton = process.env.PORTAL_RUNNER_SUBSCRIPTION;
|
||||
const portalRunnerResourceGroup = process.env.PORTAL_RUNNER_RESOURCE_GROUP;
|
||||
|
||||
const testExplorerUrl = new URL("testExplorer.html", "https://localhost:1234");
|
||||
testExplorerUrl.searchParams.append(
|
||||
TestExplorerParams.notebooksTestRunnerTenantId,
|
||||
encodeURI(notebooksTestRunnerTenantId)
|
||||
);
|
||||
testExplorerUrl.searchParams.append(
|
||||
TestExplorerParams.notebooksTestRunnerClientId,
|
||||
encodeURI(notebooksTestRunnerClientId)
|
||||
);
|
||||
testExplorerUrl.searchParams.append(
|
||||
TestExplorerParams.notebooksTestRunnerClientSecret,
|
||||
encodeURI(notebooksTestRunnerClientSecret)
|
||||
);
|
||||
testExplorerUrl.searchParams.append(
|
||||
TestExplorerParams.portalRunnerDatabaseAccount,
|
||||
encodeURI(portalRunnerDatabaseAccount)
|
||||
);
|
||||
testExplorerUrl.searchParams.append(
|
||||
TestExplorerParams.portalRunnerDatabaseAccountKey,
|
||||
encodeURI(portalRunnerDatabaseAccountKey)
|
||||
);
|
||||
testExplorerUrl.searchParams.append(TestExplorerParams.portalRunnerSubscripton, encodeURI(portalRunnerSubscripton));
|
||||
testExplorerUrl.searchParams.append(
|
||||
TestExplorerParams.portalRunnerResourceGroup,
|
||||
encodeURI(portalRunnerResourceGroup)
|
||||
);
|
||||
|
||||
await page.goto(testExplorerUrl.toString());
|
||||
|
||||
const handle = await page.waitForSelector("iframe");
|
||||
testExplorerFrame = await handle.contentFrame();
|
||||
await testExplorerFrame.waitForSelector(".galleryHeader");
|
||||
return testExplorerFrame;
|
||||
};
|
||||
|
||||
export const uploadNotebookIfNotExist = async (frame: Frame, notebookName: string): Promise<ElementHandle<Element>> => {
|
||||
const notebookNode = await getNotebookNode(frame, notebookName);
|
||||
if (notebookNode) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "expect-puppeteer";
|
||||
import { getTestExplorerFrame, uploadNotebookIfNotExist } from "./notebookTestUtils";
|
||||
import { uploadNotebookIfNotExist } from "./notebookTestUtils";
|
||||
import { ElementHandle, Frame } from "puppeteer";
|
||||
import { getTestExplorerFrame } from "../testExplorer/TestExplorerUtils";
|
||||
|
||||
jest.setTimeout(300000);
|
||||
|
||||
@@ -12,6 +12,7 @@ describe("Notebook UI tests", () => {
|
||||
it("Upload, Open and Delete Notebook", async () => {
|
||||
try {
|
||||
frame = await getTestExplorerFrame();
|
||||
await frame.waitForSelector(".galleryHeader");
|
||||
uploadedNotebookNode = await uploadNotebookIfNotExist(frame, notebookName);
|
||||
await uploadedNotebookNode.click();
|
||||
await frame.waitForSelector(".tabNavText");
|
||||
|
||||
27
test/selfServe/selfServeExample.spec.ts
Normal file
27
test/selfServe/selfServeExample.spec.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Frame } from "puppeteer";
|
||||
import { TestExplorerParams } from "../testExplorer/TestExplorerParams";
|
||||
import { getTestExplorerFrame } from "../testExplorer/TestExplorerUtils";
|
||||
import { SelfServeType } from "../../src/SelfServe/SelfServeUtils";
|
||||
|
||||
jest.setTimeout(300000);
|
||||
|
||||
let frame: Frame;
|
||||
describe("Self Serve", () => {
|
||||
it("Launch Self Serve Example", async () => {
|
||||
try {
|
||||
frame = await getTestExplorerFrame(
|
||||
new Map<string, string>([[TestExplorerParams.selfServeType, SelfServeType.example]])
|
||||
);
|
||||
await frame.waitForSelector("#regions-dropown-input");
|
||||
await frame.waitForSelector("#enableLogging-radioSwitch-input");
|
||||
await frame.waitForSelector("#accountName-textBox-input");
|
||||
await frame.waitForSelector("#dbThroughput-slider-input");
|
||||
await frame.waitForSelector("#collectionThroughput-spinner-input");
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const testName = (expect as any).getState().currentTestName;
|
||||
await page.screenshot({ path: `Test Failed ${testName}.jpg` });
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,11 @@
|
||||
import { MessageTypes } from "../../../src/Contracts/ExplorerContracts";
|
||||
import "../../../less/hostedexplorer.less";
|
||||
import { MessageTypes } from "../../src/Contracts/ExplorerContracts";
|
||||
import "../../less/hostedexplorer.less";
|
||||
import { TestExplorerParams } from "./TestExplorerParams";
|
||||
import { ClientSecretCredential } from "@azure/identity";
|
||||
import { DatabaseAccountsGetResponse } from "@azure/arm-cosmosdb/esm/models";
|
||||
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
|
||||
import * as msRest from "@azure/ms-rest-js";
|
||||
import * as ViewModels from "../../../src/Contracts/ViewModels";
|
||||
import * as ViewModels from "../../src/Contracts/ViewModels";
|
||||
|
||||
class CustomSigner implements msRest.ServiceClientCredentials {
|
||||
private token: string;
|
||||
@@ -87,6 +87,7 @@ const initTestExplorer = async (): Promise<void> => {
|
||||
const portalRunnerResourceGroup = decodeURIComponent(
|
||||
urlSearchParams.get(TestExplorerParams.portalRunnerResourceGroup)
|
||||
);
|
||||
const selfServeType = urlSearchParams.get(TestExplorerParams.selfServeType);
|
||||
|
||||
const token = await AADLogin(
|
||||
notebooksTestRunnerTenantId,
|
||||
@@ -128,7 +129,8 @@ const initTestExplorer = async (): Promise<void> => {
|
||||
throughput: { fixed: 400, unlimited: 400, unlimitedmax: 100000, unlimitedmin: 400, shared: 400 }
|
||||
},
|
||||
// add UI test only when feature is not dependent on flights anymore
|
||||
flights: []
|
||||
flights: [],
|
||||
selfServeType: selfServeType
|
||||
} as ViewModels.DataExplorerInputsFrame
|
||||
};
|
||||
|
||||
@@ -5,5 +5,6 @@ export enum TestExplorerParams {
|
||||
portalRunnerDatabaseAccount = "portalRunnerDatabaseAccount",
|
||||
portalRunnerDatabaseAccountKey = "portalRunnerDatabaseAccountKey",
|
||||
portalRunnerSubscripton = "portalRunnerSubscripton",
|
||||
portalRunnerResourceGroup = "portalRunnerResourceGroup"
|
||||
portalRunnerResourceGroup = "portalRunnerResourceGroup",
|
||||
selfServeType = "selfServeType"
|
||||
}
|
||||
54
test/testExplorer/TestExplorerUtils.ts
Normal file
54
test/testExplorer/TestExplorerUtils.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Frame } from "puppeteer";
|
||||
import { TestExplorerParams } from "./TestExplorerParams";
|
||||
|
||||
let testExplorerFrame: Frame;
|
||||
export const getTestExplorerFrame = async (params?: Map<string, string>): Promise<Frame> => {
|
||||
if (testExplorerFrame) {
|
||||
return testExplorerFrame;
|
||||
}
|
||||
|
||||
const notebooksTestRunnerTenantId = process.env.NOTEBOOKS_TEST_RUNNER_TENANT_ID;
|
||||
const notebooksTestRunnerClientId = process.env.NOTEBOOKS_TEST_RUNNER_CLIENT_ID;
|
||||
const notebooksTestRunnerClientSecret = process.env.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET;
|
||||
const portalRunnerDatabaseAccount = process.env.PORTAL_RUNNER_DATABASE_ACCOUNT;
|
||||
const portalRunnerDatabaseAccountKey = process.env.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY;
|
||||
const portalRunnerSubscripton = process.env.PORTAL_RUNNER_SUBSCRIPTION;
|
||||
const portalRunnerResourceGroup = process.env.PORTAL_RUNNER_RESOURCE_GROUP;
|
||||
|
||||
const testExplorerUrl = new URL("testExplorer.html", "https://localhost:1234");
|
||||
testExplorerUrl.searchParams.append(
|
||||
TestExplorerParams.notebooksTestRunnerTenantId,
|
||||
encodeURI(notebooksTestRunnerTenantId)
|
||||
);
|
||||
testExplorerUrl.searchParams.append(
|
||||
TestExplorerParams.notebooksTestRunnerClientId,
|
||||
encodeURI(notebooksTestRunnerClientId)
|
||||
);
|
||||
testExplorerUrl.searchParams.append(
|
||||
TestExplorerParams.notebooksTestRunnerClientSecret,
|
||||
encodeURI(notebooksTestRunnerClientSecret)
|
||||
);
|
||||
testExplorerUrl.searchParams.append(
|
||||
TestExplorerParams.portalRunnerDatabaseAccount,
|
||||
encodeURI(portalRunnerDatabaseAccount)
|
||||
);
|
||||
testExplorerUrl.searchParams.append(
|
||||
TestExplorerParams.portalRunnerDatabaseAccountKey,
|
||||
encodeURI(portalRunnerDatabaseAccountKey)
|
||||
);
|
||||
testExplorerUrl.searchParams.append(TestExplorerParams.portalRunnerSubscripton, encodeURI(portalRunnerSubscripton));
|
||||
testExplorerUrl.searchParams.append(
|
||||
TestExplorerParams.portalRunnerResourceGroup,
|
||||
encodeURI(portalRunnerResourceGroup)
|
||||
);
|
||||
|
||||
if (params) {
|
||||
for (const key of params.keys()) {
|
||||
testExplorerUrl.searchParams.append(key, encodeURI(params.get(key)));
|
||||
}
|
||||
}
|
||||
|
||||
await page.goto(testExplorerUrl.toString());
|
||||
const handle = await page.waitForSelector("iframe");
|
||||
return await handle.contentFrame();
|
||||
};
|
||||
@@ -12,6 +12,8 @@
|
||||
"downlevelIteration": true,
|
||||
"module": "esnext",
|
||||
"target": "es5",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"lib": ["es5", "es6", "dom", "webworker.importscripts"],
|
||||
"jsx": "react",
|
||||
"moduleResolution": "node",
|
||||
@@ -19,6 +21,6 @@
|
||||
"noEmit": true,
|
||||
"types": ["jest"]
|
||||
},
|
||||
"include": ["./src/**/*", "./test/notebooks/testExplorer/**/*"],
|
||||
"include": ["./src/**/*", "./test/testExplorer/TestExplorer.ts"],
|
||||
"exclude": ["./src/**/__mocks__/**/*"]
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"./src/Common/ArrayHashMap.ts",
|
||||
"./src/Common/Constants.ts",
|
||||
"./src/Common/DeleteFeedback.ts",
|
||||
"./src/Common/DocumentUtility.ts",
|
||||
"./src/Common/EnvironmentUtility.ts",
|
||||
"./src/Common/HashMap.ts",
|
||||
"./src/Common/HeadersUtility.ts",
|
||||
@@ -20,8 +21,10 @@
|
||||
"./src/Common/MessageHandler.ts",
|
||||
"./src/Common/MongoUtility.ts",
|
||||
"./src/Common/ObjectCache.ts",
|
||||
"./src/Common/OfferUtility.ts",
|
||||
"./src/Common/ThemeUtility.ts",
|
||||
"./src/Common/UrlUtility.ts",
|
||||
"./src/Common/Splitter.ts",
|
||||
"./src/ConfigContext.ts",
|
||||
"./src/Contracts/ActionContracts.ts",
|
||||
"./src/Contracts/DataModels.ts",
|
||||
@@ -39,9 +42,15 @@
|
||||
"./src/Definitions/plotly.js-cartesian-dist.d-min.ts",
|
||||
"./src/Definitions/svg.d.ts",
|
||||
"./src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts",
|
||||
"./src/Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.ts",
|
||||
"./src/Explorer/Controls/GitHub/GitHubStyleConstants.ts",
|
||||
"./src/Explorer/Controls/InputTypeahead/InputTypeahead.ts",
|
||||
"./src/Explorer/Controls/SmartUi/InputUtils.ts",
|
||||
"./src/Explorer/Graph/GraphExplorerComponent/__mocks__/GremlinClient.ts",
|
||||
"./src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.ts",
|
||||
"./src/Explorer/Graph/GraphExplorerComponent/EdgeInfoCache.ts",
|
||||
"./src/Explorer/Graph/GraphExplorerComponent/GraphData.ts",
|
||||
"./src/Explorer/Notebook/NotebookComponent/__mocks__/rx-jupyter.ts",
|
||||
"./src/Explorer/Notebook/FileSystemUtil.ts",
|
||||
"./src/Explorer/Notebook/NTeractUtil.ts",
|
||||
"./src/Explorer/Notebook/NotebookComponent/actions.ts",
|
||||
@@ -50,13 +59,18 @@
|
||||
"./src/Explorer/Notebook/NotebookComponent/types.ts",
|
||||
"./src/Explorer/Notebook/NotebookContentItem.ts",
|
||||
"./src/Explorer/Notebook/NotebookUtil.ts",
|
||||
"./src/Explorer/Tree/AccessibleVerticalList.ts",
|
||||
"./src/Explorer/Panes/PaneComponents.ts",
|
||||
"./src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts",
|
||||
"./src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts",
|
||||
"./src/Explorer/Tables/Constants.ts",
|
||||
"./src/Explorer/Tables/CqlUtilities.ts",
|
||||
"./src/Explorer/Tables/QueryBuilder/DateTimeUtilities.ts",
|
||||
"./src/Explorer/Tables/DataTable/CacheBase.ts",
|
||||
"./src/Explorer/Tables/Entities.ts",
|
||||
"./src/Explorer/Tabs/TabComponents.ts",
|
||||
"./src/Explorer/Notebook/NotebookComponent/__mocks__/rx-jupyter.ts",
|
||||
"./src/Explorer/Notebook/NotebookContentClient.ts",
|
||||
"./src/GitHub/GitHubConnector.ts",
|
||||
"./src/Index.ts",
|
||||
"./src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts",
|
||||
@@ -70,6 +84,8 @@
|
||||
"./src/Shared/Telemetry/TelemetryConstants.ts",
|
||||
"./src/Shared/Telemetry/TelemetryProcessor.ts",
|
||||
"./src/Shared/appInsights.ts",
|
||||
"./src/Shared/DefaultExperienceUtility.ts",
|
||||
"./src/Terminal/index.ts",
|
||||
"./src/Terminal/JupyterLabAppFactory.ts",
|
||||
"./src/UserContext.ts",
|
||||
"./src/Utils/Base64Utils.ts",
|
||||
@@ -79,6 +95,27 @@
|
||||
"./src/Utils/StringUtils.ts",
|
||||
"./src/Utils/WindowUtils.ts",
|
||||
"./src/Utils/arm/generatedClients/2020-04-01/types.ts",
|
||||
"./src/Utils/AutoPilotUtils.ts",
|
||||
"./src/Utils/PricingUtils.ts",
|
||||
"./src/Utils/arm/generatedClients/2020-04-01/cassandraResources.ts",
|
||||
"./src/Utils/arm/generatedClients/2020-04-01/collection.ts",
|
||||
"./src/Utils/arm/generatedClients/2020-04-01/collectionPartition.ts",
|
||||
"./src/Utils/arm/generatedClients/2020-04-01/collectionPartitionRegion.ts",
|
||||
"./src/Utils/arm/generatedClients/2020-04-01/collectionRegion.ts",
|
||||
"./src/Utils/arm/generatedClients/2020-04-01/database.ts",
|
||||
"./src/Utils/arm/generatedClients/2020-04-01/databaseAccountRegion.ts",
|
||||
"./src/Utils/arm/generatedClients/2020-04-01/databaseAccounts.ts",
|
||||
"./src/Utils/arm/generatedClients/2020-04-01/gremlinResources.ts",
|
||||
"./src/Utils/arm/generatedClients/2020-04-01/mongoDBResources.ts",
|
||||
"./src/Utils/arm/generatedClients/2020-04-01/operations.ts",
|
||||
"./src/Utils/arm/generatedClients/2020-04-01/partitionKeyRangeId.ts",
|
||||
"./src/Utils/arm/generatedClients/2020-04-01/partitionKeyRangeIdRegion.ts",
|
||||
"./src/Utils/arm/generatedClients/2020-04-01/percentile.ts",
|
||||
"./src/Utils/arm/generatedClients/2020-04-01/percentileSourceTarget.ts",
|
||||
"./src/Utils/arm/generatedClients/2020-04-01/percentileTarget.ts",
|
||||
"./src/Utils/arm/generatedClients/2020-04-01/sqlResources.ts",
|
||||
"./src/Utils/arm/generatedClients/2020-04-01/tableResources.ts",
|
||||
"./src/Utils/arm/request.ts",
|
||||
"./src/quickstart.ts",
|
||||
"./src/setupTests.ts",
|
||||
"./src/workers/upload/definitions.ts"
|
||||
|
||||
@@ -142,7 +142,7 @@ module.exports = function(env = {}, argv = {}) {
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "testExplorer.html",
|
||||
template: "test/notebooks/testExplorer/testExplorer.html",
|
||||
template: "test/testExplorer/testExplorer.html",
|
||||
chunks: ["testExplorer"]
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
@@ -183,7 +183,7 @@ module.exports = function(env = {}, argv = {}) {
|
||||
index: "./src/Index.ts",
|
||||
quickstart: "./src/quickstart.ts",
|
||||
hostedExplorer: "./src/HostedExplorer.ts",
|
||||
testExplorer: "./test/notebooks/testExplorer/TestExplorer.ts",
|
||||
testExplorer: "./test/testExplorer/TestExplorer.ts",
|
||||
heatmap: "./src/Controls/Heatmap/Heatmap.ts",
|
||||
terminal: "./src/Terminal/index.ts",
|
||||
notebookViewer: "./src/NotebookViewer/NotebookViewer.tsx",
|
||||
|
||||
Reference in New Issue
Block a user