mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 19:01:28 +00:00
Compare commits
93 Commits
fixed-ts-s
...
migrate_qu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cec6b27d2e | ||
|
|
7d905159c6 | ||
|
|
6ca8e3c6f4 | ||
|
|
f7e7240010 | ||
|
|
acc095a482 | ||
|
|
288e6410f3 | ||
|
|
9ac3392271 | ||
|
|
9a908dde9a | ||
|
|
ddd2e63fe7 | ||
|
|
34c59b4872 | ||
|
|
7d28af4fc7 | ||
|
|
50b99ceb42 | ||
|
|
15a26d6500 | ||
|
|
a8150af269 | ||
|
|
6a9a0156a3 | ||
|
|
ead28f043f | ||
|
|
b05e5a2145 | ||
|
|
5e8aa491ba | ||
|
|
a730c08292 | ||
|
|
3dce5cd129 | ||
|
|
7c186c3ef2 | ||
|
|
d8840a0dfd | ||
|
|
f853c4ec2f | ||
|
|
9bf5f48165 | ||
|
|
0b2a204b70 | ||
|
|
c28593b752 | ||
|
|
4cbbef9574 | ||
|
|
300c952a9b | ||
|
|
38c3761260 | ||
|
|
3032f689b6 | ||
|
|
8b30af3d9e | ||
|
|
e10240bd7a | ||
|
|
ae9c27795e | ||
|
|
d7997d716e | ||
|
|
af0dc3094b | ||
|
|
665270296f | ||
|
|
2d945c8231 | ||
|
|
8866976bb4 | ||
|
|
d10f3c69f1 | ||
|
|
7e4f030547 | ||
|
|
05f46dd635 | ||
|
|
65882ea831 | ||
|
|
95c9b7ee31 | ||
|
|
39dd293fc1 | ||
|
|
126a572078 | ||
|
|
597357e62d | ||
|
|
6c545c454d | ||
|
|
d4817de14e | ||
|
|
43b6e25e86 | ||
|
|
30fa40c50e | ||
|
|
1f2e528f46 | ||
|
|
e431692e0d | ||
|
|
1985843b93 | ||
|
|
5287e6e29f | ||
|
|
27e99d7f2f | ||
|
|
884f06a729 | ||
|
|
b7cb0b55a6 | ||
|
|
c1c085f712 | ||
|
|
2b22e518e0 | ||
|
|
e95245f1df | ||
|
|
046e6eb5a4 | ||
|
|
d7963b3ef4 | ||
|
|
d70dd6bc7e | ||
|
|
87a368858c | ||
|
|
e1d32bde36 | ||
|
|
91d86dd271 | ||
|
|
0df0c4b420 | ||
|
|
07b3e30f05 | ||
|
|
1931e775d9 | ||
|
|
fe684fd6d2 | ||
|
|
c681c14be1 | ||
|
|
0cb897451d | ||
|
|
18217b71c6 | ||
|
|
d805a0ba4a | ||
|
|
ec3ac87a20 | ||
|
|
9a74a8c2ab | ||
|
|
8a1920714d | ||
|
|
7fa5883072 | ||
|
|
d062c85e94 | ||
|
|
239c5f6895 | ||
|
|
594c4026d5 | ||
|
|
dc08ba740e | ||
|
|
f1ffa968a7 | ||
|
|
8ec551f6e6 | ||
|
|
08d04295b1 | ||
|
|
13f94e83f0 | ||
|
|
4f632b234f | ||
|
|
754f0b392c | ||
|
|
3bce7f764e | ||
|
|
079593cec4 | ||
|
|
27cc1337ef | ||
|
|
bb5aecac1b | ||
|
|
c4a1d4cea7 |
@@ -80,8 +80,8 @@ src/Explorer/Tables/DataTable/CacheBase.ts
|
|||||||
src/Explorer/Tables/DataTable/DataTableBindingManager.ts
|
src/Explorer/Tables/DataTable/DataTableBindingManager.ts
|
||||||
src/Explorer/Tables/DataTable/DataTableBuilder.ts
|
src/Explorer/Tables/DataTable/DataTableBuilder.ts
|
||||||
src/Explorer/Tables/DataTable/DataTableContextMenu.ts
|
src/Explorer/Tables/DataTable/DataTableContextMenu.ts
|
||||||
src/Explorer/Tables/DataTable/DataTableOperationManager.ts
|
# src/Explorer/Tables/DataTable/DataTableOperationManager.ts
|
||||||
src/Explorer/Tables/DataTable/DataTableOperations.ts
|
# src/Explorer/Tables/DataTable/DataTableOperations.ts
|
||||||
src/Explorer/Tables/DataTable/DataTableViewModel.ts
|
src/Explorer/Tables/DataTable/DataTableViewModel.ts
|
||||||
src/Explorer/Tables/DataTable/TableCommands.ts
|
src/Explorer/Tables/DataTable/TableCommands.ts
|
||||||
src/Explorer/Tables/DataTable/TableEntityCache.ts
|
src/Explorer/Tables/DataTable/TableEntityCache.ts
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<svg
|
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
version="1.1"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
<path
|
<path
|
||||||
d="M14,2.691l-5.301,5.309l5.301,5.309l-0.691,0.691l-5.309,-5.301l-5.309,5.301l-0.691,-0.691l5.301,-5.309l-5.301,-5.309l0.691,-0.691l5.309,5.301l5.309,-5.301l0.691,0.691Z"
|
d="M14,2.691l-5.301,5.309l5.301,5.309l-0.691,0.691l-5.309,-5.301l-5.309,5.301l-0.691,-0.691l5.301,-5.309l-5.301,-5.309l0.691,-0.691l5.309,5.301l5.309,-5.301l0.691,0.691Z"
|
||||||
transform="scale(0.5)"
|
transform="scale(0.5)" fill="#000" stroke="#000">
|
||||||
fill="#000"
|
|
||||||
stroke="#CCC"
|
|
||||||
>
|
|
||||||
</path>
|
</path>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 503 B After Width: | Height: | Size: 449 B |
@@ -37,8 +37,8 @@ module.exports = {
|
|||||||
global: {
|
global: {
|
||||||
branches: 25,
|
branches: 25,
|
||||||
functions: 25,
|
functions: 25,
|
||||||
lines: 30,
|
lines: 29.5,
|
||||||
statements: 30,
|
statements: 29.5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -3,337 +3,358 @@
|
|||||||
@import "../Common/Constants";
|
@import "../Common/Constants";
|
||||||
|
|
||||||
.query-panel {
|
.query-panel {
|
||||||
display: table;
|
display: table;
|
||||||
display: none;
|
display: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-top: 1px solid #DDDDDD;
|
border-top: 1px solid #dddddd;
|
||||||
/*[{environment-commandbar-toolbar-separator}]*/
|
/*[{environment-commandbar-toolbar-separator}]*/
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
/*[{plugin-background-color}]*/
|
/*[{plugin-background-color}]*/
|
||||||
padding: 2px 0px 0px 2px;
|
padding: 2px 0px 0px 2px;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-panel .row {
|
.query-panel .row {
|
||||||
display: table-row;
|
display: table-row;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-panel .row .cell {
|
.query-panel .row .cell {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-panel.transition-in {
|
.query-panel.transition-in {
|
||||||
display: table;
|
display: table;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
-webkit-transition: top 2s linear;
|
-webkit-transition: top 2s linear;
|
||||||
-ms-transition: top 2s linear;
|
-ms-transition: top 2s linear;
|
||||||
-moz-transition: top 2s linear;
|
-moz-transition: top 2s linear;
|
||||||
-khtml-transition: top 2s linear;
|
-khtml-transition: top 2s linear;
|
||||||
-o-transition: top 2s linear;
|
-o-transition: top 2s linear;
|
||||||
transition: top 2s linear;
|
transition: top 2s linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-builder {
|
.query-builder {
|
||||||
width:100%;
|
width: 100%;
|
||||||
padding-right: @DefaultSpace;
|
padding-right: @DefaultSpace;
|
||||||
border-bottom: 1px solid @BaseMedium;
|
border-bottom: 1px solid @BaseMedium;
|
||||||
margin-bottom: @DefaultSpace;
|
margin-bottom: @DefaultSpace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-builder-toolbar {
|
.query-builder-toolbar {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
/*[{plugin-background-color}]*/
|
/*[{plugin-background-color}]*/
|
||||||
min-width: 600px;
|
min-width: 600px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
border-bottom: 1px solid #DDDDDD;
|
border-bottom: 1px solid #dddddd;
|
||||||
/*[1px solid {environment-commandbar-toolbar-separator}]*/
|
/*[1px solid {environment-commandbar-toolbar-separator}]*/
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-builder-toolbar .query-toolbar-group {
|
.query-builder-toolbar .query-toolbar-group {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
margin: 2px 0px;
|
margin: 2px 0px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-builder-toolbar .query-toolbar-group .query-toolbar-button {
|
.query-builder-toolbar .query-toolbar-group .query-toolbar-button {
|
||||||
min-width: 0px;
|
min-width: 0px;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: solid transparent;
|
border: solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:active {
|
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:active {
|
||||||
outline: 2px solid dodgerblue;
|
outline: 2px solid dodgerblue;
|
||||||
/*[2px solid {common-common-controls-button-border-hover}]*/
|
/*[2px solid {common-common-controls-button-border-hover}]*/
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:hover {
|
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:hover {
|
||||||
background-color: #CCCEDB;
|
background-color: #cccedb;
|
||||||
/*[{common-controls-button-hover-background}]*/
|
/*[{common-controls-button-hover-background}]*/
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-builder-toolbar .query-toolbar-group .query-toolbar-button.active {
|
.query-builder-toolbar .query-toolbar-group .query-toolbar-button.active {
|
||||||
background-color: #E6E7ED;
|
background-color: #e6e7ed;
|
||||||
/*[{common-controls-inner-tab-active-background}]*/
|
/*[{common-controls-inner-tab-active-background}]*/
|
||||||
outline: none
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:disabled,
|
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:disabled,
|
||||||
.query-builder-toolbar .query-toolbar-group .query-toolbar-button.disabled {
|
.query-builder-toolbar .query-toolbar-group .query-toolbar-button.disabled {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
/*[{common-controls-button-disabled-background}]*/
|
/*[{common-controls-button-disabled-background}]*/
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
outline: none;
|
outline: none;
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tableContainer {
|
.tableContainer {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
.flex-display();
|
.flex-display();
|
||||||
.flex-direction();
|
.flex-direction();
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tablesQueryTab{
|
.tablesQueryTab {
|
||||||
padding-left: @MediumSpace;
|
padding-left: @MediumSpace;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom:@LargeSpace;
|
margin-bottom: 60px;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entity-error-Img {
|
.entity-error-Img {
|
||||||
width: @WarningErrorIconSize;
|
width: @WarningErrorIconSize;
|
||||||
height: @WarningErrorIconSize;
|
height: @WarningErrorIconSize;
|
||||||
margin: @DefaultSpace 0px 0px @SmallSpace;
|
margin: @DefaultSpace 0px 0px @SmallSpace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-editor-panel {
|
.query-editor-panel {
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
margin-top: 25px;
|
margin-top: 25px;
|
||||||
position: relative;
|
position: relative;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-editor-text {
|
.query-editor-text {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
border: solid 1px #A9ACB3;
|
border: solid 1px #a9acb3;
|
||||||
/*[{plugin-textbox-disabled-color}]*/
|
/*[{plugin-textbox-disabled-color}]*/
|
||||||
resize: none;
|
resize: none;
|
||||||
margin-top: -39px;
|
margin-top: -39px;
|
||||||
background-color: #ddd;
|
background-color: #ddd;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-bar {
|
.error-bar {
|
||||||
padding: @LargeSpace 34px @MediumSpace 24px;
|
padding: @LargeSpace 34px @MediumSpace 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
background-color: @BaseLow;
|
background-color: @BaseLow;
|
||||||
padding: @DefaultSpace;
|
padding: @DefaultSpace;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-text {
|
.error-text {
|
||||||
padding-left: @MediumSpace;
|
padding-left: @MediumSpace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-editor-text-invalid {
|
.query-editor-text-invalid {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
border: 1px solid #e51400;
|
border: 1px solid #e51400;
|
||||||
resize: none;
|
resize: none;
|
||||||
margin-top: -30px;
|
margin-top: -30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-editor-panel .warning-bar {
|
.query-editor-panel .warning-bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
/*[{plugin-background-color}]*/
|
/*[{plugin-background-color}]*/
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -24px;
|
top: -24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-editor-panel .warning-bar .warning-message {
|
.query-editor-panel .warning-bar .warning-message {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-editor-panel .warning-bar .warning-message .warning-text {
|
.query-editor-panel .warning-bar .warning-message .warning-text {
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.advanced-options-panel{
|
.advanced-options-panel {
|
||||||
margin-bottom: @DefaultSpace;
|
margin-bottom: @DefaultSpace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.advanced-options-panel .advanced-heading .advanced-title {
|
.advanced-options-panel .advanced-heading .advanced-title {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
margin-left: 27px;
|
margin-left: 27px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.advanced-options-panel .advanced-options {
|
.advanced-options-panel .advanced-options {
|
||||||
margin-left: 32px;
|
margin-left: 32px;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-top: 1px solid #ccc;
|
border-top: 1px solid #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
input::-webkit-outer-spin-button,
|
input::-webkit-outer-spin-button,
|
||||||
input::-webkit-inner-spin-button {
|
input::-webkit-inner-spin-button {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.advanced-options-panel .advanced-options .top .top-input {
|
.advanced-options-panel .advanced-options .top .top-input {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
word-spacing: normal;
|
word-spacing: normal;
|
||||||
color: #1E1E1E;
|
color: #1e1e1e;
|
||||||
/*[{common-controls-button-foreground}]*/
|
/*[{common-controls-button-foreground}]*/
|
||||||
border: 1px solid #CCCEDB;
|
border: 1px solid #cccedb;
|
||||||
/*[1px solid {plugin-textbox-border-color}]*/
|
/*[1px solid {plugin-textbox-border-color}]*/
|
||||||
height: 20px;
|
height: 20px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.advanced-options-panel .advanced-options .top .invalid-top {
|
.advanced-options-panel .advanced-options .top .invalid-top {
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
.advanced-options-panel .advanced-options .select {
|
.advanced-options-panel .advanced-options .select {
|
||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.advanced-options-icon {
|
.advanced-options-icon {
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
vertical-align: sub;
|
vertical-align: sub;
|
||||||
}
|
}
|
||||||
|
|
||||||
.advanced-options-panel .advanced-options .select .select-options-text {
|
.advanced-options-panel .advanced-options .select .select-options-text {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.advanced-options-panel .advanced-options .select .select-options-link {
|
.advanced-options-panel .advanced-options .select .select-options-link {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-panel .row .column-headers .Field {
|
.query-panel .row .column-headers .Field {
|
||||||
padding-left: 95px;
|
padding-left: 95px;
|
||||||
padding-right: 0px;
|
padding-right: 0px;
|
||||||
padding-bottom: 6px;
|
padding-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clause-table {
|
.clause-table {
|
||||||
border-spacing: 0px;
|
border-spacing: 0px;
|
||||||
display: table;
|
display: table;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: -3px;
|
margin-top: -3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clause-table-row {
|
.clause-table-row {
|
||||||
display: row;
|
display: row;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clause-table-cell {
|
.clause-table-cell {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-column>button,
|
.action-column > button,
|
||||||
.group-control-header>button,
|
.group-control-header > button,
|
||||||
.group-indicator-column>button {
|
.group-indicator-column > button {
|
||||||
min-width: 20px;
|
min-width: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-control-header>button:disabled {
|
.group-control-header > button:disabled {
|
||||||
min-width: 20px;
|
min-width: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
outline: none;
|
outline: none;
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clause-table-field {
|
.clause-table-field {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid #bbbbbb;
|
border: 1px solid #bbbbbb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clause-table-cell button {
|
.clause-table-cell button {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clause-table-cell input[type="checkbox"] {
|
.clause-table-cell input[type="checkbox"] {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.and-or-svg {
|
.and-or-svg {
|
||||||
margin-top: -8px;
|
margin-top: -8px;
|
||||||
margin-right: -5px;
|
margin-right: -26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.and-or-label {
|
||||||
|
margin-left: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
margin-left: 69px;
|
||||||
|
}
|
||||||
|
.data-type-label {
|
||||||
|
margin-left: 54px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operator-label {
|
||||||
|
margin-left: 80px;
|
||||||
|
}
|
||||||
|
.value-label {
|
||||||
|
margin-left: 62px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll-box {
|
.scroll-box {
|
||||||
border-bottom: 1px transparent #DDD;
|
border-bottom: 1px transparent #ddd;
|
||||||
/*[1px solid {plugin-table-border-color}]*/
|
/*[1px solid {plugin-table-border-color}]*/
|
||||||
border-top: 1px transparent #DDD;
|
border-top: 1px transparent #ddd;
|
||||||
/*[1px solid {plugin-table-border-color}]*/
|
/*[1px solid {plugin-table-border-color}]*/
|
||||||
max-height: 20vh;
|
max-height: 20vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollable {
|
.scrollable {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.and-or-column,
|
.and-or-column,
|
||||||
.and-or-header {
|
.and-or-header {
|
||||||
min-width: 65px;
|
min-width: 65px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.operator-column,
|
.operator-column,
|
||||||
.operator-header {
|
.operator-header {
|
||||||
min-width: 65px;
|
min-width: 65px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-header,
|
.field-header,
|
||||||
.field-column {
|
.field-column {
|
||||||
min-width: 125px;
|
min-width: 125px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.type-header,
|
.type-header,
|
||||||
.type-column {
|
.type-column {
|
||||||
min-width: 85px;
|
min-width: 85px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.and-or-column,
|
.and-or-column,
|
||||||
@@ -345,41 +366,41 @@ input::-webkit-inner-spin-button {
|
|||||||
.type-header,
|
.type-header,
|
||||||
.type-column,
|
.type-column,
|
||||||
.action-header {
|
.action-header {
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.value-header,
|
.value-header,
|
||||||
.value-column,
|
.value-column,
|
||||||
.time-column {
|
.time-column {
|
||||||
min-width: 230px;
|
min-width: 230px;
|
||||||
padding: 0px 4px 0px 0px;
|
padding: 0px 4px 0px 0px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-control-header,
|
.group-control-header,
|
||||||
.group-control-column {
|
.group-control-column {
|
||||||
min-width: 25px;
|
min-width: 25px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-indicator-table {
|
.group-indicator-table {
|
||||||
border-spacing: 0px;
|
border-spacing: 0px;
|
||||||
min-height: 24px
|
min-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-indicator-column {
|
.group-indicator-column {
|
||||||
min-width: 21px;
|
min-width: 21px;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
border-style: none;
|
border-style: none;
|
||||||
height: 29px;
|
height: 29px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clause-table-cell.action-column,
|
.clause-table-cell.action-column,
|
||||||
.clause-table-cell.action-header {
|
.clause-table-cell.action-header {
|
||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
padding-left: @SmallSpace;
|
padding-left: @SmallSpace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-header,
|
.action-header,
|
||||||
@@ -388,15 +409,14 @@ input::-webkit-inner-spin-button {
|
|||||||
.operator-header,
|
.operator-header,
|
||||||
.value-header,
|
.value-header,
|
||||||
.and-or-header {
|
.and-or-header {
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-background {
|
.header-background {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*.type-header {
|
/*.type-header {
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
@@ -410,111 +430,165 @@ input::-webkit-inner-spin-button {
|
|||||||
}*/
|
}*/
|
||||||
|
|
||||||
.clause-table-field[readonly] {
|
.clause-table-field[readonly] {
|
||||||
background-color: #EEEEF2;
|
background-color: #eeeef2;
|
||||||
/*[{plugin-table-header-background-color}]*/
|
/*[{plugin-table-header-background-color}]*/
|
||||||
border: 1px solid #CCCEDB;
|
border: 1px solid #cccedb;
|
||||||
/*[{plugin-table-border-color}]*/
|
/*[{plugin-table-border-color}]*/
|
||||||
}
|
}
|
||||||
|
|
||||||
.addClause-title {
|
.addClause-title {
|
||||||
/*[{common-common-controls-button-border-hover}]*/
|
/*[{common-common-controls-button-border-hover}]*/
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-left: -5px;
|
margin-left: -5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.addClause {
|
.addClause {
|
||||||
width: 125px;
|
width: 125px;
|
||||||
padding: 8px 0px 5px 5px;
|
padding: 8px 0px 5px 5px;
|
||||||
border: 1px solid #fff;
|
border: 1px solid #fff;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.addClause:hover {
|
.addClause:hover {
|
||||||
.hover();
|
.hover();
|
||||||
}
|
}
|
||||||
|
|
||||||
.addClause:active {
|
.addClause:active {
|
||||||
.active();
|
.active();
|
||||||
border: 1px dashed @AccentMedium;
|
border: 1px dashed @AccentMedium;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clause-table-row {
|
.clause-table-row {
|
||||||
min-width: 550px;
|
min-width: 550px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clause-table-field field-column {
|
.clause-table-field field-column {
|
||||||
min-width: 75px;
|
min-width: 75px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clause-table-field field-input {
|
.clause-table-field field-input {
|
||||||
min-width: 54px;
|
min-width: 54px;
|
||||||
margin-left: -78px;
|
margin-left: -78px;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-panel .row .spacing {
|
.query-panel .row .spacing {
|
||||||
padding-bottom: 6px;
|
padding-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-panel .divider.horizontal {
|
.query-panel .divider.horizontal {
|
||||||
height: 10px;
|
height: 10px;
|
||||||
width: 100%
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-div {
|
.inline-div {
|
||||||
display: inline
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.querybuilder-addpropertyImg,
|
.querybuilder-addpropertyImg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin-left: 3px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
.querybuilder-cancelImg {
|
.querybuilder-cancelImg {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
margin-left: 3px;
|
margin-left: 3px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.addclauseProperty-Img {
|
.addclauseProperty-Img {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entity-Add-Cancel {
|
.entity-Add-Cancel {
|
||||||
padding: @DefaultSpace @SmallSpace @SmallSpace;
|
padding: @DefaultSpace @SmallSpace @SmallSpace;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entity-Add-Cancel:hover {
|
.entity-Add-Cancel:hover {
|
||||||
.hover();
|
.hover();
|
||||||
}
|
}
|
||||||
|
|
||||||
.entity-Add-Cancel:active {
|
.entity-Add-Cancel:active {
|
||||||
.active();
|
.active();
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-builder-isDisabled {
|
.query-builder-isDisabled {
|
||||||
border: 1px solid #CCCEDB;
|
border: 1px solid #cccedb;
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-value-text {
|
.edit-value-text {
|
||||||
padding-left: @DefaultSpace;
|
padding-left: @DefaultSpace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expand-triangle {
|
.expand-triangle {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expand-triangle-right {
|
.expand-triangle-right {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.query-document-detail-list {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.query-table-clause-container {
|
||||||
|
max-height: 150px;
|
||||||
|
overflow: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
.query-tab-document-pagination {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
padding-left: 12px;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
.pagination {
|
||||||
|
margin: 15px 0 !important;
|
||||||
|
order: 2;
|
||||||
|
padding-right: 15px;
|
||||||
|
li > .item-link {
|
||||||
|
position: relative;
|
||||||
|
float: left;
|
||||||
|
padding: 6px 12px;
|
||||||
|
margin-left: -1px;
|
||||||
|
line-height: 1.42857143;
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
font-weight: bold;
|
||||||
|
background: #eef7ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.noData {
|
||||||
|
background-color: #e3e2e6;
|
||||||
|
color: #e3e2e6;
|
||||||
|
padding-top: 1px;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@media only screen and (max-width: 1200px) {
|
@media only screen and (max-width: 1200px) {
|
||||||
@@ -524,4 +598,4 @@ input::-webkit-inner-spin-button {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|||||||
@@ -2357,6 +2357,8 @@ a:link {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
min-height: 300px;
|
||||||
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
@@ -2832,6 +2834,8 @@ a:link {
|
|||||||
|
|
||||||
#explorerNotificationConsole {
|
#explorerNotificationConsole {
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uniqueIndexesContainer {
|
.uniqueIndexesContainer {
|
||||||
|
|||||||
34145
package-lock.json
generated
34145
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,5 @@
|
|||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as ReactBindingHandler from "./ReactBindingHandler";
|
import * as ReactBindingHandler from "./ReactBindingHandler";
|
||||||
import "../Explorer/Tables/DataTable/DataTableBindingManager";
|
|
||||||
|
|
||||||
export class BindingHandlersRegisterer {
|
export class BindingHandlersRegisterer {
|
||||||
public static registerBindingHandlers() {
|
public static registerBindingHandlers() {
|
||||||
ko.bindingHandlers.setTemplateReady = {
|
ko.bindingHandlers.setTemplateReady = {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { FunctionComponent } from "react";
|
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
|
||||||
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
import { NormalizedEventKey } from "./Constants";
|
||||||
|
|
||||||
export interface CollapsedResourceTreeProps {
|
export interface CollapsedResourceTreeProps {
|
||||||
toggleLeftPaneExpanded: () => void;
|
toggleLeftPaneExpanded: () => void;
|
||||||
@@ -11,6 +12,21 @@ export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps
|
|||||||
toggleLeftPaneExpanded,
|
toggleLeftPaneExpanded,
|
||||||
isLeftPaneExpanded,
|
isLeftPaneExpanded,
|
||||||
}: CollapsedResourceTreeProps): JSX.Element => {
|
}: CollapsedResourceTreeProps): JSX.Element => {
|
||||||
|
const focusButton = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (focusButton.current) {
|
||||||
|
focusButton.current.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onKeyPressToggleLeftPaneExpanded = (event: React.KeyboardEvent) => {
|
||||||
|
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
|
||||||
|
toggleLeftPaneExpanded();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="mini" className={!isLeftPaneExpanded ? "mini toggle-mini" : "hiddenMain"}>
|
<div id="mini" className={!isLeftPaneExpanded ? "mini toggle-mini" : "hiddenMain"}>
|
||||||
<div className="main-nav nav">
|
<div className="main-nav nav">
|
||||||
@@ -21,11 +37,14 @@ export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps
|
|||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-label="Expand Tree"
|
aria-label="Expand Tree"
|
||||||
|
onClick={toggleLeftPaneExpanded}
|
||||||
|
onKeyPress={onKeyPressToggleLeftPaneExpanded}
|
||||||
|
ref={focusButton}
|
||||||
>
|
>
|
||||||
<span className="leftarrowCollapsed" onClick={toggleLeftPaneExpanded}>
|
<span className="leftarrowCollapsed">
|
||||||
<img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" />
|
<img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" />
|
||||||
</span>
|
</span>
|
||||||
<span className="collectionCollapsed" onClick={toggleLeftPaneExpanded}>
|
<span className="collectionCollapsed">
|
||||||
<span>{userContext.apiType} API</span>
|
<span>{userContext.apiType} API</span>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export class Flights {
|
|||||||
public static readonly AutoscaleTest = "autoscaletest";
|
public static readonly AutoscaleTest = "autoscaletest";
|
||||||
public static readonly PartitionKeyTest = "partitionkeytest";
|
public static readonly PartitionKeyTest = "partitionkeytest";
|
||||||
public static readonly PKPartitionKeyTest = "pkpartitionkeytest";
|
public static readonly PKPartitionKeyTest = "pkpartitionkeytest";
|
||||||
|
public static readonly Phoenix = "phoenix";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AfecFeatures {
|
export class AfecFeatures {
|
||||||
@@ -337,6 +338,12 @@ export enum ConflictOperationType {
|
|||||||
Delete = "delete",
|
Delete = "delete",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ConnectionStatusType {
|
||||||
|
Connecting = "Connecting",
|
||||||
|
Connected = "Connected",
|
||||||
|
Failed = "Connection Failed",
|
||||||
|
}
|
||||||
|
|
||||||
export const EmulatorMasterKey =
|
export const EmulatorMasterKey =
|
||||||
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
|
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
|
||||||
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
|
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
|
|||||||
<TextField
|
<TextField
|
||||||
label={entityValueLabel && entityValueLabel}
|
label={entityValueLabel && entityValueLabel}
|
||||||
className="addEntityTextField"
|
className="addEntityTextField"
|
||||||
id="entityValueId"
|
// id="entityValueId"
|
||||||
autoFocus
|
autoFocus
|
||||||
disabled={isEntityValueDisable}
|
disabled={isEntityValueDisable}
|
||||||
type={entityValueType}
|
type={entityValueType}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
jest.mock("./MessageHandler");
|
jest.mock("./MessageHandler");
|
||||||
|
import { LogEntryLevel } from "../Contracts/Diagnostics";
|
||||||
import * as Logger from "./Logger";
|
import * as Logger from "./Logger";
|
||||||
|
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||||
import { sendMessage } from "./MessageHandler";
|
import { sendMessage } from "./MessageHandler";
|
||||||
|
|
||||||
describe("Logger", () => {
|
describe("Logger", () => {
|
||||||
|
|||||||
@@ -3,8 +3,16 @@ import { resetConfigContext, updateConfigContext } from "../ConfigContext";
|
|||||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||||
import { Collection } from "../Contracts/ViewModels";
|
import { Collection } from "../Contracts/ViewModels";
|
||||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||||
|
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
||||||
import { updateUserContext } from "../UserContext";
|
import { updateUserContext } from "../UserContext";
|
||||||
import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
|
import {
|
||||||
|
deleteDocument,
|
||||||
|
getEndpoint,
|
||||||
|
getFeatureEndpointOrDefault,
|
||||||
|
queryDocuments,
|
||||||
|
readDocument,
|
||||||
|
updateDocument,
|
||||||
|
} from "./MongoProxyClient";
|
||||||
|
|
||||||
const databaseId = "testDB";
|
const databaseId = "testDB";
|
||||||
|
|
||||||
@@ -246,4 +254,31 @@ describe("MongoProxyClient", () => {
|
|||||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
|
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe("getFeatureEndpointOrDefault", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetConfigContext();
|
||||||
|
updateConfigContext({
|
||||||
|
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||||
|
});
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
"feature.mongoProxyEndpoint": "https://localhost:12901",
|
||||||
|
"feature.mongoProxyAPIs": "readDocument|createDocument",
|
||||||
|
});
|
||||||
|
const features = extractFeatures(params);
|
||||||
|
updateUserContext({
|
||||||
|
authType: AuthType.AAD,
|
||||||
|
features: features,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a local endpoint", () => {
|
||||||
|
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||||
|
expect(endpoint).toEqual("https://localhost:12901/api/mongo/explorer");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a production endpoint", () => {
|
||||||
|
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
||||||
|
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import * as DataModels from "../Contracts/DataModels";
|
|||||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||||
import { Collection } from "../Contracts/ViewModels";
|
import { Collection } from "../Contracts/ViewModels";
|
||||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||||
|
import { hasFlag } from "../Platform/Hosted/extractFeatures";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
||||||
@@ -78,7 +79,7 @@ export function queryDocuments(
|
|||||||
: "",
|
: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint() || "";
|
const endpoint = getFeatureEndpointOrDefault("resourcelist") || "";
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
...defaultHeaders,
|
...defaultHeaders,
|
||||||
@@ -141,7 +142,8 @@ export function readDocument(
|
|||||||
: "",
|
: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint();
|
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -181,7 +183,7 @@ export function createDocument(
|
|||||||
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
|
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint();
|
const endpoint = getFeatureEndpointOrDefault("createDocument");
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, {
|
.fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, {
|
||||||
@@ -225,7 +227,7 @@ export function updateDocument(
|
|||||||
? documentId.partitionKeyProperty
|
? documentId.partitionKeyProperty
|
||||||
: "",
|
: "",
|
||||||
};
|
};
|
||||||
const endpoint = getEndpoint();
|
const endpoint = getFeatureEndpointOrDefault("updateDocument");
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||||
@@ -266,7 +268,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
|
|||||||
? documentId.partitionKeyProperty
|
? documentId.partitionKeyProperty
|
||||||
: "",
|
: "",
|
||||||
};
|
};
|
||||||
const endpoint = getEndpoint();
|
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||||
@@ -309,7 +311,7 @@ export function createMongoCollectionWithProxy(
|
|||||||
autoPilotThroughput: params.autoPilotMaxThroughput?.toString(),
|
autoPilotThroughput: params.autoPilotMaxThroughput?.toString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint();
|
const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy");
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(
|
.fetch(
|
||||||
@@ -333,8 +335,15 @@ export function createMongoCollectionWithProxy(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEndpoint(): string {
|
export function getFeatureEndpointOrDefault(feature: string): string {
|
||||||
let url = (configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT) + "/api/mongo/explorer";
|
return hasFlag(userContext.features.mongoProxyAPIs, feature)
|
||||||
|
? getEndpoint(userContext.features.mongoProxyEndpoint)
|
||||||
|
: getEndpoint();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEndpoint(customEndpoint?: string): string {
|
||||||
|
let url = customEndpoint ? customEndpoint : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||||
|
url += "/api/mongo/explorer";
|
||||||
|
|
||||||
if (userContext.authType === AuthType.EncryptedToken) {
|
if (userContext.authType === AuthType.EncryptedToken) {
|
||||||
url = url.replace("api/mongo", "api/guest/mongo");
|
url = url.replace("api/mongo", "api/guest/mongo");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { FunctionComponent } from "react";
|
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
|
||||||
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
||||||
import refreshImg from "../../images/refresh-cosmos.svg";
|
import refreshImg from "../../images/refresh-cosmos.svg";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
@@ -6,6 +6,7 @@ import Explorer from "../Explorer/Explorer";
|
|||||||
import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree";
|
import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree";
|
||||||
import { ResourceTree } from "../Explorer/Tree/ResourceTree";
|
import { ResourceTree } from "../Explorer/Tree/ResourceTree";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
import { NormalizedEventKey } from "./Constants";
|
||||||
|
|
||||||
export interface ResourceTreeContainerProps {
|
export interface ResourceTreeContainerProps {
|
||||||
toggleLeftPaneExpanded: () => void;
|
toggleLeftPaneExpanded: () => void;
|
||||||
@@ -18,6 +19,22 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
|
|||||||
isLeftPaneExpanded,
|
isLeftPaneExpanded,
|
||||||
container,
|
container,
|
||||||
}: ResourceTreeContainerProps): JSX.Element => {
|
}: ResourceTreeContainerProps): JSX.Element => {
|
||||||
|
const focusButton = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLeftPaneExpanded) {
|
||||||
|
if (focusButton.current) {
|
||||||
|
focusButton.current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onKeyPressToggleLeftPaneExpanded = (event: React.KeyboardEvent) => {
|
||||||
|
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
|
||||||
|
toggleLeftPaneExpanded();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div id="main" className={isLeftPaneExpanded ? "main" : "hiddenMain"}>
|
<div id="main" className={isLeftPaneExpanded ? "main" : "hiddenMain"}>
|
||||||
{/* Collections Window - - Start */}
|
{/* Collections Window - - Start */}
|
||||||
@@ -43,9 +60,11 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
|
|||||||
id="expandToggleLeftPaneButton"
|
id="expandToggleLeftPaneButton"
|
||||||
role="button"
|
role="button"
|
||||||
onClick={toggleLeftPaneExpanded}
|
onClick={toggleLeftPaneExpanded}
|
||||||
|
onKeyPress={onKeyPressToggleLeftPaneExpanded}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-label="Collapse Tree"
|
aria-label="Collapse Tree"
|
||||||
title="Collapse Tree"
|
title="Collapse Tree"
|
||||||
|
ref={focusButton}
|
||||||
>
|
>
|
||||||
<img className="refreshcol1" src={arrowLeftImg} alt="Hide" />
|
<img className="refreshcol1" src={arrowLeftImg} alt="Hide" />
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
|||||||
<Stack horizontal tokens={sectionStackTokens}>
|
<Stack horizontal tokens={sectionStackTokens}>
|
||||||
<TextField
|
<TextField
|
||||||
label={entityPropertyLabel && entityPropertyLabel}
|
label={entityPropertyLabel && entityPropertyLabel}
|
||||||
id="entityPropertyId"
|
|
||||||
autoFocus
|
autoFocus
|
||||||
disabled={isPropertyTypeDisable}
|
disabled={isPropertyTypeDisable}
|
||||||
placeholder={entityPropertyPlaceHolder}
|
placeholder={entityPropertyPlaceHolder}
|
||||||
@@ -109,7 +108,6 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
|||||||
onChange={onEntityTypeChange}
|
onChange={onEntityTypeChange}
|
||||||
options={options}
|
options={options}
|
||||||
disabled={isPropertyTypeDisable}
|
disabled={isPropertyTypeDisable}
|
||||||
id="entityTypeId"
|
|
||||||
styles={dropdownStyles}
|
styles={dropdownStyles}
|
||||||
/>
|
/>
|
||||||
<EntityValue
|
<EntityValue
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children }:
|
|||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<TooltipHost content={children}>
|
<TooltipHost content={children}>
|
||||||
<Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" />
|
<Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { ConnectionStatusType } from "../Common/Constants";
|
||||||
|
|
||||||
export interface DatabaseAccount {
|
export interface DatabaseAccount {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -496,3 +498,8 @@ export interface MemoryUsageInfo {
|
|||||||
freeKB: number;
|
freeKB: number;
|
||||||
totalKB: number;
|
totalKB: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ContainerConnectionInfo {
|
||||||
|
status: ConnectionStatusType;
|
||||||
|
//need to add ram and rom info
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Icon, Label, Stack } from "@fluentui/react";
|
import { Icon, Label, Stack } from "@fluentui/react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { NormalizedEventKey } from "../../../Common/Constants";
|
||||||
import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
|
import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
|
||||||
|
|
||||||
export interface CollapsibleSectionProps {
|
export interface CollapsibleSectionProps {
|
||||||
@@ -30,6 +31,13 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onKeyPress = (event: React.KeyboardEvent) => {
|
||||||
|
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
|
||||||
|
this.toggleCollapsed();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -39,6 +47,11 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
|||||||
verticalAlign="center"
|
verticalAlign="center"
|
||||||
tokens={accordionStackTokens}
|
tokens={accordionStackTokens}
|
||||||
onClick={this.toggleCollapsed}
|
onClick={this.toggleCollapsed}
|
||||||
|
onKeyPress={this.onKeyPress}
|
||||||
|
tabIndex={0}
|
||||||
|
aria-name="Advanced"
|
||||||
|
role="button"
|
||||||
|
aria-expanded={this.state.isExpanded}
|
||||||
>
|
>
|
||||||
<Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} />
|
<Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} />
|
||||||
<Label>{this.props.title}</Label>
|
<Label>{this.props.title}</Label>
|
||||||
|
|||||||
@@ -3,9 +3,14 @@
|
|||||||
exports[`CollapsibleSectionComponent renders 1`] = `
|
exports[`CollapsibleSectionComponent renders 1`] = `
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Stack
|
<Stack
|
||||||
|
aria-expanded={true}
|
||||||
|
aria-name="Advanced"
|
||||||
className="collapsibleSection"
|
className="collapsibleSection"
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
onKeyPress={[Function]}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
tokens={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
"childrenGap": 10,
|
"childrenGap": 10,
|
||||||
|
|||||||
@@ -181,8 +181,7 @@ export const Dialog: FC = () => {
|
|||||||
text: secondaryButtonText,
|
text: secondaryButtonText,
|
||||||
onClick: onSecondaryButtonClick,
|
onClick: onSecondaryButtonClick,
|
||||||
}
|
}
|
||||||
: {};
|
: undefined;
|
||||||
|
|
||||||
return visible ? (
|
return visible ? (
|
||||||
<FluentDialog {...dialogProps}>
|
<FluentDialog {...dialogProps}>
|
||||||
{choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />}
|
{choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
.input-type-head-text-field {
|
.input-type-head-text-field {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.input-query-form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
textarea {
|
textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|||||||
@@ -160,18 +160,21 @@ export class InputTypeaheadComponent extends React.Component<
|
|||||||
return (
|
return (
|
||||||
<div className="input-typeahead-container">
|
<div className="input-typeahead-container">
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<TextField
|
<form aria-labelledby="input" className="input-query-form">
|
||||||
multiline={useTextarea}
|
<TextField
|
||||||
rows={1}
|
multiline={useTextarea}
|
||||||
defaultValue={defaultValue}
|
rows={1}
|
||||||
ariaLabel="Input query"
|
id="input"
|
||||||
placeholder={placeholder}
|
defaultValue={defaultValue}
|
||||||
className="input-type-head-text-field"
|
ariaLabel="Input query"
|
||||||
value={defaultValue}
|
placeholder={placeholder}
|
||||||
onKeyDown={this.onSubmit}
|
className="input-type-head-text-field"
|
||||||
onFocus={() => this.setState({ isSuggestionVisible: true })}
|
value={defaultValue}
|
||||||
onChange={(_event, newValue?: string) => this.handleChange(newValue)}
|
onKeyDown={this.onSubmit}
|
||||||
/>
|
onFocus={() => this.setState({ isSuggestionVisible: true })}
|
||||||
|
onChange={(_event, newValue?: string) => this.handleChange(newValue)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
{this.props.showCancelButton && (
|
{this.props.showCancelButton && (
|
||||||
<IconButton
|
<IconButton
|
||||||
styles={iconButtonStyles}
|
styles={iconButtonStyles}
|
||||||
|
|||||||
@@ -7,16 +7,22 @@ exports[`inputTypeahead renders <input /> 1`] = `
|
|||||||
<Stack
|
<Stack
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
>
|
>
|
||||||
<StyledTextFieldBase
|
<form
|
||||||
ariaLabel="Input query"
|
aria-labelledby="input"
|
||||||
className="input-type-head-text-field"
|
className="input-query-form"
|
||||||
multiline={false}
|
>
|
||||||
onChange={[Function]}
|
<StyledTextFieldBase
|
||||||
onFocus={[Function]}
|
ariaLabel="Input query"
|
||||||
onKeyDown={[Function]}
|
className="input-type-head-text-field"
|
||||||
placeholder="placeholder"
|
id="input"
|
||||||
rows={1}
|
multiline={false}
|
||||||
/>
|
onChange={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
placeholder="placeholder"
|
||||||
|
rows={1}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -28,16 +34,22 @@ exports[`inputTypeahead renders <textarea /> 1`] = `
|
|||||||
<Stack
|
<Stack
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
>
|
>
|
||||||
<StyledTextFieldBase
|
<form
|
||||||
ariaLabel="Input query"
|
aria-labelledby="input"
|
||||||
className="input-type-head-text-field"
|
className="input-query-form"
|
||||||
multiline={true}
|
>
|
||||||
onChange={[Function]}
|
<StyledTextFieldBase
|
||||||
onFocus={[Function]}
|
ariaLabel="Input query"
|
||||||
onKeyDown={[Function]}
|
className="input-type-head-text-field"
|
||||||
placeholder="placeholder"
|
id="input"
|
||||||
rows={1}
|
multiline={true}
|
||||||
/>
|
onChange={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
placeholder="placeholder"
|
||||||
|
rows={1}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,45 +1,45 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
|
||||||
import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants";
|
|
||||||
import { Urls, StyleConstants } from "../../../Common/Constants";
|
|
||||||
import {
|
import {
|
||||||
getPriceCurrency,
|
DetailsList,
|
||||||
getCurrencySign,
|
DetailsListLayoutMode,
|
||||||
getAutoscalePricePerRu,
|
DetailsRow,
|
||||||
getMultimasterMultiplier,
|
|
||||||
computeRUUsagePriceHourly,
|
|
||||||
getPricePerRu,
|
|
||||||
estimatedCostDisclaimer,
|
|
||||||
} from "../../../Utils/PricingUtils";
|
|
||||||
import {
|
|
||||||
ITextFieldStyles,
|
|
||||||
ICheckboxStyles,
|
ICheckboxStyles,
|
||||||
IStackProps,
|
|
||||||
IStackTokens,
|
|
||||||
IChoiceGroupStyles,
|
IChoiceGroupStyles,
|
||||||
Link,
|
IColumn,
|
||||||
Text,
|
IDetailsColumnStyles,
|
||||||
IMessageBarStyles,
|
|
||||||
ITextStyles,
|
|
||||||
IDetailsRowStyles,
|
|
||||||
IStackStyles,
|
|
||||||
IDetailsListStyles,
|
IDetailsListStyles,
|
||||||
|
IDetailsRowProps,
|
||||||
|
IDetailsRowStyles,
|
||||||
IDropdownStyles,
|
IDropdownStyles,
|
||||||
|
IMessageBarStyles,
|
||||||
ISeparatorStyles,
|
ISeparatorStyles,
|
||||||
|
IStackProps,
|
||||||
|
IStackStyles,
|
||||||
|
IStackTokens,
|
||||||
|
ITextFieldStyles,
|
||||||
|
ITextStyles,
|
||||||
|
Link,
|
||||||
MessageBar,
|
MessageBar,
|
||||||
MessageBarType,
|
MessageBarType,
|
||||||
Stack,
|
SelectionMode,
|
||||||
Spinner,
|
Spinner,
|
||||||
SpinnerSize,
|
SpinnerSize,
|
||||||
DetailsList,
|
Stack,
|
||||||
IColumn,
|
Text,
|
||||||
SelectionMode,
|
|
||||||
DetailsListLayoutMode,
|
|
||||||
IDetailsRowProps,
|
|
||||||
DetailsRow,
|
|
||||||
IDetailsColumnStyles,
|
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { isDirtyTypes, isDirty } from "./SettingsUtils";
|
import * as React from "react";
|
||||||
|
import { StyleConstants, Urls } from "../../../Common/Constants";
|
||||||
|
import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants";
|
||||||
|
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||||
|
import {
|
||||||
|
computeRUUsagePriceHourly,
|
||||||
|
estimatedCostDisclaimer,
|
||||||
|
getAutoscalePricePerRu,
|
||||||
|
getCurrencySign,
|
||||||
|
getMultimasterMultiplier,
|
||||||
|
getPriceCurrency,
|
||||||
|
getPricePerRu,
|
||||||
|
} from "../../../Utils/PricingUtils";
|
||||||
|
import { isDirty, isDirtyTypes } from "./SettingsUtils";
|
||||||
|
|
||||||
export interface EstimatedSpendingDisplayProps {
|
export interface EstimatedSpendingDisplayProps {
|
||||||
costType: JSX.Element;
|
costType: JSX.Element;
|
||||||
@@ -65,7 +65,7 @@ export interface PriceBreakdown {
|
|||||||
currencySign: string;
|
currencySign: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14 } };
|
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "windowtext" } };
|
||||||
|
|
||||||
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
||||||
label: {
|
label: {
|
||||||
@@ -223,14 +223,15 @@ export const getRuPriceBreakdown = (
|
|||||||
multimasterEnabled: isMultimaster,
|
multimasterEnabled: isMultimaster,
|
||||||
isAutoscale: isAutoscale,
|
isAutoscale: isAutoscale,
|
||||||
});
|
});
|
||||||
const basePricePerRu: number = isAutoscale
|
const multimasterMultiplier = getMultimasterMultiplier(numberOfRegions, isMultimaster);
|
||||||
? getAutoscalePricePerRu(serverId, getMultimasterMultiplier(numberOfRegions, isMultimaster))
|
const pricePerRu: number = isAutoscale
|
||||||
: getPricePerRu(serverId);
|
? getAutoscalePricePerRu(serverId, multimasterMultiplier)
|
||||||
|
: getPricePerRu(serverId, multimasterMultiplier);
|
||||||
return {
|
return {
|
||||||
hourlyPrice: hourlyPrice,
|
hourlyPrice,
|
||||||
dailyPrice: hourlyPrice * 24,
|
dailyPrice: hourlyPrice * 24,
|
||||||
monthlyPrice: hourlyPrice * hoursInAMonth,
|
monthlyPrice: hourlyPrice * hoursInAMonth,
|
||||||
pricePerRu: basePricePerRu * getMultimasterMultiplier(numberOfRegions, isMultimaster),
|
pricePerRu,
|
||||||
currency: getPriceCurrency(serverId),
|
currency: getPriceCurrency(serverId),
|
||||||
currencySign: getCurrencySign(serverId),
|
currencySign: getCurrencySign(serverId),
|
||||||
};
|
};
|
||||||
@@ -271,7 +272,7 @@ export const manualToAutoscaleDisclaimerElement: JSX.Element = (
|
|||||||
<Text styles={infoAndToolTipTextStyle} id="manualToAutoscaleDisclaimerElement">
|
<Text styles={infoAndToolTipTextStyle} id="manualToAutoscaleDisclaimerElement">
|
||||||
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings
|
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.{" "}
|
and storage of your resource. After autoscale has been enabled, you can change the max RU/s.{" "}
|
||||||
<a href={Urls.autoscaleMigration}>Learn more</a>
|
<Link href={Urls.autoscaleMigration}>Learn more</Link>
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ exports[`IndexingPolicyRefreshComponent renders 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -39,6 +40,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -73,6 +75,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -80,11 +83,11 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
|||||||
>
|
>
|
||||||
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.
|
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
|
<StyledLinkBase
|
||||||
href="https://aka.ms/cosmos-autoscale-migration"
|
href="https://aka.ms/cosmos-autoscale-migration"
|
||||||
>
|
>
|
||||||
Learn more
|
Learn more
|
||||||
</a>
|
</StyledLinkBase>
|
||||||
</Text>
|
</Text>
|
||||||
</StyledMessageBar>
|
</StyledMessageBar>
|
||||||
<StyledChoiceGroup
|
<StyledChoiceGroup
|
||||||
@@ -186,6 +189,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -460,6 +464,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -412,6 +413,7 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -952,6 +954,7 @@ exports[`SubSettingsComponent renders 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1228,6 +1231,7 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
|
"phoenixClient": PhoenixClient {},
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
@@ -101,6 +102,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
|
"phoenixClient": PhoenixClient {},
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -166,16 +167,17 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
>
|
>
|
||||||
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.
|
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
|
<StyledLinkBase
|
||||||
href="https://aka.ms/cosmos-autoscale-migration"
|
href="https://aka.ms/cosmos-autoscale-migration"
|
||||||
>
|
>
|
||||||
Learn more
|
Learn more
|
||||||
</a>
|
</StyledLinkBase>
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -195,6 +197,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -207,6 +210,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -219,6 +223,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -230,6 +235,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -249,6 +255,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -264,6 +271,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -278,6 +286,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -291,6 +300,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -302,6 +312,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -321,6 +332,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -363,6 +375,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -378,6 +391,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -394,6 +408,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
|
"color": "windowtext",
|
||||||
"fontSize": 14,
|
"fontSize": 14,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
@import "../../../../less/Common/Constants";
|
@import "../../../../less/Common/Constants";
|
||||||
|
|
||||||
.tabComponentContainer {
|
.tabComponentContainer {
|
||||||
overflow: hidden;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
.flex-display();
|
.flex-display();
|
||||||
.flex-direction();
|
.flex-direction();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { userContext } from "../../../../UserContext";
|
|||||||
import {
|
import {
|
||||||
calculateEstimateNumber,
|
calculateEstimateNumber,
|
||||||
computeRUUsagePriceHourly,
|
computeRUUsagePriceHourly,
|
||||||
|
estimatedCostDisclaimer,
|
||||||
getAutoscalePricePerRu,
|
getAutoscalePricePerRu,
|
||||||
getCurrencySign,
|
getCurrencySign,
|
||||||
getMultimasterMultiplier,
|
getMultimasterMultiplier,
|
||||||
@@ -42,11 +43,9 @@ export const CostEstimateText: FunctionComponent<CostEstimateTextProps> = ({
|
|||||||
const currency: string = getPriceCurrency(serverId);
|
const currency: string = getPriceCurrency(serverId);
|
||||||
const currencySign: string = getCurrencySign(serverId);
|
const currencySign: string = getCurrencySign(serverId);
|
||||||
const multiplier = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
|
const multiplier = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
|
||||||
const pricePerRu = isAutoscale
|
const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multiplier) : getPricePerRu(serverId, multiplier);
|
||||||
? getAutoscalePricePerRu(serverId, multiplier) * multiplier
|
|
||||||
: getPricePerRu(serverId) * multiplier;
|
|
||||||
|
|
||||||
const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>PricingUtils.estimatedCostDisclaimer</InfoTooltip>;
|
const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>{estimatedCostDisclaimer}</InfoTooltip>;
|
||||||
|
|
||||||
if (isAutoscale) {
|
if (isAutoscale) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
<input
|
<input
|
||||||
className="throughputInputRadioBtn"
|
className="throughputInputRadioBtn"
|
||||||
aria-label="Autoscale mode"
|
aria-label="Autoscale mode"
|
||||||
|
aria-required={true}
|
||||||
checked={isAutoscaleSelected}
|
checked={isAutoscaleSelected}
|
||||||
type="radio"
|
type="radio"
|
||||||
role="radio"
|
role="radio"
|
||||||
@@ -131,6 +132,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
aria-label="Manual mode"
|
aria-label="Manual mode"
|
||||||
checked={!isAutoscaleSelected}
|
checked={!isAutoscaleSelected}
|
||||||
type="radio"
|
type="radio"
|
||||||
|
aria-required={true}
|
||||||
role="radio"
|
role="radio"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onChange={(e) => handleOnChangeMode(e, "Manual")}
|
onChange={(e) => handleOnChangeMode(e, "Manual")}
|
||||||
|
|||||||
@@ -345,12 +345,14 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
ariaLabel="Info"
|
ariaLabel="Info"
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<IconBase
|
<IconBase
|
||||||
ariaLabel="Info"
|
ariaLabel="Info"
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
styles={[Function]}
|
styles={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
"disableGlobalClassNames": false,
|
"disableGlobalClassNames": false,
|
||||||
@@ -630,6 +632,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
className="panelInfoIcon root-57"
|
className="panelInfoIcon root-57"
|
||||||
data-icon-name="Info"
|
data-icon-name="Info"
|
||||||
role="img"
|
role="img"
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
|
|
||||||
</i>
|
</i>
|
||||||
@@ -651,6 +654,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-label="Autoscale mode"
|
aria-label="Autoscale mode"
|
||||||
|
aria-required={true}
|
||||||
checked={true}
|
checked={true}
|
||||||
className="throughputInputRadioBtn"
|
className="throughputInputRadioBtn"
|
||||||
key=".0:$.0"
|
key=".0:$.0"
|
||||||
@@ -667,6 +671,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
aria-label="Manual mode"
|
aria-label="Manual mode"
|
||||||
|
aria-required={true}
|
||||||
checked={false}
|
checked={false}
|
||||||
className="throughputInputRadioBtn"
|
className="throughputInputRadioBtn"
|
||||||
key=".0:$.2"
|
key=".0:$.2"
|
||||||
@@ -1327,12 +1332,14 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
ariaLabel="Info"
|
ariaLabel="Info"
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<IconBase
|
<IconBase
|
||||||
ariaLabel="Info"
|
ariaLabel="Info"
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
styles={[Function]}
|
styles={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
"disableGlobalClassNames": false,
|
"disableGlobalClassNames": false,
|
||||||
@@ -1612,6 +1619,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
className="panelInfoIcon root-57"
|
className="panelInfoIcon root-57"
|
||||||
data-icon-name="Info"
|
data-icon-name="Info"
|
||||||
role="img"
|
role="img"
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
|
|
||||||
</i>
|
</i>
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
|||||||
<div ref={this.contextMenuRef} onContextMenu={this.onRightClick} onKeyPress={this.onMoreButtonKeyPress}>
|
<div ref={this.contextMenuRef} onContextMenu={this.onRightClick} onKeyPress={this.onMoreButtonKeyPress}>
|
||||||
<IconButton
|
<IconButton
|
||||||
name="More"
|
name="More"
|
||||||
|
title="More"
|
||||||
className="treeMenuEllipsis"
|
className="treeMenuEllipsis"
|
||||||
ariaLabel={menuItemLabel}
|
ariaLabel={menuItemLabel}
|
||||||
menuIconProps={{
|
menuIconProps={{
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
title="More"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -423,6 +424,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
title="More"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
|||||||
import { useSidePanel } from "../hooks/useSidePanel";
|
import { useSidePanel } from "../hooks/useSidePanel";
|
||||||
import { useTabs } from "../hooks/useTabs";
|
import { useTabs } from "../hooks/useTabs";
|
||||||
import { IGalleryItem } from "../Juno/JunoClient";
|
import { IGalleryItem } from "../Juno/JunoClient";
|
||||||
|
import { PhoenixClient } from "../Phoenix/PhoenixClient";
|
||||||
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
||||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||||
@@ -87,12 +88,13 @@ export default class Explorer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private static readonly MaxNbDatabasesToAutoExpand = 5;
|
private static readonly MaxNbDatabasesToAutoExpand = 5;
|
||||||
|
private phoenixClient: PhoenixClient;
|
||||||
constructor() {
|
constructor() {
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
|
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
|
||||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||||
});
|
});
|
||||||
this._isInitializingNotebooks = false;
|
this._isInitializingNotebooks = false;
|
||||||
|
this.phoenixClient = new PhoenixClient();
|
||||||
useNotebook.subscribe(
|
useNotebook.subscribe(
|
||||||
() => this.refreshCommandBarButtons(),
|
() => this.refreshCommandBarButtons(),
|
||||||
(state) => state.isNotebooksEnabledForAccount
|
(state) => state.isNotebooksEnabledForAccount
|
||||||
@@ -343,19 +345,36 @@ export default class Explorer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._isInitializingNotebooks = true;
|
this._isInitializingNotebooks = true;
|
||||||
|
if (userContext.features.phoenix) {
|
||||||
|
const provisionData = {
|
||||||
|
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
|
||||||
|
resourceId: userContext.databaseAccount.id,
|
||||||
|
dbAccountName: userContext.databaseAccount.name,
|
||||||
|
aadToken: userContext.authorizationToken,
|
||||||
|
resourceGroup: userContext.resourceGroup,
|
||||||
|
subscriptionId: userContext.subscriptionId,
|
||||||
|
};
|
||||||
|
const connectionInfo = await this.phoenixClient.containerConnectionInfo(provisionData);
|
||||||
|
if (connectionInfo.data && connectionInfo.data.notebookServerUrl) {
|
||||||
|
useNotebook.getState().setNotebookServerInfo({
|
||||||
|
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl,
|
||||||
|
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this.ensureNotebookWorkspaceRunning();
|
||||||
|
const connectionInfo = await listConnectionInfo(
|
||||||
|
userContext.subscriptionId,
|
||||||
|
userContext.resourceGroup,
|
||||||
|
databaseAccount.name,
|
||||||
|
"default"
|
||||||
|
);
|
||||||
|
|
||||||
await this.ensureNotebookWorkspaceRunning();
|
useNotebook.getState().setNotebookServerInfo({
|
||||||
const connectionInfo = await listConnectionInfo(
|
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
|
||||||
userContext.subscriptionId,
|
authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
|
||||||
userContext.resourceGroup,
|
});
|
||||||
databaseAccount.name,
|
}
|
||||||
"default"
|
|
||||||
);
|
|
||||||
|
|
||||||
useNotebook.getState().setNotebookServerInfo({
|
|
||||||
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
|
|
||||||
authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
useNotebook.getState().initializeNotebooksTree(this.notebookManager);
|
useNotebook.getState().initializeNotebooksTree(this.notebookManager);
|
||||||
|
|
||||||
@@ -364,7 +383,7 @@ export default class Explorer {
|
|||||||
this._isInitializingNotebooks = false;
|
this._isInitializingNotebooks = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public resetNotebookWorkspace() {
|
public resetNotebookWorkspace(): void {
|
||||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
|
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
|
||||||
handleError(
|
handleError(
|
||||||
"Attempt to reset notebook workspace, but notebook is not enabled",
|
"Attempt to reset notebook workspace, but notebook is not enabled",
|
||||||
@@ -389,7 +408,6 @@ export default class Explorer {
|
|||||||
if (!databaseAccount) {
|
if (!databaseAccount) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { value: workspaces } = await listByDatabaseAccount(
|
const { value: workspaces } = await listByDatabaseAccount(
|
||||||
userContext.subscriptionId,
|
userContext.subscriptionId,
|
||||||
@@ -906,7 +924,7 @@ export default class Explorer {
|
|||||||
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
|
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public openNotebookTerminal(kind: ViewModels.TerminalKind) {
|
public openNotebookTerminal(kind: ViewModels.TerminalKind): void {
|
||||||
let title: string;
|
let title: string;
|
||||||
|
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
@@ -1026,7 +1044,10 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async handleOpenFileAction(path: string): Promise<void> {
|
public async handleOpenFileAction(path: string): Promise<void> {
|
||||||
if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) {
|
if (
|
||||||
|
userContext.features.phoenix === false &&
|
||||||
|
!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))
|
||||||
|
) {
|
||||||
this._openSetupNotebooksPaneForQuickstart();
|
this._openSetupNotebooksPaneForQuickstart();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1072,10 +1093,13 @@ export default class Explorer {
|
|||||||
? this.refreshDatabaseForResourceToken()
|
? this.refreshDatabaseForResourceToken()
|
||||||
: this.refreshAllDatabases();
|
: this.refreshAllDatabases();
|
||||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||||
const isNotebookEnabled: boolean =
|
let isNotebookEnabled = true;
|
||||||
userContext.authType !== AuthType.ResourceToken &&
|
if (!userContext.features.phoenix) {
|
||||||
((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) ||
|
isNotebookEnabled =
|
||||||
userContext.features.enableNotebooks);
|
userContext.authType !== AuthType.ResourceToken &&
|
||||||
|
((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) ||
|
||||||
|
userContext.features.enableNotebooks);
|
||||||
|
}
|
||||||
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
|
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
|
||||||
useNotebook.getState().setIsShellEnabled(isNotebookEnabled && isPublicInternetAccessAllowed());
|
useNotebook.getState().setIsShellEnabled(isNotebookEnabled && isPublicInternetAccessAllowed());
|
||||||
|
|
||||||
|
|||||||
@@ -736,7 +736,7 @@ export class D3ForceGraph implements GraphRenderer {
|
|||||||
.on("dblclick", function (this: Element, _: MouseEvent, d: D3Node) {
|
.on("dblclick", function (this: Element, _: MouseEvent, d: D3Node) {
|
||||||
// https://stackoverflow.com/a/41945742 ('this' implicitly has type 'any' because it does not have a type annotation)
|
// https://stackoverflow.com/a/41945742 ('this' implicitly has type 'any' because it does not have a type annotation)
|
||||||
// this is the <g> element
|
// this is the <g> element
|
||||||
self.onNodeClicked(this.parentNode, d);
|
return self.onNodeClicked(this.parentNode, d);
|
||||||
})
|
})
|
||||||
.on("click", function (this: Element, _: MouseEvent, d: D3Node) {
|
.on("click", function (this: Element, _: MouseEvent, d: D3Node) {
|
||||||
// this is the <g> element
|
// this is the <g> element
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { GraphData, GremlinEdge, GremlinVertex } from "./GraphData";
|
import { GraphData, GremlinVertex, GremlinEdge } from "./GraphData";
|
||||||
|
|
||||||
describe("Graph Data", () => {
|
describe("Graph Data", () => {
|
||||||
it("should set only one node as root", () => {
|
it("should set only one node as root", () => {
|
||||||
const graphData = new GraphData<GremlinVertex, GremlinEdge>();
|
const graphData = new GraphData<GremlinVertex, GremlinEdge>();
|
||||||
const v1: GremlinVertex = { id: "1", label: undefined };
|
const v1: GremlinVertex = { id: "1", label: null };
|
||||||
const v2: GremlinVertex = { id: "2", label: undefined };
|
const v2: GremlinVertex = { id: "2", label: null };
|
||||||
const v3: GremlinVertex = { id: "3", label: undefined };
|
const v3: GremlinVertex = { id: "3", label: null };
|
||||||
v3._isRoot = true;
|
v3._isRoot = true;
|
||||||
|
|
||||||
graphData.addVertex(v1);
|
graphData.addVertex(v1);
|
||||||
@@ -28,9 +28,9 @@ describe("Graph Data", () => {
|
|||||||
|
|
||||||
it("should properly find root id", () => {
|
it("should properly find root id", () => {
|
||||||
const graphData = new GraphData();
|
const graphData = new GraphData();
|
||||||
const v1: GremlinVertex = { id: "1", label: undefined };
|
const v1: GremlinVertex = { id: "1", label: null };
|
||||||
const v2: GremlinVertex = { id: "2", label: undefined };
|
const v2: GremlinVertex = { id: "2", label: null };
|
||||||
const v3: GremlinVertex = { id: "3", label: undefined };
|
const v3: GremlinVertex = { id: "3", label: null };
|
||||||
|
|
||||||
graphData.addVertex(v1);
|
graphData.addVertex(v1);
|
||||||
graphData.addVertex(v2);
|
graphData.addVertex(v2);
|
||||||
@@ -44,12 +44,12 @@ describe("Graph Data", () => {
|
|||||||
it("should remove edge from graph", () => {
|
it("should remove edge from graph", () => {
|
||||||
const graphData = new GraphData();
|
const graphData = new GraphData();
|
||||||
|
|
||||||
graphData.addVertex({ id: "v1", label: undefined });
|
graphData.addVertex({ id: "v1", label: null });
|
||||||
graphData.addVertex({ id: "v2", label: undefined });
|
graphData.addVertex({ id: "v2", label: null });
|
||||||
graphData.addVertex({ id: "v3", label: undefined });
|
graphData.addVertex({ id: "v3", label: null });
|
||||||
|
|
||||||
graphData.addEdge({ id: "e1", inV: "v1", outV: "v2", label: "" });
|
graphData.addEdge({ id: "e1", inV: "v1", outV: "v2", label: null });
|
||||||
graphData.addEdge({ id: "e2", inV: "v1", outV: "v3", label: "" });
|
graphData.addEdge({ id: "e2", inV: "v1", outV: "v3", label: null });
|
||||||
|
|
||||||
// in edge
|
// in edge
|
||||||
graphData.removeEdge("e1", false);
|
graphData.removeEdge("e1", false);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { SimulationLinkDatum, SimulationNodeDatum } from "d3";
|
|
||||||
import _ from "underscore";
|
import _ from "underscore";
|
||||||
|
import { SimulationNodeDatum, SimulationLinkDatum } from "d3";
|
||||||
|
|
||||||
export interface PaginationInfo {
|
export interface PaginationInfo {
|
||||||
total: number;
|
total: number;
|
||||||
@@ -10,7 +10,7 @@ export interface PaginationInfo {
|
|||||||
|
|
||||||
export interface GremlinVertex {
|
export interface GremlinVertex {
|
||||||
id: string;
|
id: string;
|
||||||
label?: string;
|
label: string;
|
||||||
inE?: { [label: string]: GremlinShortInEdge[] };
|
inE?: { [label: string]: GremlinShortInEdge[] };
|
||||||
outE?: { [label: string]: GremlinShortOutEdge[] };
|
outE?: { [label: string]: GremlinShortOutEdge[] };
|
||||||
properties?: { [propName: string]: GremlinProperty[] };
|
properties?: { [propName: string]: GremlinProperty[] };
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { GraphVizComponent, GraphVizComponentProps } from "./GraphVizComponent";
|
|
||||||
import CollapseArrowIcon from "../../../../images/Collapse_arrow_14x14.svg";
|
import CollapseArrowIcon from "../../../../images/Collapse_arrow_14x14.svg";
|
||||||
import ExpandIcon from "../../../../images/Expand_14x14.svg";
|
import ExpandIcon from "../../../../images/Expand_14x14.svg";
|
||||||
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
|
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
|
||||||
|
import { GraphVizComponent, GraphVizComponentProps } from "./GraphVizComponent";
|
||||||
|
|
||||||
interface MiddlePaneComponentProps {
|
interface MiddlePaneComponentProps {
|
||||||
isTabsContentExpanded: boolean;
|
isTabsContentExpanded: boolean;
|
||||||
@@ -17,7 +17,14 @@ export class MiddlePaneComponent extends React.Component<MiddlePaneComponentProp
|
|||||||
<div className="middlePane">
|
<div className="middlePane">
|
||||||
<div className="graphTitle">
|
<div className="graphTitle">
|
||||||
<span className="paneTitle">Graph</span>
|
<span className="paneTitle">Graph</span>
|
||||||
<span className="graphExpandCollapseBtn pull-right" onClick={this.props.toggleExpandGraph}>
|
<span
|
||||||
|
className="graphExpandCollapseBtn pull-right"
|
||||||
|
onClick={this.props.toggleExpandGraph}
|
||||||
|
role="button"
|
||||||
|
aria-expanded={this.props.isTabsContentExpanded}
|
||||||
|
aria-name="View graph in full screen"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src={this.props.isTabsContentExpanded ? CollapseArrowIcon : ExpandIcon}
|
src={this.props.isTabsContentExpanded ? CollapseArrowIcon : ExpandIcon}
|
||||||
alt={this.props.isTabsContentExpanded ? "collapse graph content" : "expand graph content"}
|
alt={this.props.isTabsContentExpanded ? "collapse graph content" : "expand graph content"}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ import create, { UseStore } from "zustand";
|
|||||||
import { StyleConstants } from "../../../Common/Constants";
|
import { StyleConstants } from "../../../Common/Constants";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import { useTabs } from "../../../hooks/useTabs";
|
import { useTabs } from "../../../hooks/useTabs";
|
||||||
|
import { userContext } from "../../../UserContext";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
@@ -54,6 +55,14 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
|
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
|
||||||
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||||
|
|
||||||
|
if (
|
||||||
|
userContext.features.notebooksTemporarilyDown === false &&
|
||||||
|
userContext.features.phoenix === true &&
|
||||||
|
useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2
|
||||||
|
) {
|
||||||
|
uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus("connectionStatus"));
|
||||||
|
}
|
||||||
|
|
||||||
if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
|
if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
|
||||||
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker"));
|
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,8 +80,9 @@ export function createStaticCommandBarButtons(
|
|||||||
}
|
}
|
||||||
|
|
||||||
notebookButtons.push(createOpenTerminalButton(container));
|
notebookButtons.push(createOpenTerminalButton(container));
|
||||||
|
if (userContext.features.phoenix === false) {
|
||||||
notebookButtons.push(createNotebookWorkspaceResetButton(container));
|
notebookButtons.push(createNotebookWorkspaceResetButton(container));
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
(userContext.apiType === "Mongo" &&
|
(userContext.apiType === "Mongo" &&
|
||||||
useNotebook.getState().isShellEnabled &&
|
useNotebook.getState().isShellEnabled &&
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { StyleConstants } from "../../../Common/Constants";
|
|||||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
|
import { ConnectionStatus } from "./ConnectionStatusComponent";
|
||||||
import { MemoryTracker } from "./MemoryTrackerComponent";
|
import { MemoryTracker } from "./MemoryTrackerComponent";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -201,3 +202,10 @@ export const createMemoryTracker = (key: string): ICommandBarItemProps => {
|
|||||||
onRender: () => <MemoryTracker />,
|
onRender: () => <MemoryTracker />,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createConnectionStatus = (key: string): ICommandBarItemProps => {
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
onRender: () => <ConnectionStatus />,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
79
src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less
Normal file
79
src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
@import "../../../../less/Common/Constants";
|
||||||
|
|
||||||
|
.connectionStatusContainer {
|
||||||
|
cursor: default;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 9px;
|
||||||
|
border: 1px;
|
||||||
|
min-height: 44px;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
padding-right: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: @DataExplorerFont;
|
||||||
|
color: @DefaultFontColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.connectionStatusFailed{
|
||||||
|
color: #bd1919;
|
||||||
|
}
|
||||||
|
.ring-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ringringGreen {
|
||||||
|
border: 3px solid green;
|
||||||
|
border-radius: 30px;
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
position: absolute;
|
||||||
|
margin: .4285em 0em 0em 0.07477em;
|
||||||
|
animation: pulsate 3s ease-out;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
opacity: 0.0
|
||||||
|
}
|
||||||
|
.ringringYellow{
|
||||||
|
border: 3px solid #ffbf00;
|
||||||
|
border-radius: 30px;
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
position: absolute;
|
||||||
|
margin: .4285em 0em 0em 0.07477em;
|
||||||
|
animation: pulsate 3s ease-out;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
opacity: 0.0
|
||||||
|
}
|
||||||
|
.ringringRed{
|
||||||
|
border: 3px solid #bd1919;
|
||||||
|
border-radius: 30px;
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
position: absolute;
|
||||||
|
margin: .4285em 0em 0em 0.07477em;
|
||||||
|
animation: pulsate 3s ease-out;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
opacity: 0.0
|
||||||
|
}
|
||||||
|
@keyframes pulsate {
|
||||||
|
0% {-webkit-transform: scale(0.1, 0.1); opacity: 0.0;}
|
||||||
|
15% {opacity: 0.8;}
|
||||||
|
25% {opacity: 0.6;}
|
||||||
|
45% {opacity: 0.4;}
|
||||||
|
70% {opacity: 0.3;}
|
||||||
|
100% {-webkit-transform: scale(.7, .7); opacity: 0.1;}
|
||||||
|
}
|
||||||
|
.locationGreenDot{
|
||||||
|
font-size: 20px;
|
||||||
|
margin-right: 0.07em;
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
.locationYellowDot{
|
||||||
|
font-size: 20px;
|
||||||
|
margin-right: 0.07em;
|
||||||
|
color: #ffbf00;
|
||||||
|
}
|
||||||
|
.locationRedDot{
|
||||||
|
font-size: 20px;
|
||||||
|
margin-right: 0.07em;
|
||||||
|
color: #bd1919;
|
||||||
|
}
|
||||||
72
src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx
Normal file
72
src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { Icon, ProgressIndicator, Stack, TooltipHost } from "@fluentui/react";
|
||||||
|
import * as React from "react";
|
||||||
|
import { ConnectionStatusType } from "../../../Common/Constants";
|
||||||
|
import { useNotebook } from "../../Notebook/useNotebook";
|
||||||
|
import "../CommandBar/ConnectionStatusComponent.less";
|
||||||
|
|
||||||
|
export const ConnectionStatus: React.FC = (): JSX.Element => {
|
||||||
|
const [second, setSecond] = React.useState("00");
|
||||||
|
const [minute, setMinute] = React.useState("00");
|
||||||
|
const [isActive, setIsActive] = React.useState(false);
|
||||||
|
const [counter, setCounter] = React.useState(0);
|
||||||
|
const [statusColor, setStatusColor] = React.useState("locationYellowDot");
|
||||||
|
const [statusColorAnimation, setStatusColorAnimation] = React.useState("ringringYellow");
|
||||||
|
const toolTipContent = "Hosted runtime status.";
|
||||||
|
React.useEffect(() => {
|
||||||
|
let intervalId: NodeJS.Timeout;
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
intervalId = setInterval(() => {
|
||||||
|
const secondCounter = counter % 60;
|
||||||
|
const minuteCounter = Math.floor(counter / 60);
|
||||||
|
const computedSecond: string = String(secondCounter).length === 1 ? `0${secondCounter}` : `${secondCounter}`;
|
||||||
|
const computedMinute: string = String(minuteCounter).length === 1 ? `0${minuteCounter}` : `${minuteCounter}`;
|
||||||
|
|
||||||
|
setSecond(computedSecond);
|
||||||
|
setMinute(computedMinute);
|
||||||
|
|
||||||
|
setCounter((counter) => counter + 1);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, [isActive, counter]);
|
||||||
|
|
||||||
|
const stopTimer = () => {
|
||||||
|
setIsActive(false);
|
||||||
|
setCounter(0);
|
||||||
|
setSecond("00");
|
||||||
|
setMinute("00");
|
||||||
|
};
|
||||||
|
|
||||||
|
const connectionInfo = useNotebook((state) => state.connectionInfo);
|
||||||
|
if (!connectionInfo) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connecting && isActive === false) {
|
||||||
|
setIsActive(true);
|
||||||
|
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connected && isActive === true) {
|
||||||
|
stopTimer();
|
||||||
|
setStatusColor("locationGreenDot");
|
||||||
|
setStatusColorAnimation("ringringGreen");
|
||||||
|
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Failed && isActive === true) {
|
||||||
|
stopTimer();
|
||||||
|
setStatusColor("locationRedDot");
|
||||||
|
setStatusColorAnimation("ringringRed");
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<TooltipHost content={toolTipContent}>
|
||||||
|
<Stack className="connectionStatusContainer" horizontal>
|
||||||
|
<div className="ring-container">
|
||||||
|
<div className={statusColorAnimation}></div>
|
||||||
|
<Icon iconName="LocationDot" className={statusColor} />
|
||||||
|
</div>
|
||||||
|
<span className={connectionInfo.status === ConnectionStatusType.Failed ? "connectionStatusFailed" : ""}>
|
||||||
|
{connectionInfo.status}
|
||||||
|
</span>
|
||||||
|
{connectionInfo.status === ConnectionStatusType.Connecting && isActive && (
|
||||||
|
<ProgressIndicator description={minute + ":" + second} />
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</TooltipHost>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -129,7 +129,7 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
className="expandCollapseButton"
|
className="expandCollapseButton"
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-label={"console button" + (this.props.isConsoleExpanded ? " collapsed" : " expanded")}
|
aria-label={"console button" + (this.props.isConsoleExpanded ? " expanded" : " collapsed")}
|
||||||
aria-expanded={!this.props.isConsoleExpanded}
|
aria-expanded={!this.props.isConsoleExpanded}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -205,7 +205,9 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
{item.type === ConsoleDataType.Error && <img className="errorIcon" src={ErrorRedIcon} alt="error" />}
|
{item.type === ConsoleDataType.Error && <img className="errorIcon" src={ErrorRedIcon} alt="error" />}
|
||||||
{item.type === ConsoleDataType.InProgress && <img className="loaderIcon" src={LoaderIcon} alt="in progress" />}
|
{item.type === ConsoleDataType.InProgress && <img className="loaderIcon" src={LoaderIcon} alt="in progress" />}
|
||||||
<span className="date">{item.date}</span>
|
<span className="date">{item.date}</span>
|
||||||
<span className="message">{item.message}</span>
|
<span className="message" role="alert" aria-live="assertive">
|
||||||
|
{item.message}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
aria-expanded={true}
|
aria-expanded={true}
|
||||||
aria-label="console button expanded"
|
aria-label="console button collapsed"
|
||||||
className="expandCollapseButton"
|
className="expandCollapseButton"
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -236,7 +236,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
aria-expanded={true}
|
aria-expanded={true}
|
||||||
aria-label="console button expanded"
|
aria-label="console button collapsed"
|
||||||
className="expandCollapseButton"
|
className="expandCollapseButton"
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -340,7 +340,9 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
date
|
date
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
|
aria-live="assertive"
|
||||||
className="message"
|
className="message"
|
||||||
|
role="alert"
|
||||||
>
|
>
|
||||||
message
|
message
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ const formWebSocketURL = (serverConfig: NotebookServiceConfig, kernelId: string,
|
|||||||
const q = params.toString();
|
const q = params.toString();
|
||||||
const suffix = q !== "" ? `?${q}` : "";
|
const suffix = q !== "" ? `?${q}` : "";
|
||||||
|
|
||||||
const url = (serverConfig.endpoint || "") + `api/kernels/${kernelId}/channels${suffix}`;
|
const url = (serverConfig.endpoint.slice(0, -1) || "") + `api/kernels/${kernelId}/channels${suffix}`;
|
||||||
|
|
||||||
return url.replace(/^http(s)?/, "ws$1");
|
return url.replace(/^http(s)?/, "ws$1");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export class NotebookContainerClient {
|
|||||||
|
|
||||||
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
|
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${notebookServerEndpoint}/api/metrics/memory`, {
|
const response = await fetch(`${notebookServerEndpoint}api/metrics/memory`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: authToken,
|
Authorization: authToken,
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ export class NotebookContentClient {
|
|||||||
*
|
*
|
||||||
* @param parent parent folder
|
* @param parent parent folder
|
||||||
*/
|
*/
|
||||||
public createNewNotebookFile(parent: NotebookContentItem, isGithubTree?: boolean): Promise<NotebookContentItem> {
|
public async createNewNotebookFile(
|
||||||
|
parent: NotebookContentItem,
|
||||||
|
isGithubTree?: boolean
|
||||||
|
): Promise<NotebookContentItem> {
|
||||||
if (!parent || parent.type !== NotebookContentItemType.Directory) {
|
if (!parent || parent.type !== NotebookContentItemType.Directory) {
|
||||||
throw new Error(`Parent must be a directory: ${parent}`);
|
throw new Error(`Parent must be a directory: ${parent}`);
|
||||||
}
|
}
|
||||||
@@ -99,7 +102,6 @@ export class NotebookContentClient {
|
|||||||
if (!parent || parent.type !== NotebookContentItemType.Directory) {
|
if (!parent || parent.type !== NotebookContentItemType.Directory) {
|
||||||
throw new Error(`Parent must be a directory: ${parent}`);
|
throw new Error(`Parent must be a directory: ${parent}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filepath = NotebookUtil.getFilePath(parent.path, name);
|
const filepath = NotebookUtil.getFilePath(parent.path, name);
|
||||||
if (await this.checkIfFilepathExists(filepath)) {
|
if (await this.checkIfFilepathExists(filepath)) {
|
||||||
throw new Error(`File already exists: ${filepath}`);
|
throw new Error(`File already exists: ${filepath}`);
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ interface InitialProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Redux
|
// Redux
|
||||||
const makeMapStateToProps = (_state: AppState, initialProps: InitialProps) => {
|
const makeMapStateToProps = (state: AppState, initialProps: InitialProps) => {
|
||||||
const mapStateToProps = (state: AppState): StateProps => ({
|
const mapStateToProps = (state: AppState): StateProps => ({
|
||||||
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, initialProps.contentRef),
|
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, initialProps.contentRef),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ interface NotebookState {
|
|||||||
myNotebooksContentRoot: NotebookContentItem;
|
myNotebooksContentRoot: NotebookContentItem;
|
||||||
gitHubNotebooksContentRoot: NotebookContentItem;
|
gitHubNotebooksContentRoot: NotebookContentItem;
|
||||||
galleryContentRoot: NotebookContentItem;
|
galleryContentRoot: NotebookContentItem;
|
||||||
|
connectionInfo: DataModels.ContainerConnectionInfo;
|
||||||
|
notebookFolderName: string;
|
||||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
|
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
|
||||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
|
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
|
||||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
|
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
|
||||||
@@ -36,6 +38,7 @@ interface NotebookState {
|
|||||||
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => void;
|
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => void;
|
||||||
setIsShellEnabled: (isShellEnabled: boolean) => void;
|
setIsShellEnabled: (isShellEnabled: boolean) => void;
|
||||||
setNotebookBasePath: (notebookBasePath: string) => void;
|
setNotebookBasePath: (notebookBasePath: string) => void;
|
||||||
|
setNotebookFolderName: (notebookFolderName: string) => void;
|
||||||
refreshNotebooksEnabledStateForAccount: () => Promise<void>;
|
refreshNotebooksEnabledStateForAccount: () => Promise<void>;
|
||||||
findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem;
|
findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem;
|
||||||
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean) => void;
|
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean) => void;
|
||||||
@@ -43,6 +46,7 @@ interface NotebookState {
|
|||||||
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void;
|
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void;
|
||||||
initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<void>;
|
initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<void>;
|
||||||
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
|
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
|
||||||
|
setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||||
@@ -65,6 +69,8 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
|||||||
myNotebooksContentRoot: undefined,
|
myNotebooksContentRoot: undefined,
|
||||||
gitHubNotebooksContentRoot: undefined,
|
gitHubNotebooksContentRoot: undefined,
|
||||||
galleryContentRoot: undefined,
|
galleryContentRoot: undefined,
|
||||||
|
connectionInfo: undefined,
|
||||||
|
notebookFolderName: undefined,
|
||||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
|
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
|
||||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
|
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
|
||||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
|
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
|
||||||
@@ -75,6 +81,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
|||||||
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }),
|
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }),
|
||||||
setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }),
|
setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }),
|
||||||
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
|
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
|
||||||
|
setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }),
|
||||||
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
|
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
|
||||||
const { databaseAccount, authType } = userContext;
|
const { databaseAccount, authType } = userContext;
|
||||||
if (
|
if (
|
||||||
@@ -168,8 +175,10 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
|||||||
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
|
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
|
||||||
},
|
},
|
||||||
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
|
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
|
||||||
|
const notebookFolderName = userContext.features.phoenix === true ? "Temporary Notebooks" : "My Notebooks";
|
||||||
|
set({ notebookFolderName });
|
||||||
const myNotebooksContentRoot = {
|
const myNotebooksContentRoot = {
|
||||||
name: "My Notebooks",
|
name: get().notebookFolderName,
|
||||||
path: get().notebookBasePath,
|
path: get().notebookBasePath,
|
||||||
type: NotebookContentItemType.Directory,
|
type: NotebookContentItemType.Directory,
|
||||||
};
|
};
|
||||||
@@ -185,6 +194,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
|||||||
type: NotebookContentItemType.Directory,
|
type: NotebookContentItemType.Directory,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
set({
|
set({
|
||||||
myNotebooksContentRoot,
|
myNotebooksContentRoot,
|
||||||
galleryContentRoot,
|
galleryContentRoot,
|
||||||
@@ -246,4 +256,5 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
|||||||
set({ gitHubNotebooksContentRoot });
|
set({ gitHubNotebooksContentRoot });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => set({ connectionInfo }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
true
|
true
|
||||||
).toLocaleLowerCase()}.`}
|
).toLocaleLowerCase()}.`}
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -210,6 +210,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
aria-label="New database id"
|
aria-label="New database id"
|
||||||
autoFocus
|
autoFocus
|
||||||
|
tabIndex={0}
|
||||||
value={this.state.newDatabaseId}
|
value={this.state.newDatabaseId}
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
this.setState({ newDatabaseId: event.target.value })
|
this.setState({ newDatabaseId: event.target.value })
|
||||||
@@ -236,7 +237,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
true
|
true
|
||||||
).toLocaleLowerCase()} within the database.`}
|
).toLocaleLowerCase()} within the database.`}
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
@@ -279,7 +280,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -362,7 +363,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
|
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -409,7 +410,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
content={this.getPartitionKeyTooltipText()}
|
content={this.getPartitionKeyTooltipText()}
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -467,7 +468,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
does not count towards the throughput you provisioned for the database. This throughput amount will be
|
does not count towards the throughput you provisioned for the database. This throughput amount will be
|
||||||
billed in addition to the throughput amount you provisioned at the database level.`}
|
billed in addition to the throughput amount you provisioned at the database level.`}
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
@@ -497,7 +498,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
creating a unique key policy when a container is created, you ensure the uniqueness of one or more values
|
creating a unique key policy when a container is created, you ensure the uniqueness of one or more values
|
||||||
per partition key."
|
per partition key."
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -560,7 +561,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
content={this.getAnalyticalStorageTooltipContent()}
|
content={this.getAnalyticalStorageTooltipContent()}
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -637,7 +638,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
content="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
|
content="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
|||||||
@@ -23,10 +23,12 @@ import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneFor
|
|||||||
|
|
||||||
export interface AddDatabasePaneProps {
|
export interface AddDatabasePaneProps {
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
|
buttonElement?: HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||||
explorer: container,
|
explorer: container,
|
||||||
|
buttonElement,
|
||||||
}: AddDatabasePaneProps) => {
|
}: AddDatabasePaneProps) => {
|
||||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||||
let throughput: number;
|
let throughput: number;
|
||||||
@@ -77,6 +79,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
|||||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||||
};
|
};
|
||||||
TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage);
|
TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage);
|
||||||
|
buttonElement.focus();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
|||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<TextField
|
<TextField
|
||||||
aria-required="true"
|
aria-required="true"
|
||||||
|
required={true}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
styles={getTextFieldStyles()}
|
styles={getTextFieldStyles()}
|
||||||
pattern="[^/?#\\]*[^/?# \\]"
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
@@ -285,6 +286,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
|||||||
underlined
|
underlined
|
||||||
styles={getTextFieldStyles({ fontSize: 12, width: 150 })}
|
styles={getTextFieldStyles({ fontSize: 12, width: 150 })}
|
||||||
aria-required="true"
|
aria-required="true"
|
||||||
|
required={true}
|
||||||
ariaLabel="addCollection-tableId"
|
ariaLabel="addCollection-tableId"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
pattern="[^/?#\\]*[^/?# \\]"
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils
|
|||||||
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
|
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient";
|
import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient";
|
||||||
|
import { userContext } from "../../../UserContext";
|
||||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
@@ -75,6 +76,8 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
|
|||||||
selectedLocation.owner,
|
selectedLocation.owner,
|
||||||
selectedLocation.repo
|
selectedLocation.repo
|
||||||
)} - ${selectedLocation.branch}`;
|
)} - ${selectedLocation.branch}`;
|
||||||
|
} else if (selectedLocation.type === "MyNotebooks" && userContext.features.phoenix) {
|
||||||
|
destination = "My Notebooks Scratch";
|
||||||
}
|
}
|
||||||
|
|
||||||
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`);
|
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import React, { FormEvent, FunctionComponent } from "react";
|
import React, { FormEvent, FunctionComponent } from "react";
|
||||||
import { IPinnedRepo } from "../../../Juno/JunoClient";
|
import { IPinnedRepo } from "../../../Juno/JunoClient";
|
||||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||||
|
import { useNotebook } from "../../Notebook/useNotebook";
|
||||||
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
|
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
|
||||||
|
|
||||||
interface Location {
|
interface Location {
|
||||||
@@ -46,11 +47,10 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
|
|||||||
|
|
||||||
const getDropDownOptions = (): IDropdownOption[] => {
|
const getDropDownOptions = (): IDropdownOption[] => {
|
||||||
const options: IDropdownOption[] = [];
|
const options: IDropdownOption[] = [];
|
||||||
|
|
||||||
options.push({
|
options.push({
|
||||||
key: "MyNotebooks-Item",
|
key: "MyNotebooks-Item",
|
||||||
text: ResourceTreeAdapter.MyNotebooksTitle,
|
text: useNotebook.getState().notebookFolderName,
|
||||||
title: ResourceTreeAdapter.MyNotebooksTitle,
|
title: useNotebook.getState().notebookFolderName,
|
||||||
data: {
|
data: {
|
||||||
type: "MyNotebooks",
|
type: "MyNotebooks",
|
||||||
} as Location,
|
} as Location,
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
|||||||
submitButtonText: "OK",
|
submitButtonText: "OK",
|
||||||
onSubmit,
|
onSubmit,
|
||||||
};
|
};
|
||||||
|
const confirmContainer = `Confirm by typing the ${collectionName.toLowerCase()} id`;
|
||||||
|
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${collectionName}?`;
|
||||||
return (
|
return (
|
||||||
<RightPaneForm {...props}>
|
<RightPaneForm {...props}>
|
||||||
<div className="panelFormWrapper">
|
<div className="panelFormWrapper">
|
||||||
@@ -123,6 +125,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
|||||||
onChange={(event, newInput?: string) => {
|
onChange={(event, newInput?: string) => {
|
||||||
setInputCollectionName(newInput);
|
setInputCollectionName(newInput);
|
||||||
}}
|
}}
|
||||||
|
ariaLabel={confirmContainer}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{shouldRecordFeedback() && (
|
{shouldRecordFeedback() && (
|
||||||
@@ -142,6 +145,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
|||||||
onChange={(event, newInput?: string) => {
|
onChange={(event, newInput?: string) => {
|
||||||
setDeleteCollectionFeedback(newInput);
|
setDeleteCollectionFeedback(newInput);
|
||||||
}}
|
}}
|
||||||
|
ariaLabel={reasonInfo}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||||||
</span>
|
</span>
|
||||||
</Text>
|
</Text>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
|
ariaLabel="Confirm by typing the container id"
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
id="confirmCollectionId"
|
id="confirmCollectionId"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
@@ -53,6 +54,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||||||
value=""
|
value=""
|
||||||
>
|
>
|
||||||
<TextFieldBase
|
<TextFieldBase
|
||||||
|
ariaLabel="Confirm by typing the container id"
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
deferredValidationTime={200}
|
deferredValidationTime={200}
|
||||||
id="confirmCollectionId"
|
id="confirmCollectionId"
|
||||||
@@ -346,6 +348,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-invalid={false}
|
aria-invalid={false}
|
||||||
|
aria-label="Confirm by typing the container id"
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
className="ms-TextField-field field-57"
|
className="ms-TextField-field field-57"
|
||||||
id="confirmCollectionId"
|
id="confirmCollectionId"
|
||||||
|
|||||||
@@ -118,7 +118,8 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
|||||||
message:
|
message:
|
||||||
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
|
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
|
||||||
};
|
};
|
||||||
|
const confirmDatabase = "Confirm by typing the database id";
|
||||||
|
const reasonInfo = "Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?";
|
||||||
return (
|
return (
|
||||||
<RightPaneForm {...props}>
|
<RightPaneForm {...props}>
|
||||||
{!formError && <PanelInfoErrorComponent {...errorProps} />}
|
{!formError && <PanelInfoErrorComponent {...errorProps} />}
|
||||||
@@ -133,6 +134,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
|||||||
onChange={(event, newInput?: string) => {
|
onChange={(event, newInput?: string) => {
|
||||||
setDatabaseInput(newInput);
|
setDatabaseInput(newInput);
|
||||||
}}
|
}}
|
||||||
|
ariaLabel={confirmDatabase}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isLastNonEmptyDatabase() && (
|
{isLastNonEmptyDatabase() && (
|
||||||
@@ -151,6 +153,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
|||||||
onChange={(event, newInput?: string) => {
|
onChange={(event, newInput?: string) => {
|
||||||
setDatabaseFeedbackInput(newInput);
|
setDatabaseFeedbackInput(newInput);
|
||||||
}}
|
}}
|
||||||
|
ariaLabel={reasonInfo}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
|
|||||||
selectedKey={paramKeyValue && paramKeyValue.key}
|
selectedKey={paramKeyValue && paramKeyValue.key}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<Stack horizontal onClick={addNewParamAtLastIndex}>
|
<Stack horizontal onClick={addNewParamAtLastIndex} tabIndex={0}>
|
||||||
<Image {...imageProps} src={AddPropertyIcon} alt="Add param" />
|
<Image {...imageProps} src={AddPropertyIcon} alt="Add param" />
|
||||||
<Text className="addNewParamStyle">Add New Param</Text>
|
<Text className="addNewParamStyle">Add New Param</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -59,30 +59,36 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
|
|||||||
onChange={onParamKeyChange}
|
onChange={onParamKeyChange}
|
||||||
options={options}
|
options={options}
|
||||||
styles={dropdownStyles}
|
styles={dropdownStyles}
|
||||||
|
tabIndex={0}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label={inputLabel && inputLabel}
|
label={inputLabel && inputLabel}
|
||||||
id="confirmCollectionId"
|
id="confirmCollectionId"
|
||||||
autoFocus
|
|
||||||
value={paramValue}
|
value={paramValue}
|
||||||
onChange={onParamValueChange}
|
onChange={onParamValueChange}
|
||||||
/>
|
/>
|
||||||
{isAddRemoveVisible && (
|
{isAddRemoveVisible && (
|
||||||
<>
|
<>
|
||||||
<Image
|
<div tabIndex={0}>
|
||||||
{...imageProps}
|
<Image
|
||||||
src={EntityCancelIcon}
|
{...imageProps}
|
||||||
alt="Delete param"
|
src={EntityCancelIcon}
|
||||||
id="deleteparam"
|
alt="Delete param"
|
||||||
onClick={onDeleteParamKeyPress}
|
id="deleteparam"
|
||||||
/>
|
role="button"
|
||||||
<Image
|
onClick={onDeleteParamKeyPress}
|
||||||
{...imageProps}
|
/>
|
||||||
src={AddPropertyIcon}
|
</div>
|
||||||
alt="Add param"
|
<div tabIndex={0}>
|
||||||
id="addparam"
|
<Image
|
||||||
onClick={onAddNewParamKeyPress}
|
{...imageProps}
|
||||||
/>
|
src={AddPropertyIcon}
|
||||||
|
alt="Add param"
|
||||||
|
id="addparam"
|
||||||
|
role="button"
|
||||||
|
onClick={onAddNewParamKeyPress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,7 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
|
|||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
|
"phoenixClient": PhoenixClient {},
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
.panelFormWrapper {
|
.panelFormWrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
min-height: 100%;
|
||||||
|
|
||||||
.panelMainContent {
|
.panelMainContent {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding: 0 34px;
|
padding: 0 34px;
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
overflow: auto;
|
overflow-x: hidden;
|
||||||
|
|
||||||
& > :not(.collapsibleSection) {
|
& > :not(.collapsibleSection) {
|
||||||
margin-bottom: @DefaultSpace;
|
margin-bottom: @DefaultSpace;
|
||||||
|
|||||||
@@ -33,7 +33,13 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
|
|||||||
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="center">
|
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="center">
|
||||||
{icon}
|
{icon}
|
||||||
<span className="panelWarningErrorDetailsLinkContainer">
|
<span className="panelWarningErrorDetailsLinkContainer">
|
||||||
<Text className="panelWarningErrorMessage" variant="small" aria-label="message">
|
<Text
|
||||||
|
role="alert"
|
||||||
|
aria-live="assertive"
|
||||||
|
aria-label={message}
|
||||||
|
className="panelWarningErrorMessage"
|
||||||
|
variant="small"
|
||||||
|
>
|
||||||
{message}
|
{message}
|
||||||
{link && linkText && (
|
{link && linkText && (
|
||||||
<Link target="_blank" href={link}>
|
<Link target="_blank" href={link}>
|
||||||
|
|||||||
@@ -34,6 +34,6 @@ describe("Right Pane Form", () => {
|
|||||||
it("should render error in header", () => {
|
it("should render error in header", () => {
|
||||||
render(<RightPaneForm {...props} formError="file already Exist" />);
|
render(<RightPaneForm {...props} formError="file already Exist" />);
|
||||||
expect(screen.getByLabelText("error")).toBeDefined();
|
expect(screen.getByLabelText("error")).toBeDefined();
|
||||||
expect(screen.getByLabelText("message").innerHTML).toEqual("file already Exist");
|
expect(screen.getByRole("alert").innerHTML).toEqual("file already Exist");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -195,7 +195,6 @@ export const SettingsPane: FunctionComponent = () => {
|
|||||||
step={1}
|
step={1}
|
||||||
className="textfontclr"
|
className="textfontclr"
|
||||||
role="textbox"
|
role="textbox"
|
||||||
tabIndex={0}
|
|
||||||
id="max-degree"
|
id="max-degree"
|
||||||
value={"" + maxDegreeOfParallelism}
|
value={"" + maxDegreeOfParallelism}
|
||||||
onIncrement={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism)}
|
onIncrement={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism)}
|
||||||
|
|||||||
@@ -123,7 +123,6 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
onValidate={[Function]}
|
onValidate={[Function]}
|
||||||
role="textbox"
|
role="textbox"
|
||||||
step={1}
|
step={1}
|
||||||
tabIndex={0}
|
|
||||||
value="6"
|
value="6"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
|||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
|
"phoenixClient": PhoenixClient {},
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import React from "react";
|
|||||||
import TableListViewModal from "../../Tables/DataTable/TableEntityListViewModel";
|
import TableListViewModal from "../../Tables/DataTable/TableEntityListViewModel";
|
||||||
import * as Entities from "../../Tables/Entities";
|
import * as Entities from "../../Tables/Entities";
|
||||||
import { CassandraAPIDataClient, TablesAPIDataClient } from "../../Tables/TableDataClient";
|
import { CassandraAPIDataClient, TablesAPIDataClient } from "../../Tables/TableDataClient";
|
||||||
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
import QueryTablesTab from "../../Tabs/QueryTablesTab/QueryTablesTab";
|
||||||
import { AddTableEntityPanel } from "./AddTableEntityPanel";
|
import { AddTableEntityPanel } from "./AddTableEntityPanel";
|
||||||
|
|
||||||
describe("Excute Add Table Entity Pane", () => {
|
describe("Excute Add Table Entity Pane", () => {
|
||||||
@@ -18,6 +18,8 @@ describe("Excute Add Table Entity Pane", () => {
|
|||||||
queryTablesTab: fakeQueryTablesTab,
|
queryTablesTab: fakeQueryTablesTab,
|
||||||
tableEntityListViewModel: fakeTableEntityListViewModel,
|
tableEntityListViewModel: fakeTableEntityListViewModel,
|
||||||
cassandraApiClient: fakeCassandraApiClient,
|
cassandraApiClient: fakeCassandraApiClient,
|
||||||
|
reloadEntities: () => "{}",
|
||||||
|
headerItems: ["email"],
|
||||||
};
|
};
|
||||||
|
|
||||||
it("should render Default properly", () => {
|
it("should render Default properly", () => {
|
||||||
@@ -27,13 +29,13 @@ describe("Excute Add Table Entity Pane", () => {
|
|||||||
|
|
||||||
it("initially display 4 input field, 2 properties and 2 entity values", () => {
|
it("initially display 4 input field, 2 properties and 2 entity values", () => {
|
||||||
const wrapper = mount(<AddTableEntityPanel {...props} />);
|
const wrapper = mount(<AddTableEntityPanel {...props} />);
|
||||||
expect(wrapper.find("input[type='text']")).toHaveLength(0);
|
expect(wrapper.find("input[type='text']")).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("add a new entity row", () => {
|
it("add a new entity row", () => {
|
||||||
const wrapper = mount(<AddTableEntityPanel {...props} />);
|
const wrapper = mount(<AddTableEntityPanel {...props} />);
|
||||||
wrapper.find(".addButtonEntiy").last().simulate("click");
|
wrapper.find(".addButtonEntiy").last().simulate("click");
|
||||||
expect(wrapper.find("input[type='text']")).toHaveLength(1);
|
expect(wrapper.find("input[type='text']")).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("remove a entity field", () => {
|
it("remove a entity field", () => {
|
||||||
@@ -41,6 +43,6 @@ describe("Excute Add Table Entity Pane", () => {
|
|||||||
// Since default entity row doesn't have delete option, so added row then delete for test cases.
|
// Since default entity row doesn't have delete option, so added row then delete for test cases.
|
||||||
wrapper.find(".addButtonEntiy").last().simulate("click");
|
wrapper.find(".addButtonEntiy").last().simulate("click");
|
||||||
wrapper.find("#deleteEntity").last().simulate("click");
|
wrapper.find("#deleteEntity").last().simulate("click");
|
||||||
expect(wrapper.find("input[type='text']")).toHaveLength(0);
|
expect(wrapper.find("input[type='text']")).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
|
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
|
||||||
import { useBoolean } from "@fluentui/react-hooks";
|
import { useBoolean } from "@fluentui/react-hooks";
|
||||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||||
import * as _ from "underscore";
|
|
||||||
import AddPropertyIcon from "../../../../images/Add-property.svg";
|
import AddPropertyIcon from "../../../../images/Add-property.svg";
|
||||||
import RevertBackIcon from "../../../../images/RevertBack.svg";
|
import RevertBackIcon from "../../../../images/RevertBack.svg";
|
||||||
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
|
||||||
import { TableEntity } from "../../../Common/TableEntity";
|
import { TableEntity } from "../../../Common/TableEntity";
|
||||||
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
import * as TableConstants from "../../Tables/Constants";
|
import * as TableConstants from "../../Tables/Constants";
|
||||||
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
|
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
|
||||||
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
|
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
|
||||||
import * as Entities from "../../Tables/Entities";
|
import * as Entities from "../../Tables/Entities";
|
||||||
import { CassandraAPIDataClient, CassandraTableKey, TableDataClient } from "../../Tables/TableDataClient";
|
import { CassandraAPIDataClient, CassandraTableKey, TableDataClient } from "../../Tables/TableDataClient";
|
||||||
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
|
|
||||||
import * as Utilities from "../../Tables/Utilities";
|
import * as Utilities from "../../Tables/Utilities";
|
||||||
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
import NewQueryTablesTab from "../../Tabs/QueryTablesTab/QueryTablesTab";
|
||||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||||
import {
|
import {
|
||||||
attributeNameLabel,
|
attributeNameLabel,
|
||||||
@@ -36,9 +35,11 @@ import {
|
|||||||
|
|
||||||
interface AddTableEntityPanelProps {
|
interface AddTableEntityPanelProps {
|
||||||
tableDataClient: TableDataClient;
|
tableDataClient: TableDataClient;
|
||||||
queryTablesTab: QueryTablesTab;
|
queryTablesTab: NewQueryTablesTab;
|
||||||
tableEntityListViewModel: TableEntityListViewModel;
|
tableEntityListViewModel: TableEntityListViewModel;
|
||||||
cassandraApiClient: CassandraAPIDataClient;
|
cassandraApiClient: CassandraAPIDataClient;
|
||||||
|
reloadEntities: () => void;
|
||||||
|
headerItems: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EntityRowType {
|
interface EntityRowType {
|
||||||
@@ -58,7 +59,10 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
|||||||
queryTablesTab,
|
queryTablesTab,
|
||||||
tableEntityListViewModel,
|
tableEntityListViewModel,
|
||||||
cassandraApiClient,
|
cassandraApiClient,
|
||||||
|
reloadEntities,
|
||||||
|
headerItems,
|
||||||
}: AddTableEntityPanelProps): JSX.Element => {
|
}: AddTableEntityPanelProps): JSX.Element => {
|
||||||
|
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||||
const [entities, setEntities] = useState<EntityRowType[]>([]);
|
const [entities, setEntities] = useState<EntityRowType[]>([]);
|
||||||
const [selectedRow, setSelectedRow] = useState<number>(0);
|
const [selectedRow, setSelectedRow] = useState<number>(0);
|
||||||
const [entityAttributeValue, setEntityAttributeValue] = useState<string>("");
|
const [entityAttributeValue, setEntityAttributeValue] = useState<string>("");
|
||||||
@@ -76,7 +80,7 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getDefaultEntitiesAttribute = async (): Promise<void> => {
|
const getDefaultEntitiesAttribute = async (): Promise<void> => {
|
||||||
let headers = tableEntityListViewModel.headers;
|
let headers = tableEntityListViewModel.headers?.length > 1 ? tableEntityListViewModel.headers : headerItems;
|
||||||
if (DataTableUtilities.checkForDefaultHeader(headers)) {
|
if (DataTableUtilities.checkForDefaultHeader(headers)) {
|
||||||
headers = [];
|
headers = [];
|
||||||
if (userContext.apiType === "Tables") {
|
if (userContext.apiType === "Tables") {
|
||||||
@@ -116,47 +120,19 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
|||||||
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
|
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
|
||||||
try {
|
try {
|
||||||
await tableEntityListViewModel.addEntityToCache(newEntity);
|
await tableEntityListViewModel.addEntityToCache(newEntity);
|
||||||
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
|
reloadEntities();
|
||||||
tableEntityListViewModel.redrawTableThrottled();
|
setFormError("");
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
setFormError(errorMessage);
|
setFormError(errorMessage);
|
||||||
handleError(errorMessage, "AddTableRow");
|
handleError(errorMessage, "AddTableRow");
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
|
closeSidePanel();
|
||||||
setIsExecuting(false);
|
setIsExecuting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
|
|
||||||
let newHeaders: string[] = [];
|
|
||||||
const keys = Object.keys(newEntity);
|
|
||||||
keys &&
|
|
||||||
keys.forEach((key: string) => {
|
|
||||||
if (
|
|
||||||
!_.contains(viewModel.headers, key) &&
|
|
||||||
key !== TableEntityProcessor.keyProperties.attachments &&
|
|
||||||
key !== TableEntityProcessor.keyProperties.etag &&
|
|
||||||
key !== TableEntityProcessor.keyProperties.resourceId &&
|
|
||||||
key !== TableEntityProcessor.keyProperties.self &&
|
|
||||||
(!(userContext.apiType === "Cassandra") || key !== TableConstants.EntityKeyNames.RowKey)
|
|
||||||
) {
|
|
||||||
newHeaders.push(key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let newHeadersInserted = false;
|
|
||||||
if (newHeaders.length) {
|
|
||||||
if (!DataTableUtilities.checkForDefaultHeader(viewModel.headers)) {
|
|
||||||
newHeaders = viewModel.headers.concat(newHeaders);
|
|
||||||
}
|
|
||||||
viewModel.updateHeaders(newHeaders, /* notifyColumnChanges */ true, /* enablePrompt */ false);
|
|
||||||
newHeadersInserted = true;
|
|
||||||
}
|
|
||||||
return newHeadersInserted;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Add new entity row */
|
/* Add new entity row */
|
||||||
const addNewEntity = (): void => {
|
const addNewEntity = (): void => {
|
||||||
const cloneEntities: EntityRowType[] = [...entities];
|
const cloneEntities: EntityRowType[] = [...entities];
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import React from "react";
|
|||||||
import TableListViewModal from "../../Tables/DataTable/TableEntityListViewModel";
|
import TableListViewModal from "../../Tables/DataTable/TableEntityListViewModel";
|
||||||
import * as Entities from "../../Tables/Entities";
|
import * as Entities from "../../Tables/Entities";
|
||||||
import { CassandraAPIDataClient, TablesAPIDataClient } from "../../Tables/TableDataClient";
|
import { CassandraAPIDataClient, TablesAPIDataClient } from "../../Tables/TableDataClient";
|
||||||
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
import QueryTablesTab from "../../Tabs/QueryTablesTab/QueryTablesTab";
|
||||||
import { EditTableEntityPanel } from "./EditTableEntityPanel";
|
import { EditTableEntityPanel } from "./EditTableEntityPanel";
|
||||||
|
|
||||||
describe("Excute Edit Table Entity Pane", () => {
|
describe("Excute Edit Table Entity Pane", () => {
|
||||||
@@ -15,11 +15,14 @@ describe("Excute Edit Table Entity Pane", () => {
|
|||||||
fakeTableEntityListViewModel.headers = [];
|
fakeTableEntityListViewModel.headers = [];
|
||||||
fakeTableEntityListViewModel.selected = ko.observableArray<Entities.ITableEntity>([{}]);
|
fakeTableEntityListViewModel.selected = ko.observableArray<Entities.ITableEntity>([{}]);
|
||||||
|
|
||||||
|
const fakeSelectedItem = [{ PartitionKey: { _: "test", $: "String" } }];
|
||||||
const props = {
|
const props = {
|
||||||
tableDataClient: new TablesAPIDataClient(),
|
tableDataClient: new TablesAPIDataClient(),
|
||||||
queryTablesTab: fakeQueryTablesTab,
|
queryTablesTab: fakeQueryTablesTab,
|
||||||
tableEntityListViewModel: fakeTableEntityListViewModel,
|
tableEntityListViewModel: fakeTableEntityListViewModel,
|
||||||
cassandraApiClient: fakeCassandraApiClient,
|
cassandraApiClient: fakeCassandraApiClient,
|
||||||
|
selectedEntity: fakeSelectedItem,
|
||||||
|
reloadEntities: () => "{}",
|
||||||
};
|
};
|
||||||
|
|
||||||
it("should render Default properly", () => {
|
it("should render Default properly", () => {
|
||||||
@@ -29,13 +32,13 @@ describe("Excute Edit Table Entity Pane", () => {
|
|||||||
|
|
||||||
it("initially display 4 input field, 2 properties and 1 entity values", () => {
|
it("initially display 4 input field, 2 properties and 1 entity values", () => {
|
||||||
const wrapper = mount(<EditTableEntityPanel {...props} />);
|
const wrapper = mount(<EditTableEntityPanel {...props} />);
|
||||||
expect(wrapper.find("input[type='text']")).toHaveLength(0);
|
expect(wrapper.find("input[type='text']")).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("add a new entity row", () => {
|
it("add a new entity row", () => {
|
||||||
const wrapper = mount(<EditTableEntityPanel {...props} />);
|
const wrapper = mount(<EditTableEntityPanel {...props} />);
|
||||||
wrapper.find(".addButtonEntiy").last().simulate("click");
|
wrapper.find(".addButtonEntiy").last().simulate("click");
|
||||||
expect(wrapper.find("input[type='text']")).toHaveLength(1);
|
expect(wrapper.find("input[type='text']")).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("remove a entity field", () => {
|
it("remove a entity field", () => {
|
||||||
@@ -43,6 +46,6 @@ describe("Excute Edit Table Entity Pane", () => {
|
|||||||
// Since default entity row doesn't have delete option, so added row then delete for test cases.
|
// Since default entity row doesn't have delete option, so added row then delete for test cases.
|
||||||
wrapper.find(".addButtonEntiy").last().simulate("click");
|
wrapper.find(".addButtonEntiy").last().simulate("click");
|
||||||
wrapper.find("#deleteEntity").last().simulate("click");
|
wrapper.find("#deleteEntity").last().simulate("click");
|
||||||
expect(wrapper.find("input[type='text']")).toHaveLength(0);
|
expect(wrapper.find("input[type='text']")).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import AddPropertyIcon from "../../../../images/Add-property.svg";
|
|||||||
import RevertBackIcon from "../../../../images/RevertBack.svg";
|
import RevertBackIcon from "../../../../images/RevertBack.svg";
|
||||||
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
|
||||||
import { TableEntity } from "../../../Common/TableEntity";
|
import { TableEntity } from "../../../Common/TableEntity";
|
||||||
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
import * as TableConstants from "../../Tables/Constants";
|
import * as TableConstants from "../../Tables/Constants";
|
||||||
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
|
|
||||||
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
|
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
|
||||||
import * as Entities from "../../Tables/Entities";
|
import * as Entities from "../../Tables/Entities";
|
||||||
import { CassandraAPIDataClient, TableDataClient } from "../../Tables/TableDataClient";
|
import { CassandraAPIDataClient, TableDataClient } from "../../Tables/TableDataClient";
|
||||||
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
|
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
|
||||||
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
import NewQueryTablesTab from "../../Tabs/QueryTablesTab/QueryTablesTab";
|
||||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||||
import {
|
import {
|
||||||
attributeNameLabel,
|
attributeNameLabel,
|
||||||
@@ -34,9 +34,11 @@ import {
|
|||||||
|
|
||||||
interface EditTableEntityPanelProps {
|
interface EditTableEntityPanelProps {
|
||||||
tableDataClient: TableDataClient;
|
tableDataClient: TableDataClient;
|
||||||
queryTablesTab: QueryTablesTab;
|
queryTablesTab: NewQueryTablesTab;
|
||||||
tableEntityListViewModel: TableEntityListViewModel;
|
tableEntityListViewModel: TableEntityListViewModel;
|
||||||
cassandraApiClient: CassandraAPIDataClient;
|
cassandraApiClient: CassandraAPIDataClient;
|
||||||
|
selectedEntity: Entities.ITableEntity[];
|
||||||
|
reloadEntities: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EntityRowType {
|
interface EntityRowType {
|
||||||
@@ -57,7 +59,10 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
|||||||
queryTablesTab,
|
queryTablesTab,
|
||||||
tableEntityListViewModel,
|
tableEntityListViewModel,
|
||||||
cassandraApiClient,
|
cassandraApiClient,
|
||||||
|
selectedEntity,
|
||||||
|
reloadEntities,
|
||||||
}: EditTableEntityPanelProps): JSX.Element => {
|
}: EditTableEntityPanelProps): JSX.Element => {
|
||||||
|
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||||
const [entities, setEntities] = useState<EntityRowType[]>([]);
|
const [entities, setEntities] = useState<EntityRowType[]>([]);
|
||||||
const [selectedRow, setSelectedRow] = useState<number>(0);
|
const [selectedRow, setSelectedRow] = useState<number>(0);
|
||||||
const [entityAttributeValue, setEntityAttributeValue] = useState<string>("");
|
const [entityAttributeValue, setEntityAttributeValue] = useState<string>("");
|
||||||
@@ -75,8 +80,8 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let originalDocument: { [key: string]: any } = {};
|
let originalDocument: { [key: string]: any } = {};
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const entityAttribute: any = tableEntityListViewModel.selected();
|
const entityAttribute: any = selectedEntity;
|
||||||
const entityFormattedAttribute = constructDisplayedAttributes(entityAttribute[0]);
|
const entityFormattedAttribute = constructDisplayedAttributes(entityAttribute && entityAttribute[0]);
|
||||||
setEntities(entityFormattedAttribute);
|
setEntities(entityFormattedAttribute);
|
||||||
|
|
||||||
if (userContext.apiType === "Tables") {
|
if (userContext.apiType === "Tables") {
|
||||||
@@ -86,6 +91,7 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
|||||||
originalDocument = entityAttribute;
|
originalDocument = entityAttribute;
|
||||||
}
|
}
|
||||||
setOriginalDocument(originalDocument);
|
setOriginalDocument(originalDocument);
|
||||||
|
//eslint-disable-next-line
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const constructDisplayedAttributes = (entity: Entities.ITableEntity): EntityRowType[] => {
|
const constructDisplayedAttributes = (entity: Entities.ITableEntity): EntityRowType[] => {
|
||||||
@@ -216,9 +222,8 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
|||||||
entity
|
entity
|
||||||
);
|
);
|
||||||
await tableEntityListViewModel.updateCachedEntity(newEntity);
|
await tableEntityListViewModel.updateCachedEntity(newEntity);
|
||||||
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
|
reloadEntities();
|
||||||
tableEntityListViewModel.redrawTableThrottled();
|
closeSidePanel();
|
||||||
}
|
|
||||||
tableEntityListViewModel.selected.removeAll();
|
tableEntityListViewModel.selected.removeAll();
|
||||||
tableEntityListViewModel.selected.push(newEntity);
|
tableEntityListViewModel.selected.push(newEntity);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -230,34 +235,6 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
|
|
||||||
let newHeaders: string[] = [];
|
|
||||||
const keys = Object.keys(newEntity);
|
|
||||||
keys &&
|
|
||||||
keys.forEach((key: string) => {
|
|
||||||
if (
|
|
||||||
!_.contains(viewModel.headers, key) &&
|
|
||||||
key !== TableEntityProcessor.keyProperties.attachments &&
|
|
||||||
key !== TableEntityProcessor.keyProperties.etag &&
|
|
||||||
key !== TableEntityProcessor.keyProperties.resourceId &&
|
|
||||||
key !== TableEntityProcessor.keyProperties.self &&
|
|
||||||
(!(userContext.apiType === "Cassandra") || key !== TableConstants.EntityKeyNames.RowKey)
|
|
||||||
) {
|
|
||||||
newHeaders.push(key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let newHeadersInserted = false;
|
|
||||||
if (newHeaders.length) {
|
|
||||||
if (!DataTableUtilities.checkForDefaultHeader(viewModel.headers)) {
|
|
||||||
newHeaders = viewModel.headers.concat(newHeaders);
|
|
||||||
}
|
|
||||||
viewModel.updateHeaders(newHeaders, /* notifyColumnChanges */ true, /* enablePrompt */ false);
|
|
||||||
newHeadersInserted = true;
|
|
||||||
}
|
|
||||||
return newHeadersInserted;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add new entity row
|
// Add new entity row
|
||||||
const addNewEntity = (): void => {
|
const addNewEntity = (): void => {
|
||||||
const cloneEntities = [...entities];
|
const cloneEntities = [...entities];
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
import { mount } from "enzyme";
|
import { mount } from "enzyme";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Explorer from "../../../Explorer";
|
|
||||||
import QueryViewModel from "../../../Tables/QueryBuilder/QueryViewModel";
|
import QueryViewModel from "../../../Tables/QueryBuilder/QueryViewModel";
|
||||||
import { TableQuerySelectPanel } from "./TableQuerySelectPanel";
|
import { TableQuerySelectPanel } from "./TableQuerySelectPanel";
|
||||||
|
|
||||||
describe("Table query select Panel", () => {
|
describe("Table query select Panel", () => {
|
||||||
const fakeExplorer = {} as Explorer;
|
|
||||||
const fakeQueryViewModal = {} as QueryViewModel;
|
const fakeQueryViewModal = {} as QueryViewModel;
|
||||||
fakeQueryViewModal.columnOptions = ko.observableArray<string>([""]);
|
fakeQueryViewModal.columnOptions = ko.observableArray<string>([""]);
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
explorer: fakeExplorer,
|
headers: [""],
|
||||||
closePanel: (): void => undefined,
|
getSelectMessage: () => "{}",
|
||||||
queryViewModel: fakeQueryViewModal,
|
queryViewModel: fakeQueryViewModal,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29,6 +27,7 @@ describe("Table query select Panel", () => {
|
|||||||
it("Should checked availableCheckbox by default", () => {
|
it("Should checked availableCheckbox by default", () => {
|
||||||
const wrapper = mount(<TableQuerySelectPanel {...props} />);
|
const wrapper = mount(<TableQuerySelectPanel {...props} />);
|
||||||
expect(wrapper.find("#availableCheckbox").first().props()).toEqual({
|
expect(wrapper.find("#availableCheckbox").first().props()).toEqual({
|
||||||
|
ariaPositionInSet: 0,
|
||||||
id: "availableCheckbox",
|
id: "availableCheckbox",
|
||||||
label: "Available Columns",
|
label: "Available Columns",
|
||||||
checked: true,
|
checked: true,
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { RightPaneForm, RightPaneFormProps } from "../../RightPaneForm/RightPane
|
|||||||
|
|
||||||
interface TableQuerySelectPanelProps {
|
interface TableQuerySelectPanelProps {
|
||||||
queryViewModel: QueryViewModel;
|
queryViewModel: QueryViewModel;
|
||||||
|
headers: string[];
|
||||||
|
getSelectMessage: (selectMessage: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ISelectColumn {
|
interface ISelectColumn {
|
||||||
@@ -18,6 +20,8 @@ interface ISelectColumn {
|
|||||||
|
|
||||||
export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps> = ({
|
export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps> = ({
|
||||||
queryViewModel,
|
queryViewModel,
|
||||||
|
headers,
|
||||||
|
getSelectMessage,
|
||||||
}: TableQuerySelectPanelProps): JSX.Element => {
|
}: TableQuerySelectPanelProps): JSX.Element => {
|
||||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||||
|
|
||||||
@@ -29,6 +33,7 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
|
|||||||
const onSubmit = (): void => {
|
const onSubmit = (): void => {
|
||||||
queryViewModel.selectText(getParameters());
|
queryViewModel.selectText(getParameters());
|
||||||
queryViewModel.getSelectMessage();
|
queryViewModel.getSelectMessage();
|
||||||
|
getSelectMessage(queryViewModel.selectMessage());
|
||||||
closeSidePanel();
|
closeSidePanel();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,7 +57,8 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
queryViewModel && setTableColumns(queryViewModel.columnOptions());
|
// queryViewModel && setTableColumns(queryViewModel.columnOptions());
|
||||||
|
headers && setTableColumns(headers);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setTableColumns = (columnNames: string[]): void => {
|
const setTableColumns = (columnNames: string[]): void => {
|
||||||
@@ -128,8 +134,9 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
|
|||||||
label="Available Columns"
|
label="Available Columns"
|
||||||
checked={isAvailableColumnChecked}
|
checked={isAvailableColumnChecked}
|
||||||
onChange={availableColumnsCheckboxClick}
|
onChange={availableColumnsCheckboxClick}
|
||||||
|
ariaPositionInSet={0}
|
||||||
/>
|
/>
|
||||||
{columnOptions.map((column) => {
|
{columnOptions.map((column, index) => {
|
||||||
return (
|
return (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={column.columnName}
|
label={column.columnName}
|
||||||
@@ -137,6 +144,7 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
|
|||||||
key={column.columnName}
|
key={column.columnName}
|
||||||
checked={column.selected}
|
checked={column.selected}
|
||||||
disabled={!column.editable}
|
disabled={!column.editable}
|
||||||
|
ariaPositionInSet={index + 1}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -2,8 +2,12 @@
|
|||||||
|
|
||||||
exports[`Table query select Panel should render Default properly 1`] = `
|
exports[`Table query select Panel should render Default properly 1`] = `
|
||||||
<TableQuerySelectPanel
|
<TableQuerySelectPanel
|
||||||
closePanel={[Function]}
|
getSelectMessage={[Function]}
|
||||||
explorer={Object {}}
|
headers={
|
||||||
|
Array [
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
}
|
||||||
queryViewModel={
|
queryViewModel={
|
||||||
Object {
|
Object {
|
||||||
"columnOptions": [Function],
|
"columnOptions": [Function],
|
||||||
@@ -37,12 +41,14 @@ exports[`Table query select Panel should render Default properly 1`] = `
|
|||||||
className="column-select-view"
|
className="column-select-view"
|
||||||
>
|
>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
|
ariaPositionInSet={0}
|
||||||
checked={true}
|
checked={true}
|
||||||
id="availableCheckbox"
|
id="availableCheckbox"
|
||||||
label="Available Columns"
|
label="Available Columns"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
>
|
>
|
||||||
<CheckboxBase
|
<CheckboxBase
|
||||||
|
ariaPositionInSet={0}
|
||||||
checked={true}
|
checked={true}
|
||||||
id="availableCheckbox"
|
id="availableCheckbox"
|
||||||
label="Available Columns"
|
label="Available Columns"
|
||||||
@@ -328,6 +334,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
|
|||||||
<input
|
<input
|
||||||
aria-checked="true"
|
aria-checked="true"
|
||||||
aria-label="Available Columns"
|
aria-label="Available Columns"
|
||||||
|
aria-posinset={0}
|
||||||
checked={true}
|
checked={true}
|
||||||
className="input-55"
|
className="input-55"
|
||||||
data-ktp-execute-target={true}
|
data-ktp-execute-target={true}
|
||||||
@@ -646,6 +653,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
|
|||||||
</CheckboxBase>
|
</CheckboxBase>
|
||||||
</StyledCheckboxBase>
|
</StyledCheckboxBase>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
|
ariaPositionInSet={1}
|
||||||
checked={true}
|
checked={true}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
key=""
|
key=""
|
||||||
@@ -653,6 +661,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
|
|||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
>
|
>
|
||||||
<CheckboxBase
|
<CheckboxBase
|
||||||
|
ariaPositionInSet={1}
|
||||||
checked={true}
|
checked={true}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
label=""
|
label=""
|
||||||
@@ -939,6 +948,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
|
|||||||
aria-checked="true"
|
aria-checked="true"
|
||||||
aria-disabled={false}
|
aria-disabled={false}
|
||||||
aria-label=""
|
aria-label=""
|
||||||
|
aria-posinset={1}
|
||||||
checked={true}
|
checked={true}
|
||||||
className="input-55"
|
className="input-55"
|
||||||
data-ktp-execute-target={true}
|
data-ktp-execute-target={true}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -327,13 +327,17 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
key=".0:$.1"
|
key=".0:$.1"
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
aria-label="message"
|
aria-label="Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources."
|
||||||
|
aria-live="assertive"
|
||||||
className="panelWarningErrorMessage"
|
className="panelWarningErrorMessage"
|
||||||
|
role="alert"
|
||||||
variant="small"
|
variant="small"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-label="message"
|
aria-label="Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources."
|
||||||
|
aria-live="assertive"
|
||||||
className="panelWarningErrorMessage css-56"
|
className="panelWarningErrorMessage css-56"
|
||||||
|
role="alert"
|
||||||
>
|
>
|
||||||
Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.
|
Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.
|
||||||
</span>
|
</span>
|
||||||
@@ -363,6 +367,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
</Text>
|
</Text>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
|
ariaLabel="Confirm by typing the database id"
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
id="confirmDatabaseId"
|
id="confirmDatabaseId"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
@@ -375,6 +380,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TextFieldBase
|
<TextFieldBase
|
||||||
|
ariaLabel="Confirm by typing the database id"
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
deferredValidationTime={200}
|
deferredValidationTime={200}
|
||||||
id="confirmDatabaseId"
|
id="confirmDatabaseId"
|
||||||
@@ -667,6 +673,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-invalid={false}
|
aria-invalid={false}
|
||||||
|
aria-label="Confirm by typing the database id"
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
className="ms-TextField-field field-60"
|
className="ms-TextField-field field-60"
|
||||||
id="confirmDatabaseId"
|
id="confirmDatabaseId"
|
||||||
@@ -707,6 +714,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
</span>
|
</span>
|
||||||
</Text>
|
</Text>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
|
ariaLabel="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?"
|
||||||
id="deleteDatabaseFeedbackInput"
|
id="deleteDatabaseFeedbackInput"
|
||||||
multiline={true}
|
multiline={true}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
@@ -720,6 +728,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TextFieldBase
|
<TextFieldBase
|
||||||
|
ariaLabel="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?"
|
||||||
deferredValidationTime={200}
|
deferredValidationTime={200}
|
||||||
id="deleteDatabaseFeedbackInput"
|
id="deleteDatabaseFeedbackInput"
|
||||||
multiline={true}
|
multiline={true}
|
||||||
@@ -1013,6 +1022,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
|||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
aria-invalid={false}
|
aria-invalid={false}
|
||||||
|
aria-label="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?"
|
||||||
className="ms-TextField-field field-71"
|
className="ms-TextField-field field-71"
|
||||||
id="deleteDatabaseFeedbackInput"
|
id="deleteDatabaseFeedbackInput"
|
||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
|
|||||||
@@ -307,16 +307,23 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
iconSrc: AddDatabaseIcon,
|
iconSrc: AddDatabaseIcon,
|
||||||
title: "New " + getDatabaseName(),
|
title: "New " + getDatabaseName(),
|
||||||
description: undefined,
|
description: undefined,
|
||||||
onClick: () =>
|
onClick: () => this.openAddDatabasePanel(),
|
||||||
useSidePanel
|
|
||||||
.getState()
|
|
||||||
.openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={this.container} />),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private openAddDatabasePanel() {
|
||||||
|
const newDatabaseButton = document.activeElement as HTMLElement;
|
||||||
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel(
|
||||||
|
"New " + getDatabaseName(),
|
||||||
|
<AddDatabasePanel explorer={this.container} buttonElement={newDatabaseButton} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
|
private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
|
||||||
return {
|
return {
|
||||||
iconSrc: NotebookIcon,
|
iconSrc: NotebookIcon,
|
||||||
|
|||||||
@@ -1,393 +0,0 @@
|
|||||||
import * as ko from "knockout";
|
|
||||||
import * as _ from "underscore";
|
|
||||||
|
|
||||||
import * as Constants from "../Constants";
|
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
|
||||||
import * as DataTableBuilder from "./DataTableBuilder";
|
|
||||||
import DataTableOperationManager from "./DataTableOperationManager";
|
|
||||||
import * as DataTableOperations from "./DataTableOperations";
|
|
||||||
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
|
||||||
import TableEntityListViewModel from "./TableEntityListViewModel";
|
|
||||||
import * as Utilities from "../Utilities";
|
|
||||||
import * as Entities from "../Entities";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom binding manager of datatable
|
|
||||||
*/
|
|
||||||
var tableEntityListViewModelMap: {
|
|
||||||
[key: string]: {
|
|
||||||
tableViewModel: TableEntityListViewModel;
|
|
||||||
operationManager: DataTableOperationManager;
|
|
||||||
$dataTable: JQuery;
|
|
||||||
};
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
function bindDataTable(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) {
|
|
||||||
var tableEntityListViewModel = bindingContext.$data;
|
|
||||||
tableEntityListViewModel.notifyColumnChanges = onTableColumnChange;
|
|
||||||
var $dataTable = $(element);
|
|
||||||
var queryTablesTab = bindingContext.$parent;
|
|
||||||
var operationManager = new DataTableOperationManager(
|
|
||||||
$dataTable,
|
|
||||||
tableEntityListViewModel,
|
|
||||||
queryTablesTab.tableCommands
|
|
||||||
);
|
|
||||||
|
|
||||||
tableEntityListViewModelMap[queryTablesTab.tabId] = {
|
|
||||||
tableViewModel: tableEntityListViewModel,
|
|
||||||
operationManager: operationManager,
|
|
||||||
$dataTable: $dataTable,
|
|
||||||
};
|
|
||||||
|
|
||||||
createDataTable(0, tableEntityListViewModel, queryTablesTab); // Fake a DataTable to start.
|
|
||||||
$(window).resize(updateTableScrollableRegionMetrics);
|
|
||||||
operationManager.focusTable(); // Also selects the first row if needed.
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTableColumnChange(enablePrompt: boolean = true, queryTablesTab: QueryTablesTab) {
|
|
||||||
var columnsFilter: boolean[] = null;
|
|
||||||
var tableEntityListViewModel = tableEntityListViewModelMap[queryTablesTab.tabId].tableViewModel;
|
|
||||||
if (queryTablesTab.queryViewModel()) {
|
|
||||||
queryTablesTab.queryViewModel().queryBuilderViewModel().updateColumnOptions();
|
|
||||||
}
|
|
||||||
createDataTable(
|
|
||||||
tableEntityListViewModel.tablePageStartIndex,
|
|
||||||
tableEntityListViewModel,
|
|
||||||
queryTablesTab,
|
|
||||||
true,
|
|
||||||
columnsFilter
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDataTable(
|
|
||||||
startIndex: number,
|
|
||||||
tableEntityListViewModel: TableEntityListViewModel,
|
|
||||||
queryTablesTab: QueryTablesTab,
|
|
||||||
destroy: boolean = false,
|
|
||||||
columnsFilter: boolean[] = null
|
|
||||||
): void {
|
|
||||||
var $dataTable = tableEntityListViewModelMap[queryTablesTab.tabId].$dataTable;
|
|
||||||
if (destroy) {
|
|
||||||
// Find currently displayed columns.
|
|
||||||
var currentColumns: string[] = tableEntityListViewModel.headers;
|
|
||||||
|
|
||||||
// Calculate how many more columns need to added to the current table.
|
|
||||||
var columnsToAdd: number = _.difference(tableEntityListViewModel.headers, currentColumns).length;
|
|
||||||
|
|
||||||
// This is needed as current solution of adding column is more like a workaround
|
|
||||||
// The official support for dynamically add column is not yet there
|
|
||||||
// Please track github issue https://github.com/DataTables/DataTables/issues/273 for its offical support
|
|
||||||
for (var i = 0; i < columnsToAdd; i++) {
|
|
||||||
$(".dataTables_scrollHead table thead tr th").eq(0).after("<th></th>");
|
|
||||||
}
|
|
||||||
tableEntityListViewModel.table.destroy();
|
|
||||||
$dataTable.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
var jsonColTable = [];
|
|
||||||
|
|
||||||
for (var i = 0; i < tableEntityListViewModel.headers.length; i++) {
|
|
||||||
jsonColTable.push({
|
|
||||||
sTitle: tableEntityListViewModel.headers[i],
|
|
||||||
data: tableEntityListViewModel.headers[i],
|
|
||||||
aTargets: [i],
|
|
||||||
mRender: bindColumn,
|
|
||||||
visible: !!columnsFilter ? columnsFilter[i] : true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
tableEntityListViewModel.table = DataTableBuilder.createDataTable($dataTable, <DataTables.Settings>{
|
|
||||||
// WARNING!!! SECURITY: If you add new columns, make sure you encode them if they are user strings from Azure (see encodeText)
|
|
||||||
// so that they don't get interpreted as HTML in our page.
|
|
||||||
colReorder: true,
|
|
||||||
aoColumnDefs: jsonColTable,
|
|
||||||
stateSave: false,
|
|
||||||
dom: "RZlfrtip",
|
|
||||||
oColReorder: {
|
|
||||||
iFixedColumns: 1,
|
|
||||||
},
|
|
||||||
displayStart: startIndex,
|
|
||||||
bPaginate: true,
|
|
||||||
pagingType: "full_numbers",
|
|
||||||
bProcessing: true,
|
|
||||||
oLanguage: {
|
|
||||||
sInfo: "Results _START_ - _END_ of _TOTAL_",
|
|
||||||
oPaginate: {
|
|
||||||
sFirst: "<<",
|
|
||||||
sNext: ">",
|
|
||||||
sPrevious: "<",
|
|
||||||
sLast: ">>",
|
|
||||||
},
|
|
||||||
sProcessing: '<img style="width: 28px; height: 6px; " src="images/LoadingIndicator_3Squares.gif">',
|
|
||||||
oAria: {
|
|
||||||
sSortAscending: "",
|
|
||||||
sSortDescending: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
destroy: destroy,
|
|
||||||
bInfo: true,
|
|
||||||
bLength: false,
|
|
||||||
bLengthChange: false,
|
|
||||||
scrollX: true,
|
|
||||||
scrollCollapse: true,
|
|
||||||
iDisplayLength: 100,
|
|
||||||
serverSide: true,
|
|
||||||
ajax: queryTablesTab.tabId, // Using this settings to make sure for getServerData we update the table based on the appropriate tab
|
|
||||||
fnServerData: getServerData,
|
|
||||||
fnRowCallback: bindClientId,
|
|
||||||
fnInitComplete: initializeTable,
|
|
||||||
fnDrawCallback: updateSelectionStatus,
|
|
||||||
});
|
|
||||||
|
|
||||||
(tableEntityListViewModel.table.table(0).container() as Element)
|
|
||||||
.querySelectorAll(Constants.htmlSelectors.dataTableHeaderTableSelector)
|
|
||||||
.forEach((table) => {
|
|
||||||
table.setAttribute(
|
|
||||||
"summary",
|
|
||||||
`Header for sorting results for container ${tableEntityListViewModel.queryTablesTab.collection.id()}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
(tableEntityListViewModel.table.table(0).container() as Element)
|
|
||||||
.querySelectorAll(Constants.htmlSelectors.dataTableBodyTableSelector)
|
|
||||||
.forEach((table) => {
|
|
||||||
table.setAttribute("summary", `Results for container ${tableEntityListViewModel.queryTablesTab.collection.id()}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindColumn(data: any, type: string, full: any) {
|
|
||||||
var displayedValue: any = null;
|
|
||||||
if (data) {
|
|
||||||
displayedValue = data._;
|
|
||||||
|
|
||||||
// SECURITY: Make sure we don't allow cross-site scripting by interpreting the values as HTML
|
|
||||||
displayedValue = Utilities.htmlEncode(displayedValue);
|
|
||||||
|
|
||||||
// Css' empty psuedo class can only tell the difference of whether a cell has values.
|
|
||||||
// A cell has no values no matter it's empty or it has no such a property.
|
|
||||||
// To distinguish between an empty cell and a non-existing property cell,
|
|
||||||
// we add a whitespace to the empty cell so that css will treat it as a cell with values.
|
|
||||||
if (displayedValue === "" && data.$ === Constants.TableType.String) {
|
|
||||||
displayedValue = " ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return displayedValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getServerData(sSource: any, aoData: any, fnCallback: any, oSettings: any) {
|
|
||||||
tableEntityListViewModelMap[oSettings.ajax].tableViewModel.renderNextPageAndupdateCache(
|
|
||||||
sSource,
|
|
||||||
aoData,
|
|
||||||
fnCallback,
|
|
||||||
oSettings
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bind table data information to row element so that we can track back to the table data
|
|
||||||
* from UI elements.
|
|
||||||
*/
|
|
||||||
function bindClientId(nRow: Node, aData: Entities.ITableEntity) {
|
|
||||||
$(nRow).attr(Constants.htmlAttributeNames.dataTableRowKeyAttr, aData.RowKey._);
|
|
||||||
return nRow;
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectionChanged(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) {
|
|
||||||
$(".dataTable tr.selected").attr("tabindex", "-1").removeClass("selected");
|
|
||||||
|
|
||||||
const selected =
|
|
||||||
bindingContext && bindingContext.$data && bindingContext.$data.selected && bindingContext.$data.selected();
|
|
||||||
selected &&
|
|
||||||
selected.forEach((b: Entities.ITableEntity) => {
|
|
||||||
var sel = DataTableOperations.getRowSelector([
|
|
||||||
{
|
|
||||||
key: Constants.htmlAttributeNames.dataTableRowKeyAttr,
|
|
||||||
value: b.RowKey && b.RowKey._ && b.RowKey._.toString(),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
$(sel).attr("tabindex", "0").focus().addClass("selected");
|
|
||||||
});
|
|
||||||
//selected = bindingContext.$data.selected();
|
|
||||||
}
|
|
||||||
|
|
||||||
function dataChanged(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) {
|
|
||||||
// do nothing for now
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeTable(): void {
|
|
||||||
updateTableScrollableRegionMetrics();
|
|
||||||
initializeEventHandlers();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTableScrollableRegionMetrics(): void {
|
|
||||||
updateTableScrollableRegionHeight();
|
|
||||||
updateTableScrollableRegionWidth();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Update the table's scrollable region height. So the pagination control is always shown at the bottom of the page.
|
|
||||||
*/
|
|
||||||
function updateTableScrollableRegionHeight(): void {
|
|
||||||
$(".tab-pane").each(function (index, tabElement) {
|
|
||||||
if (!$(tabElement).hasClass("tableContainer")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add some padding to the table so it doesn't get too close to the container border.
|
|
||||||
var dataTablePaddingBottom = 10;
|
|
||||||
var bodyHeight = $(window).height();
|
|
||||||
var dataTablesScrollBodyPosY = $(tabElement).find(Constants.htmlSelectors.dataTableScrollBodySelector).offset().top;
|
|
||||||
var dataTablesInfoElem = $(tabElement).find(".dataTables_info");
|
|
||||||
var dataTablesPaginateElem = $(tabElement).find(".dataTables_paginate");
|
|
||||||
const notificationConsoleHeight = 32; /** Header height **/
|
|
||||||
|
|
||||||
var scrollHeight =
|
|
||||||
bodyHeight -
|
|
||||||
dataTablesScrollBodyPosY -
|
|
||||||
dataTablesPaginateElem.outerHeight(true) -
|
|
||||||
dataTablePaddingBottom -
|
|
||||||
notificationConsoleHeight;
|
|
||||||
|
|
||||||
//info and paginate control are stacked
|
|
||||||
if (dataTablesInfoElem.offset().top < dataTablesPaginateElem.offset().top) {
|
|
||||||
scrollHeight -= dataTablesInfoElem.outerHeight(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO This is a work around for setting the outerheight since we don't have access to the JQuery.outerheight(numberValue)
|
|
||||||
// in the current version of JQuery we are using. Ideally, we would upgrade JQuery and use this line instead:
|
|
||||||
// $(Constants.htmlSelectors.dataTableScrollBodySelector).outerHeight(scrollHeight);
|
|
||||||
var element = $(tabElement).find(Constants.htmlSelectors.dataTableScrollBodySelector)[0];
|
|
||||||
var style = getComputedStyle(element);
|
|
||||||
var actualHeight = parseInt(style.height);
|
|
||||||
var change = element.offsetHeight - scrollHeight;
|
|
||||||
$(tabElement)
|
|
||||||
.find(Constants.htmlSelectors.dataTableScrollBodySelector)
|
|
||||||
.height(actualHeight - change);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Update the table's scrollable region width to make efficient use of the remaining space.
|
|
||||||
*/
|
|
||||||
function updateTableScrollableRegionWidth(): void {
|
|
||||||
$(".tab-pane").each(function (index, tabElement) {
|
|
||||||
if (!$(tabElement).hasClass("tableContainer")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bodyWidth = $(window).width();
|
|
||||||
var dataTablesScrollBodyPosLeft = $(tabElement).find(Constants.htmlSelectors.dataTableScrollBodySelector).offset()
|
|
||||||
.left;
|
|
||||||
var scrollWidth = bodyWidth - dataTablesScrollBodyPosLeft;
|
|
||||||
|
|
||||||
// jquery datatables automatically sets width:100% to both the header and the body when we use it's column autoWidth feature.
|
|
||||||
// We work around that by setting the height for it's container instead.
|
|
||||||
$(tabElement).find(Constants.htmlSelectors.dataTableScrollContainerSelector).width(scrollWidth);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeEventHandlers(): void {
|
|
||||||
var $headers: JQuery = $(Constants.htmlSelectors.dataTableHeaderTypeSelector);
|
|
||||||
var $firstHeader: JQuery = $headers.first();
|
|
||||||
var firstIndex: string = $firstHeader.attr(Constants.htmlAttributeNames.dataTableHeaderIndex);
|
|
||||||
|
|
||||||
$headers
|
|
||||||
.on("keydown", (event: JQueryEventObject) => {
|
|
||||||
Utilities.onEnter(event, ($sourceElement: JQuery) => {
|
|
||||||
$sourceElement.css("background-color", Constants.cssColors.commonControlsButtonActive);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bind shift+tab from first header back to search input field
|
|
||||||
Utilities.onTab(
|
|
||||||
event,
|
|
||||||
($sourceElement: JQuery) => {
|
|
||||||
var sourceIndex: string = $sourceElement.attr(Constants.htmlAttributeNames.dataTableHeaderIndex);
|
|
||||||
|
|
||||||
if (sourceIndex === firstIndex) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/* metaKey */ null,
|
|
||||||
/* shiftKey */ true,
|
|
||||||
/* altKey */ null
|
|
||||||
);
|
|
||||||
|
|
||||||
// Also reset color if [shift-] tabbing away from button while holding down 'enter'
|
|
||||||
Utilities.onTab(event, ($sourceElement: JQuery) => {
|
|
||||||
$sourceElement.css("background-color", "");
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.on("keyup", (event: JQueryEventObject) => {
|
|
||||||
Utilities.onEnter(event, ($sourceElement: JQuery) => {
|
|
||||||
$sourceElement.css("background-color", "");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSelectionStatus(oSettings: any): void {
|
|
||||||
var $dataTableRows: JQuery = $(Constants.htmlSelectors.dataTableAllRowsSelector);
|
|
||||||
if ($dataTableRows) {
|
|
||||||
for (var i = 0; i < $dataTableRows.length; i++) {
|
|
||||||
var $row: JQuery = $dataTableRows.eq(i);
|
|
||||||
var rowKey: string = $row.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr);
|
|
||||||
var table = tableEntityListViewModelMap[oSettings.ajax].tableViewModel;
|
|
||||||
if (table.isItemSelected(table.getTableEntityKeys(rowKey))) {
|
|
||||||
$row.attr("tabindex", "0");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDataTableFocus(oSettings.ajax);
|
|
||||||
|
|
||||||
DataTableOperations.setPaginationButtonEventHandlers();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO consider centralizing this "post-command" logic into some sort of Command Manager entity.
|
|
||||||
// See VSO:166520: "[Storage Explorer] Consider adding a 'command manager' to track command post-effects."
|
|
||||||
function updateDataTableFocus(queryTablesTabId: string): void {
|
|
||||||
var $activeElement: JQuery = $(document.activeElement);
|
|
||||||
var isFocusLost: boolean = $activeElement.is("body"); // When focus is lost, "body" becomes the active element.
|
|
||||||
var storageExplorerFrameHasFocus: boolean = document.hasFocus();
|
|
||||||
var operationManager = tableEntityListViewModelMap[queryTablesTabId].operationManager;
|
|
||||||
if (operationManager) {
|
|
||||||
if (isFocusLost && storageExplorerFrameHasFocus) {
|
|
||||||
// We get here when no control is active, meaning that the table update was triggered
|
|
||||||
// from a dialog, the context menu or by clicking on a toolbar control or header.
|
|
||||||
// Note that giving focus to the table also selects the first row if needed.
|
|
||||||
// The document.hasFocus() ensures that the table will only get focus when the
|
|
||||||
// focus was lost (i.e. "body has the focus") within the Storage Explorer frame
|
|
||||||
// i.e. not when the focus is lost because it is in another frame
|
|
||||||
// e.g. a daytona dialog or in the Activity Log.
|
|
||||||
operationManager.focusTable();
|
|
||||||
}
|
|
||||||
if ($activeElement.is(".sorting_asc") || $activeElement.is(".sorting_desc")) {
|
|
||||||
// If table header is selected, focus is shifted to the selected element as part of accessibility
|
|
||||||
$activeElement && $activeElement.focus();
|
|
||||||
} else {
|
|
||||||
// If some control is active, we don't give focus back to the table,
|
|
||||||
// just select the first row if needed (empty selection).
|
|
||||||
operationManager.selectFirstIfNeeded();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(<any>ko.bindingHandlers).tableSource = {
|
|
||||||
init: bindDataTable,
|
|
||||||
update: dataChanged,
|
|
||||||
};
|
|
||||||
|
|
||||||
(<any>ko.bindingHandlers).tableSelection = {
|
|
||||||
update: selectionChanged,
|
|
||||||
};
|
|
||||||
|
|
||||||
(<any>ko.bindingHandlers).readOnly = {
|
|
||||||
update: function (element: any, valueAccessor: any) {
|
|
||||||
var value = ko.utils.unwrapObservable(valueAccessor());
|
|
||||||
if (value) {
|
|
||||||
element.setAttribute("readOnly", true);
|
|
||||||
} else {
|
|
||||||
element.removeAttribute("readOnly");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import * as Utilities from "../Utilities";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper function for creating data tables. Call this method, not the
|
|
||||||
* data tables constructor when you want to create a data table. This
|
|
||||||
* function makes sure that content without a render function is properly
|
|
||||||
* encoded to prevent XSS.
|
|
||||||
* @param{$dataTableElem} JQuery data table element
|
|
||||||
* @param{$settings} Settings to use when creating the data table
|
|
||||||
*/
|
|
||||||
export function createDataTable($dataTableElem: JQuery, settings: any): DataTables.DataTable {
|
|
||||||
return $dataTableElem.DataTable(applyDefaultRendering(settings));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Go through the settings for a data table and apply a simple HTML encode to any column
|
|
||||||
* without a render function to prevent XSS.
|
|
||||||
* @param{settings} The settings to check
|
|
||||||
* @return The given settings with all columns having a rendering function
|
|
||||||
*/
|
|
||||||
function applyDefaultRendering(settings: any): DataTables.SettingsLegacy {
|
|
||||||
var tableColumns: DataTables.ColumnLegacy[] = null;
|
|
||||||
|
|
||||||
if (settings.aoColumns) {
|
|
||||||
tableColumns = settings.aoColumns;
|
|
||||||
} else if (settings.aoColumnDefs) {
|
|
||||||
// for tables we use aoColumnDefs instead of aoColumns
|
|
||||||
tableColumns = settings.aoColumnDefs;
|
|
||||||
}
|
|
||||||
|
|
||||||
// either the settings had no columns defined, or they were called
|
|
||||||
// by a property name which we have not used before
|
|
||||||
if (!tableColumns) {
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < tableColumns.length; i++) {
|
|
||||||
// the column does not have a render function
|
|
||||||
if (!tableColumns[i].mRender) {
|
|
||||||
tableColumns[i].mRender = defaultDataRender;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default data render function, whatever is done to data in here
|
|
||||||
* will be done to any data which we do not specify a render for.
|
|
||||||
*/
|
|
||||||
function defaultDataRender(data: any, type: string, full: any) {
|
|
||||||
return Utilities.htmlEncode(data);
|
|
||||||
}
|
|
||||||
@@ -1,300 +0,0 @@
|
|||||||
import ko from "knockout";
|
|
||||||
|
|
||||||
import * as DataTableOperations from "./DataTableOperations";
|
|
||||||
import * as Constants from "../Constants";
|
|
||||||
import TableCommands from "./TableCommands";
|
|
||||||
import TableEntityListViewModel from "./TableEntityListViewModel";
|
|
||||||
import * as Utilities from "../Utilities";
|
|
||||||
import * as Entities from "../Entities";
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Base class for data table row selection.
|
|
||||||
*/
|
|
||||||
export default class DataTableOperationManager {
|
|
||||||
private _tableEntityListViewModel: TableEntityListViewModel;
|
|
||||||
private _tableCommands: TableCommands;
|
|
||||||
private dataTable: JQuery;
|
|
||||||
|
|
||||||
constructor(table: JQuery, viewModel: TableEntityListViewModel, tableCommands: TableCommands) {
|
|
||||||
this.dataTable = table;
|
|
||||||
this._tableEntityListViewModel = viewModel;
|
|
||||||
this._tableCommands = tableCommands;
|
|
||||||
|
|
||||||
this.bind();
|
|
||||||
this._tableEntityListViewModel.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private click = (event: JQueryEventObject) => {
|
|
||||||
var elem: JQuery = $(event.currentTarget);
|
|
||||||
this.updateLastSelectedItem(elem, event.shiftKey);
|
|
||||||
|
|
||||||
if (Utilities.isEnvironmentCtrlPressed(event)) {
|
|
||||||
this.applyCtrlSelection(elem);
|
|
||||||
} else if (event.shiftKey) {
|
|
||||||
this.applyShiftSelection(elem);
|
|
||||||
} else {
|
|
||||||
this.applySingleSelection(elem);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private doubleClick = (event: JQueryEventObject) => {
|
|
||||||
this.tryOpenEditor();
|
|
||||||
};
|
|
||||||
|
|
||||||
private keyDown = (event: JQueryEventObject): boolean => {
|
|
||||||
var isUpArrowKey: boolean = event.keyCode === Constants.keyCodes.UpArrow,
|
|
||||||
isDownArrowKey: boolean = event.keyCode === Constants.keyCodes.DownArrow,
|
|
||||||
handled: boolean = false;
|
|
||||||
|
|
||||||
if (isUpArrowKey || isDownArrowKey) {
|
|
||||||
var lastSelectedItem: Entities.ITableEntity = this._tableEntityListViewModel.lastSelectedItem;
|
|
||||||
var dataTableRows: JQuery = $(Constants.htmlSelectors.dataTableAllRowsSelector);
|
|
||||||
var maximumIndex = dataTableRows.length - 1;
|
|
||||||
|
|
||||||
// If can't find an index for lastSelectedItem, then either no item is previously selected or it goes across page.
|
|
||||||
// Simply select the first item in this case.
|
|
||||||
var lastSelectedItemIndex = lastSelectedItem
|
|
||||||
? this._tableEntityListViewModel.getItemIndexFromCurrentPage(
|
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(lastSelectedItem.RowKey._)
|
|
||||||
)
|
|
||||||
: -1;
|
|
||||||
var nextIndex: number = isUpArrowKey ? lastSelectedItemIndex - 1 : lastSelectedItemIndex + 1;
|
|
||||||
var safeIndex: number = Utilities.ensureBetweenBounds(nextIndex, 0, maximumIndex);
|
|
||||||
var selectedRowElement: JQuery = dataTableRows.eq(safeIndex);
|
|
||||||
|
|
||||||
if (selectedRowElement) {
|
|
||||||
if (event.shiftKey) {
|
|
||||||
this.applyShiftSelection(selectedRowElement);
|
|
||||||
} else {
|
|
||||||
this.applySingleSelection(selectedRowElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateLastSelectedItem(selectedRowElement, event.shiftKey);
|
|
||||||
handled = true;
|
|
||||||
DataTableOperations.scrollToRowIfNeeded(dataTableRows, safeIndex, isUpArrowKey);
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
Utilities.isEnvironmentCtrlPressed(event) &&
|
|
||||||
!Utilities.isEnvironmentShiftPressed(event) &&
|
|
||||||
!Utilities.isEnvironmentAltPressed(event) &&
|
|
||||||
event.keyCode === Constants.keyCodes.A
|
|
||||||
) {
|
|
||||||
this.applySelectAll();
|
|
||||||
handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !handled;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Note: There is one key up event each time a key is pressed;
|
|
||||||
// in contrast, there may be more than one key down and key
|
|
||||||
// pressed events.
|
|
||||||
private keyUp = (event: JQueryEventObject): boolean => {
|
|
||||||
var handled: boolean = false;
|
|
||||||
|
|
||||||
switch (event.keyCode) {
|
|
||||||
case Constants.keyCodes.Enter:
|
|
||||||
handled = this.tryOpenEditor();
|
|
||||||
break;
|
|
||||||
case Constants.keyCodes.Delete:
|
|
||||||
handled = this.tryHandleDeleteSelected();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !handled;
|
|
||||||
};
|
|
||||||
|
|
||||||
private itemDropped = (event: JQueryEventObject): boolean => {
|
|
||||||
var handled: boolean = false;
|
|
||||||
var items = (<any>event.originalEvent).dataTransfer.items;
|
|
||||||
|
|
||||||
if (!items) {
|
|
||||||
// On browsers outside of Chromium
|
|
||||||
// we can't discern between dirs and files
|
|
||||||
// so we will disable drag & drop for now
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < items.length; i++) {
|
|
||||||
var item = items[i];
|
|
||||||
var entry = item.webkitGetAsEntry();
|
|
||||||
|
|
||||||
if (entry.isFile) {
|
|
||||||
// TODO: parse the file and insert content as entities
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return !handled;
|
|
||||||
};
|
|
||||||
|
|
||||||
private tryOpenEditor(): boolean {
|
|
||||||
return this._tableCommands.tryOpenEntityEditor(this._tableEntityListViewModel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private tryHandleDeleteSelected(): boolean {
|
|
||||||
var selectedEntities: Entities.ITableEntity[] = this._tableEntityListViewModel.selected();
|
|
||||||
var handled: boolean = false;
|
|
||||||
|
|
||||||
if (selectedEntities && selectedEntities.length) {
|
|
||||||
this._tableCommands.deleteEntitiesCommand(this._tableEntityListViewModel);
|
|
||||||
handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return handled;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getEntityIdentity($elem: JQuery): Entities.ITableEntityIdentity {
|
|
||||||
return {
|
|
||||||
RowKey: $elem.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateLastSelectedItem($elem: JQuery, isShiftSelect: boolean) {
|
|
||||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
|
||||||
var entity = this._tableEntityListViewModel.getItemFromCurrentPage(
|
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
|
|
||||||
);
|
|
||||||
|
|
||||||
this._tableEntityListViewModel.lastSelectedItem = entity;
|
|
||||||
|
|
||||||
if (!isShiftSelect) {
|
|
||||||
this._tableEntityListViewModel.lastSelectedAnchorItem = entity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private applySingleSelection($elem: JQuery) {
|
|
||||||
if ($elem) {
|
|
||||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
|
||||||
|
|
||||||
this._tableEntityListViewModel.clearSelection();
|
|
||||||
this.addToSelection(entityIdentity.RowKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private applySelectAll() {
|
|
||||||
this._tableEntityListViewModel.clearSelection();
|
|
||||||
ko.utils.arrayPushAll<Entities.ITableEntity>(
|
|
||||||
this._tableEntityListViewModel.selected,
|
|
||||||
this._tableEntityListViewModel.getAllItemsInCurrentPage()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private applyCtrlSelection($elem: JQuery): void {
|
|
||||||
var koSelected: ko.ObservableArray<Entities.ITableEntity> = this._tableEntityListViewModel
|
|
||||||
? this._tableEntityListViewModel.selected
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (koSelected) {
|
|
||||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!this._tableEntityListViewModel.isItemSelected(
|
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// Adding item not previously in selection
|
|
||||||
this.addToSelection(entityIdentity.RowKey);
|
|
||||||
} else {
|
|
||||||
koSelected.remove((item: Entities.ITableEntity) => item.RowKey._ === entityIdentity.RowKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private applyShiftSelection($elem: JQuery): void {
|
|
||||||
var anchorItem = this._tableEntityListViewModel.lastSelectedAnchorItem;
|
|
||||||
|
|
||||||
// If anchor item doesn't exist, use the first available item of current page instead
|
|
||||||
if (!anchorItem && this._tableEntityListViewModel.items().length > 0) {
|
|
||||||
anchorItem = this._tableEntityListViewModel.items()[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (anchorItem) {
|
|
||||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
|
||||||
var elementIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
|
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
|
|
||||||
);
|
|
||||||
var anchorIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
|
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(anchorItem.RowKey._)
|
|
||||||
);
|
|
||||||
|
|
||||||
var startIndex = Math.min(elementIndex, anchorIndex);
|
|
||||||
var endIndex = Math.max(elementIndex, anchorIndex);
|
|
||||||
|
|
||||||
this._tableEntityListViewModel.clearSelection();
|
|
||||||
ko.utils.arrayPushAll<Entities.ITableEntity>(
|
|
||||||
this._tableEntityListViewModel.selected,
|
|
||||||
this._tableEntityListViewModel.getItemsFromAllPagesWithinRange(startIndex, endIndex + 1)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private applyContextMenuSelection($elem: JQuery) {
|
|
||||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!this._tableEntityListViewModel.isItemSelected(
|
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
if (this._tableEntityListViewModel.selected().length) {
|
|
||||||
this._tableEntityListViewModel.clearSelection();
|
|
||||||
}
|
|
||||||
this.addToSelection(entityIdentity.RowKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private addToSelection(rowKey: string) {
|
|
||||||
var selectedEntity: Entities.ITableEntity = this._tableEntityListViewModel.getItemFromCurrentPage(
|
|
||||||
this._tableEntityListViewModel.getTableEntityKeys(rowKey)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selectedEntity != null) {
|
|
||||||
this._tableEntityListViewModel.selected.push(selectedEntity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Selecting first row if the selection is empty.
|
|
||||||
public selectFirstIfNeeded(): void {
|
|
||||||
var koSelected: ko.ObservableArray<Entities.ITableEntity> = this._tableEntityListViewModel
|
|
||||||
? this._tableEntityListViewModel.selected
|
|
||||||
: null;
|
|
||||||
var koEntities: ko.ObservableArray<Entities.ITableEntity> = this._tableEntityListViewModel
|
|
||||||
? this._tableEntityListViewModel.items
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (!koSelected().length && koEntities().length) {
|
|
||||||
var firstEntity: Entities.ITableEntity = koEntities()[0];
|
|
||||||
|
|
||||||
// Clear last selection: lastSelectedItem and lastSelectedAnchorItem
|
|
||||||
this._tableEntityListViewModel.clearLastSelected();
|
|
||||||
|
|
||||||
this.addToSelection(firstEntity.RowKey._);
|
|
||||||
|
|
||||||
// Update last selection
|
|
||||||
this._tableEntityListViewModel.lastSelectedItem = firstEntity;
|
|
||||||
|
|
||||||
// Finally, make sure first row is visible
|
|
||||||
DataTableOperations.scrollToTopIfNeeded();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bind() {
|
|
||||||
this.dataTable.on("click", "tr", this.click);
|
|
||||||
this.dataTable.on("dblclick", "tr", this.doubleClick);
|
|
||||||
this.dataTable.on("keydown", "td", this.keyDown);
|
|
||||||
this.dataTable.on("keyup", "td", this.keyUp);
|
|
||||||
|
|
||||||
// Keyboard navigation - selecting first row if the selection is empty when the table gains focus.
|
|
||||||
this.dataTable.on("focus", () => {
|
|
||||||
this.selectFirstIfNeeded();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bind drag & drop behavior
|
|
||||||
$("body").on("drop", this.itemDropped);
|
|
||||||
}
|
|
||||||
|
|
||||||
public focusTable(): void {
|
|
||||||
this.dataTable.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
import _ from "underscore";
|
|
||||||
import Q from "q";
|
|
||||||
|
|
||||||
import * as Entities from "../Entities";
|
|
||||||
import * as QueryBuilderConstants from "../Constants";
|
|
||||||
import * as Utilities from "../Utilities";
|
|
||||||
|
|
||||||
export function getRowSelector(selectorSchema: Entities.IProperty[]): string {
|
|
||||||
var selector: string = "";
|
|
||||||
selectorSchema &&
|
|
||||||
selectorSchema.forEach((p: Entities.IProperty) => {
|
|
||||||
selector += "[" + p.key + '="' + Utilities.jQuerySelectorEscape(p.value) + '"]';
|
|
||||||
});
|
|
||||||
return QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector + selector;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isRowVisible(dataTableScrollBodyQuery: JQuery, element: HTMLElement): boolean {
|
|
||||||
var isVisible = false;
|
|
||||||
|
|
||||||
if (dataTableScrollBodyQuery.length && element) {
|
|
||||||
var elementRect: ClientRect = element.getBoundingClientRect(),
|
|
||||||
dataTableScrollBodyRect: ClientRect = dataTableScrollBodyQuery.get(0).getBoundingClientRect();
|
|
||||||
|
|
||||||
isVisible = elementRect.bottom <= dataTableScrollBodyRect.bottom && dataTableScrollBodyRect.top <= elementRect.top;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isVisible;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function scrollToRowIfNeeded(dataTableRows: JQuery, currentIndex: number, isScrollUp: boolean): void {
|
|
||||||
if (dataTableRows.length) {
|
|
||||||
var dataTableScrollBodyQuery: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector),
|
|
||||||
selectedRowElement: HTMLElement = dataTableRows.get(currentIndex);
|
|
||||||
|
|
||||||
if (dataTableScrollBodyQuery.length && selectedRowElement) {
|
|
||||||
var isVisible: boolean = isRowVisible(dataTableScrollBodyQuery, selectedRowElement);
|
|
||||||
|
|
||||||
if (!isVisible) {
|
|
||||||
var selectedRowQuery: JQuery = $(selectedRowElement),
|
|
||||||
scrollPosition: number = dataTableScrollBodyQuery.scrollTop(),
|
|
||||||
selectedElementPosition: number = selectedRowQuery.position().top,
|
|
||||||
newScrollPosition: number = 0;
|
|
||||||
|
|
||||||
if (isScrollUp) {
|
|
||||||
newScrollPosition = scrollPosition + selectedElementPosition;
|
|
||||||
} else {
|
|
||||||
newScrollPosition =
|
|
||||||
scrollPosition + (selectedElementPosition + selectedRowQuery.height() - dataTableScrollBodyQuery.height());
|
|
||||||
}
|
|
||||||
|
|
||||||
dataTableScrollBodyQuery.scrollTop(newScrollPosition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function scrollToTopIfNeeded(): void {
|
|
||||||
var $dataTableRows: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector),
|
|
||||||
$dataTableScrollBody: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector);
|
|
||||||
|
|
||||||
if ($dataTableRows.length && $dataTableScrollBody.length) {
|
|
||||||
$dataTableScrollBody.scrollTop(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setPaginationButtonEventHandlers(): void {
|
|
||||||
$(QueryBuilderConstants.htmlSelectors.dataTablePaginationButtonSelector)
|
|
||||||
.on("mousedown", (event: JQueryEventObject) => {
|
|
||||||
// Prevents the table contents from briefly jumping when clicking on "Load more"
|
|
||||||
event.preventDefault();
|
|
||||||
})
|
|
||||||
.attr("role", "button");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function filterColumns(table: DataTables.DataTable, settings: boolean[]): void {
|
|
||||||
settings &&
|
|
||||||
settings.forEach((value: boolean, index: number) => {
|
|
||||||
table.column(index).visible(value, false);
|
|
||||||
});
|
|
||||||
table.columns.adjust().draw(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reorder columns based on current order.
|
|
||||||
* If no current order is specified, reorder the columns based on intial order.
|
|
||||||
*/
|
|
||||||
export function reorderColumns(
|
|
||||||
table: DataTables.DataTable,
|
|
||||||
targetOrder: number[],
|
|
||||||
currentOrder?: number[]
|
|
||||||
): Q.Promise<any> {
|
|
||||||
var columnsCount: number = targetOrder.length;
|
|
||||||
var isCurrentOrderPassedIn: boolean = !!currentOrder;
|
|
||||||
if (!isCurrentOrderPassedIn) {
|
|
||||||
currentOrder = getInitialOrder(columnsCount);
|
|
||||||
}
|
|
||||||
var isSameOrder: boolean = Utilities.isEqual(currentOrder, targetOrder);
|
|
||||||
|
|
||||||
// if the targetOrder is the same as current order, do nothing.
|
|
||||||
if (!isSameOrder) {
|
|
||||||
// Otherwise, calculate the transformation order.
|
|
||||||
// If current order not specified, then it'll be set to initial order,
|
|
||||||
// i.e., either no reorder happened before or reordering to its initial order,
|
|
||||||
// Then the transformation order will be the same as target order.
|
|
||||||
// If current order is specified, then a transformation order is calculated.
|
|
||||||
// Refer to calculateTransformationOrder for details about transformation order.
|
|
||||||
var transformationOrder: number[] = isCurrentOrderPassedIn
|
|
||||||
? calculateTransformationOrder(currentOrder, targetOrder)
|
|
||||||
: targetOrder;
|
|
||||||
try {
|
|
||||||
$.fn.dataTable.ColReorder(table).fnOrder(transformationOrder);
|
|
||||||
} catch (err) {
|
|
||||||
return Q.reject(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Q.resolve(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resetColumns(table: DataTables.DataTable): void {
|
|
||||||
$.fn.dataTable.ColReorder(table).fnReset();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A table's initial order is described in the form of a natural ascending order.
|
|
||||||
* E.g., for a table with 9 columns, the initial order will be: [0, 1, 2, 3, 4, 5, 6, 7, 8]
|
|
||||||
*/
|
|
||||||
export function getInitialOrder(columnsCount: number): number[] {
|
|
||||||
return _.range(columnsCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current table's column order which is described based on initial table. E.g.,
|
|
||||||
* Initial order: I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8}
|
|
||||||
* Current order: C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8}
|
|
||||||
*/
|
|
||||||
export function getCurrentOrder(table: DataTables.DataTable): number[] {
|
|
||||||
return $.fn.dataTable.ColReorder(table).fnOrder();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Switch the index and value for each element of an array. e.g.,
|
|
||||||
* InputArray: [0, 1, 2, 6, 7, 3, 4, 5, 8]
|
|
||||||
* Result: [0, 1, 2, 5, 6, 7, 3, 4, 8]
|
|
||||||
*/
|
|
||||||
export function invertIndexValues(inputArray: number[]): number[] {
|
|
||||||
var invertedArray: number[] = [];
|
|
||||||
if (inputArray) {
|
|
||||||
inputArray.forEach((value: number, index: number) => {
|
|
||||||
invertedArray[inputArray[index]] = index;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return invertedArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DataTable fnOrder API is based on the current table. So we need to map the order targeting original table to targeting current table.
|
|
||||||
* An detailed example for this. Assume the table has 9 columns.
|
|
||||||
* Initial order (order of the initial table): I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8}
|
|
||||||
* Current order (order of the current table): C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8}
|
|
||||||
* Target order (order of the targeting table): T = [0, 1, 2, 5, 6, 7, 8, 3, 4] <----> {prop0, prop1, prop2, prop5, prop6, prop7, prop8, prop3, prop4}
|
|
||||||
* Transformation order: an order passed to fnOrder API that transforms table from current order to target order.
|
|
||||||
* When the table is constructed, it has the intial order. After an reordering with current order array, now the table is shown in current order, e.g.,
|
|
||||||
* column 3 in the current table is actually column C[3]=6 in the intial table, both indicate the column with header prop6.
|
|
||||||
* Now we want to continue to do another reorder to make the target table in the target order. Directly invoking API with the new order won't work as
|
|
||||||
* the API only do reorder based on the current table like the first time we invoke the API. So an order based on the current table needs to be calulated.
|
|
||||||
* Here is an example of how to calculate the transformation order:
|
|
||||||
* In target table, column 3 should be column T[3]=5 in the intial table with header prop5, while in current table, column with header prop5 is column 7 as C[7]=5.
|
|
||||||
* As a result, in transformation order, column 3 in the target table should be column 7 in the current table, Trans[3] = 7. In the same manner, we can get the
|
|
||||||
* transformation order: Trans = [0, 1, 2, 7, 3, 4, 8, 5, 6]
|
|
||||||
*/
|
|
||||||
export function calculateTransformationOrder(currentOrder: number[], targetOrder: number[]): number[] {
|
|
||||||
var transformationOrder: number[] = [];
|
|
||||||
if (currentOrder && targetOrder && currentOrder.length === targetOrder.length) {
|
|
||||||
var invertedCurrentOrder: number[] = invertIndexValues(currentOrder);
|
|
||||||
transformationOrder = targetOrder.map((value: number) => invertedCurrentOrder[value]);
|
|
||||||
}
|
|
||||||
return transformationOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDataTableHeaders(table: DataTables.DataTable): string[] {
|
|
||||||
var columns: DataTables.ColumnsMethods = table.columns();
|
|
||||||
var headers: string[] = [];
|
|
||||||
if (columns) {
|
|
||||||
// table.columns() return ColumnsMethods which is an array of arrays
|
|
||||||
var columnIndexes: number[] = (<any>columns)[0];
|
|
||||||
if (columnIndexes) {
|
|
||||||
headers = columnIndexes.map((value: number) => $(table.columns(value).header()).html());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
@@ -123,10 +123,3 @@ export function checkForDefaultHeader(headers: string[]): boolean {
|
|||||||
export function forceRecalculateTableSize(): void {
|
export function forceRecalculateTableSize(): void {
|
||||||
$("body").trigger("resize");
|
$("body").trigger("resize");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Turns off the spinning progress indicator on the data table.
|
|
||||||
*/
|
|
||||||
export function turnOffProgressIndicator(): void {
|
|
||||||
$("div.dataTables_processing").hide();
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
|
import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
|
|
||||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
|
||||||
import CacheBase from "./CacheBase";
|
|
||||||
import * as CommonConstants from "../../../Common/Constants";
|
import * as CommonConstants from "../../../Common/Constants";
|
||||||
import * as Constants from "../Constants";
|
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as Entities from "../Entities";
|
|
||||||
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
|
import NewQueryTablesTab from "../../Tabs/QueryTablesTab/QueryTablesTab";
|
||||||
|
import * as Entities from "../Entities";
|
||||||
|
import CacheBase from "./CacheBase";
|
||||||
|
|
||||||
// This is the format of the data we will have to pass to Datatable render callback,
|
// This is the format of the data we will have to pass to Datatable render callback,
|
||||||
// and property names are defined by Datatable as well.
|
// and property names are defined by Datatable as well.
|
||||||
@@ -47,19 +45,11 @@ abstract class DataTableViewModel {
|
|||||||
private pendingRedraw = false;
|
private pendingRedraw = false;
|
||||||
private lastRedrawTime = new Date().getTime();
|
private lastRedrawTime = new Date().getTime();
|
||||||
|
|
||||||
private dataTableOperationManager: IDataTableOperation;
|
public queryTablesTab: NewQueryTablesTab;
|
||||||
|
|
||||||
public queryTablesTab: QueryTablesTab;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.items([]);
|
this.items([]);
|
||||||
this.selected([]);
|
this.selected([]);
|
||||||
// Late bound
|
|
||||||
this.dataTableOperationManager = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bind(dataTableOperationManager: IDataTableOperation): void {
|
|
||||||
this.dataTableOperationManager = dataTableOperationManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public clearLastSelected(): void {
|
public clearLastSelected(): void {
|
||||||
@@ -101,10 +91,6 @@ abstract class DataTableViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public focusDataTable(): void {
|
|
||||||
this.dataTableOperationManager.focusTable();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getItemFromSelectedItems(itemKeys: Entities.IProperty[]): Entities.ITableEntity {
|
public getItemFromSelectedItems(itemKeys: Entities.IProperty[]): Entities.ITableEntity {
|
||||||
return _.find(this.selected(), (item: Entities.ITableEntity) => {
|
return _.find(this.selected(), (item: Entities.ITableEntity) => {
|
||||||
return this.matchesKeys(item, itemKeys);
|
return this.matchesKeys(item, itemKeys);
|
||||||
@@ -170,35 +156,12 @@ abstract class DataTableViewModel {
|
|||||||
this.cache.sortOrder = sortOrder;
|
this.cache.sortOrder = sortOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected renderPage(
|
protected renderPage(startIndex: number, pageSize: number) {
|
||||||
renderCallBack: any,
|
|
||||||
draw: number,
|
|
||||||
startIndex: number,
|
|
||||||
pageSize: number,
|
|
||||||
oSettings: any,
|
|
||||||
postRenderTasks: (startIndex: number, pageSize: number) => Promise<void> = null
|
|
||||||
) {
|
|
||||||
this.updatePaginationControls(oSettings);
|
|
||||||
|
|
||||||
// pageSize < 0 means to show all data
|
|
||||||
var endIndex = pageSize < 0 ? this.cache.length : startIndex + pageSize;
|
var endIndex = pageSize < 0 ? this.cache.length : startIndex + pageSize;
|
||||||
var renderData = this.cache.data.slice(startIndex, endIndex);
|
var renderData = this.cache.data.slice(startIndex, endIndex);
|
||||||
|
|
||||||
this.items(renderData);
|
this.items(renderData);
|
||||||
|
|
||||||
var render: IDataTableRenderData = {
|
|
||||||
draw: draw,
|
|
||||||
aaData: renderData,
|
|
||||||
recordsTotal: this.cache.length,
|
|
||||||
recordsFiltered: this.cache.length,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!!postRenderTasks) {
|
|
||||||
postRenderTasks(startIndex, pageSize).then(() => {
|
|
||||||
this.table.rows().invalidate();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
renderCallBack(render);
|
|
||||||
if (this.queryTablesTab.onLoadStartKey != null && this.queryTablesTab.onLoadStartKey != undefined) {
|
if (this.queryTablesTab.onLoadStartKey != null && this.queryTablesTab.onLoadStartKey != undefined) {
|
||||||
TelemetryProcessor.traceSuccess(
|
TelemetryProcessor.traceSuccess(
|
||||||
Action.Tab,
|
Action.Tab,
|
||||||
@@ -217,16 +180,6 @@ abstract class DataTableViewModel {
|
|||||||
protected matchesKeys(item: Entities.ITableEntity, itemKeys: Entities.IProperty[]): boolean {
|
protected matchesKeys(item: Entities.ITableEntity, itemKeys: Entities.IProperty[]): boolean {
|
||||||
return itemKeys.every((property: Entities.IProperty) => {
|
return itemKeys.every((property: Entities.IProperty) => {
|
||||||
var itemValue = item[property.key];
|
var itemValue = item[property.key];
|
||||||
|
|
||||||
// if (itemValue && property.subkey) {
|
|
||||||
// itemValue = itemValue._[property.subkey];
|
|
||||||
// if (!itemValue) {
|
|
||||||
// itemValue = "";
|
|
||||||
// }
|
|
||||||
// } else if (property.subkey) {
|
|
||||||
// itemValue = "";
|
|
||||||
// }
|
|
||||||
|
|
||||||
return this.stringCompare(itemValue._, property.value);
|
return this.stringCompare(itemValue._, property.value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -238,27 +191,6 @@ abstract class DataTableViewModel {
|
|||||||
protected stringCompare(s1: string, s2: string): boolean {
|
protected stringCompare(s1: string, s2: string): boolean {
|
||||||
return s1 === s2;
|
return s1 === s2;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updatePaginationControls(oSettings: any) {
|
|
||||||
var pageInfo = this.table.page.info();
|
|
||||||
var pageSize = pageInfo.length;
|
|
||||||
var paginateElement = $(oSettings.nTableWrapper).find(Constants.htmlSelectors.paginateSelector);
|
|
||||||
|
|
||||||
if (this.allDownloaded) {
|
|
||||||
if (this.cache.length <= pageSize) {
|
|
||||||
// Hide pagination controls if everything fits in one page!.
|
|
||||||
paginateElement.hide();
|
|
||||||
} else {
|
|
||||||
// Enable pagination controls.
|
|
||||||
paginateElement.show();
|
|
||||||
oSettings.oLanguage.oPaginate.sLast = DataTableViewModel.lastPageLabel;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Enable pagination controls and show load more button.
|
|
||||||
paginateElement.show();
|
|
||||||
oSettings.oLanguage.oPaginate.sLast = DataTableViewModel.loadMoreLabel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IDataTableOperation {
|
interface IDataTableOperation {
|
||||||
|
|||||||
@@ -94,8 +94,4 @@ export default class TableCommands {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public resetColumns(viewModel: TableEntityListViewModel): void {
|
|
||||||
viewModel.reloadTable();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
|||||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
import NewQueryTablesTab from "../../Tabs/QueryTablesTab/QueryTablesTab";
|
||||||
import * as Constants from "../Constants";
|
import * as Constants from "../Constants";
|
||||||
import { getQuotedCqlIdentifier } from "../CqlUtilities";
|
import { getQuotedCqlIdentifier } from "../CqlUtilities";
|
||||||
import * as Entities from "../Entities";
|
import * as Entities from "../Entities";
|
||||||
@@ -101,7 +101,8 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
public useSetting: boolean = true;
|
public useSetting: boolean = true;
|
||||||
|
|
||||||
//public tableExplorerContext: TableExplorerContext;
|
//public tableExplorerContext: TableExplorerContext;
|
||||||
public notifyColumnChanges: (enablePrompt: boolean, queryTablesTab: QueryTablesTab) => void;
|
public notifyColumnChanges: (enablePrompt: boolean, queryTablesTab: NewQueryTablesTab) => void;
|
||||||
|
|
||||||
public tablePageStartIndex: number;
|
public tablePageStartIndex: number;
|
||||||
public tableQuery: Entities.ITableQuery = {};
|
public tableQuery: Entities.ITableQuery = {};
|
||||||
public cqlQuery: ko.Observable<string>;
|
public cqlQuery: ko.Observable<string>;
|
||||||
@@ -112,7 +113,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
public queryErrorMessage: ko.Observable<string>;
|
public queryErrorMessage: ko.Observable<string>;
|
||||||
public id: string;
|
public id: string;
|
||||||
|
|
||||||
constructor(tableCommands: TableCommands, queryTablesTab: QueryTablesTab) {
|
constructor(tableCommands: TableCommands, queryTablesTab: NewQueryTablesTab) {
|
||||||
super();
|
super();
|
||||||
this.cache = new TableEntityCache();
|
this.cache = new TableEntityCache();
|
||||||
this.queryErrorMessage = ko.observable<string>();
|
this.queryErrorMessage = ko.observable<string>();
|
||||||
@@ -131,24 +132,8 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
return [{ key: Constants.EntityKeyNames.RowKey, value: rowKey }];
|
return [{ key: Constants.EntityKeyNames.RowKey, value: rowKey }];
|
||||||
}
|
}
|
||||||
|
|
||||||
public reloadTable(useSetting: boolean = true, resetHeaders: boolean = true): DataTables.DataTable {
|
|
||||||
this.clearCache();
|
|
||||||
this.clearSelection();
|
|
||||||
this.isCancelled = false;
|
|
||||||
|
|
||||||
this.useSetting = useSetting;
|
|
||||||
if (resetHeaders) {
|
|
||||||
this.updateHeaders([Constants.defaultHeader]);
|
|
||||||
}
|
|
||||||
return this.table.ajax.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
public updateHeaders(newHeaders: string[], notifyColumnChanges: boolean = false, enablePrompt: boolean = true): void {
|
public updateHeaders(newHeaders: string[], notifyColumnChanges: boolean = false, enablePrompt: boolean = true): void {
|
||||||
this.headers = newHeaders;
|
this.headers = newHeaders;
|
||||||
if (notifyColumnChanges) {
|
|
||||||
this.clearSelection();
|
|
||||||
this.notifyColumnChanges(enablePrompt, this.queryTablesTab);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -158,40 +143,21 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
* fnCallback - is the render callback with data to render.
|
* fnCallback - is the render callback with data to render.
|
||||||
* oSetting: current settings used for table initialization.
|
* oSetting: current settings used for table initialization.
|
||||||
*/
|
*/
|
||||||
public renderNextPageAndupdateCache(sSource: any, aoData: any, fnCallback: any, oSettings: any) {
|
|
||||||
|
public async renderNextPageAndupdateCache(): Promise<Entities.ITableEntity[]> {
|
||||||
var tablePageSize: number;
|
var tablePageSize: number;
|
||||||
var draw: number;
|
|
||||||
var prefetchNeeded = true;
|
var prefetchNeeded = true;
|
||||||
var columnSortOrder: any;
|
|
||||||
// Threshold(pages) for triggering cache prefetch.
|
// Threshold(pages) for triggering cache prefetch.
|
||||||
// If number remaining pages in cache falls below prefetchThreshold prefetch will be triggered.
|
// If number remaining pages in cache falls below prefetchThreshold prefetch will be triggered.
|
||||||
var prefetchThreshold = 10;
|
var prefetchThreshold = 10;
|
||||||
var tableQuery = this.tableQuery;
|
var tableQuery = this.tableQuery;
|
||||||
|
|
||||||
for (var index in aoData) {
|
|
||||||
var data = aoData[index];
|
|
||||||
if (data.name === "length") {
|
|
||||||
tablePageSize = data.value;
|
|
||||||
}
|
|
||||||
if (data.name === "start") {
|
|
||||||
this.tablePageStartIndex = data.value;
|
|
||||||
}
|
|
||||||
if (data.name === "draw") {
|
|
||||||
draw = data.value;
|
|
||||||
}
|
|
||||||
if (data.name === "order") {
|
|
||||||
columnSortOrder = data.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Try cache if valid.
|
// Try cache if valid.
|
||||||
if (this.isCacheValid(tableQuery)) {
|
if (this.isCacheValid(tableQuery)) {
|
||||||
// Check if prefetch needed.
|
// Check if prefetch needed.
|
||||||
if (this.tablePageStartIndex + tablePageSize <= this.cache.length || this.allDownloaded) {
|
if (this.tablePageStartIndex + tablePageSize <= this.cache.length || this.allDownloaded) {
|
||||||
prefetchNeeded = false;
|
prefetchNeeded = false;
|
||||||
if (columnSortOrder && (!this.cache.sortOrder || !_.isEqual(this.cache.sortOrder, columnSortOrder))) {
|
this.tablePageStartIndex = 0;
|
||||||
this.sortColumns(columnSortOrder, oSettings);
|
this.renderPage(this.tablePageStartIndex, this.cache.length);
|
||||||
}
|
|
||||||
this.renderPage(fnCallback, draw, this.tablePageStartIndex, tablePageSize, oSettings);
|
|
||||||
if (
|
if (
|
||||||
!this.allDownloaded &&
|
!this.allDownloaded &&
|
||||||
this.tablePageStartIndex > 0 && // This is a case now that we can hit this as we re-construct table when we update column
|
this.tablePageStartIndex > 0 && // This is a case now that we can hit this as we re-construct table when we update column
|
||||||
@@ -208,41 +174,21 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
|
|
||||||
if (prefetchNeeded) {
|
if (prefetchNeeded) {
|
||||||
var downloadSize = tableQuery.top || this.downloadSize;
|
var downloadSize = tableQuery.top || this.downloadSize;
|
||||||
this.prefetchAndRender(
|
return await this.prefetchAndRender(tableQuery, 0, tablePageSize, downloadSize);
|
||||||
tableQuery,
|
} else {
|
||||||
this.tablePageStartIndex,
|
return this.cache.data;
|
||||||
tablePageSize,
|
|
||||||
downloadSize,
|
|
||||||
draw,
|
|
||||||
fnCallback,
|
|
||||||
oSettings,
|
|
||||||
columnSortOrder
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public addEntityToCache(entity: Entities.ITableEntity): Q.Promise<any> {
|
public addEntityToCache(entity: Entities.ITableEntity): Q.Promise<any> {
|
||||||
// Delay the add operation if we are fetching data from server, so as to avoid race condition.
|
// Delay the add operation if we are fetching data from server, so as to avoid race condition.
|
||||||
if (this.cache.serverCallInProgress) {
|
if (this.cache.serverCallInProgress) {
|
||||||
return Utilities.delay(this.pollingInterval).then(() => {
|
Utilities.delay(this.pollingInterval).then(() => {
|
||||||
return this.updateCachedEntity(entity);
|
this.updateCachedEntity(entity);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the first item which is greater than the added entity.
|
this.cache.data.splice(this.cache.length, 0, entity);
|
||||||
var oSettings: any = (<any>this.table).context[0];
|
|
||||||
var index: number = _.findIndex(this.cache.data, (data: any) => {
|
|
||||||
return this.dataComparer(data, entity, this.cache.sortOrder, oSettings) > 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
// If no such item, then insert at last.
|
|
||||||
var insertIndex: number = Utilities.ensureBetweenBounds(
|
|
||||||
index < 0 ? this.cache.length : index,
|
|
||||||
0,
|
|
||||||
this.cache.length
|
|
||||||
);
|
|
||||||
|
|
||||||
this.cache.data.splice(insertIndex, 0, entity);
|
|
||||||
|
|
||||||
// Finally, select newly added entity
|
// Finally, select newly added entity
|
||||||
this.clearSelection();
|
this.clearSelection();
|
||||||
@@ -254,8 +200,8 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
public updateCachedEntity(entity: Entities.ITableEntity): Q.Promise<any> {
|
public updateCachedEntity(entity: Entities.ITableEntity): Q.Promise<any> {
|
||||||
// Delay the add operation if we are fetching data from server, so as to avoid race condition.
|
// Delay the add operation if we are fetching data from server, so as to avoid race condition.
|
||||||
if (this.cache.serverCallInProgress) {
|
if (this.cache.serverCallInProgress) {
|
||||||
return Utilities.delay(this.pollingInterval).then(() => {
|
Utilities.delay(this.pollingInterval).then(() => {
|
||||||
return this.updateCachedEntity(entity);
|
this.updateCachedEntity(entity);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
var oldEntityIndex: number = _.findIndex(
|
var oldEntityIndex: number = _.findIndex(
|
||||||
@@ -275,8 +221,8 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
|
|
||||||
// Delay the remove operation if we are fetching data from server, so as to avoid race condition.
|
// Delay the remove operation if we are fetching data from server, so as to avoid race condition.
|
||||||
if (this.cache.serverCallInProgress) {
|
if (this.cache.serverCallInProgress) {
|
||||||
return Utilities.delay(this.pollingInterval).then(() => {
|
Utilities.delay(this.pollingInterval).then(() => {
|
||||||
return this.removeEntitiesFromCache(entities);
|
this.removeEntitiesFromCache(entities);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,14 +238,6 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
});
|
});
|
||||||
this.clearSelection();
|
this.clearSelection();
|
||||||
|
|
||||||
// Show last available page if there is not enough data
|
|
||||||
var pageInfo = this.table.page.info();
|
|
||||||
if (this.cache.length <= pageInfo.start) {
|
|
||||||
var availablePages = Math.ceil(this.cache.length / pageInfo.length);
|
|
||||||
var pageToShow = availablePages > 0 ? availablePages - 1 : 0;
|
|
||||||
this.table.page(pageToShow);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Q.resolve(null);
|
return Q.resolve(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,86 +330,75 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private prefetchAndRender(
|
private async prefetchAndRender(
|
||||||
tableQuery: Entities.ITableQuery,
|
tableQuery: Entities.ITableQuery,
|
||||||
tablePageStartIndex: number,
|
tablePageStartIndex: number,
|
||||||
tablePageSize: number,
|
tablePageSize: number,
|
||||||
downloadSize: number,
|
downloadSize: number
|
||||||
draw: number,
|
): Promise<Entities.ITableEntity[]> {
|
||||||
renderCallBack: Function,
|
|
||||||
oSettings: any,
|
|
||||||
columnSortOrder: any
|
|
||||||
): void {
|
|
||||||
this.queryErrorMessage(null);
|
this.queryErrorMessage(null);
|
||||||
if (this.cache.serverCallInProgress) {
|
if (this.cache.serverCallInProgress) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
this.prefetchData(tableQuery, downloadSize, /* currentRetry */ 0)
|
try {
|
||||||
.then((result: IListTableEntitiesSegmentedResult) => {
|
const result = await this.prefetchData(tableQuery, downloadSize, /* currentRetry */ 0);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return;
|
return undefined;
|
||||||
|
}
|
||||||
|
// Cache is assigned using prefetchData
|
||||||
|
var entities = this.cache.data;
|
||||||
|
if (userContext.apiType === "Cassandra" && DataTableUtilities.checkForDefaultHeader(this.headers)) {
|
||||||
|
(<CassandraAPIDataClient>this.queryTablesTab.container.tableDataClient)
|
||||||
|
.getTableSchema(this.queryTablesTab.collection)
|
||||||
|
.then((headers: CassandraTableKey[]) => {
|
||||||
|
this.updateHeaders(
|
||||||
|
headers.map((header) => header.property),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
var selectedHeadersUnion: string[] = DataTableUtilities.getPropertyIntersectionFromTableEntities(
|
||||||
|
entities,
|
||||||
|
userContext.apiType === "Cassandra"
|
||||||
|
);
|
||||||
|
var newHeaders: string[] = _.difference(selectedHeadersUnion, this.headers);
|
||||||
|
if (newHeaders.length > 0) {
|
||||||
|
// Any new columns found will be added into headers array, which will trigger a re-render of the DataTable.
|
||||||
|
// So there is no need to call it here.
|
||||||
|
this.updateHeaders(newHeaders, /* notifyColumnChanges */ true);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
this.renderPage(tablePageStartIndex, entities.length);
|
||||||
|
|
||||||
var entities = this.cache.data;
|
return result;
|
||||||
if (userContext.apiType === "Cassandra" && DataTableUtilities.checkForDefaultHeader(this.headers)) {
|
} catch (error) {
|
||||||
(<CassandraAPIDataClient>this.queryTablesTab.container.tableDataClient)
|
const parsedErrors = parseError(error);
|
||||||
.getTableSchema(this.queryTablesTab.collection)
|
var errors = parsedErrors.map((error) => {
|
||||||
.then((headers: CassandraTableKey[]) => {
|
return <ViewModels.QueryError>{
|
||||||
this.updateHeaders(
|
message: error.message,
|
||||||
headers.map((header) => header.property),
|
start: error.location ? error.location.start : undefined,
|
||||||
true
|
end: error.location ? error.location.end : undefined,
|
||||||
);
|
code: error.code,
|
||||||
});
|
severity: error.severity,
|
||||||
} else {
|
};
|
||||||
var selectedHeadersUnion: string[] = DataTableUtilities.getPropertyIntersectionFromTableEntities(
|
|
||||||
entities,
|
|
||||||
userContext.apiType === "Cassandra"
|
|
||||||
);
|
|
||||||
var newHeaders: string[] = _.difference(selectedHeadersUnion, this.headers);
|
|
||||||
if (newHeaders.length > 0) {
|
|
||||||
// Any new columns found will be added into headers array, which will trigger a re-render of the DataTable.
|
|
||||||
// So there is no need to call it here.
|
|
||||||
this.updateHeaders(newHeaders, /* notifyColumnChanges */ true);
|
|
||||||
} else {
|
|
||||||
if (columnSortOrder) {
|
|
||||||
this.sortColumns(columnSortOrder, oSettings);
|
|
||||||
}
|
|
||||||
this.renderPage(renderCallBack, draw, tablePageStartIndex, tablePageSize, oSettings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.ExceedMaximumRetries) {
|
|
||||||
var message: string = "We are having trouble getting your data. Please try again."; // localize
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error: any) => {
|
|
||||||
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,
|
|
||||||
{
|
|
||||||
databaseName: this.queryTablesTab.collection.databaseId,
|
|
||||||
collectionName: this.queryTablesTab.collection.id(),
|
|
||||||
dataExplorerArea: Areas.Tab,
|
|
||||||
tabTitle: this.queryTablesTab.tabTitle(),
|
|
||||||
error: error,
|
|
||||||
},
|
|
||||||
this.queryTablesTab.onLoadStartKey
|
|
||||||
);
|
|
||||||
this.queryTablesTab.onLoadStartKey = null;
|
|
||||||
}
|
|
||||||
DataTableUtilities.turnOffProgressIndicator();
|
|
||||||
});
|
});
|
||||||
|
this.queryErrorMessage(errors[0].message);
|
||||||
|
if (this.queryTablesTab.onLoadStartKey != null && this.queryTablesTab.onLoadStartKey != undefined) {
|
||||||
|
TelemetryProcessor.traceFailure(
|
||||||
|
Action.Tab,
|
||||||
|
{
|
||||||
|
databaseName: this.queryTablesTab.collection.databaseId,
|
||||||
|
collectionName: this.queryTablesTab.collection.id(),
|
||||||
|
dataExplorerArea: Areas.Tab,
|
||||||
|
tabTitle: this.queryTablesTab.tabTitle(),
|
||||||
|
error: error,
|
||||||
|
},
|
||||||
|
this.queryTablesTab.onLoadStartKey
|
||||||
|
);
|
||||||
|
this.queryTablesTab.onLoadStartKey = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -485,51 +412,52 @@ 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.
|
* 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
|
* See Microsoft Azure API Documentation at: https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx
|
||||||
*/
|
*/
|
||||||
private prefetchData(
|
|
||||||
|
private async prefetchData(
|
||||||
tableQuery: Entities.ITableQuery,
|
tableQuery: Entities.ITableQuery,
|
||||||
downloadSize: number,
|
downloadSize: number,
|
||||||
currentRetry: number = 0
|
currentRetry: number = 0
|
||||||
): Q.Promise<any> {
|
): Promise<any> {
|
||||||
|
var entities: any;
|
||||||
if (!this.cache.serverCallInProgress) {
|
if (!this.cache.serverCallInProgress) {
|
||||||
this.cache.serverCallInProgress = true;
|
this.cache.serverCallInProgress = true;
|
||||||
this.allDownloaded = false;
|
this.allDownloaded = false;
|
||||||
this.lastPrefetchTime = new Date().getTime();
|
this.lastPrefetchTime = new Date().getTime();
|
||||||
var time = this.lastPrefetchTime;
|
var time = this.lastPrefetchTime;
|
||||||
|
|
||||||
var promise: Q.Promise<IListTableEntitiesSegmentedResult>;
|
try {
|
||||||
if (this._documentIterator && this.continuationToken) {
|
if (this._documentIterator && this.continuationToken) {
|
||||||
// TODO handle Cassandra case
|
// TODO handle Cassandra case
|
||||||
|
const fetchNext = await this._documentIterator.fetchNext();
|
||||||
promise = Q(this._documentIterator.fetchNext().then((response) => response.resources)).then(
|
let fetchNextEntities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(
|
||||||
(documents: any[]) => {
|
fetchNext.resources
|
||||||
let entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(documents);
|
);
|
||||||
let finalEntities: IListTableEntitiesSegmentedResult = <IListTableEntitiesSegmentedResult>{
|
let finalEntities: IListTableEntitiesSegmentedResult = <IListTableEntitiesSegmentedResult>{
|
||||||
Results: entities,
|
Results: fetchNextEntities,
|
||||||
ContinuationToken: this._documentIterator.hasMoreResults(),
|
ContinuationToken: this._documentIterator.hasMoreResults(),
|
||||||
};
|
};
|
||||||
return Q.resolve(finalEntities);
|
entities = finalEntities;
|
||||||
}
|
} else if (this.continuationToken && userContext.apiType === "Cassandra") {
|
||||||
);
|
entities = await this.queryTablesTab.container.tableDataClient.queryDocuments(
|
||||||
} else if (this.continuationToken && userContext.apiType === "Cassandra") {
|
|
||||||
promise = Q(
|
|
||||||
this.queryTablesTab.container.tableDataClient.queryDocuments(
|
|
||||||
this.queryTablesTab.collection,
|
this.queryTablesTab.collection,
|
||||||
this.cqlQuery(),
|
this.cqlQuery(),
|
||||||
true,
|
true,
|
||||||
this.continuationToken
|
this.continuationToken
|
||||||
)
|
);
|
||||||
);
|
} else {
|
||||||
} else {
|
let query = this.sqlQuery();
|
||||||
let query = this.sqlQuery();
|
if (userContext.apiType === "Cassandra") {
|
||||||
if (userContext.apiType === "Cassandra") {
|
query = this.cqlQuery();
|
||||||
query = this.cqlQuery();
|
}
|
||||||
|
entities = await this.queryTablesTab.container.tableDataClient.queryDocuments(
|
||||||
|
this.queryTablesTab.collection,
|
||||||
|
query,
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
promise = Q(
|
|
||||||
this.queryTablesTab.container.tableDataClient.queryDocuments(this.queryTablesTab.collection, query, true)
|
const result = entities;
|
||||||
);
|
if (result) {
|
||||||
}
|
|
||||||
return promise
|
|
||||||
.then((result: IListTableEntitiesSegmentedResult) => {
|
|
||||||
if (!this._documentIterator) {
|
if (!this._documentIterator) {
|
||||||
this._documentIterator = result.iterator;
|
this._documentIterator = result.iterator;
|
||||||
}
|
}
|
||||||
@@ -539,14 +467,14 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
// And as another service call is during process, we don't set serverCallInProgress to false here.
|
// And as another service call is during process, we don't set serverCallInProgress to false here.
|
||||||
// Thus, end the prefetch.
|
// Thus, end the prefetch.
|
||||||
if (this.lastPrefetchTime !== time) {
|
if (this.lastPrefetchTime !== time) {
|
||||||
return Q.resolve(null);
|
return Promise.resolve(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
var entities = result.Results;
|
var entities = result.Results;
|
||||||
actualDownloadSize = entities.length;
|
actualDownloadSize = entities.length;
|
||||||
|
|
||||||
// Queries can fetch no results and still return a continuation header. See prefetchAndRender() method.
|
// Queries can fetch no results and still return a continuation header. See prefetchAndRender() method.
|
||||||
this.continuationToken = this.isCancelled ? null : result.ContinuationToken;
|
this.continuationToken = this.isCancelled ? undefined : result.ContinuationToken;
|
||||||
|
|
||||||
if (!this.continuationToken) {
|
if (!this.continuationToken) {
|
||||||
this.allDownloaded = true;
|
this.allDownloaded = true;
|
||||||
@@ -568,30 +496,20 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
|||||||
this.allDownloaded = true;
|
this.allDownloaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// There are three possible results for a prefetch:
|
|
||||||
// 1. Continuation token is null or fetched items' size reaches predefined.
|
|
||||||
// 2. Continuation token is not null and fetched items' size hasn't reach predefined.
|
|
||||||
// 2.1 Retry times has reached predefined maximum.
|
|
||||||
// 2.2 Retry times hasn't reached predefined maximum.
|
|
||||||
// Correspondingly,
|
|
||||||
// For #1, end prefetch.
|
|
||||||
// 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) {
|
if (this.allDownloaded || nextDownloadSize === 0) {
|
||||||
return Q.resolve(result);
|
return Promise.resolve(this.cache.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentRetry >= TableEntityListViewModel._maximumNumberOfPrefetchRetries) {
|
if (currentRetry >= TableEntityListViewModel._maximumNumberOfPrefetchRetries) {
|
||||||
result.ExceedMaximumRetries = true;
|
result.ExceedMaximumRetries = true;
|
||||||
return Q.resolve(result);
|
return Promise.resolve(this.cache.data);
|
||||||
}
|
}
|
||||||
return this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1);
|
return this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1);
|
||||||
})
|
}
|
||||||
.catch((error: Error) => {
|
} catch (error) {
|
||||||
this.cache.serverCallInProgress = false;
|
this.cache.serverCallInProgress = false;
|
||||||
return Q.reject(error);
|
return Promise.reject(error);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import { KeyCodes } from "../../../Common/Constants";
|
import { KeyCodes } from "../../../Common/Constants";
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
|
import { IQueryTableRowsType } from "../../Tabs/QueryTablesTab/QueryTableTabUtils";
|
||||||
import * as Constants from "../Constants";
|
import * as Constants from "../Constants";
|
||||||
import { getQuotedCqlIdentifier } from "../CqlUtilities";
|
import { getQuotedCqlIdentifier } from "../CqlUtilities";
|
||||||
import * as DataTableUtilities from "../DataTable/DataTableUtilities";
|
import * as DataTableUtilities from "../DataTable/DataTableUtilities";
|
||||||
@@ -106,14 +107,14 @@ export default class QueryBuilderViewModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public setExample() {
|
public setExample(pk: string, rk: string) {
|
||||||
var example1 = new QueryClauseViewModel(
|
var example1 = new QueryClauseViewModel(
|
||||||
this,
|
this,
|
||||||
"",
|
"",
|
||||||
"PartitionKey",
|
"PartitionKey",
|
||||||
this.edmTypes()[0],
|
this.edmTypes()[0],
|
||||||
Constants.Operator.Equal,
|
Constants.Operator.Equal,
|
||||||
this.tableEntityListViewModel.items()[0].PartitionKey._,
|
pk,
|
||||||
false,
|
false,
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
@@ -127,7 +128,7 @@ export default class QueryBuilderViewModel {
|
|||||||
"RowKey",
|
"RowKey",
|
||||||
this.edmTypes()[0],
|
this.edmTypes()[0],
|
||||||
Constants.Operator.Equal,
|
Constants.Operator.Equal,
|
||||||
this.tableEntityListViewModel.items()[0].RowKey._,
|
rk,
|
||||||
true,
|
true,
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
@@ -139,40 +140,31 @@ export default class QueryBuilderViewModel {
|
|||||||
this.addClauseImpl(example2, 1);
|
this.addClauseImpl(example2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getODataFilterFromClauses = (): string => {
|
public getODataFilterFromClauses = (queryClauses: IQueryTableRowsType[]): string => {
|
||||||
var filterString: string = "";
|
var filterString: string = "";
|
||||||
var treeTraversal = (group: ClauseGroup): void => {
|
if (queryClauses != undefined) {
|
||||||
for (var i = 0; i < group.children.length; i++) {
|
for (var i = 0; i < queryClauses.length; i++) {
|
||||||
var currentItem = group.children[i];
|
var currentItem = queryClauses[i];
|
||||||
|
|
||||||
if (currentItem instanceof QueryClauseViewModel) {
|
this.timestampToValue(currentItem);
|
||||||
var clause = <QueryClauseViewModel>currentItem;
|
filterString = filterString.concat(
|
||||||
this.timestampToValue(clause);
|
this.constructODataClause(
|
||||||
filterString = filterString.concat(
|
filterString === "" ? "" : currentItem.selectedOperation,
|
||||||
this.constructODataClause(
|
this.generateLeftParentheses(currentItem),
|
||||||
filterString === "" ? "" : clause.and_or(),
|
currentItem.selectedField,
|
||||||
this.generateLeftParentheses(clause),
|
currentItem.selectedEntityType,
|
||||||
clause.field(),
|
currentItem.selectedOperator,
|
||||||
clause.type(),
|
currentItem.entityValue,
|
||||||
clause.operator(),
|
this.generateRightParentheses(currentItem)
|
||||||
clause.value(),
|
)
|
||||||
this.generateRightParentheses(clause)
|
);
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentItem instanceof ClauseGroup) {
|
|
||||||
treeTraversal(<ClauseGroup>currentItem);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
treeTraversal(this.queryClauses);
|
|
||||||
|
|
||||||
return filterString.trim();
|
return filterString.trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
public getSqlFilterFromClauses = (): string => {
|
public getSqlFilterFromClauses = (queryTableRows: IQueryTableRowsType[]): string => {
|
||||||
var filterString: string = "SELECT * FROM c";
|
var filterString: string = "SELECT * FROM c";
|
||||||
if (this._queryViewModel.selectText() && this._queryViewModel.selectText().length > 0) {
|
if (this._queryViewModel.selectText() && this._queryViewModel.selectText().length > 0) {
|
||||||
filterString = "SELECT";
|
filterString = "SELECT";
|
||||||
@@ -195,48 +187,37 @@ export default class QueryBuilderViewModel {
|
|||||||
});
|
});
|
||||||
filterString = filterString.concat(" FROM c");
|
filterString = filterString.concat(" FROM c");
|
||||||
}
|
}
|
||||||
if (this.queryClauses.children.length === 0) {
|
if (queryTableRows.length === 0) {
|
||||||
return filterString;
|
return filterString;
|
||||||
}
|
}
|
||||||
filterString = filterString.concat(" WHERE");
|
filterString = filterString.concat(" WHERE");
|
||||||
var first = true;
|
var first = true;
|
||||||
var treeTraversal = (group: ClauseGroup): void => {
|
for (var i = 0; i < queryTableRows.length; i++) {
|
||||||
for (var i = 0; i < group.children.length; i++) {
|
var currentItem = queryTableRows[i];
|
||||||
var currentItem = group.children[i];
|
|
||||||
|
|
||||||
if (currentItem instanceof QueryClauseViewModel) {
|
let timeStampValue: string = this.timestampToSqlValue(currentItem);
|
||||||
var clause = <QueryClauseViewModel>currentItem;
|
var value = currentItem.entityValue;
|
||||||
let timeStampValue: string = this.timestampToSqlValue(clause);
|
if (!currentItem.isValue) {
|
||||||
var value = clause.value();
|
value = timeStampValue;
|
||||||
if (!clause.isValue()) {
|
|
||||||
value = timeStampValue;
|
|
||||||
}
|
|
||||||
filterString = filterString.concat(
|
|
||||||
this.constructSqlClause(
|
|
||||||
first ? "" : clause.and_or(),
|
|
||||||
this.generateLeftParentheses(clause),
|
|
||||||
clause.field(),
|
|
||||||
clause.type(),
|
|
||||||
clause.operator(),
|
|
||||||
value,
|
|
||||||
this.generateRightParentheses(clause)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentItem instanceof ClauseGroup) {
|
|
||||||
treeTraversal(<ClauseGroup>currentItem);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
filterString = filterString.concat(
|
||||||
|
this.constructSqlClause(
|
||||||
treeTraversal(this.queryClauses);
|
first ? "" : currentItem.selectedOperation,
|
||||||
|
this.generateLeftParentheses(currentItem),
|
||||||
|
currentItem.selectedField,
|
||||||
|
currentItem.selectedEntityType,
|
||||||
|
currentItem.selectedOperator,
|
||||||
|
value,
|
||||||
|
this.generateRightParentheses(currentItem)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
return filterString.trim();
|
return filterString.trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
public getCqlFilterFromClauses = (): string => {
|
public getCqlFilterFromClauses = (queryTableRows: IQueryTableRowsType[]): string => {
|
||||||
const databaseId = this._queryViewModel.queryTablesTab.collection.databaseId;
|
const databaseId = this._queryViewModel.queryTablesTab.collection.databaseId;
|
||||||
const collectionId = this._queryViewModel.queryTablesTab.collection.id();
|
const collectionId = this._queryViewModel.queryTablesTab.collection.id();
|
||||||
const tableToQuery = `${getQuotedCqlIdentifier(databaseId)}.${getQuotedCqlIdentifier(collectionId)}`;
|
const tableToQuery = `${getQuotedCqlIdentifier(databaseId)}.${getQuotedCqlIdentifier(collectionId)}`;
|
||||||
@@ -251,43 +232,32 @@ export default class QueryBuilderViewModel {
|
|||||||
});
|
});
|
||||||
filterString = filterString.concat(` FROM ${tableToQuery}`);
|
filterString = filterString.concat(` FROM ${tableToQuery}`);
|
||||||
}
|
}
|
||||||
if (this.queryClauses.children.length === 0) {
|
if (queryTableRows === undefined || queryTableRows.length === 0) {
|
||||||
return filterString;
|
return filterString;
|
||||||
}
|
}
|
||||||
filterString = filterString.concat(" WHERE");
|
filterString = filterString.concat(" WHERE");
|
||||||
var first = true;
|
var first = true;
|
||||||
var treeTraversal = (group: ClauseGroup): void => {
|
for (var i = 0; i < queryTableRows.length; i++) {
|
||||||
for (var i = 0; i < group.children.length; i++) {
|
var currentItem = queryTableRows[i];
|
||||||
var currentItem = group.children[i];
|
|
||||||
|
|
||||||
if (currentItem instanceof QueryClauseViewModel) {
|
let timeStampValue: string = this.timestampToSqlValue(currentItem);
|
||||||
var clause = <QueryClauseViewModel>currentItem;
|
var value = currentItem.entityValue;
|
||||||
let timeStampValue: string = this.timestampToSqlValue(clause);
|
if (!currentItem.isValue) {
|
||||||
var value = clause.value();
|
value = timeStampValue;
|
||||||
if (!clause.isValue()) {
|
|
||||||
value = timeStampValue;
|
|
||||||
}
|
|
||||||
filterString = filterString.concat(
|
|
||||||
this.constructCqlClause(
|
|
||||||
first ? "" : clause.and_or(),
|
|
||||||
this.generateLeftParentheses(clause),
|
|
||||||
clause.field(),
|
|
||||||
clause.type(),
|
|
||||||
clause.operator(),
|
|
||||||
value,
|
|
||||||
this.generateRightParentheses(clause)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentItem instanceof ClauseGroup) {
|
|
||||||
treeTraversal(<ClauseGroup>currentItem);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
filterString = filterString.concat(
|
||||||
|
this.constructCqlClause(
|
||||||
treeTraversal(this.queryClauses);
|
first ? "" : currentItem.selectedOperation,
|
||||||
|
this.generateLeftParentheses(currentItem),
|
||||||
|
currentItem.selectedField,
|
||||||
|
currentItem.selectedEntityType,
|
||||||
|
currentItem.selectedOperator,
|
||||||
|
value,
|
||||||
|
this.generateRightParentheses(currentItem)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
return filterString.trim();
|
return filterString.trim();
|
||||||
};
|
};
|
||||||
@@ -298,7 +268,7 @@ export default class QueryBuilderViewModel {
|
|||||||
this.columnOptions(newHeaders.sort(DataTableUtilities.compareTableColumns));
|
this.columnOptions(newHeaders.sort(DataTableUtilities.compareTableColumns));
|
||||||
};
|
};
|
||||||
|
|
||||||
private generateLeftParentheses(clause: QueryClauseViewModel): string {
|
private generateLeftParentheses(clause: IQueryTableRowsType): string {
|
||||||
var result = "";
|
var result = "";
|
||||||
|
|
||||||
if (clause.clauseGroup.isRootGroup || clause.clauseGroup.children.indexOf(clause) !== 0) {
|
if (clause.clauseGroup.isRootGroup || clause.clauseGroup.children.indexOf(clause) !== 0) {
|
||||||
@@ -321,7 +291,7 @@ export default class QueryBuilderViewModel {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateRightParentheses(clause: QueryClauseViewModel): string {
|
private generateRightParentheses(clause: IQueryTableRowsType): string {
|
||||||
var result = "";
|
var result = "";
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -504,7 +474,7 @@ export default class QueryBuilderViewModel {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
public onAddNewClauseKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
public onAddNewClauseKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): boolean => {
|
||||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||||
this.addClauseIndex(this.clauseArray().length - 1, null);
|
this.addClauseIndex(this.clauseArray().length - 1, null);
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -630,16 +600,11 @@ export default class QueryBuilderViewModel {
|
|||||||
return groupViewModels;
|
return groupViewModels;
|
||||||
};
|
};
|
||||||
|
|
||||||
public runQuery = (): DataTables.DataTable => {
|
|
||||||
return this._queryViewModel.runQuery();
|
|
||||||
};
|
|
||||||
|
|
||||||
public addCustomRange(timestamp: CustomTimestampHelper.ITimestampQuery, clauseToAdd: QueryClauseViewModel): void {
|
public addCustomRange(timestamp: CustomTimestampHelper.ITimestampQuery, clauseToAdd: QueryClauseViewModel): void {
|
||||||
var index = this.clauseArray.peek().indexOf(clauseToAdd);
|
var index = this.clauseArray.peek().indexOf(clauseToAdd);
|
||||||
|
|
||||||
var newClause = new QueryClauseViewModel(
|
var newClause = new QueryClauseViewModel(
|
||||||
this,
|
this,
|
||||||
//this._tableEntityListViewModel.tableExplorerContext.hostProxy,
|
|
||||||
"And",
|
"And",
|
||||||
clauseToAdd.field(),
|
clauseToAdd.field(),
|
||||||
"DateTime",
|
"DateTime",
|
||||||
@@ -713,67 +678,67 @@ export default class QueryBuilderViewModel {
|
|||||||
//DataTableUtilities.forceRecalculateTableSize();
|
//DataTableUtilities.forceRecalculateTableSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
private timestampToValue(clause: QueryClauseViewModel): void {
|
private timestampToValue(clause: IQueryTableRowsType): void {
|
||||||
if (clause.isValue()) {
|
if (clause.isValue) {
|
||||||
return;
|
return;
|
||||||
} else if (clause.isTimestamp()) {
|
} else if (clause.isTimestamp) {
|
||||||
this.getTimeStampToQuery(clause);
|
this.getTimeStampToQuery(clause);
|
||||||
// } else if (clause.isCustomLastTimestamp()) {
|
// } else if (clause.isCustomLastTimestamp()) {
|
||||||
// clause.value(`datetime'${CustomTimestampHelper._queryLastTime(clause.customLastTimestamp().lastNumber, clause.customLastTimestamp().lastTimeUnit)}'`);
|
// clause.value(`datetime'${CustomTimestampHelper._queryLastTime(clause.customLastTimestamp().lastNumber, clause.customLastTimestamp().lastTimeUnit)}'`);
|
||||||
} else if (clause.isCustomRangeTimestamp()) {
|
} else if (clause.isCustomRangeTimestamp) {
|
||||||
if (clause.isLocal()) {
|
if (clause.isLocal) {
|
||||||
clause.value(`datetime'${DateTimeUtilities.getUTCDateTime(clause.customTimeValue())}'`);
|
clause.entityValue = `datetime'${DateTimeUtilities.getUTCDateTime(clause.customTimeValue)}'`;
|
||||||
} else {
|
} else {
|
||||||
clause.value(`datetime'${clause.customTimeValue()}Z'`);
|
clause.entityValue = `datetime'${clause.customTimeValue}Z'`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private timestampToSqlValue(clause: QueryClauseViewModel): string {
|
private timestampToSqlValue(clause: IQueryTableRowsType): string {
|
||||||
if (clause.isValue()) {
|
if (clause.isValue) {
|
||||||
return null;
|
return null;
|
||||||
} else if (clause.isTimestamp()) {
|
} else if (clause.isTimestamp) {
|
||||||
return this.getTimeStampToSqlQuery(clause);
|
return this.getTimeStampToSqlQuery(clause);
|
||||||
// } else if (clause.isCustomLastTimestamp()) {
|
// } else if (clause.isCustomLastTimestamp()) {
|
||||||
// clause.value(CustomTimestampHelper._queryLastTime(clause.customLastTimestamp().lastNumber, clause.customLastTimestamp().lastTimeUnit));
|
// clause.value(CustomTimestampHelper._queryLastTime(clause.customLastTimestamp().lastNumber, clause.customLastTimestamp().lastTimeUnit));
|
||||||
} else if (clause.isCustomRangeTimestamp()) {
|
} else if (clause.isCustomRangeTimestamp) {
|
||||||
if (clause.isLocal()) {
|
if (clause.isLocal) {
|
||||||
return DateTimeUtilities.getUTCDateTime(clause.customTimeValue());
|
return DateTimeUtilities.getUTCDateTime(clause.customTimeValue);
|
||||||
} else {
|
} else {
|
||||||
return clause.customTimeValue();
|
return clause.customTimeValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTimeStampToQuery(clause: QueryClauseViewModel): void {
|
private getTimeStampToQuery(clause: IQueryTableRowsType): void {
|
||||||
switch (clause.timeValue()) {
|
switch (clause.timeValue) {
|
||||||
case Constants.timeOptions.lastHour:
|
case Constants.timeOptions.lastHour:
|
||||||
clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(0, 1)}'`);
|
clause.entityValue = `datetime'${CustomTimestampHelper._queryLastDaysHours(0, 1)}'`;
|
||||||
break;
|
break;
|
||||||
case Constants.timeOptions.last24Hours:
|
case Constants.timeOptions.last24Hours:
|
||||||
clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(0, 24)}'`);
|
clause.entityValue = `datetime'${CustomTimestampHelper._queryLastDaysHours(0, 24)}'`;
|
||||||
break;
|
break;
|
||||||
case Constants.timeOptions.last7Days:
|
case Constants.timeOptions.last7Days:
|
||||||
clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(7, 0)}'`);
|
clause.entityValue = `datetime'${CustomTimestampHelper._queryLastDaysHours(7, 0)}'`;
|
||||||
break;
|
break;
|
||||||
case Constants.timeOptions.last31Days:
|
case Constants.timeOptions.last31Days:
|
||||||
clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(31, 0)}'`);
|
clause.entityValue = `datetime'${CustomTimestampHelper._queryLastDaysHours(31, 0)}'`;
|
||||||
break;
|
break;
|
||||||
case Constants.timeOptions.last365Days:
|
case Constants.timeOptions.last365Days:
|
||||||
clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(365, 0)}'`);
|
clause.entityValue = `datetime'${CustomTimestampHelper._queryLastDaysHours(365, 0)}'`;
|
||||||
break;
|
break;
|
||||||
case Constants.timeOptions.currentMonth:
|
case Constants.timeOptions.currentMonth:
|
||||||
clause.value(`datetime'${CustomTimestampHelper._queryCurrentMonthLocal()}'`);
|
clause.entityValue = `datetime'${CustomTimestampHelper._queryCurrentMonthLocal()}'`;
|
||||||
break;
|
break;
|
||||||
case Constants.timeOptions.currentYear:
|
case Constants.timeOptions.currentYear:
|
||||||
clause.value(`datetime'${CustomTimestampHelper._queryCurrentYearLocal()}'`);
|
clause.entityValue = `datetime'${CustomTimestampHelper._queryCurrentYearLocal()}'`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTimeStampToSqlQuery(clause: QueryClauseViewModel): string {
|
private getTimeStampToSqlQuery(clause: IQueryTableRowsType): string {
|
||||||
switch (clause.timeValue()) {
|
switch (clause.timeValue) {
|
||||||
case Constants.timeOptions.lastHour:
|
case Constants.timeOptions.lastHour:
|
||||||
return CustomTimestampHelper._queryLastDaysHours(0, 1);
|
return CustomTimestampHelper._queryLastDaysHours(0, 1);
|
||||||
case Constants.timeOptions.last24Hours:
|
case Constants.timeOptions.last24Hours:
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { KeyCodes } from "../../../Common/Constants";
|
|||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
import { TableQuerySelectPanel } from "../../Panes/Tables/TableQuerySelectPanel/TableQuerySelectPanel";
|
import { TableQuerySelectPanel } from "../../Panes/Tables/TableQuerySelectPanel/TableQuerySelectPanel";
|
||||||
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
import NewQueryTablesTab from "../../Tabs/QueryTablesTab/QueryTablesTab";
|
||||||
|
import { IQueryTableRowsType } from "../../Tabs/QueryTablesTab/QueryTableTabUtils";
|
||||||
import { getQuotedCqlIdentifier } from "../CqlUtilities";
|
import { getQuotedCqlIdentifier } from "../CqlUtilities";
|
||||||
import * as DataTableUtilities from "../DataTable/DataTableUtilities";
|
import * as DataTableUtilities from "../DataTable/DataTableUtilities";
|
||||||
import TableEntityListViewModel from "../DataTable/TableEntityListViewModel";
|
import TableEntityListViewModel from "../DataTable/TableEntityListViewModel";
|
||||||
@@ -39,14 +40,14 @@ export default class QueryViewModel {
|
|||||||
|
|
||||||
public columnOptions: ko.ObservableArray<string>;
|
public columnOptions: ko.ObservableArray<string>;
|
||||||
|
|
||||||
public queryTablesTab: QueryTablesTab;
|
public queryTablesTab: NewQueryTablesTab;
|
||||||
public id: string;
|
public id: string;
|
||||||
private _tableEntityListViewModel: TableEntityListViewModel;
|
private _tableEntityListViewModel: TableEntityListViewModel;
|
||||||
|
|
||||||
constructor(queryTablesTab: QueryTablesTab) {
|
constructor(queryTablesTab: NewQueryTablesTab) {
|
||||||
this.queryTablesTab = queryTablesTab;
|
this.queryTablesTab = queryTablesTab;
|
||||||
this.id = `queryViewModel${this.queryTablesTab.tabId}`;
|
this.id = `queryViewModel${this.queryTablesTab.tabId}`;
|
||||||
this._tableEntityListViewModel = queryTablesTab.tableEntityListViewModel();
|
this._tableEntityListViewModel = queryTablesTab.tableEntityListViewModel;
|
||||||
|
|
||||||
this.queryTextIsReadOnly = ko.computed<boolean>(() => {
|
this.queryTextIsReadOnly = ko.computed<boolean>(() => {
|
||||||
return userContext.apiType !== "Cassandra";
|
return userContext.apiType !== "Cassandra";
|
||||||
@@ -103,7 +104,7 @@ export default class QueryViewModel {
|
|||||||
DataTableUtilities.forceRecalculateTableSize();
|
DataTableUtilities.forceRecalculateTableSize();
|
||||||
};
|
};
|
||||||
|
|
||||||
public toggleAdvancedOptions = () => {
|
public toggleAdvancedOptions = (): void => {
|
||||||
this.isExpanded(!this.isExpanded());
|
this.isExpanded(!this.isExpanded());
|
||||||
if (this.isExpanded()) {
|
if (this.isExpanded()) {
|
||||||
this.focusTopResult(true);
|
this.focusTopResult(true);
|
||||||
@@ -126,23 +127,19 @@ export default class QueryViewModel {
|
|||||||
return this.selectText();
|
return this.selectText();
|
||||||
};
|
};
|
||||||
|
|
||||||
private setFilter = (): string => {
|
private setFilter = (queryTableRows?: IQueryTableRowsType[]): string => {
|
||||||
const queryString = this.isEditorActive()
|
const queryString = this.isEditorActive()
|
||||||
? this.queryText()
|
? this.queryText()
|
||||||
: userContext.apiType === "Cassandra"
|
: userContext.apiType === "Cassandra"
|
||||||
? this.queryBuilderViewModel().getCqlFilterFromClauses()
|
? this.queryBuilderViewModel().getCqlFilterFromClauses(queryTableRows)
|
||||||
: this.queryBuilderViewModel().getODataFilterFromClauses();
|
: this.queryBuilderViewModel().getODataFilterFromClauses(queryTableRows);
|
||||||
const filter = queryString;
|
const filter = queryString;
|
||||||
this.queryText(filter);
|
this.queryText(filter);
|
||||||
return this.queryText();
|
return this.queryText();
|
||||||
};
|
};
|
||||||
|
|
||||||
private setSqlFilter = (): string => {
|
private setSqlFilter = (queryTableRows: IQueryTableRowsType[]): string => {
|
||||||
return this.queryBuilderViewModel().getSqlFilterFromClauses();
|
return this.queryBuilderViewModel().getSqlFilterFromClauses(queryTableRows);
|
||||||
};
|
|
||||||
|
|
||||||
private setCqlFilter = (): string => {
|
|
||||||
return this.queryBuilderViewModel().getCqlFilterFromClauses();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public isHelperEnabled = ko
|
public isHelperEnabled = ko
|
||||||
@@ -158,8 +155,9 @@ export default class QueryViewModel {
|
|||||||
notify: "always",
|
notify: "always",
|
||||||
});
|
});
|
||||||
|
|
||||||
public runQuery = (): DataTables.DataTable => {
|
public runQuery = (queryTableRows: IQueryTableRowsType[]): string => {
|
||||||
let filter = this.setFilter();
|
let filter = this.setFilter(queryTableRows);
|
||||||
|
|
||||||
if (filter && userContext.apiType !== "Cassandra") {
|
if (filter && userContext.apiType !== "Cassandra") {
|
||||||
filter = filter.replace(/"/g, "'");
|
filter = filter.replace(/"/g, "'");
|
||||||
}
|
}
|
||||||
@@ -170,13 +168,15 @@ export default class QueryViewModel {
|
|||||||
this._tableEntityListViewModel.tableQuery.top = top;
|
this._tableEntityListViewModel.tableQuery.top = top;
|
||||||
this._tableEntityListViewModel.tableQuery.select = select;
|
this._tableEntityListViewModel.tableQuery.select = select;
|
||||||
this._tableEntityListViewModel.oDataQuery(filter);
|
this._tableEntityListViewModel.oDataQuery(filter);
|
||||||
this._tableEntityListViewModel.sqlQuery(this.setSqlFilter());
|
this._tableEntityListViewModel.sqlQuery(this.setSqlFilter(queryTableRows));
|
||||||
this._tableEntityListViewModel.cqlQuery(filter);
|
this._tableEntityListViewModel.cqlQuery(filter);
|
||||||
|
|
||||||
return this._tableEntityListViewModel.reloadTable(/*useSetting*/ false, /*resetHeaders*/ false);
|
return userContext.apiType !== "Cassandra"
|
||||||
|
? this._tableEntityListViewModel.sqlQuery()
|
||||||
|
: this._tableEntityListViewModel.cqlQuery();
|
||||||
};
|
};
|
||||||
|
|
||||||
public clearQuery = (): DataTables.DataTable => {
|
public clearQuery = (): void => {
|
||||||
this.queryText();
|
this.queryText();
|
||||||
this.topValue();
|
this.topValue();
|
||||||
this.selectText();
|
this.selectText();
|
||||||
@@ -194,16 +194,26 @@ export default class QueryViewModel {
|
|||||||
this.queryTablesTab.collection.id()
|
this.queryTablesTab.collection.id()
|
||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
return this._tableEntityListViewModel.reloadTable(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public selectQueryOptions() {
|
public selectQueryOptions(headers: string[], getSelectMessage: (selectMessage: string) => void): void {
|
||||||
useSidePanel.getState().openSidePanel("Select Column", <TableQuerySelectPanel queryViewModel={this} />);
|
this.columnOptions(headers);
|
||||||
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel(
|
||||||
|
"Select Column",
|
||||||
|
<TableQuerySelectPanel queryViewModel={this} headers={headers} getSelectMessage={getSelectMessage} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onselectQueryOptionsKeyDown = (source: string, event: KeyboardEvent): boolean => {
|
public onselectQueryOptionsKeyDown = (
|
||||||
|
source: string,
|
||||||
|
event: KeyboardEvent,
|
||||||
|
headers: string[],
|
||||||
|
getSelectMessage: (selectMessage: string) => void
|
||||||
|
): boolean => {
|
||||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||||
this.selectQueryOptions();
|
this.selectQueryOptions(headers, getSelectMessage);
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DetailsList, DetailsListLayoutMode, IColumn, Pivot, PivotItem, SelectionMode } from "@fluentui/react";
|
import { DetailsList, DetailsListLayoutMode, IColumn, Pivot, PivotItem, SelectionMode, Text } from "@fluentui/react";
|
||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
import SplitterLayout from "react-splitter-layout";
|
import SplitterLayout from "react-splitter-layout";
|
||||||
import "react-splitter-layout/lib/index.css";
|
import "react-splitter-layout/lib/index.css";
|
||||||
@@ -120,21 +120,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
constructor(props: IQueryTabComponentProps) {
|
constructor(props: IQueryTabComponentProps) {
|
||||||
super(props);
|
super(props);
|
||||||
const columns: IColumn[] = [
|
const columns: IColumn[] = [
|
||||||
{
|
|
||||||
key: "column1",
|
|
||||||
name: "",
|
|
||||||
minWidth: 16,
|
|
||||||
maxWidth: 16,
|
|
||||||
data: String,
|
|
||||||
fieldName: "toolTip",
|
|
||||||
onRender: this.onRenderColumnItem,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: "column2",
|
key: "column2",
|
||||||
name: "METRIC",
|
name: "METRIC",
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
data: String,
|
data: String,
|
||||||
fieldName: "metric",
|
fieldName: "metric",
|
||||||
|
onRender: this.onRenderColumnItem,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "column3",
|
key: "column3",
|
||||||
@@ -206,7 +198,12 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
|
|
||||||
public onRenderColumnItem(item: IDocument): JSX.Element {
|
public onRenderColumnItem(item: IDocument): JSX.Element {
|
||||||
if (item.toolTip !== "") {
|
if (item.toolTip !== "") {
|
||||||
return <InfoTooltip>{`${item.toolTip}`}</InfoTooltip>;
|
return (
|
||||||
|
<>
|
||||||
|
<InfoTooltip>{`${item.toolTip}`}</InfoTooltip>
|
||||||
|
<Text style={{ paddingLeft: 10, margin: 0 }}>{`${item.metric}`}</Text>
|
||||||
|
</>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -1002,7 +999,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
"data-order": 2,
|
"data-order": 2,
|
||||||
"data-title": "Query Stats",
|
"data-title": "Query Stats",
|
||||||
}}
|
}}
|
||||||
style={{ height: "100%" }}
|
style={{ height: "100%", overflowY: "scroll" }}
|
||||||
>
|
>
|
||||||
{this.state.allResultsMetadata.length > 0 && !this.state.error && (
|
{this.state.allResultsMetadata.length > 0 && !this.state.error && (
|
||||||
<div className="queryMetricsSummaryContainer">
|
<div className="queryMetricsSummaryContainer">
|
||||||
|
|||||||
@@ -1,271 +0,0 @@
|
|||||||
<div class="tab-pane tableContainer" data-bind="attr:{id: tabId}" role="tabpanel">
|
|
||||||
<!-- Tables Query Tab Query Builder - Start-->
|
|
||||||
<div
|
|
||||||
class="query-builder"
|
|
||||||
data-bind="with: queryViewModel, attr: {
|
|
||||||
id: queryViewModel.id
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<!-- Tables Query Tab Errors - Start-->
|
|
||||||
<div class="error-bar">
|
|
||||||
<div class="error-message" aria-label="Error Message" data-bind="visible: hasQueryError">
|
|
||||||
<span><img class="entity-error-Img" src="/error_red.svg" /></span>
|
|
||||||
<span class="error-text" role="alert" data-bind="text: queryErrorMessage"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Tables Query Tab Errors - End-->
|
|
||||||
<!-- Tables Query Tab Query Text - Start-->
|
|
||||||
<div class="query-editor-panel" data-bind="visible: isEditorActive">
|
|
||||||
<div>
|
|
||||||
<textarea
|
|
||||||
class="query-editor-text"
|
|
||||||
data-bind="textInput: queryText,
|
|
||||||
css: { 'query-editor-text-invalid': hasQueryError },
|
|
||||||
readOnly: true"
|
|
||||||
name="query-editor"
|
|
||||||
rows="5"
|
|
||||||
cols="100"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Tables Query Tab Query Text - End-->
|
|
||||||
<!-- Tables Query Tab Query Helper - Start-->
|
|
||||||
<div data-bind="visible: isHelperActive" style="padding-left: 13px">
|
|
||||||
<div class="clause-table" data-bind="with: queryBuilderViewModel ">
|
|
||||||
<div class="scroll-box scrollable" id="scroll">
|
|
||||||
<table class="clause-table">
|
|
||||||
<thead>
|
|
||||||
<tr class="clause-table-row">
|
|
||||||
<th class="clause-table-cell header-background action-header">
|
|
||||||
<span data-bind="text: actionLabel"></span>
|
|
||||||
</th>
|
|
||||||
<th class="clause-table-cell header-background group-control-header">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
data-bind="enable: canGroupClauses, attr:{title: groupSelectedClauses}, click: groupClauses"
|
|
||||||
>
|
|
||||||
<img class="and-or-svg" src="/And-Or.svg" alt="Group selected clauses" />
|
|
||||||
</button>
|
|
||||||
</th>
|
|
||||||
<th class="clause-table-cell header-background"><!-- Grouping indicator --></th>
|
|
||||||
<th class="clause-table-cell header-background and-or-header">
|
|
||||||
<span data-bind="text: andLabel"></span>
|
|
||||||
</th>
|
|
||||||
<th class="clause-table-cell header-background field-header">
|
|
||||||
<span data-bind="text: fieldLabel"></span>
|
|
||||||
</th>
|
|
||||||
<th class="clause-table-cell header-background type-header">
|
|
||||||
<span data-bind="text: dataTypeLabel"></span>
|
|
||||||
</th>
|
|
||||||
<th class="clause-table-cell header-background operator-header">
|
|
||||||
<span data-bind="text: operatorLabel"></span>
|
|
||||||
</th>
|
|
||||||
<th class="clause-table-cell header-background value-header">
|
|
||||||
<span data-bind="text: valueLabel"></span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody data-bind="template: { name: 'queryClause-template', foreach: clauseArray, as: 'clause' }"></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="addClause"
|
|
||||||
role="button"
|
|
||||||
data-bind="click: addNewClause, event: { keydown: onAddNewClauseKeyDown }, attr: { title: addNewClauseLine }"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<div class="addClause-heading">
|
|
||||||
<span class="clause-table addClause-title">
|
|
||||||
<img
|
|
||||||
class="addclauseProperty-Img"
|
|
||||||
style="margin-bottom: 5px"
|
|
||||||
src="/Add-property.svg"
|
|
||||||
alt="Add new clause"
|
|
||||||
/>
|
|
||||||
<span style="margin-left: 5px" data-bind="text: addNewClauseLine"></span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Tables Query Tab Query Helper - End-->
|
|
||||||
<!-- Tables Query Tab Advanced Options - Start-->
|
|
||||||
<div class="advanced-options-panel">
|
|
||||||
<div class="advanced-heading">
|
|
||||||
<span
|
|
||||||
class="advanced-title"
|
|
||||||
role="button"
|
|
||||||
data-bind="click:toggleAdvancedOptions, event: { keydown: ontoggleAdvancedOptionsKeyDown }, attr:{ 'aria-expanded': isExpanded() ? 'true' : 'false' }"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<!-- ko template: { ifnot: isExpanded} -->
|
|
||||||
<div class="themed-images" type="text/html" id="ExpandChevronRight" data-bind="hasFocus: focusExpandIcon">
|
|
||||||
<img class="imgiconwidth expand-triangle expand-triangle-right" src="/Triangle-right.svg" alt="toggle" />
|
|
||||||
</div>
|
|
||||||
<!-- /ko -->
|
|
||||||
<!-- ko template: { if: isExpanded} -->
|
|
||||||
<div class="themed-images" type="text/html" id="ExpandChevronDown">
|
|
||||||
<img class="imgiconwidth expand-triangle" src="/Triangle-down.svg" alt="toggle" />
|
|
||||||
</div>
|
|
||||||
<!-- /ko -->
|
|
||||||
<span>Advanced Options</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="advanced-options" data-bind="visible: isExpanded">
|
|
||||||
<div class="top">
|
|
||||||
<span>Show top results:</span>
|
|
||||||
<input
|
|
||||||
class="top-input"
|
|
||||||
type="number"
|
|
||||||
data-bind="hasFocus: focusTopResult, textInput: topValue, attr: { title: topValueLimitMessage }"
|
|
||||||
role="textbox"
|
|
||||||
aria-label="Show top results"
|
|
||||||
/>
|
|
||||||
<div role="alert" aria-atomic="true" class="inline-div" data-bind="visible: isExceedingLimit">
|
|
||||||
<img class="advanced-options-icon" src="/QueryBuilder/StatusWarning_16x.png" />
|
|
||||||
<span data-bind="text: topValueLimitMessage"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="select">
|
|
||||||
<span> Select fields for query: </span>
|
|
||||||
<div data-bind="visible: isSelected">
|
|
||||||
<img class="advanced-options-icon" src="/QueryBuilder/QueryInformation_16x.png" />
|
|
||||||
<span class="select-options-text" data-bind="text: selectMessage" />
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
class="select-options-link"
|
|
||||||
data-bind="click: selectQueryOptions, event: { keydown: onselectQueryOptionsKeyDown }"
|
|
||||||
tabindex="0"
|
|
||||||
role="link"
|
|
||||||
>
|
|
||||||
<span>Choose Columns... </span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Tables Query Tab Advanced Options - End-->
|
|
||||||
</div>
|
|
||||||
<!-- Tables Query Tab Query Builder - End-->
|
|
||||||
<div
|
|
||||||
class="tablesQueryTab tableContainer"
|
|
||||||
data-bind="with: tableEntityListViewModel, attr: {
|
|
||||||
id: tableEntityListViewModel.id
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<!-- Keyboard navigation - tabindex is required to make the table focusable. -->
|
|
||||||
<table
|
|
||||||
id="storageTable"
|
|
||||||
class="storage azure-table show-gridlines"
|
|
||||||
tabindex="0"
|
|
||||||
data-bind="tableSource: items, tableSelection: selected"
|
|
||||||
></table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Script for each clause in the tables query builder -->
|
|
||||||
<script type="text/html" id="queryClause-template">
|
|
||||||
<tr class="clause-table-row">
|
|
||||||
<td class="clause-table-cell action-column">
|
|
||||||
<span
|
|
||||||
class="entity-Add-Cancel"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
data-bind="click: $parent.addClauseIndex.bind($data, $index()), event: { keydown: $parent.onAddClauseKeyDown.bind($data, $index()) }, attr:{title: $parent.insertNewFilterLine}"
|
|
||||||
>
|
|
||||||
<img class="querybuilder-addpropertyImg" src="/Add-property.svg" alt="Add clause" />
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="entity-Add-Cancel"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
data-bind="hasFocus: isDeleteButtonFocused, click: $parent.deleteClause.bind($data, $index()), event: { keydown: $parent.onDeleteClauseKeyDown.bind($data, $index()) }, attr:{title: $parent.removeThisFilterLine}"
|
|
||||||
>
|
|
||||||
<img class="querybuilder-cancelImg" src="/Entity_cancel.svg" alt="Delete clause" />
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="clause-table-cell group-control-column">
|
|
||||||
<input type="checkbox" aria-label="And/Or" data-bind="checked: checkedForGrouping" />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<table class="group-indicator-table">
|
|
||||||
<tbody>
|
|
||||||
<tr
|
|
||||||
data-bind="template: { name: 'groupIndicator-template', foreach: $parent.getClauseGroupViewModels($data), as: 'gi' }"
|
|
||||||
></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
<td class="clause-table-cell and-or-column">
|
|
||||||
<select
|
|
||||||
class="clause-table-field and-or-column"
|
|
||||||
data-bind="hasFocus: isAndOrFocused, options: $parent.clauseRules, value: and_or, visible: canAnd, attr:{ 'aria-label': and_or }"
|
|
||||||
></select>
|
|
||||||
</td>
|
|
||||||
<td class="clause-table-cell field-column" data-bind="click: $parent.updateColumnOptions">
|
|
||||||
<select
|
|
||||||
class="clause-table-field field-column"
|
|
||||||
data-bind="options: $parent.columnOptions, value: field, attr:{ 'aria-label': field }"
|
|
||||||
></select>
|
|
||||||
</td>
|
|
||||||
<td class="clause-table-cell type-column">
|
|
||||||
<select
|
|
||||||
class="clause-table-field type-column"
|
|
||||||
data-bind="
|
|
||||||
options: $parent.edmTypes,
|
|
||||||
enable: isTypeEditable,
|
|
||||||
value: type,
|
|
||||||
css: {'query-builder-isDisabled': !isTypeEditable()},
|
|
||||||
attr: { 'aria-label': type }"
|
|
||||||
></select>
|
|
||||||
</td>
|
|
||||||
<td class="clause-table-cell operator-column">
|
|
||||||
<select
|
|
||||||
class="clause-table-field operator-column"
|
|
||||||
data-bind="
|
|
||||||
options: $parent.operators,
|
|
||||||
enable: isOperaterEditable,
|
|
||||||
value: operator,
|
|
||||||
css: {'query-builder-isDisabled': !isOperaterEditable()},
|
|
||||||
attr: { 'aria-label': operator }"
|
|
||||||
></select>
|
|
||||||
</td>
|
|
||||||
<td class="clause-table-cell value-column">
|
|
||||||
<!-- ko template: {if: isValue} -->
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="clause-table-field value-column"
|
|
||||||
type="search"
|
|
||||||
data-bind="textInput: value, attr: {'aria-label': $parent.valueLabel}"
|
|
||||||
/>
|
|
||||||
<!-- /ko -->
|
|
||||||
|
|
||||||
<!-- ko template: {if: isTimestamp} -->
|
|
||||||
<select
|
|
||||||
class="clause-table-field time-column"
|
|
||||||
data-bind="options: $parent.timeOptions, value: timeValue"
|
|
||||||
></select>
|
|
||||||
<!-- /ko -->
|
|
||||||
|
|
||||||
<!-- ko template: {if: isCustomLastTimestamp} -->
|
|
||||||
<input class="clause-table-field time-column" data-bind="value: customTimeValue, click: customTimestampDialog" />
|
|
||||||
<!-- /ko -->
|
|
||||||
<!-- ko template: {if: isCustomRangeTimestamp} -->
|
|
||||||
<input class="clause-table-field time-column" type="datetime-local" step="1" data-bind="value: customTimeValue" />
|
|
||||||
<!-- /ko -->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Script for each clause group in the tables query builder -->
|
|
||||||
<script type="text/html" id="groupIndicator-template">
|
|
||||||
<td
|
|
||||||
class="group-indicator-column"
|
|
||||||
data-bind="style: {backgroundColor: gi.backgroundColor, borderTop: gi.showTopBorder.peek() ? 'solid thin #CCCCCC' : 'none', borderLeft: gi.showLeftBorder.peek() ? 'solid thin #CCCCCC' : 'none', borderBottom: gi.showBottomBorder.peek() ? 'solid thin #CCCCCC' : gi.borderBackgroundColor}"
|
|
||||||
>
|
|
||||||
<!-- ko template: {if: gi.canUngroup} -->
|
|
||||||
<button type="button" data-bind="click: ungroupClauses, attr: {title: ungroupClausesLabel}">
|
|
||||||
<img src="/QueryBuilder/UngroupClause_16x.png" alt="Ungroup clauses" />
|
|
||||||
</button>
|
|
||||||
<!-- /ko -->
|
|
||||||
</td>
|
|
||||||
</script>
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
import * as ko from "knockout";
|
|
||||||
import React from "react";
|
|
||||||
import AddEntityIcon from "../../../images/AddEntity.svg";
|
|
||||||
import DeleteEntitiesIcon from "../../../images/DeleteEntities.svg";
|
|
||||||
import EditEntityIcon from "../../../images/Edit-entity.svg";
|
|
||||||
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
|
||||||
import QueryBuilderIcon from "../../../images/Query-Builder.svg";
|
|
||||||
import QueryTextIcon from "../../../images/Query-Text.svg";
|
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
|
||||||
import { useSidePanel } from "../../hooks/useSidePanel";
|
|
||||||
import { userContext } from "../../UserContext";
|
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
|
||||||
import Explorer from "../Explorer";
|
|
||||||
import { AddTableEntityPanel } from "../Panes/Tables/AddTableEntityPanel";
|
|
||||||
import { EditTableEntityPanel } from "../Panes/Tables/EditTableEntityPanel";
|
|
||||||
import TableCommands from "../Tables/DataTable/TableCommands";
|
|
||||||
import TableEntityListViewModel from "../Tables/DataTable/TableEntityListViewModel";
|
|
||||||
import QueryViewModel from "../Tables/QueryBuilder/QueryViewModel";
|
|
||||||
import { CassandraAPIDataClient, TableDataClient } from "../Tables/TableDataClient";
|
|
||||||
import template from "./QueryTablesTab.html";
|
|
||||||
import TabsBase from "./TabsBase";
|
|
||||||
|
|
||||||
// Will act as table explorer class
|
|
||||||
export default class QueryTablesTab extends TabsBase {
|
|
||||||
public readonly html = template;
|
|
||||||
public collection: ViewModels.Collection;
|
|
||||||
public tableEntityListViewModel = ko.observable<TableEntityListViewModel>();
|
|
||||||
public queryViewModel = ko.observable<QueryViewModel>();
|
|
||||||
public tableCommands: TableCommands;
|
|
||||||
public tableDataClient: TableDataClient;
|
|
||||||
|
|
||||||
public queryText = ko.observable("PartitionKey eq 'partitionKey1'"); // Start out with an example they can modify
|
|
||||||
public selectedQueryText = ko.observable("").extend({ notify: "always" });
|
|
||||||
|
|
||||||
public executeQueryButton: ViewModels.Button;
|
|
||||||
public addEntityButton: ViewModels.Button;
|
|
||||||
public editEntityButton: ViewModels.Button;
|
|
||||||
public deleteEntityButton: ViewModels.Button;
|
|
||||||
public queryBuilderButton: ViewModels.Button;
|
|
||||||
public queryTextButton: ViewModels.Button;
|
|
||||||
public container: Explorer;
|
|
||||||
|
|
||||||
constructor(options: ViewModels.TabOptions) {
|
|
||||||
super(options);
|
|
||||||
|
|
||||||
this.container = options.collection && options.collection.container;
|
|
||||||
this.tableCommands = new TableCommands(this.container);
|
|
||||||
this.tableDataClient = this.container.tableDataClient;
|
|
||||||
this.tableEntityListViewModel(new TableEntityListViewModel(this.tableCommands, this));
|
|
||||||
this.tableEntityListViewModel().queryTablesTab = this;
|
|
||||||
this.queryViewModel(new QueryViewModel(this));
|
|
||||||
const sampleQuerySubscription = this.tableEntityListViewModel().items.subscribe(() => {
|
|
||||||
if (this.tableEntityListViewModel().items().length > 0 && userContext.apiType === "Tables") {
|
|
||||||
this.queryViewModel().queryBuilderViewModel().setExample();
|
|
||||||
}
|
|
||||||
sampleQuerySubscription.dispose();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.executeQueryButton = {
|
|
||||||
enabled: ko.computed<boolean>(() => {
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
|
|
||||||
visible: ko.computed<boolean>(() => {
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.queryBuilderButton = {
|
|
||||||
enabled: ko.computed<boolean>(() => {
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
|
|
||||||
visible: ko.computed<boolean>(() => {
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
|
|
||||||
isSelected: ko.computed<boolean>(() => {
|
|
||||||
return this.queryViewModel() ? this.queryViewModel().isHelperActive() : false;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.queryTextButton = {
|
|
||||||
enabled: ko.computed<boolean>(() => {
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
|
|
||||||
visible: ko.computed<boolean>(() => {
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
|
|
||||||
isSelected: ko.computed<boolean>(() => {
|
|
||||||
return this.queryViewModel() ? this.queryViewModel().isEditorActive() : false;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.addEntityButton = {
|
|
||||||
enabled: ko.computed<boolean>(() => {
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
|
|
||||||
visible: ko.computed<boolean>(() => {
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.editEntityButton = {
|
|
||||||
enabled: ko.computed<boolean>(() => {
|
|
||||||
return this.tableCommands.isEnabled(
|
|
||||||
TableCommands.editEntityCommand,
|
|
||||||
this.tableEntityListViewModel().selected()
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
|
|
||||||
visible: ko.computed<boolean>(() => {
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.deleteEntityButton = {
|
|
||||||
enabled: ko.computed<boolean>(() => {
|
|
||||||
return this.tableCommands.isEnabled(
|
|
||||||
TableCommands.deleteEntitiesCommand,
|
|
||||||
this.tableEntityListViewModel().selected()
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
|
|
||||||
visible: ko.computed<boolean>(() => {
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.buildCommandBarOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
public onAddEntityClick = (): void => {
|
|
||||||
useSidePanel
|
|
||||||
.getState()
|
|
||||||
.openSidePanel(
|
|
||||||
"Add Table Row",
|
|
||||||
<AddTableEntityPanel
|
|
||||||
tableDataClient={this.tableDataClient}
|
|
||||||
queryTablesTab={this}
|
|
||||||
tableEntityListViewModel={this.tableEntityListViewModel()}
|
|
||||||
cassandraApiClient={new CassandraAPIDataClient()}
|
|
||||||
/>,
|
|
||||||
"700px"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
public onEditEntityClick = (): void => {
|
|
||||||
useSidePanel
|
|
||||||
.getState()
|
|
||||||
.openSidePanel(
|
|
||||||
"Edit Table Entity",
|
|
||||||
<EditTableEntityPanel
|
|
||||||
tableDataClient={this.tableDataClient}
|
|
||||||
queryTablesTab={this}
|
|
||||||
tableEntityListViewModel={this.tableEntityListViewModel()}
|
|
||||||
cassandraApiClient={new CassandraAPIDataClient()}
|
|
||||||
/>,
|
|
||||||
"700px"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
public onDeleteEntityClick = (): void => {
|
|
||||||
this.tableCommands.deleteEntitiesCommand(this.tableEntityListViewModel());
|
|
||||||
};
|
|
||||||
|
|
||||||
public onActivate(): void {
|
|
||||||
super.onActivate();
|
|
||||||
const columns =
|
|
||||||
!!this.tableEntityListViewModel() &&
|
|
||||||
!!this.tableEntityListViewModel().table &&
|
|
||||||
this.tableEntityListViewModel().table.columns;
|
|
||||||
if (columns) {
|
|
||||||
columns.adjust();
|
|
||||||
$(window).resize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
|
||||||
const buttons: CommandButtonComponentProps[] = [];
|
|
||||||
if (this.queryBuilderButton.visible()) {
|
|
||||||
const label = userContext.apiType === "Cassandra" ? "CQL Query Builder" : "Query Builder";
|
|
||||||
buttons.push({
|
|
||||||
iconSrc: QueryBuilderIcon,
|
|
||||||
iconAlt: label,
|
|
||||||
onCommandClick: () => this.queryViewModel().selectHelper(),
|
|
||||||
commandButtonLabel: label,
|
|
||||||
ariaLabel: label,
|
|
||||||
hasPopup: false,
|
|
||||||
disabled: !this.queryBuilderButton.enabled(),
|
|
||||||
isSelected: this.queryBuilderButton.isSelected(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.queryTextButton.visible()) {
|
|
||||||
const label = userContext.apiType === "Cassandra" ? "CQL Query Text" : "Query Text";
|
|
||||||
buttons.push({
|
|
||||||
iconSrc: QueryTextIcon,
|
|
||||||
iconAlt: label,
|
|
||||||
onCommandClick: () => this.queryViewModel().selectEditor(),
|
|
||||||
commandButtonLabel: label,
|
|
||||||
ariaLabel: label,
|
|
||||||
hasPopup: false,
|
|
||||||
disabled: !this.queryTextButton.enabled(),
|
|
||||||
isSelected: this.queryTextButton.isSelected(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.executeQueryButton.visible()) {
|
|
||||||
const label = "Run Query";
|
|
||||||
buttons.push({
|
|
||||||
iconSrc: ExecuteQueryIcon,
|
|
||||||
iconAlt: label,
|
|
||||||
onCommandClick: () => this.queryViewModel().runQuery(),
|
|
||||||
commandButtonLabel: label,
|
|
||||||
ariaLabel: label,
|
|
||||||
hasPopup: false,
|
|
||||||
disabled: !this.executeQueryButton.enabled(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.addEntityButton.visible()) {
|
|
||||||
const label = userContext.apiType === "Cassandra" ? "Add Row" : "Add Entity";
|
|
||||||
buttons.push({
|
|
||||||
iconSrc: AddEntityIcon,
|
|
||||||
iconAlt: label,
|
|
||||||
onCommandClick: this.onAddEntityClick,
|
|
||||||
commandButtonLabel: label,
|
|
||||||
ariaLabel: label,
|
|
||||||
hasPopup: true,
|
|
||||||
disabled: !this.addEntityButton.enabled(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.editEntityButton.visible()) {
|
|
||||||
const label = userContext.apiType === "Cassandra" ? "Edit Row" : "Edit Entity";
|
|
||||||
buttons.push({
|
|
||||||
iconSrc: EditEntityIcon,
|
|
||||||
iconAlt: label,
|
|
||||||
onCommandClick: this.onEditEntityClick,
|
|
||||||
commandButtonLabel: label,
|
|
||||||
ariaLabel: label,
|
|
||||||
hasPopup: true,
|
|
||||||
disabled: !this.editEntityButton.enabled(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.deleteEntityButton.visible()) {
|
|
||||||
const label = userContext.apiType === "Cassandra" ? "Delete Rows" : "Delete Entities";
|
|
||||||
buttons.push({
|
|
||||||
iconSrc: DeleteEntitiesIcon,
|
|
||||||
iconAlt: label,
|
|
||||||
onCommandClick: this.onDeleteEntityClick,
|
|
||||||
commandButtonLabel: label,
|
|
||||||
ariaLabel: label,
|
|
||||||
hasPopup: true,
|
|
||||||
disabled: !this.deleteEntityButton.enabled(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return buttons;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected buildCommandBarOptions(): void {
|
|
||||||
ko.computed(() =>
|
|
||||||
ko.toJSON([
|
|
||||||
this.queryBuilderButton.visible,
|
|
||||||
this.queryBuilderButton.enabled,
|
|
||||||
this.queryTextButton.visible,
|
|
||||||
this.queryTextButton.enabled,
|
|
||||||
this.executeQueryButton.visible,
|
|
||||||
this.executeQueryButton.enabled,
|
|
||||||
this.addEntityButton.visible,
|
|
||||||
this.addEntityButton.enabled,
|
|
||||||
this.editEntityButton.visible,
|
|
||||||
this.editEntityButton.enabled,
|
|
||||||
this.deleteEntityButton.visible,
|
|
||||||
this.deleteEntityButton.enabled,
|
|
||||||
])
|
|
||||||
).subscribe(() => this.updateNavbarWithTabsButtons());
|
|
||||||
this.updateNavbarWithTabsButtons();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
169
src/Explorer/Tabs/QueryTablesTab/QueryTableEntityClause.tsx
Normal file
169
src/Explorer/Tabs/QueryTablesTab/QueryTableEntityClause.tsx
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import {
|
||||||
|
Checkbox,
|
||||||
|
Dropdown,
|
||||||
|
IDropdownOption,
|
||||||
|
IDropdownStyles,
|
||||||
|
IImageProps,
|
||||||
|
Image,
|
||||||
|
IStackTokens,
|
||||||
|
Stack,
|
||||||
|
TextField,
|
||||||
|
TooltipHost,
|
||||||
|
} from "@fluentui/react";
|
||||||
|
import React, { FunctionComponent } from "react";
|
||||||
|
import AddIcon from "../../../../images/Add-property.svg";
|
||||||
|
import CancelIcon from "../../../../images/Entity_cancel.svg";
|
||||||
|
import { userContext } from "../../../UserContext";
|
||||||
|
import { IOption } from "./QueryTableTabUtils";
|
||||||
|
const dropdownStyles: Partial<IDropdownStyles> = { dropdown: { width: 100 } };
|
||||||
|
|
||||||
|
export interface IQueryTableEntityClauseProps {
|
||||||
|
index: number;
|
||||||
|
entityValue: string;
|
||||||
|
entityValuePlaceHolder?: string;
|
||||||
|
selectedOperator: string;
|
||||||
|
selectedOperation: string;
|
||||||
|
operatorOptions: IOption[];
|
||||||
|
operationOptions: IOption[];
|
||||||
|
isQueryTableEntityChecked: boolean;
|
||||||
|
selectedField: string;
|
||||||
|
fieldOptions: IOption[];
|
||||||
|
entityTypeOptions: IOption[];
|
||||||
|
selectedEntityType: string;
|
||||||
|
isTimeStampSelected?: boolean;
|
||||||
|
selectedTimestamp: string;
|
||||||
|
timestampOptions: IOption[];
|
||||||
|
onAddNewClause?: () => void;
|
||||||
|
onDeleteClause?: () => void;
|
||||||
|
onQueryTableEntityCheck: (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => void;
|
||||||
|
onDropdownChange: (selectedOption: IDropdownOption, selectedOptionType: string) => void;
|
||||||
|
onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
|
||||||
|
onAddNewClauseKeyDown?: (ev: React.KeyboardEvent<HTMLImageElement>) => void;
|
||||||
|
onDeleteCaluseKeyDown?: (ev: React.KeyboardEvent<HTMLImageElement>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QueryTableEntityClause: FunctionComponent<IQueryTableEntityClauseProps> = ({
|
||||||
|
index,
|
||||||
|
entityValue,
|
||||||
|
entityValuePlaceHolder,
|
||||||
|
selectedOperator,
|
||||||
|
operatorOptions,
|
||||||
|
selectedField,
|
||||||
|
isQueryTableEntityChecked,
|
||||||
|
fieldOptions,
|
||||||
|
entityTypeOptions,
|
||||||
|
selectedEntityType,
|
||||||
|
selectedOperation,
|
||||||
|
operationOptions,
|
||||||
|
isTimeStampSelected,
|
||||||
|
selectedTimestamp,
|
||||||
|
timestampOptions,
|
||||||
|
onQueryTableEntityCheck,
|
||||||
|
onAddNewClause,
|
||||||
|
onDeleteClause,
|
||||||
|
onDropdownChange,
|
||||||
|
onEntityValueChange,
|
||||||
|
onAddNewClauseKeyDown,
|
||||||
|
onDeleteCaluseKeyDown,
|
||||||
|
}: IQueryTableEntityClauseProps): JSX.Element => {
|
||||||
|
const cancelImageProps: IImageProps = {
|
||||||
|
className: "querybuilder-cancelImg",
|
||||||
|
};
|
||||||
|
|
||||||
|
const addImageProps: IImageProps = {
|
||||||
|
className: "querybuilder-addpropertyImg",
|
||||||
|
};
|
||||||
|
|
||||||
|
const sectionStackTokens: IStackTokens = { childrenGap: 12 };
|
||||||
|
|
||||||
|
const validateEntityTypeOption = (): boolean => {
|
||||||
|
if (userContext.apiType === "Cassandra") {
|
||||||
|
return true;
|
||||||
|
} else if (selectedField === "PartitionKey" || selectedField === "RowKey" || selectedField === "Timestamp") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack horizontal tokens={sectionStackTokens}>
|
||||||
|
<TooltipHost content="Add new clause" id="addNewClause">
|
||||||
|
<Image
|
||||||
|
{...addImageProps}
|
||||||
|
src={AddIcon}
|
||||||
|
alt="Add new clause"
|
||||||
|
id="addNewClause"
|
||||||
|
onClick={onAddNewClause}
|
||||||
|
onKeyDown={onAddNewClauseKeyDown}
|
||||||
|
tabIndex={0}
|
||||||
|
/>
|
||||||
|
</TooltipHost>
|
||||||
|
<TooltipHost content="Delete clause" id="deleteClause">
|
||||||
|
<Image
|
||||||
|
{...cancelImageProps}
|
||||||
|
src={CancelIcon}
|
||||||
|
alt="delete clause"
|
||||||
|
id="deleteClause"
|
||||||
|
onClick={onDeleteClause}
|
||||||
|
onKeyDown={onDeleteCaluseKeyDown}
|
||||||
|
tabIndex={0}
|
||||||
|
/>
|
||||||
|
</TooltipHost>
|
||||||
|
<Checkbox checked={isQueryTableEntityChecked} onChange={onQueryTableEntityCheck} />
|
||||||
|
<Dropdown
|
||||||
|
style={{ visibility: index > 0 ? "visible" : "hidden" }}
|
||||||
|
selectedKey={selectedOperation}
|
||||||
|
onChange={(_event: React.FormEvent<HTMLElement>, selectedOption: IDropdownOption) =>
|
||||||
|
onDropdownChange(selectedOption, "selectedOperation")
|
||||||
|
}
|
||||||
|
options={operationOptions}
|
||||||
|
styles={dropdownStyles}
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
selectedKey={selectedField}
|
||||||
|
onChange={(_event: React.FormEvent<HTMLElement>, selectedOption: IDropdownOption) =>
|
||||||
|
onDropdownChange(selectedOption, "selectedField")
|
||||||
|
}
|
||||||
|
options={fieldOptions}
|
||||||
|
styles={dropdownStyles}
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
selectedKey={selectedEntityType}
|
||||||
|
onChange={(_event: React.FormEvent<HTMLElement>, selectedOption: IDropdownOption) =>
|
||||||
|
onDropdownChange(selectedOption, "selectedEntityType")
|
||||||
|
}
|
||||||
|
options={entityTypeOptions}
|
||||||
|
disabled={validateEntityTypeOption()}
|
||||||
|
styles={dropdownStyles}
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
selectedKey={selectedOperator}
|
||||||
|
onChange={(_event: React.FormEvent<HTMLElement>, selectedOption: IDropdownOption) =>
|
||||||
|
onDropdownChange(selectedOption, "selectedOperator")
|
||||||
|
}
|
||||||
|
options={operatorOptions}
|
||||||
|
styles={dropdownStyles}
|
||||||
|
/>
|
||||||
|
{isTimeStampSelected ? (
|
||||||
|
<Dropdown
|
||||||
|
selectedKey={selectedTimestamp}
|
||||||
|
onChange={(_event: React.FormEvent<HTMLElement>, selectedOption: IDropdownOption) =>
|
||||||
|
onDropdownChange(selectedOption, "selectedTimestamp")
|
||||||
|
}
|
||||||
|
options={timestampOptions}
|
||||||
|
styles={dropdownStyles}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
placeholder={entityValuePlaceHolder}
|
||||||
|
value={entityValue}
|
||||||
|
onChange={onEntityValueChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
104
src/Explorer/Tabs/QueryTablesTab/QueryTableTabUtils.tsx
Normal file
104
src/Explorer/Tabs/QueryTablesTab/QueryTableTabUtils.tsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { IColumn, Selection } from "@fluentui/react";
|
||||||
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
import Explorer from "../../Explorer";
|
||||||
|
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
|
||||||
|
import * as Entities from "../../Tables/Entities";
|
||||||
|
import ClauseGroup from "../../Tables/QueryBuilder/ClauseGroup";
|
||||||
|
import QueryViewModel from "../../Tables/QueryBuilder/QueryViewModel";
|
||||||
|
import TabsBase from "../TabsBase";
|
||||||
|
import NewQueryTablesTab from "./QueryTablesTab";
|
||||||
|
export interface Button {
|
||||||
|
visible: boolean;
|
||||||
|
enabled: boolean;
|
||||||
|
isSelected?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IOption {
|
||||||
|
key: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
export interface IDocument {
|
||||||
|
partitionKey: string;
|
||||||
|
rowKey: string;
|
||||||
|
timeStamp: string;
|
||||||
|
}
|
||||||
|
export interface IQueryTablesTabComponentProps {
|
||||||
|
tabKind: ViewModels.CollectionTabKind;
|
||||||
|
title: string;
|
||||||
|
tabPath: string;
|
||||||
|
collection: ViewModels.CollectionBase;
|
||||||
|
node: ViewModels.TreeNode;
|
||||||
|
onLoadStartKey: number;
|
||||||
|
container: Explorer;
|
||||||
|
tabsBaseInstance: TabsBase;
|
||||||
|
queryTablesTab: NewQueryTablesTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IQueryTablesTabComponentStates {
|
||||||
|
tableEntityListViewModel: TableEntityListViewModel;
|
||||||
|
queryViewModel: QueryViewModel;
|
||||||
|
queryText: string;
|
||||||
|
selectedQueryText: string;
|
||||||
|
executeQueryButton: Button;
|
||||||
|
queryBuilderButton: Button;
|
||||||
|
queryTextButton: Button;
|
||||||
|
addEntityButton: Button;
|
||||||
|
editEntityButton: Button;
|
||||||
|
deleteEntityButton: Button;
|
||||||
|
isHelperActive: boolean;
|
||||||
|
columns: IColumn[];
|
||||||
|
items: IDocument[];
|
||||||
|
isExpanded: boolean;
|
||||||
|
isEditorActive: boolean;
|
||||||
|
selectedItems: Entities.ITableEntity[];
|
||||||
|
isValue: boolean;
|
||||||
|
isTimestamp: boolean;
|
||||||
|
isCustomLastTimestamp: boolean;
|
||||||
|
isCustomRangeTimestamp: boolean;
|
||||||
|
operators: string[];
|
||||||
|
selectMessage: string;
|
||||||
|
queryTableRows: IQueryTableRowsType[];
|
||||||
|
originalItems: IDocument[];
|
||||||
|
rowSelected: boolean;
|
||||||
|
selection: Selection;
|
||||||
|
entities: Entities.ITableEntity[];
|
||||||
|
headers: string[];
|
||||||
|
isLoading: boolean;
|
||||||
|
queryErrorMessage: string;
|
||||||
|
hasQueryError: boolean;
|
||||||
|
currentPage: number;
|
||||||
|
currentStartIndex: number;
|
||||||
|
fromDocument: number;
|
||||||
|
toDocument: number;
|
||||||
|
selectedItem: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IQueryTableRowsType {
|
||||||
|
isQueryTableEntityChecked: boolean;
|
||||||
|
isTimeStampSelected: boolean;
|
||||||
|
selectedOperator: string;
|
||||||
|
selectedField: string;
|
||||||
|
entityValue: string;
|
||||||
|
selectedEntityType: string;
|
||||||
|
selectedOperation: string;
|
||||||
|
selectedTimestamp: string;
|
||||||
|
fieldOptions: IOption[];
|
||||||
|
operatorOptions: IOption[];
|
||||||
|
entityTypeOptions: IOption[];
|
||||||
|
operationOptions: IOption[];
|
||||||
|
timestampOptions: IOption[];
|
||||||
|
id: string;
|
||||||
|
clauseGroup: ClauseGroup;
|
||||||
|
isLocal: boolean;
|
||||||
|
isTimestamp: boolean;
|
||||||
|
isValue: boolean;
|
||||||
|
isCustomRangeTimestamp: boolean;
|
||||||
|
customTimeValue: string;
|
||||||
|
timeValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getformattedOptions = (options: Array<string>): IOption[] => {
|
||||||
|
return options.map((option) => {
|
||||||
|
return { key: option, text: option };
|
||||||
|
});
|
||||||
|
};
|
||||||
43
src/Explorer/Tabs/QueryTablesTab/QueryTablesTab.tsx
Normal file
43
src/Explorer/Tabs/QueryTablesTab/QueryTablesTab.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import React from "react";
|
||||||
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
import Explorer from "../../Explorer";
|
||||||
|
import TableCommands from "../../Tables/DataTable/TableCommands";
|
||||||
|
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
|
||||||
|
import TabsBase from "../TabsBase";
|
||||||
|
import QueryTablesTabComponent from "./QueryTablesTabComponent";
|
||||||
|
import { IQueryTablesTabComponentProps } from "./QueryTableTabUtils";
|
||||||
|
|
||||||
|
interface QueryTablesTabProps {
|
||||||
|
container: Explorer;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NewQueryTablesTab extends TabsBase {
|
||||||
|
public iQueryTablesTabCompProps: IQueryTablesTabComponentProps;
|
||||||
|
public collection: ViewModels.Collection;
|
||||||
|
public tableEntityListViewModel: TableEntityListViewModel;
|
||||||
|
public tableCommands: TableCommands;
|
||||||
|
public container: Explorer;
|
||||||
|
|
||||||
|
constructor(options: ViewModels.TabOptions, props: QueryTablesTabProps) {
|
||||||
|
super(options);
|
||||||
|
this.container = props.container;
|
||||||
|
this.tableCommands = new TableCommands(props.container);
|
||||||
|
this.tableEntityListViewModel = new TableEntityListViewModel(this.tableCommands, this);
|
||||||
|
this.iQueryTablesTabCompProps = {
|
||||||
|
tabKind: options.tabKind,
|
||||||
|
title: options.title,
|
||||||
|
tabPath: options.tabPath,
|
||||||
|
collection: options.collection,
|
||||||
|
node: options.node,
|
||||||
|
onLoadStartKey: options.onLoadStartKey,
|
||||||
|
container: props.container,
|
||||||
|
tabsBaseInstance: this,
|
||||||
|
queryTablesTab: this,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public render(): JSX.Element {
|
||||||
|
return <QueryTablesTabComponent {...this.iQueryTablesTabCompProps} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NewQueryTablesTab;
|
||||||
1150
src/Explorer/Tabs/QueryTablesTab/QueryTablesTabComponent.tsx
Normal file
1150
src/Explorer/Tabs/QueryTablesTab/QueryTablesTabComponent.tsx
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user