Compare commits

..

51 Commits

Author SHA1 Message Date
sunilyadav840
cec6b27d2e Merge branch 'migrate_querytablestab__to_react' of https://github.com/Azure/cosmos-explorer into migrate_querytablestab__to_react 2021-09-28 12:03:04 +05:30
sunilyadav840
7d905159c6 Merge branch 'master' 2021-09-28 12:02:15 +05:30
vaidankarswapnil
126a572078 Replaced the svg as per master 2021-08-23 15:19:21 +05:30
sunilyadav840
597357e62d Merge branch 'migrate_querytablestab__to_react' of https://github.com/Azure/cosmos-explorer into migrate_querytablestab__to_react 2021-08-23 14:56:08 +05:30
vaidankarswapnil
6c545c454d Commented add.svg for time being to check build issue 2021-08-23 13:46:24 +05:30
vaidankarswapnil
d4817de14e Executed npm install 2021-08-23 13:15:10 +05:30
sunilyadav840
43b6e25e86 Merge branch 'migrate_querytablestab__to_react' of https://github.com/Azure/cosmos-explorer into migrate_querytablestab__to_react 2021-08-17 16:17:43 +05:30
vaidankarswapnil
30fa40c50e Row selection issue fix 2021-08-17 14:26:28 +05:30
vaidankarswapnil
1f2e528f46 Fixed issue of return type 2021-08-16 21:16:51 +05:30
sunilyadav840
e431692e0d Merge branch 'master' of https://github.com/Azure/cosmos-explorer into migrate_querytablestab__to_react 2021-08-16 18:57:40 +05:30
vaidankarswapnil
1985843b93 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into migrate_querytablestab__to_react 2021-08-16 18:54:30 +05:30
sunilyadav840
5287e6e29f fixed lint issue 2021-08-16 18:50:37 +05:30
vaidankarswapnil
27e99d7f2f Code clean up round 2 2021-08-16 18:36:22 +05:30
vaidankarswapnil
884f06a729 Mergedcode 2021-08-16 13:48:19 +05:30
vaidankarswapnil
b7cb0b55a6 Async and await implemented for loadEntities 2021-08-16 13:27:30 +05:30
sunilyadav840
c1c085f712 update test cases and snapshot 2021-08-12 17:20:09 +05:30
sunilyadav840
2b22e518e0 Fixed header missing issue 2021-08-12 15:22:07 +05:30
sunilyadav840
e95245f1df fixed filter update issue 2021-08-11 22:12:37 +05:30
vaidankarswapnil
046e6eb5a4 Resolved conflicts and Master merged 2021-08-11 13:13:46 +05:30
vaidankarswapnil
d7963b3ef4 Implemented pagination 2021-08-11 12:48:54 +05:30
vaidankarswapnil
d70dd6bc7e Implemented keydown events 2021-08-10 18:22:34 +05:30
sunilyadav840
87a368858c Add pagination view and fixed scroll issue 2021-08-10 16:21:13 +05:30
sunilyadav840
e1d32bde36 fixed eslint and compilation issue 2021-08-09 19:33:46 +05:30
sunilyadav840
91d86dd271 fixed failed test case 2021-08-09 19:06:37 +05:30
vaidankarswapnil
0df0c4b420 Code clean up round 1 2021-08-09 15:54:28 +05:30
vaidankarswapnil
07b3e30f05 Merge branch 'migrate_querytablestab__to_react' of https://github.com/Azure/cosmos-explorer into migrate_querytablestab__to_react 2021-08-09 14:23:05 +05:30
vaidankarswapnil
1931e775d9 Implemented changes to Cassandra filter 2021-08-09 14:22:35 +05:30
sunilyadav840
fe684fd6d2 set first item default selection 2021-08-09 14:12:01 +05:30
vaidankarswapnil
c681c14be1 Cleaned up QueryTablesComponent file 2021-08-06 13:10:31 +05:30
vaidankarswapnil
0cb897451d Merged Cassandra related implementation 2021-08-06 12:50:52 +05:30
vaidankarswapnil
18217b71c6 Spinners, SQLQuery and sorting implemented 2021-08-06 12:33:06 +05:30
sunilyadav840
d805a0ba4a fixed document selection issue 2021-08-06 10:18:38 +05:30
sunilyadav840
ec3ac87a20 load cassandra query documents 2021-08-05 19:19:54 +05:30
vaidankarswapnil
9a74a8c2ab Spinner for initial tab loading 2021-08-05 13:06:05 +05:30
vaidankarswapnil
8a1920714d Resolved few issues related to add and edit operations 2021-08-04 19:46:54 +05:30
vaidankarswapnil
7fa5883072 async await implemented 2021-08-04 19:09:18 +05:30
vaidankarswapnil
d062c85e94 Run query implemented 2021-08-04 18:12:41 +05:30
sunilyadav840
239c5f6895 load documents data 2021-08-04 18:06:52 +05:30
vaidankarswapnil
594c4026d5 Merged dynamic filter implementation 2021-08-03 11:59:48 +05:30
vaidankarswapnil
dc08ba740e Static Filter implemented 2021-08-03 11:37:23 +05:30
sunilyadav840
f1ffa968a7 Added dynamic data of clause entity 2021-08-02 15:26:11 +05:30
vaidankarswapnil
8ec551f6e6 Filter form merged 2021-07-30 14:57:41 +05:30
vaidankarswapnil
08d04295b1 Dynamic DetailsList and others implementation 2021-07-30 14:23:10 +05:30
sunilyadav840
13f94e83f0 Complete new cause entity list 2021-07-30 13:00:54 +05:30
vaidankarswapnil
4f632b234f DetailsList implemented 2021-07-29 12:15:21 +05:30
vaidankarswapnil
754f0b392c Commit code to share 2021-07-28 11:06:16 +05:30
vaidankarswapnil
3bce7f764e Few more changes 2021-07-26 13:47:50 +05:30
vaidankarswapnil
079593cec4 changes 2021-07-23 11:27:43 +05:30
vaidankarswapnil
27cc1337ef Converted HTMLs 2021-07-22 11:08:41 +05:30
vaidankarswapnil
bb5aecac1b Initial commit 2021-07-21 16:06:39 +05:30
vaidankarswapnil
c4a1d4cea7 Initial changes commit 2021-07-21 12:38:18 +05:30
55 changed files with 42506 additions and 3532 deletions

View File

@@ -44,6 +44,8 @@ src/Explorer/DataSamples/ContainerSampleGenerator.test.ts
src/Explorer/DataSamples/ContainerSampleGenerator.ts
src/Explorer/DataSamples/DataSamplesUtil.test.ts
src/Explorer/DataSamples/DataSamplesUtil.ts
src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts
src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.ts
src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.test.ts
src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.ts
src/Explorer/Graph/GraphExplorerComponent/EdgeInfoCache.ts
@@ -78,8 +80,8 @@ src/Explorer/Tables/DataTable/CacheBase.ts
src/Explorer/Tables/DataTable/DataTableBindingManager.ts
src/Explorer/Tables/DataTable/DataTableBuilder.ts
src/Explorer/Tables/DataTable/DataTableContextMenu.ts
src/Explorer/Tables/DataTable/DataTableOperationManager.ts
src/Explorer/Tables/DataTable/DataTableOperations.ts
# src/Explorer/Tables/DataTable/DataTableOperationManager.ts
# src/Explorer/Tables/DataTable/DataTableOperations.ts
src/Explorer/Tables/DataTable/DataTableViewModel.ts
src/Explorer/Tables/DataTable/TableCommands.ts
src/Explorer/Tables/DataTable/TableEntityCache.ts

View File

@@ -3,337 +3,358 @@
@import "../Common/Constants";
.query-panel {
display: table;
display: none;
width: 100%;
border-top: 1px solid #DDDDDD;
/*[{environment-commandbar-toolbar-separator}]*/
background-color: #ffffff;
/*[{plugin-background-color}]*/
padding: 2px 0px 0px 2px;
resize: vertical;
display: table;
display: none;
width: 100%;
border-top: 1px solid #dddddd;
/*[{environment-commandbar-toolbar-separator}]*/
background-color: #ffffff;
/*[{plugin-background-color}]*/
padding: 2px 0px 0px 2px;
resize: vertical;
}
.query-panel .row {
display: table-row;
display: table-row;
}
.query-panel .row .cell {
display: table-cell;
display: table-cell;
}
.query-panel.transition-in {
display: table;
top: 0px;
-webkit-transition: top 2s linear;
-ms-transition: top 2s linear;
-moz-transition: top 2s linear;
-khtml-transition: top 2s linear;
-o-transition: top 2s linear;
transition: top 2s linear;
display: table;
top: 0px;
-webkit-transition: top 2s linear;
-ms-transition: top 2s linear;
-moz-transition: top 2s linear;
-khtml-transition: top 2s linear;
-o-transition: top 2s linear;
transition: top 2s linear;
}
.query-builder {
width:100%;
padding-right: @DefaultSpace;
border-bottom: 1px solid @BaseMedium;
margin-bottom: @DefaultSpace;
width: 100%;
padding-right: @DefaultSpace;
border-bottom: 1px solid @BaseMedium;
margin-bottom: @DefaultSpace;
}
.query-builder-toolbar {
background-color: #ffffff;
/*[{plugin-background-color}]*/
min-width: 600px;
height: 30px;
border-bottom: 1px solid #DDDDDD;
/*[1px solid {environment-commandbar-toolbar-separator}]*/
background-color: #ffffff;
/*[{plugin-background-color}]*/
min-width: 600px;
height: 30px;
border-bottom: 1px solid #dddddd;
/*[1px solid {environment-commandbar-toolbar-separator}]*/
}
.query-builder-toolbar .query-toolbar-group {
display: inline-block;
height: 24px;
margin: 2px 0px;
vertical-align: middle;
display: inline-block;
height: 24px;
margin: 2px 0px;
vertical-align: middle;
}
.query-builder-toolbar .query-toolbar-group .query-toolbar-button {
min-width: 0px;
padding: 0px;
margin-left: 2px;
background-color: transparent;
border: solid transparent;
min-width: 0px;
padding: 0px;
margin-left: 2px;
background-color: transparent;
border: solid transparent;
}
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:active {
outline: 2px solid dodgerblue;
/*[2px solid {common-common-controls-button-border-hover}]*/
outline: 2px solid dodgerblue;
/*[2px solid {common-common-controls-button-border-hover}]*/
}
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:hover {
background-color: #CCCEDB;
/*[{common-controls-button-hover-background}]*/
background-color: #cccedb;
/*[{common-controls-button-hover-background}]*/
}
.query-builder-toolbar .query-toolbar-group .query-toolbar-button.active {
background-color: #E6E7ED;
/*[{common-controls-inner-tab-active-background}]*/
outline: none
background-color: #e6e7ed;
/*[{common-controls-inner-tab-active-background}]*/
outline: none;
}
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:disabled,
.query-builder-toolbar .query-toolbar-group .query-toolbar-button.disabled {
background-color: #ffffff;
/*[{common-controls-button-disabled-background}]*/
background: transparent;
border: 1px solid transparent;
outline: none;
opacity: 0.4;
background-color: #ffffff;
/*[{common-controls-button-disabled-background}]*/
background: transparent;
border: 1px solid transparent;
outline: none;
opacity: 0.4;
}
.tableContainer {
overflow: hidden;
.flex-display();
.flex-direction();
overflow: hidden;
.flex-display();
.flex-direction();
position: relative;
}
.tablesQueryTab{
padding-left: @MediumSpace;
width: 100%;
margin-bottom:@LargeSpace;
.tablesQueryTab {
padding-left: @MediumSpace;
width: 100%;
margin-bottom: 60px;
position: relative;
height: 100%;
}
.entity-error-Img {
width: @WarningErrorIconSize;
height: @WarningErrorIconSize;
margin: @DefaultSpace 0px 0px @SmallSpace;
width: @WarningErrorIconSize;
height: @WarningErrorIconSize;
margin: @DefaultSpace 0px 0px @SmallSpace;
}
.query-editor-panel {
margin-right: 16px;
margin-left: 16px;
margin-top: 25px;
position: relative;
vertical-align: middle;
cursor: default;
margin-right: 16px;
margin-left: 16px;
margin-top: 25px;
position: relative;
vertical-align: middle;
cursor: default;
}
.query-editor-text {
width: 100%;
margin: 2px;
border: solid 1px #A9ACB3;
/*[{plugin-textbox-disabled-color}]*/
resize: none;
margin-top: -39px;
background-color: #ddd;
padding: 5px;
width: 100%;
margin: 2px;
border: solid 1px #a9acb3;
/*[{plugin-textbox-disabled-color}]*/
resize: none;
margin-top: -39px;
background-color: #ddd;
padding: 5px;
}
.error-bar {
padding: @LargeSpace 34px @MediumSpace 24px;
padding: @LargeSpace 34px @MediumSpace 24px;
}
.error-message {
background-color: @BaseLow;
padding: @DefaultSpace;
display: inline-flex;
background-color: @BaseLow;
padding: @DefaultSpace;
display: inline-flex;
}
.error-text {
padding-left: @MediumSpace;
padding-left: @MediumSpace;
}
.query-editor-text-invalid {
width: 100%;
margin: 2px;
border: 1px solid #e51400;
resize: none;
margin-top: -30px;
width: 100%;
margin: 2px;
border: 1px solid #e51400;
resize: none;
margin-top: -30px;
}
.query-editor-panel .warning-bar {
width: 100%;
height: 20px;
background-color: #ffffff;
/*[{plugin-background-color}]*/
position: absolute;
top: -24px;
width: 100%;
height: 20px;
background-color: #ffffff;
/*[{plugin-background-color}]*/
position: absolute;
top: -24px;
}
.query-editor-panel .warning-bar .warning-message {
display: inline-flex;
padding-top: 2px;
vertical-align: middle;
display: inline-flex;
padding-top: 2px;
vertical-align: middle;
}
.query-editor-panel .warning-bar .warning-message .warning-text {
margin-left: 2px;
margin-left: 2px;
}
.advanced-options-panel{
margin-bottom: @DefaultSpace;
.advanced-options-panel {
margin-bottom: @DefaultSpace;
}
.advanced-options-panel .advanced-heading .advanced-title {
display: inline-flex;
margin-left: 27px;
margin-top: 10px;
cursor: default;
display: inline-flex;
margin-left: 27px;
margin-top: 10px;
cursor: default;
}
.advanced-options-panel .advanced-options {
margin-left: 32px;
margin-top: 5px;
border: 1px solid transparent;
margin-left: 32px;
margin-top: 5px;
border: 1px solid transparent;
}
hr {
margin-top: 10px;
margin-bottom: 12px;
border: 0;
border-top: 1px solid #ccc;
margin-top: 10px;
margin-bottom: 12px;
border: 0;
border-top: 1px solid #ccc;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
-webkit-appearance: none;
}
.advanced-options-panel .advanced-options .top .top-input {
width: 100px;
word-spacing: normal;
color: #1E1E1E;
/*[{common-controls-button-foreground}]*/
border: 1px solid #CCCEDB;
/*[1px solid {plugin-textbox-border-color}]*/
height: 20px;
margin-left: 8px;
width: 100px;
word-spacing: normal;
color: #1e1e1e;
/*[{common-controls-button-foreground}]*/
border: 1px solid #cccedb;
/*[1px solid {plugin-textbox-border-color}]*/
height: 20px;
margin-left: 8px;
}
.advanced-options-panel .advanced-options .top .invalid-top {
color: red;
color: red;
}
.advanced-options-panel .advanced-options .select {
margin-top: 18px;
display: inline-flex;
margin-top: 18px;
display: inline-flex;
}
.advanced-options-icon {
margin-left: 2px;
vertical-align: sub;
margin-left: 2px;
vertical-align: sub;
}
.advanced-options-panel .advanced-options .select .select-options-text {
margin-left: 4px;
margin-left: 4px;
}
.advanced-options-panel .advanced-options .select .select-options-link {
margin-left: 4px;
cursor: pointer;
outline: none;
margin-left: 4px;
cursor: pointer;
outline: none;
}
.query-panel .row .column-headers .Field {
padding-left: 95px;
padding-right: 0px;
padding-bottom: 6px;
padding-left: 95px;
padding-right: 0px;
padding-bottom: 6px;
}
.clause-table {
border-spacing: 0px;
display: table;
width: 100%;
margin-top: -3px;
border-spacing: 0px;
display: table;
width: 100%;
margin-top: -3px;
}
.clause-table-row {
display: row;
margin-bottom: 10px;
display: row;
margin-bottom: 10px;
}
.clause-table-cell {
display: table-cell;
text-align: left;
vertical-align: middle;
display: table-cell;
text-align: left;
vertical-align: middle;
}
.action-column>button,
.group-control-header>button,
.group-indicator-column>button {
min-width: 20px;
width: 20px;
padding: 0px;
background-color: transparent;
border-color: transparent;
cursor: pointer;
.action-column > button,
.group-control-header > button,
.group-indicator-column > button {
min-width: 20px;
width: 20px;
padding: 0px;
background-color: transparent;
border-color: transparent;
cursor: pointer;
}
.group-control-header>button:disabled {
min-width: 20px;
width: 20px;
padding: 0px;
background-color: transparent;
border-color: transparent;
outline: none;
opacity: 0.4;
cursor: default;
.group-control-header > button:disabled {
min-width: 20px;
width: 20px;
padding: 0px;
background-color: transparent;
border-color: transparent;
outline: none;
opacity: 0.4;
cursor: default;
}
.clause-table-field {
width: 100%;
border: 1px solid #bbbbbb;
width: 100%;
border: 1px solid #bbbbbb;
}
.clause-table-cell button {
height: 20px;
height: 20px;
}
.clause-table-cell input[type="checkbox"] {
padding: 0px;
margin-bottom: 12px;
padding: 0px;
margin-bottom: 12px;
}
.and-or-svg {
margin-top: -8px;
margin-right: -5px;
margin-top: -8px;
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 {
border-bottom: 1px transparent #DDD;
/*[1px solid {plugin-table-border-color}]*/
border-top: 1px transparent #DDD;
/*[1px solid {plugin-table-border-color}]*/
max-height: 20vh;
width: 100%;
border-bottom: 1px transparent #ddd;
/*[1px solid {plugin-table-border-color}]*/
border-top: 1px transparent #ddd;
/*[1px solid {plugin-table-border-color}]*/
max-height: 20vh;
width: 100%;
}
.scrollable {
overflow: auto;
overflow-x: hidden;
overflow: auto;
overflow-x: hidden;
}
.and-or-column,
.and-or-header {
min-width: 65px;
padding-right: 10px;
padding-left: 5px;
min-width: 65px;
padding-right: 10px;
padding-left: 5px;
}
.operator-column,
.operator-header {
min-width: 65px;
padding-right: 10px;
min-width: 65px;
padding-right: 10px;
}
.field-header,
.field-column {
min-width: 125px;
padding-right: 10px;
min-width: 125px;
padding-right: 10px;
}
.type-header,
.type-column {
min-width: 85px;
min-width: 85px;
}
.and-or-column,
@@ -345,41 +366,41 @@ input::-webkit-inner-spin-button {
.type-header,
.type-column,
.action-header {
padding-right: 10px;
margin-bottom: 8px;
padding-right: 10px;
margin-bottom: 8px;
}
.value-header,
.value-column,
.time-column {
min-width: 230px;
padding: 0px 4px 0px 0px;
width: 100%;
margin-bottom: 8px;
min-width: 230px;
padding: 0px 4px 0px 0px;
width: 100%;
margin-bottom: 8px;
}
.group-control-header,
.group-control-column {
min-width: 25px;
text-align: right;
min-width: 25px;
text-align: right;
}
.group-indicator-table {
border-spacing: 0px;
min-height: 24px
border-spacing: 0px;
min-height: 24px;
}
.group-indicator-column {
min-width: 21px;
padding: 0px;
border-style: none;
height: 29px;
min-width: 21px;
padding: 0px;
border-style: none;
height: 29px;
}
.clause-table-cell.action-column,
.clause-table-cell.action-column,
.clause-table-cell.action-header {
min-width: 60px;
padding-left: @SmallSpace;
min-width: 60px;
padding-left: @SmallSpace;
}
.action-header,
@@ -388,15 +409,14 @@ input::-webkit-inner-spin-button {
.operator-header,
.value-header,
.and-or-header {
padding-right: 4px;
padding-bottom: 5px;
padding-right: 4px;
padding-bottom: 5px;
}
.header-background {
background-color: #ffffff;
background-color: #ffffff;
}
/*.type-header {
padding-right: 4px;
}
@@ -410,111 +430,165 @@ input::-webkit-inner-spin-button {
}*/
.clause-table-field[readonly] {
background-color: #EEEEF2;
/*[{plugin-table-header-background-color}]*/
border: 1px solid #CCCEDB;
/*[{plugin-table-border-color}]*/
background-color: #eeeef2;
/*[{plugin-table-header-background-color}]*/
border: 1px solid #cccedb;
/*[{plugin-table-border-color}]*/
}
.addClause-title {
/*[{common-common-controls-button-border-hover}]*/
cursor: pointer;
margin-left: -5px;
/*[{common-common-controls-button-border-hover}]*/
cursor: pointer;
margin-left: -5px;
}
.addClause {
width: 125px;
padding: 8px 0px 5px 5px;
border: 1px solid #fff;
margin-left: 5px;
width: 125px;
padding: 8px 0px 5px 5px;
border: 1px solid #fff;
margin-left: 5px;
}
.addClause:hover {
.hover();
.hover();
}
.addClause:active {
.active();
border: 1px dashed @AccentMedium;
.active();
border: 1px dashed @AccentMedium;
}
.clause-table-row {
min-width: 550px;
width: 100%;
min-width: 550px;
width: 100%;
}
.clause-table-field field-column {
min-width: 75px;
height: 30px;
width: 100%;
min-width: 75px;
height: 30px;
width: 100%;
}
.clause-table-field field-input {
min-width: 54px;
margin-left: -78px;
height: 25px;
border: none;
min-width: 54px;
margin-left: -78px;
height: 25px;
border: none;
}
.query-panel .row .spacing {
padding-bottom: 6px;
padding-bottom: 6px;
}
.query-panel .divider.horizontal {
height: 10px;
width: 100%
height: 10px;
width: 100%;
}
.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 {
width: 14px;
height: 14px;
margin-left: 3px;
margin-bottom: 8px;
width: 14px;
height: 14px;
margin-left: 3px;
margin-bottom: 8px;
margin-top: 8px;
}
.addclauseProperty-Img {
width: 14px;
height: 14px;
margin-bottom: 5px;
margin-left: 12px;
width: 14px;
height: 14px;
margin-bottom: 5px;
margin-left: 12px;
}
.entity-Add-Cancel {
padding: @DefaultSpace @SmallSpace @SmallSpace;
cursor: pointer;
padding: @DefaultSpace @SmallSpace @SmallSpace;
cursor: pointer;
}
.entity-Add-Cancel:hover {
.hover();
.hover();
}
.entity-Add-Cancel:active {
.active();
.active();
}
.query-builder-isDisabled {
border: 1px solid #CCCEDB;
color: #ccc;
border: 1px solid #cccedb;
color: #ccc;
}
.edit-value-text {
padding-left: @DefaultSpace;
padding-left: @DefaultSpace;
}
.expand-triangle {
width: 10px;
height: 10px;
width: 10px;
height: 10px;
}
.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) {
@@ -524,4 +598,4 @@ input::-webkit-inner-spin-button {
width: 100%;
padding-top: 10px;
}
}*/
}*/

34145
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,5 @@
import * as ko from "knockout";
import * as ReactBindingHandler from "./ReactBindingHandler";
import "../Explorer/Tables/DataTable/DataTableBindingManager";
export class BindingHandlersRegisterer {
public static registerBindingHandlers() {
ko.bindingHandlers.setTemplateReady = {

View File

@@ -339,11 +339,9 @@ export enum ConflictOperationType {
}
export enum ConnectionStatusType {
Connect = "Connect",
Connecting = "Connecting",
Connected = "Connected",
Failed = "Connection Failed",
ReConnect = "Reconnect",
}
export const EmulatorMasterKey =
@@ -355,32 +353,15 @@ export const StyleConstants = require("less-vars-loader!../../less/Common/Consta
export class Notebook {
public static readonly defaultBasePath = "./notebooks";
public static readonly heartbeatDelayMs = 60000;
public static readonly heartbeatDelayMs = 5000;
public static readonly kernelRestartInitialDelayMs = 1000;
public static readonly kernelRestartMaxDelayMs = 20000;
public static readonly autoSaveIntervalMs = 120000;
public static readonly memoryGuageToGB = 1048576;
public static readonly temporarilyDownMsg = "Notebooks is currently not available. We are working on it.";
public static readonly mongoShellTemporarilyDownMsg =
"We have identified an issue with the Mongo Shell and it is unavailable right now. We are actively working on the mitigation.";
public static readonly cassandraShellTemporarilyDownMsg =
"We have identified an issue with the Cassandra Shell and it is unavailable right now. We are actively working on the mitigation.";
public static saveNotebookModalTitle = "Save Notebook in temporary workspace";
public static saveNotebookModalContent =
"This notebook will be saved in the temporary workspace and will be removed when the session expires. To save your work permanently, save your notebooks to a GitHub repository or download the notebooks to your local machine before the session ends.";
public static newNotebookModalTitle = "Create Notebook in temporary workspace";
public static newNotebookUploadModalTitle = "Upload Notebook in temporary workspace";
public static newNotebookModalContent1 =
"A temporary workspace will be created to enable you to work with notebooks. When the session expires, any notebooks in the workspace will be removed.";
public static newNotebookModalContent2 =
"To save your work permanently, save your notebooks to a GitHub repository or download the notebooks to your local machine before the session ends. ";
public static galleryNotebookDownloadContent1 =
"To download, run, and make changes to this sample notebook, a temporary workspace will be created. When the session expires, any notebooks in the workspace will be removed.";
public static galleryNotebookDownloadContent2 =
"To save your work permanently, save your notebooks to a GitHub repository or download the Notebooks to your local machine before the session ends. ";
public static cosmosNotebookHomePageUrl = "https://aka.ms/cosmos-notebooks-limits";
public static cosmosNotebookGitDocumentationUrl = "https://aka.ms/cosmos-notebooks-github";
public static learnMore = "Learn more.";
}
export class SparkLibrary {

View File

@@ -54,7 +54,7 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
<TextField
label={entityValueLabel && entityValueLabel}
className="addEntityTextField"
id="entityValueId"
// id="entityValueId"
autoFocus
disabled={isEntityValueDisable}
type={entityValueType}

View File

@@ -95,7 +95,6 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
<Stack horizontal tokens={sectionStackTokens}>
<TextField
label={entityPropertyLabel && entityPropertyLabel}
id="entityPropertyId"
autoFocus
disabled={isPropertyTypeDisable}
placeholder={entityPropertyPlaceHolder}
@@ -109,7 +108,6 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
onChange={onEntityTypeChange}
options={options}
disabled={isPropertyTypeDisable}
id="entityTypeId"
styles={dropdownStyles}
/>
<EntityValue

View File

@@ -13,7 +13,6 @@ import {
Link,
PrimaryButton,
ProgressIndicator,
Text,
TextField,
} from "@fluentui/react";
import React, { FC } from "react";
@@ -31,7 +30,6 @@ export interface DialogState {
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
contentHtml?: JSX.Element,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps,
primaryButtonDisabled?: boolean
@@ -60,7 +58,6 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
contentHtml?: JSX.Element,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps,
primaryButtonDisabled?: boolean
@@ -79,7 +76,6 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
get().closeDialog();
onCancel && onCancel();
},
contentHtml,
choiceGroupProps,
textFieldProps,
primaryButtonDisabled,
@@ -128,7 +124,6 @@ export interface DialogProps {
type?: DialogType;
showCloseButton?: boolean;
onDismiss?: () => void;
contentHtml?: JSX.Element;
}
const DIALOG_MIN_WIDTH = "400px";
@@ -155,7 +150,6 @@ export const Dialog: FC = () => {
type,
showCloseButton,
onDismiss,
contentHtml,
} = props || {};
const dialogProps: IDialogProps = {
@@ -197,7 +191,6 @@ export const Dialog: FC = () => {
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
</Link>
)}
{contentHtml && <Text>{contentHtml}</Text>}
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
<DialogFooter>
<PrimaryButton {...primaryButtonProps} />

View File

@@ -17,8 +17,6 @@ import Explorer from "../../Explorer";
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
import { NotebookUtil } from "../../Notebook/NotebookUtil";
import { useNotebook } from "../../Notebook/useNotebook";
import { Dialog, TextFieldProps, useDialog } from "../Dialog";
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
import "./NotebookViewerComponent.less";
@@ -148,9 +146,7 @@ export class NotebookViewerComponent
<NotebookMetadataComponent
data={this.state.galleryItem}
isFavorite={this.state.isFavorite}
downloadButtonText={
this.props.container && NotebookUtil.getNotebookBtnTitle(useNotebook.getState().notebookFolderName)
}
downloadButtonText={this.props.container && "Download to my notebooks"}
onTagClick={this.props.onTagClick}
onFavoriteClick={this.favoriteItem}
onUnfavoriteClick={this.unfavoriteItem}

View File

@@ -1,11 +1,9 @@
import { Link } from "@fluentui/react/lib/Link";
import * as ko from "knockout";
import React from "react";
import _ from "underscore";
import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import * as Constants from "../Common/Constants";
import { ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases";
import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility";
@@ -13,7 +11,6 @@ import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHand
import * as Logger from "../Common/Logger";
import { QueriesClient } from "../Common/QueriesClient";
import * as DataModels from "../Contracts/DataModels";
import { ContainerConnectionInfo } from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
import { useSidePanel } from "../hooks/useSidePanel";
@@ -166,10 +163,23 @@ export default class Explorer {
useNotebook.subscribe(
async () => {
this.initiateAndRefreshNotebookList();
useNotebook.getState().setIsRefreshed(false);
if (!this.notebookManager) {
const NotebookManager = await (
await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager")
).default;
this.notebookManager = new NotebookManager();
this.notebookManager.initialize({
container: this,
resourceTree: this.resourceTree,
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
refreshNotebookList: () => this.refreshNotebookList(),
});
}
this.refreshCommandBarButtons();
this.refreshNotebookList();
},
(state) => state.isNotebookEnabled || state.isRefreshed
(state) => state.isNotebookEnabled
);
this.resourceTree = new ResourceTreeAdapter(this);
@@ -202,23 +212,6 @@ export default class Explorer {
this.refreshExplorer();
}
public async initiateAndRefreshNotebookList(): Promise<void> {
if (!this.notebookManager) {
const NotebookManager = (await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager"))
.default;
this.notebookManager = new NotebookManager();
this.notebookManager.initialize({
container: this,
resourceTree: this.resourceTree,
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
refreshNotebookList: () => this.refreshNotebookList(),
});
}
this.refreshCommandBarButtons();
this.refreshNotebookList();
}
public openEnableSynapseLinkDialog(): void {
const addSynapseLinkDialogProps: DialogProps = {
linkProps: {
@@ -352,7 +345,23 @@ export default class Explorer {
return;
}
this._isInitializingNotebooks = true;
if (userContext.features.phoenix === false) {
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,
@@ -367,59 +376,13 @@ export default class Explorer {
});
}
useNotebook.getState().initializeNotebooksTree(this.notebookManager);
this.refreshNotebookList();
this._isInitializingNotebooks = false;
}
public async allocateContainer(): Promise<void> {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
const isAllocating = useNotebook.getState().isAllocating;
if (isAllocating === false && notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined) {
const provisionData = {
aadToken: userContext.authorizationToken,
subscriptionId: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
dbAccountName: userContext.databaseAccount.name,
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
};
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Connecting,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
try {
useNotebook.getState().setIsAllocating(true);
const connectionInfo = await this.phoenixClient.containerConnectionInfo(provisionData);
if (
connectionInfo.status === HttpStatusCodes.OK &&
connectionInfo.data &&
connectionInfo.data.notebookServerUrl
) {
connectionStatus.status = ConnectionStatusType.Connected;
useNotebook.getState().setConnectionInfo(connectionStatus);
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl,
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
});
this.notebookManager?.notebookClient
.getMemoryUsage()
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
useNotebook.getState().setIsAllocating(false);
} else {
connectionStatus.status = ConnectionStatusType.Failed;
useNotebook.getState().resetConatinerConnection(connectionStatus);
}
} catch (error) {
connectionStatus.status = ConnectionStatusType.Failed;
useNotebook.getState().resetConatinerConnection(connectionStatus);
throw error;
}
this.refreshNotebookList();
this._isInitializingNotebooks = false;
}
}
public resetNotebookWorkspace(): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
handleError(
@@ -691,9 +654,6 @@ export default class Explorer {
if (!notebookContentItem || !notebookContentItem.path) {
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
}
if (notebookContentItem.type === NotebookContentItemType.Notebook && NotebookUtil.isPhoenixEnabled()) {
this.allocateContainer();
}
const notebookTabs = useTabs
.getState()
@@ -915,51 +875,9 @@ export default class Explorer {
handleError(error, "Explorer/onNewNotebookClicked");
throw new Error(error);
}
const isPhoenixEnabled = NotebookUtil.isPhoenixEnabled();
if (isPhoenixEnabled) {
if (isGithubTree) {
async () => {
await this.allocateContainer();
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
};
} else {
useDialog.getState().showOkCancelModalDialog(
Notebook.newNotebookModalTitle,
undefined,
"Create",
async () => {
await this.allocateContainer();
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
},
"Cancel",
undefined,
this.getNewNoteWarningText()
);
}
} else {
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
}
}
private getNewNoteWarningText(): JSX.Element {
return (
<>
<p>{Notebook.newNotebookModalContent1}</p>
<br />
<p>
{Notebook.newNotebookModalContent2}
<Link href={Notebook.cosmosNotebookHomePageUrl} target="_blank">
{Notebook.learnMore}
</Link>
</p>
</>
);
}
parent = parent || this.resourceTree.myNotebooksContentRoot;
private createNewNoteBook(parent?: NotebookContentItem, isGithubTree?: boolean): void {
const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, {
dataExplorerArea: Constants.Areas.Notebook,
@@ -1006,26 +924,7 @@ export default class Explorer {
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
}
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
if (NotebookUtil.isPhoenixEnabled()) {
await this.allocateContainer();
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
this.connectToNotebookTerminal(kind);
} else {
useDialog
.getState()
.showOkModalDialog(
"Failed to Connect",
"Failed to connect temporary workspace, this could happen because of network issue please refresh and try again."
);
}
} else {
this.connectToNotebookTerminal(kind);
}
}
private connectToNotebookTerminal(kind: ViewModels.TerminalKind): void {
public openNotebookTerminal(kind: ViewModels.TerminalKind): void {
let title: string;
switch (kind) {
@@ -1076,7 +975,7 @@ export default class Explorer {
notebookUrl?: string,
galleryItem?: IGalleryItem,
isFavorite?: boolean
): Promise<void> {
) {
const title = "Gallery";
const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default;
const galleryTab = useTabs
@@ -1180,27 +1079,7 @@ export default class Explorer {
}
public openUploadFilePanel(parent?: NotebookContentItem): void {
if (NotebookUtil.isPhoenixEnabled()) {
useDialog.getState().showOkCancelModalDialog(
Notebook.newNotebookUploadModalTitle,
undefined,
"Upload",
async () => {
await this.allocateContainer();
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.uploadFilePanel(parent);
},
"Cancel",
undefined,
this.getNewNoteWarningText()
);
} else {
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.uploadFilePanel(parent);
}
}
private uploadFilePanel(parent?: NotebookContentItem): void {
parent = parent || this.resourceTree.myNotebooksContentRoot;
useSidePanel
.getState()
.openSidePanel(
@@ -1209,24 +1088,6 @@ export default class Explorer {
);
}
public getDownloadModalConent(fileName: string): JSX.Element {
if (NotebookUtil.isPhoenixEnabled()) {
return (
<>
<p>{Notebook.galleryNotebookDownloadContent1}</p>
<br />
<p>
{Notebook.galleryNotebookDownloadContent2}
<Link href={Notebook.cosmosNotebookGitDocumentationUrl} target="_blank">
{Notebook.learnMore}
</Link>
</p>
</>
);
}
return <p> Download {fileName} from gallery as a copy to your notebooks to run and/or edit the notebook. </p>;
}
public async refreshExplorer(): Promise<void> {
userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken()

View File

@@ -6,7 +6,7 @@ describe("Cache arrays by key", () => {
const key = "key";
cache.insert(key, 1, 0);
cache.clear();
expect(cache.retrieve(key, 0, 1)).toBe(undefined);
expect(cache.retrieve(key, 0, 1)).toBe(null);
});
it("should invalidate oldest key to keep cache size under maximum", () => {
@@ -21,7 +21,7 @@ describe("Cache arrays by key", () => {
cache.insert(key1, 2, 4);
expect(cache.retrieve(key1, 0, 3)).toEqual([0, 2, 4]);
expect(cache.retrieve(key2, 1, 1)).toEqual(undefined);
expect(cache.retrieve(key2, 1, 1)).toEqual(null);
});
it("should cache and retrieve cached page within boundaries", () => {
@@ -39,7 +39,7 @@ describe("Cache arrays by key", () => {
const key = "key";
cache.insert(key, 0, 0);
cache.insert(key, 1, 1);
expect(cache.retrieve(key, 2, 1)).toEqual(undefined);
expect(cache.retrieve(key, 2, 1)).toEqual(null);
});
it("should not retrieve cached page overlapping boundaries", () => {
@@ -48,7 +48,7 @@ describe("Cache arrays by key", () => {
cache.insert(key, 0, 0);
cache.insert(key, 1, 1);
cache.insert(key, 2, 2);
expect(cache.retrieve(key, 2, 4)).toEqual(undefined);
expect(cache.retrieve(key, 2, 4)).toEqual(null);
});
it("should not insert non-contiguous element", () => {
@@ -57,7 +57,7 @@ describe("Cache arrays by key", () => {
cache.insert(key, 0, 0);
cache.insert(key, 1, 1);
cache.insert(key, 3, 3);
expect(cache.retrieve(key, 3, 1)).toEqual(undefined);
expect(cache.retrieve(key, 3, 1)).toEqual(null);
});
it("should cache multiple keys", () => {

View File

@@ -61,12 +61,12 @@ export class ArraysByKeyCache<T> {
* @param pageSize
*/
public retrieve(key: string, startIndex: number, pageSize: number): T[] | null {
if (!Object.prototype.hasOwnProperty.call(this.cache, key)) {
return undefined;
if (!this.cache.hasOwnProperty(key)) {
return null;
}
const elements = this.cache[key];
if (startIndex + pageSize > elements.length) {
return undefined;
return null;
}
return elements.slice(startIndex, startIndex + pageSize);

View File

@@ -736,7 +736,7 @@ export class D3ForceGraph implements GraphRenderer {
.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)
// this is the <g> element
self.onNodeClicked(this.parentNode, d);
return self.onNodeClicked(this.parentNode, d);
})
.on("click", function (this: Element, _: MouseEvent, d: D3Node) {
// this is the <g> element

View File

@@ -12,7 +12,6 @@ import { useTabs } from "../../../hooks/useTabs";
import { userContext } from "../../../UserContext";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { NotebookUtil } from "../../Notebook/NotebookUtil";
import { useSelectedNode } from "../../useSelectedNode";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
import * as CommandBarUtil from "./CommandBarUtil";
@@ -56,15 +55,15 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
if (NotebookUtil.isPhoenixEnabled()) {
uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus(container, "connectionStatus"));
}
if (
userContext.features.phoenix === false &&
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) {
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker"));
}

View File

@@ -596,7 +596,7 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
return {
iconSrc: GitHubIcon,
iconAlt: label,
onCommandClick: () => {
onCommandClick: () =>
useSidePanel
.getState()
.openSidePanel(
@@ -606,8 +606,7 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
gitHubClientProp={container.notebookManager.gitHubClient}
junoClientProp={junoClient}
/>
);
},
),
commandButtonLabel: label,
hasPopup: false,
disabled: false,

View File

@@ -13,7 +13,6 @@ import { StyleConstants } from "../../../Common/Constants";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { ConnectionStatus } from "./ConnectionStatusComponent";
import { MemoryTracker } from "./MemoryTrackerComponent";
@@ -204,9 +203,9 @@ export const createMemoryTracker = (key: string): ICommandBarItemProps => {
};
};
export const createConnectionStatus = (container: Explorer, key: string): ICommandBarItemProps => {
export const createConnectionStatus = (key: string): ICommandBarItemProps => {
return {
key,
onRender: () => <ConnectionStatus container={container} />,
onRender: () => <ConnectionStatus />,
};
};

View File

@@ -3,182 +3,77 @@
.connectionStatusContainer {
cursor: default;
align-items: center;
margin: 0 9px;
border: 1px;
min-height: 44px;
> span {
padding-right: 12px;
font-size: 12px;
font-size: 13px;
font-family: @DataExplorerFont;
color: @DefaultFontColor;
}
&:focus{
outline: 0px;
}
}
.commandReactBtn {
&:hover {
background-color: rgb(238, 247, 255);
color: rgb(32, 31, 30);
cursor: pointer;
}
&:focus{
outline: 1px dashed #605e5c;
}
.connectionStatusFailed{
color: #bd1919;
}
.connectedReactBtn {
&:hover {
background-color: rgb(238, 247, 255);
color: rgb(32, 31, 30);
cursor: pointer;
}
&:focus{
outline: 0px;
}
}
.connectIcon{
margin: 0px 4px;
height: 18px;
width: 18px;
color: rgb(0, 120, 212);
}
.status {
.ring-container {
position: relative;
display: block;
margin-right: 8px;
width: 1em;
height: 1em;
font-size: 9px!important;
padding: 0px!important;
border-radius: 0.5em;
}
.status::before,
.status::after {
}
.ringringGreen {
border: 3px solid green;
border-radius: 30px;
height: 18px;
width: 18px;
position: absolute;
content: "";
}
.status::before {
top: 0;
left: 0;
width: 1em;
height: 1em;
background-color: rgba(#fff, 0.1);
border-radius: 100%;
opacity: 1;
transform: translate3d(0, 0, 0) scale(0);
}
.connected{
background-color: green;
box-shadow:
0 0 0 0em rgba(green, 0),
0em 0.05em 0.1em rgba(#000000, 0.2);
transform: translate3d(0, 0, 0) scale(1);
}
.connecting{
background-color:#ffbf00;
box-shadow:
0 0 0 0em rgba(#ffbf00, 0),
0em 0.05em 0.1em rgba(#000000, 0.2);
transform: translate3d(0, 0, 0) scale(1);
}
.failed{
background-color:#bd1919;
box-shadow:
0 0 0 0em rgba(#bd1919, 0),
0em 0.05em 0.1em rgba(#000000, 0.2);
transform: translate3d(0, 0, 0) scale(1);
}
.status.connecting.is-animating {
animation: status-outer-connecting 3000ms infinite;
}
.status.failed.is-animating {
animation: status-outer-failed 3000ms infinite;
}
.status.connected.is-animating {
animation: status-outer-connected 3000ms infinite;
}
@keyframes status-outer-connected {
0% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #008000, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
}
20% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.6), 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
}
40% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.5), 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
}
60% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
}
80% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
}
85% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
}
}
@keyframes status-outer-failed {
0% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #bd1919, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
}
20% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #c52d2d, 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
}
40% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #b47b7b, 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
}
60% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
}
80% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
}
85% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
}
}
@keyframes status-outer-connecting {
0% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #ffbf00, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
}
20% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em #f0dfad, 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
}
40% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(198, 243, 198, 0.5), 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
}
60% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(213, 241, 213, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
}
80% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
}
85% {
transform: translate3d(0, 0, 0) scale(1);
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
}
}
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;
}

View File

@@ -1,21 +1,17 @@
import { Icon, ProgressIndicator, Stack, TooltipHost } from "@fluentui/react";
import { ActionButton } from "@fluentui/react/lib/Button";
import * as React from "react";
import "../../../../less/hostedexplorer.less";
import { ConnectionStatusType, Notebook } from "../../../Common/Constants";
import Explorer from "../../Explorer";
import { ConnectionStatusType } from "../../../Common/Constants";
import { useNotebook } from "../../Notebook/useNotebook";
import "../CommandBar/ConnectionStatusComponent.less";
interface Props {
container: Explorer;
}
export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Element => {
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("");
const [toolTipContent, setToolTipContent] = React.useState("Connect to temporary workspace.");
const [statusColor, setStatusColor] = React.useState("locationYellowDot");
const [statusColorAnimation, setStatusColorAnimation] = React.useState("ringringYellow");
const toolTipContent = "Hosted runtime status.";
React.useEffect(() => {
let intervalId: NodeJS.Timeout;
@@ -43,65 +39,34 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
};
const connectionInfo = useNotebook((state) => state.connectionInfo);
const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo);
const totalGB = memoryUsageInfo ? memoryUsageInfo.totalKB / Notebook.memoryGuageToGB : 0;
const usedGB = totalGB > 0 ? totalGB - memoryUsageInfo.freeKB / Notebook.memoryGuageToGB : 0;
if (
connectionInfo &&
(connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.ReConnect)
) {
return (
<ActionButton className="commandReactBtn" onClick={() => container.allocateContainer()}>
<TooltipHost content={toolTipContent}>
<Stack className="connectionStatusContainer" horizontal>
<Icon iconName="ConnectVirtualMachine" className="connectIcon" />
<span>{connectionInfo.status}</span>
</Stack>
</TooltipHost>
</ActionButton>
);
if (!connectionInfo) {
return <></>;
}
if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connecting && isActive === false) {
setIsActive(true);
setStatusColor("status connecting is-animating");
setToolTipContent("Connecting to temporary workspace.");
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connected && isActive === true) {
stopTimer();
setStatusColor("status connected is-animating");
setToolTipContent("Connected to temporary workspace.");
setStatusColor("locationGreenDot");
setStatusColorAnimation("ringringGreen");
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Failed && isActive === true) {
stopTimer();
setStatusColor("status failed is-animating");
setToolTipContent("Click here to Reconnect to temporary workspace.");
setStatusColor("locationRedDot");
setStatusColorAnimation("ringringRed");
}
return (
<ActionButton
className={connectionInfo.status === ConnectionStatusType.Failed ? "commandReactBtn" : "connectedReactBtn"}
onClick={(e: React.MouseEvent<HTMLSpanElement>) =>
connectionInfo.status === ConnectionStatusType.Failed ? container.allocateContainer() : e.preventDefault()
}
>
<TooltipHost content={toolTipContent}>
<Stack className="connectionStatusContainer" horizontal>
<i className={statusColor}></i>
<span className={connectionInfo.status === ConnectionStatusType.Failed ? "connectionStatusFailed" : ""}>
{connectionInfo.status}
</span>
{connectionInfo.status === ConnectionStatusType.Connecting && isActive && (
<ProgressIndicator description={minute + ":" + second} />
)}
{connectionInfo.status === ConnectionStatusType.Connected && !isActive && (
<ProgressIndicator
className={usedGB / totalGB > 0.8 ? "lowMemory" : ""}
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
percentComplete={usedGB / totalGB}
/>
)}
</Stack>
</TooltipHost>
</ActionButton>
<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>
);
};

View File

@@ -2,15 +2,12 @@
* Notebook container related stuff
*/
import * as Constants from "../../Common/Constants";
import { ConnectionStatusType } from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import * as DataModels from "../../Contracts/DataModels";
import { ContainerConnectionInfo } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext";
import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { NotebookUtil } from "./NotebookUtil";
import { useNotebook } from "./useNotebook";
export class NotebookContainerClient {
@@ -45,7 +42,7 @@ export class NotebookContainerClient {
}, delayMs);
}
public async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
private async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
const error = "No server endpoint detected";
@@ -78,12 +75,6 @@ export class NotebookContainerClient {
freeKB: memoryUsageInfo.free,
};
}
} else if (NotebookUtil.isPhoenixEnabled()) {
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.ReConnect,
};
useNotebook.getState().resetConatinerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(true);
}
return undefined;
} catch (error) {
@@ -93,13 +84,6 @@ export class NotebookContainerClient {
"Connection lost with Notebook server. Attempting to reconnect..."
);
}
if (NotebookUtil.isPhoenixEnabled()) {
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Failed,
};
useNotebook.getState().resetConatinerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(true);
}
this.onConnectionLost();
return undefined;
}

View File

@@ -212,7 +212,6 @@ export default class NotebookManager {
"Cancel",
() => reject(new Error("Commit dialog canceled")),
undefined,
undefined,
{
label: "Commit message",
autoAdjustHeight: true,

View File

@@ -3,7 +3,6 @@ import { AppState, selectors } from "@nteract/core";
import domtoimage from "dom-to-image";
import Html2Canvas from "html2canvas";
import path from "path";
import { userContext } from "../../UserContext";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import * as StringUtils from "../../Utils/StringUtils";
import { SnapshotFragment } from "./NotebookComponent/types";
@@ -329,16 +328,4 @@ export class NotebookUtil {
link.click();
document.body.removeChild(link);
}
public static getNotebookBtnTitle(fileName: string): string {
if (this.isPhoenixEnabled()) {
return `Download to ${fileName}`;
} else {
return `Download to my notebooks`;
}
}
public static isPhoenixEnabled(): boolean {
return userContext.features.notebooksTemporarilyDown === false && userContext.features.phoenix === true;
}
}

View File

@@ -2,12 +2,10 @@ import { cloneDeep } from "lodash";
import create, { UseStore } from "zustand";
import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants";
import { ConnectionStatusType } from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { configContext } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import { ContainerConnectionInfo } from "../../Contracts/DataModels";
import { IPinnedRepo } from "../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
@@ -16,7 +14,6 @@ import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import NotebookManager from "./NotebookManager";
import { NotebookUtil } from "./NotebookUtil";
interface NotebookState {
isNotebookEnabled: boolean;
@@ -31,10 +28,8 @@ interface NotebookState {
myNotebooksContentRoot: NotebookContentItem;
gitHubNotebooksContentRoot: NotebookContentItem;
galleryContentRoot: NotebookContentItem;
connectionInfo: ContainerConnectionInfo;
connectionInfo: DataModels.ContainerConnectionInfo;
notebookFolderName: string;
isAllocating: boolean;
isRefreshed: boolean;
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
@@ -51,10 +46,7 @@ interface NotebookState {
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void;
initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<void>;
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => void;
setIsAllocating: (isAllocating: boolean) => void;
resetConatinerConnection: (connectionStatus: ContainerConnectionInfo) => void;
setIsRefreshed: (isAllocating: boolean) => void;
setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => void;
}
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
@@ -77,12 +69,8 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined,
galleryContentRoot: undefined,
connectionInfo: {
status: ConnectionStatusType.Connect,
},
connectionInfo: undefined,
notebookFolderName: undefined,
isAllocating: false,
isRefreshed: false,
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
@@ -187,7 +175,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
},
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
const notebookFolderName = NotebookUtil.isPhoenixEnabled() === true ? "Temporary Notebooks" : "My Notebooks";
const notebookFolderName = userContext.features.phoenix === true ? "Temporary Notebooks" : "My Notebooks";
set({ notebookFolderName });
const myNotebooksContentRoot = {
name: get().notebookFolderName,
@@ -268,15 +256,5 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
set({ gitHubNotebooksContentRoot });
}
},
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => set({ connectionInfo }),
setIsAllocating: (isAllocating: boolean) => set({ isAllocating }),
resetConatinerConnection: (connectionStatus: ContainerConnectionInfo): void => {
useNotebook.getState().setConnectionInfo(connectionStatus);
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: undefined,
authToken: undefined,
});
useNotebook.getState().setIsAllocating(false);
},
setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }),
setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => set({ connectionInfo }),
}));

View File

@@ -77,7 +77,7 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
selectedLocation.repo
)} - ${selectedLocation.branch}`;
} else if (selectedLocation.type === "MyNotebooks" && userContext.features.phoenix) {
destination = useNotebook.getState().notebookFolderName;
destination = "My Notebooks Scratch";
}
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`);

View File

@@ -4,7 +4,7 @@ import React from "react";
import TableListViewModal from "../../Tables/DataTable/TableEntityListViewModel";
import * as Entities from "../../Tables/Entities";
import { CassandraAPIDataClient, TablesAPIDataClient } from "../../Tables/TableDataClient";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import QueryTablesTab from "../../Tabs/QueryTablesTab/QueryTablesTab";
import { AddTableEntityPanel } from "./AddTableEntityPanel";
describe("Excute Add Table Entity Pane", () => {
@@ -18,6 +18,8 @@ describe("Excute Add Table Entity Pane", () => {
queryTablesTab: fakeQueryTablesTab,
tableEntityListViewModel: fakeTableEntityListViewModel,
cassandraApiClient: fakeCassandraApiClient,
reloadEntities: () => "{}",
headerItems: ["email"],
};
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", () => {
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", () => {
const wrapper = mount(<AddTableEntityPanel {...props} />);
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", () => {
@@ -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.
wrapper.find(".addButtonEntiy").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);
});
});

View File

@@ -1,20 +1,19 @@
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import React, { FunctionComponent, useEffect, useState } from "react";
import * as _ from "underscore";
import AddPropertyIcon from "../../../../images/Add-property.svg";
import RevertBackIcon from "../../../../images/RevertBack.svg";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
import { TableEntity } from "../../../Common/TableEntity";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { userContext } from "../../../UserContext";
import * as TableConstants from "../../Tables/Constants";
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
import * as Entities from "../../Tables/Entities";
import { CassandraAPIDataClient, CassandraTableKey, TableDataClient } from "../../Tables/TableDataClient";
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
import * as Utilities from "../../Tables/Utilities";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import NewQueryTablesTab from "../../Tabs/QueryTablesTab/QueryTablesTab";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import {
attributeNameLabel,
@@ -36,9 +35,11 @@ import {
interface AddTableEntityPanelProps {
tableDataClient: TableDataClient;
queryTablesTab: QueryTablesTab;
queryTablesTab: NewQueryTablesTab;
tableEntityListViewModel: TableEntityListViewModel;
cassandraApiClient: CassandraAPIDataClient;
reloadEntities: () => void;
headerItems: string[];
}
interface EntityRowType {
@@ -58,7 +59,10 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
queryTablesTab,
tableEntityListViewModel,
cassandraApiClient,
reloadEntities,
headerItems,
}: AddTableEntityPanelProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [entities, setEntities] = useState<EntityRowType[]>([]);
const [selectedRow, setSelectedRow] = useState<number>(0);
const [entityAttributeValue, setEntityAttributeValue] = useState<string>("");
@@ -76,7 +80,7 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
}, []);
const getDefaultEntitiesAttribute = async (): Promise<void> => {
let headers = tableEntityListViewModel.headers;
let headers = tableEntityListViewModel.headers?.length > 1 ? tableEntityListViewModel.headers : headerItems;
if (DataTableUtilities.checkForDefaultHeader(headers)) {
headers = [];
if (userContext.apiType === "Tables") {
@@ -116,47 +120,19 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
const newEntity: Entities.ITableEntity = await tableDataClient.createDocument(queryTablesTab.collection, entity);
try {
await tableEntityListViewModel.addEntityToCache(newEntity);
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
tableEntityListViewModel.redrawTableThrottled();
}
reloadEntities();
setFormError("");
} catch (error) {
const errorMessage = getErrorMessage(error);
setFormError(errorMessage);
handleError(errorMessage, "AddTableRow");
throw error;
} finally {
closeSidePanel();
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 */
const addNewEntity = (): void => {
const cloneEntities: EntityRowType[] = [...entities];

View File

@@ -4,7 +4,7 @@ import React from "react";
import TableListViewModal from "../../Tables/DataTable/TableEntityListViewModel";
import * as Entities from "../../Tables/Entities";
import { CassandraAPIDataClient, TablesAPIDataClient } from "../../Tables/TableDataClient";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import QueryTablesTab from "../../Tabs/QueryTablesTab/QueryTablesTab";
import { EditTableEntityPanel } from "./EditTableEntityPanel";
describe("Excute Edit Table Entity Pane", () => {
@@ -15,11 +15,14 @@ describe("Excute Edit Table Entity Pane", () => {
fakeTableEntityListViewModel.headers = [];
fakeTableEntityListViewModel.selected = ko.observableArray<Entities.ITableEntity>([{}]);
const fakeSelectedItem = [{ PartitionKey: { _: "test", $: "String" } }];
const props = {
tableDataClient: new TablesAPIDataClient(),
queryTablesTab: fakeQueryTablesTab,
tableEntityListViewModel: fakeTableEntityListViewModel,
cassandraApiClient: fakeCassandraApiClient,
selectedEntity: fakeSelectedItem,
reloadEntities: () => "{}",
};
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", () => {
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", () => {
const wrapper = mount(<EditTableEntityPanel {...props} />);
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", () => {
@@ -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.
wrapper.find(".addButtonEntiy").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);
});
});

View File

@@ -6,14 +6,14 @@ import AddPropertyIcon from "../../../../images/Add-property.svg";
import RevertBackIcon from "../../../../images/RevertBack.svg";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
import { TableEntity } from "../../../Common/TableEntity";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { userContext } from "../../../UserContext";
import * as TableConstants from "../../Tables/Constants";
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
import * as Entities from "../../Tables/Entities";
import { CassandraAPIDataClient, TableDataClient } from "../../Tables/TableDataClient";
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import NewQueryTablesTab from "../../Tabs/QueryTablesTab/QueryTablesTab";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import {
attributeNameLabel,
@@ -34,9 +34,11 @@ import {
interface EditTableEntityPanelProps {
tableDataClient: TableDataClient;
queryTablesTab: QueryTablesTab;
queryTablesTab: NewQueryTablesTab;
tableEntityListViewModel: TableEntityListViewModel;
cassandraApiClient: CassandraAPIDataClient;
selectedEntity: Entities.ITableEntity[];
reloadEntities: () => void;
}
interface EntityRowType {
@@ -57,7 +59,10 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
queryTablesTab,
tableEntityListViewModel,
cassandraApiClient,
selectedEntity,
reloadEntities,
}: EditTableEntityPanelProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [entities, setEntities] = useState<EntityRowType[]>([]);
const [selectedRow, setSelectedRow] = useState<number>(0);
const [entityAttributeValue, setEntityAttributeValue] = useState<string>("");
@@ -75,8 +80,8 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let originalDocument: { [key: string]: any } = {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const entityAttribute: any = tableEntityListViewModel.selected();
const entityFormattedAttribute = constructDisplayedAttributes(entityAttribute[0]);
const entityAttribute: any = selectedEntity;
const entityFormattedAttribute = constructDisplayedAttributes(entityAttribute && entityAttribute[0]);
setEntities(entityFormattedAttribute);
if (userContext.apiType === "Tables") {
@@ -86,6 +91,7 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
originalDocument = entityAttribute;
}
setOriginalDocument(originalDocument);
//eslint-disable-next-line
}, []);
const constructDisplayedAttributes = (entity: Entities.ITableEntity): EntityRowType[] => {
@@ -216,9 +222,8 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
entity
);
await tableEntityListViewModel.updateCachedEntity(newEntity);
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
tableEntityListViewModel.redrawTableThrottled();
}
reloadEntities();
closeSidePanel();
tableEntityListViewModel.selected.removeAll();
tableEntityListViewModel.selected.push(newEntity);
} 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
const addNewEntity = (): void => {
const cloneEntities = [...entities];

View File

@@ -1,18 +1,16 @@
import { mount } from "enzyme";
import * as ko from "knockout";
import React from "react";
import Explorer from "../../../Explorer";
import QueryViewModel from "../../../Tables/QueryBuilder/QueryViewModel";
import { TableQuerySelectPanel } from "./TableQuerySelectPanel";
describe("Table query select Panel", () => {
const fakeExplorer = {} as Explorer;
const fakeQueryViewModal = {} as QueryViewModel;
fakeQueryViewModal.columnOptions = ko.observableArray<string>([""]);
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
headers: [""],
getSelectMessage: () => "{}",
queryViewModel: fakeQueryViewModal,
};

View File

@@ -8,6 +8,8 @@ import { RightPaneForm, RightPaneFormProps } from "../../RightPaneForm/RightPane
interface TableQuerySelectPanelProps {
queryViewModel: QueryViewModel;
headers: string[];
getSelectMessage: (selectMessage: string) => void;
}
interface ISelectColumn {
@@ -18,6 +20,8 @@ interface ISelectColumn {
export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps> = ({
queryViewModel,
headers,
getSelectMessage,
}: TableQuerySelectPanelProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
@@ -29,6 +33,7 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
const onSubmit = (): void => {
queryViewModel.selectText(getParameters());
queryViewModel.getSelectMessage();
getSelectMessage(queryViewModel.selectMessage());
closeSidePanel();
};
@@ -52,7 +57,8 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
};
useEffect(() => {
queryViewModel && setTableColumns(queryViewModel.columnOptions());
// queryViewModel && setTableColumns(queryViewModel.columnOptions());
headers && setTableColumns(headers);
}, []);
const setTableColumns = (columnNames: string[]): void => {

View File

@@ -2,8 +2,12 @@
exports[`Table query select Panel should render Default properly 1`] = `
<TableQuerySelectPanel
closePanel={[Function]}
explorer={Object {}}
getSelectMessage={[Function]}
headers={
Array [
"",
]
}
queryViewModel={
Object {
"columnOptions": [Function],

View File

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

View File

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

View File

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

View File

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

View File

@@ -123,10 +123,3 @@ export function checkForDefaultHeader(headers: string[]): boolean {
export function forceRecalculateTableSize(): void {
$("body").trigger("resize");
}
/**
* Turns off the spinning progress indicator on the data table.
*/
export function turnOffProgressIndicator(): void {
$("div.dataTables_processing").hide();
}

View File

@@ -1,14 +1,12 @@
import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import * as ko from "knockout";
import * as _ from "underscore";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import CacheBase from "./CacheBase";
import * as CommonConstants from "../../../Common/Constants";
import * as Constants from "../Constants";
import * as Entities from "../Entities";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
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,
// and property names are defined by Datatable as well.
@@ -47,19 +45,11 @@ abstract class DataTableViewModel {
private pendingRedraw = false;
private lastRedrawTime = new Date().getTime();
private dataTableOperationManager: IDataTableOperation;
public queryTablesTab: QueryTablesTab;
public queryTablesTab: NewQueryTablesTab;
constructor() {
this.items([]);
this.selected([]);
// Late bound
this.dataTableOperationManager = null;
}
public bind(dataTableOperationManager: IDataTableOperation): void {
this.dataTableOperationManager = dataTableOperationManager;
}
public clearLastSelected(): void {
@@ -101,10 +91,6 @@ abstract class DataTableViewModel {
}
}
public focusDataTable(): void {
this.dataTableOperationManager.focusTable();
}
public getItemFromSelectedItems(itemKeys: Entities.IProperty[]): Entities.ITableEntity {
return _.find(this.selected(), (item: Entities.ITableEntity) => {
return this.matchesKeys(item, itemKeys);
@@ -170,35 +156,12 @@ abstract class DataTableViewModel {
this.cache.sortOrder = sortOrder;
}
protected renderPage(
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
protected renderPage(startIndex: number, pageSize: number) {
var endIndex = pageSize < 0 ? this.cache.length : startIndex + pageSize;
var renderData = this.cache.data.slice(startIndex, endIndex);
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) {
TelemetryProcessor.traceSuccess(
Action.Tab,
@@ -217,16 +180,6 @@ abstract class DataTableViewModel {
protected matchesKeys(item: Entities.ITableEntity, itemKeys: Entities.IProperty[]): boolean {
return itemKeys.every((property: Entities.IProperty) => {
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);
});
}
@@ -238,27 +191,6 @@ abstract class DataTableViewModel {
protected stringCompare(s1: string, s2: string): boolean {
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 {

View File

@@ -94,8 +94,4 @@ export default class TableCommands {
return null;
}
public resetColumns(viewModel: TableEntityListViewModel): void {
viewModel.reloadTable();
}
}

View File

@@ -6,7 +6,7 @@ import * as ViewModels from "../../../Contracts/ViewModels";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import NewQueryTablesTab from "../../Tabs/QueryTablesTab/QueryTablesTab";
import * as Constants from "../Constants";
import { getQuotedCqlIdentifier } from "../CqlUtilities";
import * as Entities from "../Entities";
@@ -101,7 +101,8 @@ export default class TableEntityListViewModel extends DataTableViewModel {
public useSetting: boolean = true;
//public tableExplorerContext: TableExplorerContext;
public notifyColumnChanges: (enablePrompt: boolean, queryTablesTab: QueryTablesTab) => void;
public notifyColumnChanges: (enablePrompt: boolean, queryTablesTab: NewQueryTablesTab) => void;
public tablePageStartIndex: number;
public tableQuery: Entities.ITableQuery = {};
public cqlQuery: ko.Observable<string>;
@@ -112,7 +113,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
public queryErrorMessage: ko.Observable<string>;
public id: string;
constructor(tableCommands: TableCommands, queryTablesTab: QueryTablesTab) {
constructor(tableCommands: TableCommands, queryTablesTab: NewQueryTablesTab) {
super();
this.cache = new TableEntityCache();
this.queryErrorMessage = ko.observable<string>();
@@ -131,24 +132,8 @@ export default class TableEntityListViewModel extends DataTableViewModel {
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 {
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.
* 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 draw: number;
var prefetchNeeded = true;
var columnSortOrder: any;
// Threshold(pages) for triggering cache prefetch.
// If number remaining pages in cache falls below prefetchThreshold prefetch will be triggered.
var prefetchThreshold = 10;
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.
if (this.isCacheValid(tableQuery)) {
// Check if prefetch needed.
if (this.tablePageStartIndex + tablePageSize <= this.cache.length || this.allDownloaded) {
prefetchNeeded = false;
if (columnSortOrder && (!this.cache.sortOrder || !_.isEqual(this.cache.sortOrder, columnSortOrder))) {
this.sortColumns(columnSortOrder, oSettings);
}
this.renderPage(fnCallback, draw, this.tablePageStartIndex, tablePageSize, oSettings);
this.tablePageStartIndex = 0;
this.renderPage(this.tablePageStartIndex, this.cache.length);
if (
!this.allDownloaded &&
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) {
var downloadSize = tableQuery.top || this.downloadSize;
this.prefetchAndRender(
tableQuery,
this.tablePageStartIndex,
tablePageSize,
downloadSize,
draw,
fnCallback,
oSettings,
columnSortOrder
);
return await this.prefetchAndRender(tableQuery, 0, tablePageSize, downloadSize);
} else {
return this.cache.data;
}
}
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.
if (this.cache.serverCallInProgress) {
return Utilities.delay(this.pollingInterval).then(() => {
return this.updateCachedEntity(entity);
Utilities.delay(this.pollingInterval).then(() => {
this.updateCachedEntity(entity);
});
}
// Find the first item which is greater than the added 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);
this.cache.data.splice(this.cache.length, 0, entity);
// Finally, select newly added entity
this.clearSelection();
@@ -254,8 +200,8 @@ export default class TableEntityListViewModel extends DataTableViewModel {
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.
if (this.cache.serverCallInProgress) {
return Utilities.delay(this.pollingInterval).then(() => {
return this.updateCachedEntity(entity);
Utilities.delay(this.pollingInterval).then(() => {
this.updateCachedEntity(entity);
});
}
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.
if (this.cache.serverCallInProgress) {
return Utilities.delay(this.pollingInterval).then(() => {
return this.removeEntitiesFromCache(entities);
Utilities.delay(this.pollingInterval).then(() => {
this.removeEntitiesFromCache(entities);
});
}
@@ -292,14 +238,6 @@ export default class TableEntityListViewModel extends DataTableViewModel {
});
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);
}
@@ -392,86 +330,75 @@ export default class TableEntityListViewModel extends DataTableViewModel {
});
}
private prefetchAndRender(
private async prefetchAndRender(
tableQuery: Entities.ITableQuery,
tablePageStartIndex: number,
tablePageSize: number,
downloadSize: number,
draw: number,
renderCallBack: Function,
oSettings: any,
columnSortOrder: any
): void {
downloadSize: number
): Promise<Entities.ITableEntity[]> {
this.queryErrorMessage(null);
if (this.cache.serverCallInProgress) {
return;
return undefined;
}
this.prefetchData(tableQuery, downloadSize, /* currentRetry */ 0)
.then((result: IListTableEntitiesSegmentedResult) => {
if (!result) {
return;
try {
const result = await this.prefetchData(tableQuery, downloadSize, /* currentRetry */ 0);
if (!result) {
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;
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);
} 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();
return result;
} catch (error) {
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;
}
}
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.
* See Microsoft Azure API Documentation at: https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx
*/
private prefetchData(
private async prefetchData(
tableQuery: Entities.ITableQuery,
downloadSize: number,
currentRetry: number = 0
): Q.Promise<any> {
): Promise<any> {
var entities: any;
if (!this.cache.serverCallInProgress) {
this.cache.serverCallInProgress = true;
this.allDownloaded = false;
this.lastPrefetchTime = new Date().getTime();
var time = this.lastPrefetchTime;
var promise: Q.Promise<IListTableEntitiesSegmentedResult>;
if (this._documentIterator && this.continuationToken) {
// TODO handle Cassandra case
promise = Q(this._documentIterator.fetchNext().then((response) => response.resources)).then(
(documents: any[]) => {
let entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(documents);
let finalEntities: IListTableEntitiesSegmentedResult = <IListTableEntitiesSegmentedResult>{
Results: entities,
ContinuationToken: this._documentIterator.hasMoreResults(),
};
return Q.resolve(finalEntities);
}
);
} else if (this.continuationToken && userContext.apiType === "Cassandra") {
promise = Q(
this.queryTablesTab.container.tableDataClient.queryDocuments(
try {
if (this._documentIterator && this.continuationToken) {
// TODO handle Cassandra case
const fetchNext = await this._documentIterator.fetchNext();
let fetchNextEntities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(
fetchNext.resources
);
let finalEntities: IListTableEntitiesSegmentedResult = <IListTableEntitiesSegmentedResult>{
Results: fetchNextEntities,
ContinuationToken: this._documentIterator.hasMoreResults(),
};
entities = finalEntities;
} else if (this.continuationToken && userContext.apiType === "Cassandra") {
entities = await this.queryTablesTab.container.tableDataClient.queryDocuments(
this.queryTablesTab.collection,
this.cqlQuery(),
true,
this.continuationToken
)
);
} else {
let query = this.sqlQuery();
if (userContext.apiType === "Cassandra") {
query = this.cqlQuery();
);
} else {
let query = this.sqlQuery();
if (userContext.apiType === "Cassandra") {
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)
);
}
return promise
.then((result: IListTableEntitiesSegmentedResult) => {
const result = entities;
if (result) {
if (!this._documentIterator) {
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.
// Thus, end the prefetch.
if (this.lastPrefetchTime !== time) {
return Q.resolve(null);
return Promise.resolve(undefined);
}
var entities = result.Results;
actualDownloadSize = entities.length;
// Queries can fetch no results and still return a continuation header. See prefetchAndRender() method.
this.continuationToken = this.isCancelled ? null : result.ContinuationToken;
this.continuationToken = this.isCancelled ? undefined : result.ContinuationToken;
if (!this.continuationToken) {
this.allDownloaded = true;
@@ -568,30 +496,20 @@ export default class TableEntityListViewModel extends DataTableViewModel {
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) {
return Q.resolve(result);
return Promise.resolve(this.cache.data);
}
if (currentRetry >= TableEntityListViewModel._maximumNumberOfPrefetchRetries) {
result.ExceedMaximumRetries = true;
return Q.resolve(result);
return Promise.resolve(this.cache.data);
}
return this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1);
})
.catch((error: Error) => {
this.cache.serverCallInProgress = false;
return Q.reject(error);
});
}
} catch (error) {
this.cache.serverCallInProgress = false;
return Promise.reject(error);
}
}
return null;
}
}

View File

@@ -1,6 +1,7 @@
import * as ko from "knockout";
import { KeyCodes } from "../../../Common/Constants";
import { userContext } from "../../../UserContext";
import { IQueryTableRowsType } from "../../Tabs/QueryTablesTab/QueryTableTabUtils";
import * as Constants from "../Constants";
import { getQuotedCqlIdentifier } from "../CqlUtilities";
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(
this,
"",
"PartitionKey",
this.edmTypes()[0],
Constants.Operator.Equal,
this.tableEntityListViewModel.items()[0].PartitionKey._,
pk,
false,
"",
"",
@@ -127,7 +128,7 @@ export default class QueryBuilderViewModel {
"RowKey",
this.edmTypes()[0],
Constants.Operator.Equal,
this.tableEntityListViewModel.items()[0].RowKey._,
rk,
true,
"",
"",
@@ -139,40 +140,31 @@ export default class QueryBuilderViewModel {
this.addClauseImpl(example2, 1);
}
public getODataFilterFromClauses = (): string => {
public getODataFilterFromClauses = (queryClauses: IQueryTableRowsType[]): string => {
var filterString: string = "";
var treeTraversal = (group: ClauseGroup): void => {
for (var i = 0; i < group.children.length; i++) {
var currentItem = group.children[i];
if (queryClauses != undefined) {
for (var i = 0; i < queryClauses.length; i++) {
var currentItem = queryClauses[i];
if (currentItem instanceof QueryClauseViewModel) {
var clause = <QueryClauseViewModel>currentItem;
this.timestampToValue(clause);
filterString = filterString.concat(
this.constructODataClause(
filterString === "" ? "" : clause.and_or(),
this.generateLeftParentheses(clause),
clause.field(),
clause.type(),
clause.operator(),
clause.value(),
this.generateRightParentheses(clause)
)
);
}
if (currentItem instanceof ClauseGroup) {
treeTraversal(<ClauseGroup>currentItem);
}
this.timestampToValue(currentItem);
filterString = filterString.concat(
this.constructODataClause(
filterString === "" ? "" : currentItem.selectedOperation,
this.generateLeftParentheses(currentItem),
currentItem.selectedField,
currentItem.selectedEntityType,
currentItem.selectedOperator,
currentItem.entityValue,
this.generateRightParentheses(currentItem)
)
);
}
};
treeTraversal(this.queryClauses);
}
return filterString.trim();
};
public getSqlFilterFromClauses = (): string => {
public getSqlFilterFromClauses = (queryTableRows: IQueryTableRowsType[]): string => {
var filterString: string = "SELECT * FROM c";
if (this._queryViewModel.selectText() && this._queryViewModel.selectText().length > 0) {
filterString = "SELECT";
@@ -195,48 +187,37 @@ export default class QueryBuilderViewModel {
});
filterString = filterString.concat(" FROM c");
}
if (this.queryClauses.children.length === 0) {
if (queryTableRows.length === 0) {
return filterString;
}
filterString = filterString.concat(" WHERE");
var first = true;
var treeTraversal = (group: ClauseGroup): void => {
for (var i = 0; i < group.children.length; i++) {
var currentItem = group.children[i];
for (var i = 0; i < queryTableRows.length; i++) {
var currentItem = queryTableRows[i];
if (currentItem instanceof QueryClauseViewModel) {
var clause = <QueryClauseViewModel>currentItem;
let timeStampValue: string = this.timestampToSqlValue(clause);
var value = clause.value();
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);
}
let timeStampValue: string = this.timestampToSqlValue(currentItem);
var value = currentItem.entityValue;
if (!currentItem.isValue) {
value = timeStampValue;
}
};
treeTraversal(this.queryClauses);
filterString = filterString.concat(
this.constructSqlClause(
first ? "" : currentItem.selectedOperation,
this.generateLeftParentheses(currentItem),
currentItem.selectedField,
currentItem.selectedEntityType,
currentItem.selectedOperator,
value,
this.generateRightParentheses(currentItem)
)
);
first = false;
}
return filterString.trim();
};
public getCqlFilterFromClauses = (): string => {
public getCqlFilterFromClauses = (queryTableRows: IQueryTableRowsType[]): string => {
const databaseId = this._queryViewModel.queryTablesTab.collection.databaseId;
const collectionId = this._queryViewModel.queryTablesTab.collection.id();
const tableToQuery = `${getQuotedCqlIdentifier(databaseId)}.${getQuotedCqlIdentifier(collectionId)}`;
@@ -251,43 +232,32 @@ export default class QueryBuilderViewModel {
});
filterString = filterString.concat(` FROM ${tableToQuery}`);
}
if (this.queryClauses.children.length === 0) {
if (queryTableRows === undefined || queryTableRows.length === 0) {
return filterString;
}
filterString = filterString.concat(" WHERE");
var first = true;
var treeTraversal = (group: ClauseGroup): void => {
for (var i = 0; i < group.children.length; i++) {
var currentItem = group.children[i];
for (var i = 0; i < queryTableRows.length; i++) {
var currentItem = queryTableRows[i];
if (currentItem instanceof QueryClauseViewModel) {
var clause = <QueryClauseViewModel>currentItem;
let timeStampValue: string = this.timestampToSqlValue(clause);
var value = clause.value();
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);
}
let timeStampValue: string = this.timestampToSqlValue(currentItem);
var value = currentItem.entityValue;
if (!currentItem.isValue) {
value = timeStampValue;
}
};
treeTraversal(this.queryClauses);
filterString = filterString.concat(
this.constructCqlClause(
first ? "" : currentItem.selectedOperation,
this.generateLeftParentheses(currentItem),
currentItem.selectedField,
currentItem.selectedEntityType,
currentItem.selectedOperator,
value,
this.generateRightParentheses(currentItem)
)
);
first = false;
}
return filterString.trim();
};
@@ -298,7 +268,7 @@ export default class QueryBuilderViewModel {
this.columnOptions(newHeaders.sort(DataTableUtilities.compareTableColumns));
};
private generateLeftParentheses(clause: QueryClauseViewModel): string {
private generateLeftParentheses(clause: IQueryTableRowsType): string {
var result = "";
if (clause.clauseGroup.isRootGroup || clause.clauseGroup.children.indexOf(clause) !== 0) {
@@ -321,7 +291,7 @@ export default class QueryBuilderViewModel {
return result;
}
private generateRightParentheses(clause: QueryClauseViewModel): string {
private generateRightParentheses(clause: IQueryTableRowsType): string {
var result = "";
if (
@@ -504,7 +474,7 @@ export default class QueryBuilderViewModel {
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) {
this.addClauseIndex(this.clauseArray().length - 1, null);
event.stopPropagation();
@@ -630,16 +600,11 @@ export default class QueryBuilderViewModel {
return groupViewModels;
};
public runQuery = (): DataTables.DataTable => {
return this._queryViewModel.runQuery();
};
public addCustomRange(timestamp: CustomTimestampHelper.ITimestampQuery, clauseToAdd: QueryClauseViewModel): void {
var index = this.clauseArray.peek().indexOf(clauseToAdd);
var newClause = new QueryClauseViewModel(
this,
//this._tableEntityListViewModel.tableExplorerContext.hostProxy,
"And",
clauseToAdd.field(),
"DateTime",
@@ -713,67 +678,67 @@ export default class QueryBuilderViewModel {
//DataTableUtilities.forceRecalculateTableSize();
}
private timestampToValue(clause: QueryClauseViewModel): void {
if (clause.isValue()) {
private timestampToValue(clause: IQueryTableRowsType): void {
if (clause.isValue) {
return;
} else if (clause.isTimestamp()) {
} else if (clause.isTimestamp) {
this.getTimeStampToQuery(clause);
// } else if (clause.isCustomLastTimestamp()) {
// clause.value(`datetime'${CustomTimestampHelper._queryLastTime(clause.customLastTimestamp().lastNumber, clause.customLastTimestamp().lastTimeUnit)}'`);
} else if (clause.isCustomRangeTimestamp()) {
if (clause.isLocal()) {
clause.value(`datetime'${DateTimeUtilities.getUTCDateTime(clause.customTimeValue())}'`);
} else if (clause.isCustomRangeTimestamp) {
if (clause.isLocal) {
clause.entityValue = `datetime'${DateTimeUtilities.getUTCDateTime(clause.customTimeValue)}'`;
} else {
clause.value(`datetime'${clause.customTimeValue()}Z'`);
clause.entityValue = `datetime'${clause.customTimeValue}Z'`;
}
}
}
private timestampToSqlValue(clause: QueryClauseViewModel): string {
if (clause.isValue()) {
private timestampToSqlValue(clause: IQueryTableRowsType): string {
if (clause.isValue) {
return null;
} else if (clause.isTimestamp()) {
} else if (clause.isTimestamp) {
return this.getTimeStampToSqlQuery(clause);
// } else if (clause.isCustomLastTimestamp()) {
// clause.value(CustomTimestampHelper._queryLastTime(clause.customLastTimestamp().lastNumber, clause.customLastTimestamp().lastTimeUnit));
} else if (clause.isCustomRangeTimestamp()) {
if (clause.isLocal()) {
return DateTimeUtilities.getUTCDateTime(clause.customTimeValue());
} else if (clause.isCustomRangeTimestamp) {
if (clause.isLocal) {
return DateTimeUtilities.getUTCDateTime(clause.customTimeValue);
} else {
return clause.customTimeValue();
return clause.customTimeValue;
}
}
return null;
}
private getTimeStampToQuery(clause: QueryClauseViewModel): void {
switch (clause.timeValue()) {
private getTimeStampToQuery(clause: IQueryTableRowsType): void {
switch (clause.timeValue) {
case Constants.timeOptions.lastHour:
clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(0, 1)}'`);
clause.entityValue = `datetime'${CustomTimestampHelper._queryLastDaysHours(0, 1)}'`;
break;
case Constants.timeOptions.last24Hours:
clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(0, 24)}'`);
clause.entityValue = `datetime'${CustomTimestampHelper._queryLastDaysHours(0, 24)}'`;
break;
case Constants.timeOptions.last7Days:
clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(7, 0)}'`);
clause.entityValue = `datetime'${CustomTimestampHelper._queryLastDaysHours(7, 0)}'`;
break;
case Constants.timeOptions.last31Days:
clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(31, 0)}'`);
clause.entityValue = `datetime'${CustomTimestampHelper._queryLastDaysHours(31, 0)}'`;
break;
case Constants.timeOptions.last365Days:
clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(365, 0)}'`);
clause.entityValue = `datetime'${CustomTimestampHelper._queryLastDaysHours(365, 0)}'`;
break;
case Constants.timeOptions.currentMonth:
clause.value(`datetime'${CustomTimestampHelper._queryCurrentMonthLocal()}'`);
clause.entityValue = `datetime'${CustomTimestampHelper._queryCurrentMonthLocal()}'`;
break;
case Constants.timeOptions.currentYear:
clause.value(`datetime'${CustomTimestampHelper._queryCurrentYearLocal()}'`);
clause.entityValue = `datetime'${CustomTimestampHelper._queryCurrentYearLocal()}'`;
break;
}
}
private getTimeStampToSqlQuery(clause: QueryClauseViewModel): string {
switch (clause.timeValue()) {
private getTimeStampToSqlQuery(clause: IQueryTableRowsType): string {
switch (clause.timeValue) {
case Constants.timeOptions.lastHour:
return CustomTimestampHelper._queryLastDaysHours(0, 1);
case Constants.timeOptions.last24Hours:

View File

@@ -5,7 +5,8 @@ import { KeyCodes } from "../../../Common/Constants";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { userContext } from "../../../UserContext";
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 * as DataTableUtilities from "../DataTable/DataTableUtilities";
import TableEntityListViewModel from "../DataTable/TableEntityListViewModel";
@@ -39,14 +40,14 @@ export default class QueryViewModel {
public columnOptions: ko.ObservableArray<string>;
public queryTablesTab: QueryTablesTab;
public queryTablesTab: NewQueryTablesTab;
public id: string;
private _tableEntityListViewModel: TableEntityListViewModel;
constructor(queryTablesTab: QueryTablesTab) {
constructor(queryTablesTab: NewQueryTablesTab) {
this.queryTablesTab = queryTablesTab;
this.id = `queryViewModel${this.queryTablesTab.tabId}`;
this._tableEntityListViewModel = queryTablesTab.tableEntityListViewModel();
this._tableEntityListViewModel = queryTablesTab.tableEntityListViewModel;
this.queryTextIsReadOnly = ko.computed<boolean>(() => {
return userContext.apiType !== "Cassandra";
@@ -103,7 +104,7 @@ export default class QueryViewModel {
DataTableUtilities.forceRecalculateTableSize();
};
public toggleAdvancedOptions = () => {
public toggleAdvancedOptions = (): void => {
this.isExpanded(!this.isExpanded());
if (this.isExpanded()) {
this.focusTopResult(true);
@@ -126,23 +127,19 @@ export default class QueryViewModel {
return this.selectText();
};
private setFilter = (): string => {
private setFilter = (queryTableRows?: IQueryTableRowsType[]): string => {
const queryString = this.isEditorActive()
? this.queryText()
: userContext.apiType === "Cassandra"
? this.queryBuilderViewModel().getCqlFilterFromClauses()
: this.queryBuilderViewModel().getODataFilterFromClauses();
? this.queryBuilderViewModel().getCqlFilterFromClauses(queryTableRows)
: this.queryBuilderViewModel().getODataFilterFromClauses(queryTableRows);
const filter = queryString;
this.queryText(filter);
return this.queryText();
};
private setSqlFilter = (): string => {
return this.queryBuilderViewModel().getSqlFilterFromClauses();
};
private setCqlFilter = (): string => {
return this.queryBuilderViewModel().getCqlFilterFromClauses();
private setSqlFilter = (queryTableRows: IQueryTableRowsType[]): string => {
return this.queryBuilderViewModel().getSqlFilterFromClauses(queryTableRows);
};
public isHelperEnabled = ko
@@ -158,8 +155,9 @@ export default class QueryViewModel {
notify: "always",
});
public runQuery = (): DataTables.DataTable => {
let filter = this.setFilter();
public runQuery = (queryTableRows: IQueryTableRowsType[]): string => {
let filter = this.setFilter(queryTableRows);
if (filter && userContext.apiType !== "Cassandra") {
filter = filter.replace(/"/g, "'");
}
@@ -170,13 +168,15 @@ export default class QueryViewModel {
this._tableEntityListViewModel.tableQuery.top = top;
this._tableEntityListViewModel.tableQuery.select = select;
this._tableEntityListViewModel.oDataQuery(filter);
this._tableEntityListViewModel.sqlQuery(this.setSqlFilter());
this._tableEntityListViewModel.sqlQuery(this.setSqlFilter(queryTableRows));
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.topValue();
this.selectText();
@@ -194,16 +194,26 @@ export default class QueryViewModel {
this.queryTablesTab.collection.id()
)}`
);
return this._tableEntityListViewModel.reloadTable(false);
};
public selectQueryOptions() {
useSidePanel.getState().openSidePanel("Select Column", <TableQuerySelectPanel queryViewModel={this} />);
public selectQueryOptions(headers: string[], getSelectMessage: (selectMessage: string) => void): void {
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) {
this.selectQueryOptions();
this.selectQueryOptions(headers, getSelectMessage);
event.stopPropagation();
return false;
}

View File

@@ -1,273 +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="button"
>
<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>

View File

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

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

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

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

File diff suppressed because it is too large Load Diff

View File

@@ -32,7 +32,8 @@ import MongoDocumentsTab from "../Tabs/MongoDocumentsTab";
import { NewMongoQueryTab } from "../Tabs/MongoQueryTab/MongoQueryTab";
import { NewMongoShellTab } from "../Tabs/MongoShellTab/MongoShellTab";
import { NewQueryTab } from "../Tabs/QueryTab/QueryTab";
import QueryTablesTab from "../Tabs/QueryTablesTab";
// import QueryTablesTab from "../Tabs/QueryTablesTab";
import NewQueryTablesTab from "../Tabs/QueryTablesTab/QueryTablesTab";
import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
@@ -391,13 +392,13 @@ export default class Collection implements ViewModels.Collection {
});
}
const queryTablesTabs: QueryTablesTab[] = useTabs
const queryTablesTabs: NewQueryTablesTab[] = useTabs
.getState()
.getTabs(
ViewModels.CollectionTabKind.QueryTables,
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
) as QueryTablesTab[];
let queryTablesTab: QueryTablesTab = queryTablesTabs && queryTablesTabs[0];
) as NewQueryTablesTab[];
let queryTablesTab: NewQueryTablesTab = queryTablesTabs && queryTablesTabs[0];
if (queryTablesTab) {
useTabs.getState().activateTab(queryTablesTab);
@@ -415,14 +416,57 @@ export default class Collection implements ViewModels.Collection {
tabTitle: title,
});
queryTablesTab = new QueryTablesTab({
tabKind: ViewModels.CollectionTabKind.QueryTables,
title: title,
tabPath: "",
collection: this,
node: this,
onLoadStartKey: startKey,
});
queryTablesTab = new NewQueryTablesTab(
{
tabKind: ViewModels.CollectionTabKind.QueryTables,
title: title,
tabPath: "",
collection: this,
node: this,
onLoadStartKey: startKey,
},
{
container: this.container,
}
);
// const queryTablesTabs: QueryTablesTab[] = useTabs
// .getState()
// .getTabs(
// ViewModels.CollectionTabKind.QueryTables,
// (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
// ) as QueryTablesTab[];
// let queryTablesTab: QueryTablesTab = queryTablesTabs && queryTablesTabs[0];
// if (queryTablesTab) {
// useTabs.getState().activateTab(queryTablesTab);
// } else {
// this.documentIds([]);
// let title = `Entities`;
// if (userContext.apiType === "Cassandra") {
// title = `Rows`;
// }
// const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
// databaseName: this.databaseId,
// collectionName: this.id(),
// dataExplorerArea: Constants.Areas.Tab,
// tabTitle: title,
// });
// queryTablesTab = new QueryTablesTab(
// {
// tabKind: ViewModels.CollectionTabKind.QueryTables,
// title: title,
// tabPath: "",
// collection: this,
// node: this,
// onLoadStartKey: startKey,
// },
// {
// container: this.container,
// }
// );
useTabs.getState().activateNewTab(queryTablesTab);
}

View File

@@ -128,12 +128,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
notebooksTree.children.push(buildGalleryNotebooksTree());
}
if (
myNotebooksContentRoot &&
((NotebookUtil.isPhoenixEnabled() &&
useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected) ||
userContext.features.phoenix === false)
) {
if (myNotebooksContentRoot && useNotebook.getState().connectionInfo.status == ConnectionStatusType.Connected) {
notebooksTree.children.push(buildMyNotebooksTree());
}
if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
@@ -167,11 +162,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
myNotebooksContentRoot,
(item: NotebookContentItem) => {
container.openNotebook(item).then((hasOpened) => {
if (
hasOpened &&
userContext.features.notebooksTemporarilyDown === false &&
userContext.features.phoenix === false
) {
if (hasOpened) {
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
}
});
@@ -190,11 +181,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
gitHubNotebooksContentRoot,
(item: NotebookContentItem) => {
container.openNotebook(item).then((hasOpened) => {
if (
hasOpened &&
userContext.features.notebooksTemporarilyDown === false &&
userContext.features.phoenix === false
) {
if (hasOpened) {
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
}
});
@@ -226,7 +213,23 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
},
},
];
gitHubNotebooksTree.contextMenu = manageGitContextMenu;
const connectGitContextMenu: TreeNodeMenuItem[] = [
{
label: "Connect to GitHub",
onClick: () =>
useSidePanel
.getState()
.openSidePanel(
"Connect to GitHub",
<GitHubReposPanel
explorer={container}
gitHubClientProp={container.notebookManager.gitHubClient}
junoClientProp={container.notebookManager.junoClient}
/>
),
},
];
gitHubNotebooksTree.contextMenu = isConnected ? manageGitContextMenu : connectGitContextMenu;
gitHubNotebooksTree.isExpanded = true;
gitHubNotebooksTree.isAlphaSorted = true;

View File

@@ -45,6 +45,7 @@ import UserDefinedFunction from "./UserDefinedFunction";
export class ResourceTreeAdapter implements ReactAdapter {
public static readonly MyNotebooksTitle = "My Notebooks";
public static readonly MyNotebooksScratchTitle = "My Notebooks Scratch";
public static readonly GitHubReposTitle = "GitHub repos";
private static readonly DataTitle = "DATA";

View File

@@ -1,5 +1,7 @@
import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes } from "../Common/Constants";
import { configContext } from "../ConfigContext";
import { ContainerConnectionInfo } from "../Contracts/DataModels";
import { useNotebook } from "../Explorer/Notebook/useNotebook";
import { userContext } from "../UserContext";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
@@ -13,6 +15,7 @@ export interface IPhoenixConnectionInfoResult {
}
export interface IProvosionData {
cosmosEndpoint: string;
resourceId: string;
dbAccountName: string;
aadToken: string;
resourceGroup: string;
@@ -23,7 +26,11 @@ export class PhoenixClient {
provisionData: IProvosionData
): Promise<IPhoenixResponse<IPhoenixConnectionInfoResult>> {
try {
const response = await window.fetch(`${this.getPhoenixContainerPoolingEndPoint()}/allocate`, {
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Connecting,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
const response = await window.fetch(`${this.getPhoenixContainerPoolingEndPoint()}/provision`, {
method: "POST",
headers: PhoenixClient.getHeaders(),
body: JSON.stringify(provisionData),
@@ -31,20 +38,31 @@ export class PhoenixClient {
let data: IPhoenixConnectionInfoResult;
if (response.status === HttpStatusCodes.OK) {
data = await response.json();
if (data && data.notebookServerUrl) {
connectionStatus.status = ConnectionStatusType.Connected;
useNotebook.getState().setConnectionInfo(connectionStatus);
}
} else {
connectionStatus.status = ConnectionStatusType.Failed;
useNotebook.getState().setConnectionInfo(connectionStatus);
}
return {
status: response.status,
data,
};
} catch (error) {
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Failed,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
console.error(error);
throw error;
}
}
public static getPhoenixEndpoint(): string {
const phoenixEndpoint =
userContext.features.phoenixEndpoint ?? userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
const phoenixEndpoint = userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
if (configContext.allowedJunoOrigins.indexOf(new URL(phoenixEndpoint).origin) === -1) {
const error = `${phoenixEndpoint} not allowed as juno endpoint`;
console.error(error);
@@ -55,7 +73,7 @@ export class PhoenixClient {
}
public getPhoenixContainerPoolingEndPoint(): string {
return `${PhoenixClient.getPhoenixEndpoint()}/api/controlplane/toolscontainer`;
return `${PhoenixClient.getPhoenixEndpoint()}/api/containerpooling`;
}
private static getHeaders(): HeadersInit {
const authorizationHeader = getAuthorizationHeader();

View File

@@ -20,7 +20,6 @@ export type Features = {
readonly enableKoResourceTree: boolean;
readonly hostedDataExplorer: boolean;
readonly junoEndpoint?: string;
readonly phoenixEndpoint?: string;
readonly livyEndpoint?: string;
readonly notebookBasePath?: string;
readonly notebookServerToken?: string;
@@ -69,7 +68,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
mongoProxyEndpoint: get("mongoproxyendpoint"),
mongoProxyAPIs: get("mongoproxyapis"),
junoEndpoint: get("junoendpoint"),
phoenixEndpoint: get("phoenixendpoint"),
livyEndpoint: get("livyendpoint"),
notebookBasePath: get("notebookbasepath"),
notebookServerToken: get("notebookservertoken"),

View File

@@ -10,7 +10,6 @@ import {
SortBy,
} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
import Explorer from "../Explorer/Explorer";
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
import { useNotebook } from "../Explorer/Notebook/useNotebook";
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
@@ -226,89 +225,67 @@ export function downloadItem(
const name = data.name;
useDialog.getState().showOkCancelModalDialog(
`Download to ${useNotebook.getState().notebookFolderName}`,
undefined,
`Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`,
"Download",
async () => {
if (NotebookUtil.isPhoenixEnabled()) {
await container.allocateContainer();
}
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
downloadNotebookItem(name, data, junoClient, container, onComplete);
} else {
useDialog
.getState()
.showOkModalDialog(
"Failed to Connect",
"Failed to connect to temporary workspace. Please refresh the page and try again."
);
}
},
"Cancel",
undefined,
container.getDownloadModalConent(name)
);
}
export async function downloadNotebookItem(
fileName: string,
data: IGalleryItem,
junoClient: JunoClient,
container: Explorer,
onComplete: (item: IGalleryItem) => void
) {
const clearInProgressMessage = logConsoleProgress(
`Downloading ${fileName} to ${useNotebook.getState().notebookFolderName}`
);
const startKey = traceStart(Action.NotebooksGalleryDownload, {
notebookId: data.id,
downloadCount: data.downloads,
isSample: data.isSample,
});
try {
const response = await junoClient.getNotebookContent(data.id);
if (!response.data) {
throw new Error(`Received HTTP ${response.status} when fetching ${data.name}`);
}
const notebook = JSON.parse(response.data) as Notebook;
removeNotebookViewerLink(notebook, data.newCellId);
if (!data.isSample) {
const metadata = notebook.metadata as { [name: string]: unknown };
metadata.untrusted = true;
}
await container.importAndOpenContent(data.name, JSON.stringify(notebook));
logConsoleInfo(`Successfully downloaded ${data.name} to ${useNotebook.getState().notebookFolderName}`);
const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id);
if (increaseDownloadResponse.data) {
traceSuccess(
Action.NotebooksGalleryDownload,
{ notebookId: data.id, downloadCount: increaseDownloadResponse.data.downloads, isSample: data.isSample },
startKey
const clearInProgressMessage = logConsoleProgress(
`Downloading ${name} to ${useNotebook.getState().notebookFolderName}`
);
onComplete(increaseDownloadResponse.data);
}
} catch (error) {
traceFailure(
Action.NotebooksGalleryDownload,
{
const startKey = traceStart(Action.NotebooksGalleryDownload, {
notebookId: data.id,
downloadCount: data.downloads,
isSample: data.isSample,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
});
handleError(error, "GalleryUtils/downloadItem", `Failed to download ${data.name}`);
}
try {
const response = await junoClient.getNotebookContent(data.id);
if (!response.data) {
throw new Error(`Received HTTP ${response.status} when fetching ${data.name}`);
}
clearInProgressMessage();
const notebook = JSON.parse(response.data) as Notebook;
removeNotebookViewerLink(notebook, data.newCellId);
if (!data.isSample) {
const metadata = notebook.metadata as { [name: string]: unknown };
metadata.untrusted = true;
}
await container.importAndOpenContent(data.name, JSON.stringify(notebook));
logConsoleInfo(`Successfully downloaded ${name} to My Notebooks`);
const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id);
if (increaseDownloadResponse.data) {
traceSuccess(
Action.NotebooksGalleryDownload,
{ notebookId: data.id, downloadCount: increaseDownloadResponse.data.downloads, isSample: data.isSample },
startKey
);
onComplete(increaseDownloadResponse.data);
}
} catch (error) {
traceFailure(
Action.NotebooksGalleryDownload,
{
notebookId: data.id,
downloadCount: data.downloads,
isSample: data.isSample,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
handleError(error, "GalleryUtils/downloadItem", `Failed to download ${data.name}`);
}
clearInProgressMessage();
},
"Cancel",
undefined
);
}
export const removeNotebookViewerLink = (notebook: Notebook, newCellId: string): void => {
if (!newCellId) {
return;

61
src/indexO.html Normal file
View File

@@ -0,0 +1,61 @@
<!DOCTYPE html public "-//W3C//DTD HTML 4.0//en">
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="height=device-height, width=device-width, initial-scale=1.0" />
<title>Azure Cosmos DB Emulator</title>
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon" />
</head>
<body>
<header class="header HeaderBg">
<div class="items">
<img class="DocDBicon" src="/CosmosDB_20170829.svg" alt="Azure Cosmos DB" />
<a class="createdocdbacnt" href="https://aka.ms/documentdbcreate" target="_blank">
Create an Azure Cosmos DB account Old<img class="rightarrowimg" src="/Arrow.svg" alt="" />
</a>
<span class="title">Azure Cosmos DB Emulator</span>
</div>
</header>
<nav class="fixedleftpane">
<div
id="Quickstart"
data-bind="click: quickstart_click, css:{ topSelected: navigationSelection() === 'quickstart' }"
>
<img id="imgiconwidth1" src="/Quickstart.svg" alt="Open Quick Start" />
<span class="menuQuickStart">Quickstart</span>
</div>
<div id="Explorer" data-bind="click: explorer_click, css:{ topSelected: navigationSelection() === 'explorer' }">
<img id="imgiconwidth1" src="/Explorer.svg" alt="Open Data Explorer" />
<span class="menuExplorer">Explorer</span>
</div>
<div>
<a class="feedbackstyle" href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Emulator%20Feedback">
<img id="imgiconwidth1" src="/Feedback.svg" alt="Report Issue" />
<span class="menuExplorer">Report Issue</span>
</a>
</div>
</nav>
<iframe
name="quickstart"
class="iframe"
src="quickstart.html"
data-bind="visible: navigationSelection() === 'quickstart'"
>
</iframe>
<iframe
name="explorer"
class="iframe"
src="explorer.html?platform=Emulator"
data-bind="visible: navigationSelection() === 'explorer'"
>
</iframe>
</body>
</html>