Compare commits

..

2 Commits

Author SHA1 Message Date
sunilyadav840
27d129e689 configure and fixed eslint jsx-a11y issues 2021-10-01 11:51:27 +05:30
sunilyadav840
fb9f97b43a configure eslint-plugin-jsx-a11y 2021-09-30 19:53:04 +05:30
82 changed files with 3160 additions and 42420 deletions

View File

@@ -80,8 +80,8 @@ src/Explorer/Tables/DataTable/CacheBase.ts
src/Explorer/Tables/DataTable/DataTableBindingManager.ts src/Explorer/Tables/DataTable/DataTableBindingManager.ts
src/Explorer/Tables/DataTable/DataTableBuilder.ts src/Explorer/Tables/DataTable/DataTableBuilder.ts
src/Explorer/Tables/DataTable/DataTableContextMenu.ts src/Explorer/Tables/DataTable/DataTableContextMenu.ts
# src/Explorer/Tables/DataTable/DataTableOperationManager.ts src/Explorer/Tables/DataTable/DataTableOperationManager.ts
# src/Explorer/Tables/DataTable/DataTableOperations.ts src/Explorer/Tables/DataTable/DataTableOperations.ts
src/Explorer/Tables/DataTable/DataTableViewModel.ts src/Explorer/Tables/DataTable/DataTableViewModel.ts
src/Explorer/Tables/DataTable/TableCommands.ts src/Explorer/Tables/DataTable/TableCommands.ts
src/Explorer/Tables/DataTable/TableEntityCache.ts src/Explorer/Tables/DataTable/TableEntityCache.ts

View File

@@ -3,8 +3,8 @@ module.exports = {
browser: true, browser: true,
es6: true, es6: true,
}, },
plugins: ["@typescript-eslint", "no-null", "prefer-arrow", "react-hooks"], plugins: ["@typescript-eslint", "no-null", "prefer-arrow", "react-hooks", "jsx-a11y"],
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:jsx-a11y/recommended"],
globals: { globals: {
Atomics: "readonly", Atomics: "readonly",
SharedArrayBuffer: "readonly", SharedArrayBuffer: "readonly",

View File

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

View File

@@ -3079,3 +3079,6 @@ settings-pane {
background: white; background: white;
height: 100%; height: 100%;
} }
.moreOption {
color: #337ab7;
}

34236
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -61,6 +61,7 @@
"dom-to-image": "2.6.0", "dom-to-image": "2.6.0",
"dotenv": "8.2.0", "dotenv": "8.2.0",
"eslint-plugin-jest": "23.13.2", "eslint-plugin-jest": "23.13.2",
"eslint-plugin-jsx-a11y": "6.4.1",
"eslint-plugin-react": "7.20.0", "eslint-plugin-react": "7.20.0",
"hasher": "1.2.0", "hasher": "1.2.0",
"html2canvas": "1.0.0-rc.5", "html2canvas": "1.0.0-rc.5",

View File

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

View File

@@ -31,17 +31,15 @@ export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps
<div id="mini" className={!isLeftPaneExpanded ? "mini toggle-mini" : "hiddenMain"}> <div id="mini" className={!isLeftPaneExpanded ? "mini toggle-mini" : "hiddenMain"}>
<div className="main-nav nav"> <div className="main-nav nav">
<ul className="nav"> <ul className="nav">
<li <li className="resourceTreeCollapse" id="collapseToggleLeftPaneButton" aria-label="Expand Tree">
className="resourceTreeCollapse" <span
id="collapseToggleLeftPaneButton" className="leftarrowCollapsed"
role="button" role="button"
tabIndex={0} tabIndex={0}
aria-label="Expand Tree" onClick={toggleLeftPaneExpanded}
onClick={toggleLeftPaneExpanded} onKeyPress={onKeyPressToggleLeftPaneExpanded}
onKeyPress={onKeyPressToggleLeftPaneExpanded} ref={focusButton}
ref={focusButton} >
>
<span className="leftarrowCollapsed">
<img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" /> <img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" />
</span> </span>
<span className="collectionCollapsed"> <span className="collectionCollapsed">

View File

@@ -40,7 +40,6 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
<TextField <TextField
label={entityValueLabel && entityValueLabel} label={entityValueLabel && entityValueLabel}
id="entityTimeId" id="entityTimeId"
autoFocus
type="time" type="time"
value={entityTimeValue} value={entityTimeValue}
onChange={onEntityTimeValueChange} onChange={onEntityTimeValueChange}
@@ -54,8 +53,7 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
<TextField <TextField
label={entityValueLabel && entityValueLabel} label={entityValueLabel && entityValueLabel}
className="addEntityTextField" className="addEntityTextField"
// id="entityValueId" id="entityValueId"
autoFocus
disabled={isEntityValueDisable} disabled={isEntityValueDisable}
type={entityValueType} type={entityValueType}
placeholder={entityValuePlaceholder} placeholder={entityValuePlaceholder}

View File

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

View File

@@ -66,9 +66,15 @@ export const Upload: FunctionComponent<UploadProps> = ({
onChange={onUpload} onChange={onUpload}
role="button" role="button"
/> />
<a href="#" id="fileImportLinkNotebook" onClick={onImportLinkClick} onKeyPress={onImportLinkKeyPress}> <span
id="fileImportLinkNotebook"
role="button"
tabIndex={0}
onClick={onImportLinkClick}
onKeyPress={onImportLinkKeyPress}
>
<Image className="fileImportImg" src={FolderIcon} alt={title} title={title} /> <Image className="fileImportImg" src={FolderIcon} alt={title} title={title} />
</a> </span>
</Stack> </Stack>
</div> </div>
); );

View File

@@ -55,7 +55,13 @@ export class AccordionItemComponent extends React.Component<AccordionItemCompone
public render(): JSX.Element { public render(): JSX.Element {
return ( return (
<div className="accordionItemContainer"> <div className="accordionItemContainer">
<div className="accordionItemHeader" onClick={this.onHeaderClick} onKeyPress={this.onHeaderKeyPress}> <div
className="accordionItemHeader"
onClick={this.onHeaderClick}
onKeyPress={this.onHeaderKeyPress}
role="button"
tabIndex={0}
>
{this.renderCollapseExpandIcon()} {this.renderCollapseExpandIcon()}
{this.props.title} {this.props.title}
</div> </div>
@@ -74,8 +80,6 @@ export class AccordionItemComponent extends React.Component<AccordionItemCompone
className="expandCollapseIcon" className="expandCollapseIcon"
src={this.state.isExpanded ? TriangleDownIcon : TriangleRightIcon} src={this.state.isExpanded ? TriangleDownIcon : TriangleRightIcon}
alt="Hide" alt="Hide"
tabIndex={0}
role="button"
/> />
); );
} }

View File

@@ -49,7 +49,7 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
onClick={this.toggleCollapsed} onClick={this.toggleCollapsed}
onKeyPress={this.onKeyPress} onKeyPress={this.onKeyPress}
tabIndex={0} tabIndex={0}
aria-name="Advanced" aria-label="Advanced"
role="button" role="button"
aria-expanded={this.state.isExpanded} aria-expanded={this.state.isExpanded}
> >

View File

@@ -4,7 +4,7 @@ exports[`CollapsibleSectionComponent renders 1`] = `
<Fragment> <Fragment>
<Stack <Stack
aria-expanded={true} aria-expanded={true}
aria-name="Advanced" aria-label="Advanced"
className="collapsibleSection" className="collapsibleSection"
horizontal={true} horizontal={true}
onClick={[Function]} onClick={[Function]}

View File

@@ -188,12 +188,13 @@ export class CommandButtonComponent extends React.Component<CommandButtonCompone
ref={(ref: HTMLElement) => { ref={(ref: HTMLElement) => {
this.expandButtonElt = ref; this.expandButtonElt = ref;
}} }}
role="button"
onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => this.onLauncherKeyDown(e)} onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => this.onLauncherKeyDown(e)}
> >
<div className="commandDropdownLauncher"> <div className="commandDropdownLauncher">
<span className="partialSplitter" /> <span className="partialSplitter" />
<span className="expandDropdown"> <span className="expandDropdown">
<img src={CollapseChevronDownIcon} /> <img src={CollapseChevronDownIcon} alt="Collapse down icon" />
</span> </span>
</div> </div>
<div <div

View File

@@ -150,7 +150,7 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
To write a new query, open a new query tab and enter the desired query. Once ready to save, click on Save To write a new query, open a new query tab and enter the desired query. Once ready to save, click on Save
Query and follow the prompt in order to save the query. Query and follow the prompt in order to save the query.
</div> </div>
<img {...bannerProps} /> <img {...bannerProps} alt="Save query helper banner" />
</div> </div>
); );
} }

View File

@@ -122,7 +122,7 @@ export class IndexingPolicyComponent extends React.Component<
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && ( {isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
<MessageBar messageBarType={MessageBarType.warning}>{indexingPolicynUnsavedWarningMessage}</MessageBar> <MessageBar messageBarType={MessageBarType.warning}>{indexingPolicynUnsavedWarningMessage}</MessageBar>
)} )}
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div> <div className="settingsV2IndexingPolicyEditor" ref={this.indexingPolicyDiv}></div>
</Stack> </Stack>
); );
} }

View File

@@ -13,7 +13,6 @@ exports[`IndexingPolicyComponent renders 1`] = `
/> />
<div <div
className="settingsV2IndexingPolicyEditor" className="settingsV2IndexingPolicyEditor"
tabIndex={0}
/> />
</Stack> </Stack>
`; `;

View File

@@ -118,10 +118,8 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
<input <input
className="throughputInputRadioBtn" className="throughputInputRadioBtn"
aria-label="Autoscale mode" aria-label="Autoscale mode"
aria-required={true}
checked={isAutoscaleSelected} checked={isAutoscaleSelected}
type="radio" type="radio"
role="radio"
tabIndex={0} tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Autoscale")} onChange={(e) => handleOnChangeMode(e, "Autoscale")}
/> />
@@ -132,8 +130,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
aria-label="Manual mode" aria-label="Manual mode"
checked={!isAutoscaleSelected} checked={!isAutoscaleSelected}
type="radio" type="radio"
aria-required={true}
role="radio"
tabIndex={0} tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Manual")} onChange={(e) => handleOnChangeMode(e, "Manual")}
/> />

View File

@@ -654,12 +654,10 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
> >
<input <input
aria-label="Autoscale mode" aria-label="Autoscale mode"
aria-required={true}
checked={true} checked={true}
className="throughputInputRadioBtn" className="throughputInputRadioBtn"
key=".0:$.0" key=".0:$.0"
onChange={[Function]} onChange={[Function]}
role="radio"
tabIndex={0} tabIndex={0}
type="radio" type="radio"
/> />
@@ -671,12 +669,10 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
</span> </span>
<input <input
aria-label="Manual mode" aria-label="Manual mode"
aria-required={true}
checked={false} checked={false}
className="throughputInputRadioBtn" className="throughputInputRadioBtn"
key=".0:$.2" key=".0:$.2"
onChange={[Function]} onChange={[Function]}
role="radio"
tabIndex={0} tabIndex={0}
type="radio" type="radio"
/> />

View File

@@ -736,7 +736,7 @@ export class D3ForceGraph implements GraphRenderer {
.on("dblclick", function (this: Element, _: MouseEvent, d: D3Node) { .on("dblclick", function (this: Element, _: MouseEvent, d: D3Node) {
// https://stackoverflow.com/a/41945742 ('this' implicitly has type 'any' because it does not have a type annotation) // https://stackoverflow.com/a/41945742 ('this' implicitly has type 'any' because it does not have a type annotation)
// this is the <g> element // this is the <g> element
return self.onNodeClicked(this.parentNode, d); self.onNodeClicked(this.parentNode, d);
}) })
.on("click", function (this: Element, _: MouseEvent, d: D3Node) { .on("click", function (this: Element, _: MouseEvent, d: D3Node) {
// this is the <g> element // this is the <g> element

View File

@@ -214,8 +214,14 @@ export class EditorNeighborsComponent extends React.Component<EditorNeighborsCom
/> />
</td> </td>
<td className="actionCol"> <td className="actionCol">
<span className="rightPaneTrashIcon rightPaneBtns"> <span
<img src={DeleteIcon} alt="Delete" onClick={() => this.removeAddedEdgeToNeighbor(index)} /> className="rightPaneTrashIcon rightPaneBtns"
role="button"
onKeyPress={() => this.removeAddedEdgeToNeighbor(index)}
onClick={() => this.removeAddedEdgeToNeighbor(index)}
tabIndex={0}
>
<img src={DeleteIcon} alt="Delete" />
</span> </span>
</td> </td>
</tr> </tr>

View File

@@ -123,7 +123,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
<select <select
className="typeSelect" className="typeSelect"
value={singleValue.type} value={singleValue.type}
onChange={(e) => { onBlur={(e) => {
singleValue.type = e.target.value as ViewModels.InputPropertyValueTypeString; singleValue.type = e.target.value as ViewModels.InputPropertyValueTypeString;
if (singleValue.type === "null") { if (singleValue.type === "null") {
singleValue.value = undefined; singleValue.value = undefined;
@@ -217,7 +217,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
<select <select
className="typeSelect" className="typeSelect"
value={firstValue.type} value={firstValue.type}
onChange={(e) => { onBlur={(e) => {
firstValue.type = e.target.value as ViewModels.InputPropertyValueTypeString; firstValue.type = e.target.value as ViewModels.InputPropertyValueTypeString;
this.props.onUpdateProperties(this.props.editedProperties); this.props.onUpdateProperties(this.props.editedProperties);
}} }}

View File

@@ -93,7 +93,7 @@ exports[`<EditorNodePropertiesComponent /> renders component 1`] = `
<td> <td>
<select <select
className="typeSelect" className="typeSelect"
onChange={[Function]} onBlur={[Function]}
required={true} required={true}
value="string" value="string"
> >
@@ -282,7 +282,7 @@ exports[`<EditorNodePropertiesComponent /> renders proper unicode 1`] = `
<td> <td>
<select <select
className="typeSelect" className="typeSelect"
onChange={[Function]} onBlur={[Function]}
required={true} required={true}
value="string" value="string"
> >
@@ -344,7 +344,7 @@ exports[`<EditorNodePropertiesComponent /> renders proper unicode 1`] = `
<td> <td>
<select <select
className="typeSelect" className="typeSelect"
onChange={[Function]} onBlur={[Function]}
required={true} required={true}
value="string" value="string"
> >

View File

@@ -132,7 +132,6 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
onChange={(event: React.ChangeEvent<HTMLInputElement>) => { onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
onLabelChange(event); onLabelChange(event);
}} }}
autoFocus
/> />
<div className="actionCol"></div> <div className="actionCol"></div>
</div> </div>

View File

@@ -103,6 +103,7 @@ export class NotificationConsoleComponent extends React.Component<
onClick={() => this.expandCollapseConsole()} onClick={() => this.expandCollapseConsole()}
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)} onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
tabIndex={0} tabIndex={0}
role="button"
> >
<div className="statusBar"> <div className="statusBar">
<span className="dataTypeIcons"> <span className="dataTypeIcons">
@@ -162,7 +163,7 @@ export class NotificationConsoleComponent extends React.Component<
onKeyDown={(event: React.KeyboardEvent<HTMLSpanElement>) => this.onClearNotificationsKeyPress(event)} onKeyDown={(event: React.KeyboardEvent<HTMLSpanElement>) => this.onClearNotificationsKeyPress(event)}
tabIndex={0} tabIndex={0}
> >
<img src={ClearIcon} alt="clear notifications image" /> <img src={ClearIcon} alt="clear notifications icon" />
Clear Notifications Clear Notifications
</span> </span>
</div> </div>

View File

@@ -9,6 +9,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
id="notificationConsoleHeader" id="notificationConsoleHeader"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
role="button"
tabIndex={0} tabIndex={0}
> >
<div <div
@@ -150,7 +151,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
tabIndex={0} tabIndex={0}
> >
<img <img
alt="clear notifications image" alt="clear notifications icon"
src="" src=""
/> />
Clear Notifications Clear Notifications
@@ -173,6 +174,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
id="notificationConsoleHeader" id="notificationConsoleHeader"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
role="button"
tabIndex={0} tabIndex={0}
> >
<div <div
@@ -316,7 +318,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
tabIndex={0} tabIndex={0}
> >
<img <img
alt="clear notifications image" alt="clear notifications icon"
src="" src=""
/> />
Clear Notifications Clear Notifications

View File

@@ -173,7 +173,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
aria-checked={this.state.createNewDatabase} aria-checked={this.state.createNewDatabase}
name="databaseType" name="databaseType"
type="radio" type="radio"
role="radio"
id="databaseCreateNew" id="databaseCreateNew"
tabIndex={0} tabIndex={0}
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)} onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
@@ -187,7 +186,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
aria-checked={!this.state.createNewDatabase} aria-checked={!this.state.createNewDatabase}
name="databaseType" name="databaseType"
type="radio" type="radio"
role="radio"
tabIndex={0} tabIndex={0}
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)} onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
/> />
@@ -209,7 +207,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
size={40} size={40}
className="panelTextField" className="panelTextField"
aria-label="New database id" aria-label="New database id"
autoFocus
tabIndex={0} tabIndex={0}
value={this.state.newDatabaseId} value={this.state.newDatabaseId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
@@ -320,7 +317,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
aria-label="Turn on indexing" aria-label="Turn on indexing"
aria-checked={this.state.enableIndexing} aria-checked={this.state.enableIndexing}
type="radio" type="radio"
role="radio"
tabIndex={0} tabIndex={0}
onChange={this.onTurnOnIndexing.bind(this)} onChange={this.onTurnOnIndexing.bind(this)}
/> />
@@ -332,7 +328,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
aria-label="Turn off indexing" aria-label="Turn off indexing"
aria-checked={!this.state.enableIndexing} aria-checked={!this.state.enableIndexing}
type="radio" type="radio"
role="radio"
tabIndex={0} tabIndex={0}
onChange={this.onTurnOffIndexing.bind(this)} onChange={this.onTurnOffIndexing.bind(this)}
/> />
@@ -375,7 +370,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
aria-checked={!this.state.isSharded} aria-checked={!this.state.isSharded}
name="unsharded" name="unsharded"
type="radio" type="radio"
role="radio"
id="unshardedOption" id="unshardedOption"
tabIndex={0} tabIndex={0}
onChange={this.onUnshardedRadioBtnChange.bind(this)} onChange={this.onUnshardedRadioBtnChange.bind(this)}
@@ -389,7 +383,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
aria-checked={this.state.isSharded} aria-checked={this.state.isSharded}
name="sharded" name="sharded"
type="radio" type="radio"
role="radio"
id="shardedOption" id="shardedOption"
tabIndex={0} tabIndex={0}
onChange={this.onShardedRadioBtnChange.bind(this)} onChange={this.onShardedRadioBtnChange.bind(this)}
@@ -515,7 +508,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
: "Comma separated paths e.g. /firstName,/address/zipCode" : "Comma separated paths e.g. /firstName,/address/zipCode"
} }
className="panelTextField" className="panelTextField"
autoFocus
value={uniqueKey} value={uniqueKey}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => { onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
const uniqueKeys = this.state.uniqueKeys.map((uniqueKey: string, j: number) => { const uniqueKeys = this.state.uniqueKeys.map((uniqueKey: string, j: number) => {
@@ -574,7 +566,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
aria-checked={this.state.enableAnalyticalStore} aria-checked={this.state.enableAnalyticalStore}
name="analyticalStore" name="analyticalStore"
type="radio" type="radio"
role="radio"
id="enableAnalyticalStoreBtn" id="enableAnalyticalStoreBtn"
tabIndex={0} tabIndex={0}
onChange={this.onEnableAnalyticalStoreRadioBtnChange.bind(this)} onChange={this.onEnableAnalyticalStoreRadioBtnChange.bind(this)}
@@ -589,7 +580,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
aria-checked={!this.state.enableAnalyticalStore} aria-checked={!this.state.enableAnalyticalStore}
name="analyticalStore" name="analyticalStore"
type="radio" type="radio"
role="radio"
id="disableAnalyticalStoreBtn" id="disableAnalyticalStoreBtn"
tabIndex={0} tabIndex={0}
onChange={this.onDisableAnalyticalStoreRadioBtnChange.bind(this)} onChange={this.onDisableAnalyticalStoreRadioBtnChange.bind(this)}

View File

@@ -210,7 +210,6 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
placeholder={databaseIdPlaceHolder} placeholder={databaseIdPlaceHolder}
value={databaseId} value={databaseId}
onChange={handleonChangeDBId} onChange={handleonChangeDBId}
autoFocus
styles={getTextFieldStyles()} styles={getTextFieldStyles()}
/> />

View File

@@ -33,7 +33,6 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
aria-label="Database id" aria-label="Database id"
aria-required="true" aria-required="true"
autoComplete="off" autoComplete="off"
autoFocus={true}
id="database-id" id="database-id"
onChange={[Function]} onChange={[Function]}
pattern="[^/?#\\\\\\\\]*[^/?# \\\\\\\\]" pattern="[^/?#\\\\\\\\]*[^/?# \\\\\\\\]"

View File

@@ -169,7 +169,6 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
aria-label="Create new keyspace" aria-label="Create new keyspace"
checked={keyspaceCreateNew} checked={keyspaceCreateNew}
type="radio" type="radio"
role="radio"
tabIndex={0} tabIndex={0}
onChange={() => { onChange={() => {
setKeyspaceCreateNew(true); setKeyspaceCreateNew(true);
@@ -184,7 +183,6 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
aria-label="Use existing keyspace" aria-label="Use existing keyspace"
checked={!keyspaceCreateNew} checked={!keyspaceCreateNew}
type="radio" type="radio"
role="radio"
tabIndex={0} tabIndex={0}
onChange={() => { onChange={() => {
setKeyspaceCreateNew(false); setKeyspaceCreateNew(false);
@@ -208,7 +206,6 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
value={newKeyspaceId} value={newKeyspaceId}
onChange={(e, newValue) => setNewKeyspaceId(newValue)} onChange={(e, newValue) => setNewKeyspaceId(newValue)}
ariaLabel="Keyspace id" ariaLabel="Keyspace id"
autoFocus
/> />
{!isServerlessAccount() && ( {!isServerlessAccount() && (

View File

@@ -119,7 +119,6 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
<Text variant="small">Confirm by typing the {collectionName.toLowerCase()} id</Text> <Text variant="small">Confirm by typing the {collectionName.toLowerCase()} id</Text>
<TextField <TextField
id="confirmCollectionId" id="confirmCollectionId"
autoFocus
value={inputCollectionName} value={inputCollectionName}
styles={{ fieldGroup: { width: 300 } }} styles={{ fieldGroup: { width: 300 } }}
onChange={(event, newInput?: string) => { onChange={(event, newInput?: string) => {

View File

@@ -41,7 +41,6 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
</Text> </Text>
<StyledTextFieldBase <StyledTextFieldBase
ariaLabel="Confirm by typing the container id" ariaLabel="Confirm by typing the container id"
autoFocus={true}
id="confirmCollectionId" id="confirmCollectionId"
onChange={[Function]} onChange={[Function]}
styles={ styles={
@@ -55,7 +54,6 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
> >
<TextFieldBase <TextFieldBase
ariaLabel="Confirm by typing the container id" ariaLabel="Confirm by typing the container id"
autoFocus={true}
deferredValidationTime={200} deferredValidationTime={200}
id="confirmCollectionId" id="confirmCollectionId"
onChange={[Function]} onChange={[Function]}
@@ -349,7 +347,6 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
<input <input
aria-invalid={false} aria-invalid={false}
aria-label="Confirm by typing the container id" aria-label="Confirm by typing the container id"
autoFocus={true}
className="ms-TextField-field field-57" className="ms-TextField-field field-57"
id="confirmCollectionId" id="confirmCollectionId"
onBlur={[Function]} onBlur={[Function]}

View File

@@ -129,7 +129,6 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
<Text variant="small">Confirm by typing the database id</Text> <Text variant="small">Confirm by typing the database id</Text>
<TextField <TextField
id="confirmDatabaseId" id="confirmDatabaseId"
autoFocus
styles={{ fieldGroup: { width: 300 } }} styles={{ fieldGroup: { width: 300 } }}
onChange={(event, newInput?: string) => { onChange={(event, newInput?: string) => {
setDatabaseInput(newInput); setDatabaseInput(newInput);

View File

@@ -69,25 +69,11 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
/> />
{isAddRemoveVisible && ( {isAddRemoveVisible && (
<> <>
<div tabIndex={0}> <div tabIndex={0} onClick={onDeleteParamKeyPress} role="button" onKeyPress={onDeleteParamKeyPress}>
<Image <Image {...imageProps} src={EntityCancelIcon} alt="Delete param" id="deleteparam" />
{...imageProps}
src={EntityCancelIcon}
alt="Delete param"
id="deleteparam"
role="button"
onClick={onDeleteParamKeyPress}
/>
</div> </div>
<div tabIndex={0}> <div tabIndex={0} onClick={onAddNewParamKeyPress} role="button" onKeyPress={onAddNewParamKeyPress}>
<Image <Image {...imageProps} src={AddPropertyIcon} alt="Add param" id="addparam" />
{...imageProps}
src={AddPropertyIcon}
alt="Add param"
id="addparam"
role="button"
onClick={onAddNewParamKeyPress}
/>
</div> </div>
</> </>
)} )}

View File

@@ -4328,6 +4328,9 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
</TextFieldBase> </TextFieldBase>
</StyledTextFieldBase> </StyledTextFieldBase>
<div <div
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex={0} tabIndex={0}
> >
<StyledImageBase <StyledImageBase
@@ -4335,8 +4338,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
className="addRemoveIconLabel" className="addRemoveIconLabel"
height={30} height={30}
id="deleteparam" id="deleteparam"
onClick={[Function]}
role="button"
src="" src=""
width={20} width={20}
> >
@@ -4345,8 +4346,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
className="addRemoveIconLabel" className="addRemoveIconLabel"
height={30} height={30}
id="deleteparam" id="deleteparam"
onClick={[Function]}
role="button"
src="" src=""
styles={[Function]} styles={[Function]}
theme={ theme={
@@ -4638,10 +4637,8 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
className="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-87" className="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-87"
id="deleteparam" id="deleteparam"
key="fabricImage" key="fabricImage"
onClick={[Function]}
onError={[Function]} onError={[Function]}
onLoad={[Function]} onLoad={[Function]}
role="button"
src="" src=""
/> />
</div> </div>
@@ -4649,6 +4646,9 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
</StyledImageBase> </StyledImageBase>
</div> </div>
<div <div
onClick={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex={0} tabIndex={0}
> >
<StyledImageBase <StyledImageBase
@@ -4656,8 +4656,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
className="addRemoveIconLabel" className="addRemoveIconLabel"
height={30} height={30}
id="addparam" id="addparam"
onClick={[Function]}
role="button"
src="" src=""
width={20} width={20}
> >
@@ -4666,8 +4664,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
className="addRemoveIconLabel" className="addRemoveIconLabel"
height={30} height={30}
id="addparam" id="addparam"
onClick={[Function]}
role="button"
src="" src=""
styles={[Function]} styles={[Function]}
theme={ theme={
@@ -4959,10 +4955,8 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
className="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-87" className="ms-Image-image ms-Image-image--portrait is-notLoaded is-fadeIn image-87"
id="addparam" id="addparam"
key="fabricImage" key="fabricImage"
onClick={[Function]}
onError={[Function]} onError={[Function]}
onLoad={[Function]} onLoad={[Function]}
role="button"
src="" src=""
/> />
</div> </div>

View File

@@ -92,7 +92,6 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
id="confirmCollectionId" id="confirmCollectionId"
label="Select a query document" label="Select a query document"
value={selectedFileName} value={selectedFileName}
autoFocus
readOnly readOnly
styles={{ fieldGroup: { width: 300 } }} styles={{ fieldGroup: { width: 300 } }}
/> />

View File

@@ -17,7 +17,6 @@ exports[`Load Query Pane should render Default properly 1`] = `
horizontal={true} horizontal={true}
> >
<StyledTextFieldBase <StyledTextFieldBase
autoFocus={true}
id="confirmCollectionId" id="confirmCollectionId"
label="Select a query document" label="Select a query document"
readOnly={true} readOnly={true}

View File

@@ -48,9 +48,15 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
)} )}
</Text> </Text>
{showErrorDetails && ( {showErrorDetails && (
<a className="paneErrorLink" role="link" onClick={expandConsole}> <span
className="paneErrorLink moreOption"
role="link"
onClick={expandConsole}
onKeyPress={expandConsole}
tabIndex={0}
>
More details More details
</a> </span>
)} )}
</span> </span>
</Stack> </Stack>

View File

@@ -3,6 +3,6 @@ import LoadingIndicator_3Squares from "../../../images/LoadingIndicator_3Squares
export const PanelLoadingScreen: React.FunctionComponent = () => ( export const PanelLoadingScreen: React.FunctionComponent = () => (
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer"> <div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer">
<img className="dataExplorerLoader" src={LoadingIndicator_3Squares} /> <img className="dataExplorerLoader" src={LoadingIndicator_3Squares} alt="loading indicator" />
</div> </div>
); );

View File

@@ -146,7 +146,6 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({ explorer
<TextField <TextField
id="saveQueryInput" id="saveQueryInput"
label="Name" label="Name"
autoFocus
styles={{ fieldGroup: { width: 300 } }} styles={{ fieldGroup: { width: 300 } }}
onChange={(event, newInput?: string) => { onChange={(event, newInput?: string) => {
setQueryName(newInput); setQueryName(newInput);

View File

@@ -97,7 +97,6 @@ export const StringInputPane: FunctionComponent<StringInputPanelProps> = ({
label={inputLabel} label={inputLabel}
name="collectionIdConfirmation" name="collectionIdConfirmation"
value={stringInput} value={stringInput}
autoFocus
required required
onChange={(event: FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => onChange={(event: FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) =>
setStringInput(newValue) setStringInput(newValue)

View File

@@ -56,7 +56,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
> >
<StyledTextFieldBase <StyledTextFieldBase
aria-label="Enter new directory name" aria-label="Enter new directory name"
autoFocus={true}
label="Enter new directory name" label="Enter new directory name"
name="collectionIdConfirmation" name="collectionIdConfirmation"
onChange={[Function]} onChange={[Function]}
@@ -65,7 +64,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
> >
<TextFieldBase <TextFieldBase
aria-label="Enter new directory name" aria-label="Enter new directory name"
autoFocus={true}
deferredValidationTime={200} deferredValidationTime={200}
label="Enter new directory name" label="Enter new directory name"
name="collectionIdConfirmation" name="collectionIdConfirmation"
@@ -655,7 +653,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
<input <input
aria-invalid={false} aria-invalid={false}
aria-labelledby="TextFieldLabel2" aria-labelledby="TextFieldLabel2"
autoFocus={true}
className="ms-TextField-field field-56" className="ms-TextField-field field-56"
id="TextField0" id="TextField0"
name="collectionIdConfirmation" name="collectionIdConfirmation"

View File

@@ -4,7 +4,7 @@ import React from "react";
import TableListViewModal from "../../Tables/DataTable/TableEntityListViewModel"; import TableListViewModal from "../../Tables/DataTable/TableEntityListViewModel";
import * as Entities from "../../Tables/Entities"; import * as Entities from "../../Tables/Entities";
import { CassandraAPIDataClient, TablesAPIDataClient } from "../../Tables/TableDataClient"; import { CassandraAPIDataClient, TablesAPIDataClient } from "../../Tables/TableDataClient";
import QueryTablesTab from "../../Tabs/QueryTablesTab/QueryTablesTab"; import QueryTablesTab from "../../Tabs/QueryTablesTab";
import { AddTableEntityPanel } from "./AddTableEntityPanel"; import { AddTableEntityPanel } from "./AddTableEntityPanel";
describe("Excute Add Table Entity Pane", () => { describe("Excute Add Table Entity Pane", () => {
@@ -18,8 +18,6 @@ describe("Excute Add Table Entity Pane", () => {
queryTablesTab: fakeQueryTablesTab, queryTablesTab: fakeQueryTablesTab,
tableEntityListViewModel: fakeTableEntityListViewModel, tableEntityListViewModel: fakeTableEntityListViewModel,
cassandraApiClient: fakeCassandraApiClient, cassandraApiClient: fakeCassandraApiClient,
reloadEntities: () => "{}",
headerItems: ["email"],
}; };
it("should render Default properly", () => { it("should render Default properly", () => {
@@ -29,13 +27,13 @@ describe("Excute Add Table Entity Pane", () => {
it("initially display 4 input field, 2 properties and 2 entity values", () => { it("initially display 4 input field, 2 properties and 2 entity values", () => {
const wrapper = mount(<AddTableEntityPanel {...props} />); const wrapper = mount(<AddTableEntityPanel {...props} />);
expect(wrapper.find("input[type='text']")).toHaveLength(1); expect(wrapper.find("input[type='text']")).toHaveLength(0);
}); });
it("add a new entity row", () => { it("add a new entity row", () => {
const wrapper = mount(<AddTableEntityPanel {...props} />); const wrapper = mount(<AddTableEntityPanel {...props} />);
wrapper.find(".addButtonEntiy").last().simulate("click"); wrapper.find(".addButtonEntiy").last().simulate("click");
expect(wrapper.find("input[type='text']")).toHaveLength(2); expect(wrapper.find("input[type='text']")).toHaveLength(1);
}); });
it("remove a entity field", () => { it("remove a entity field", () => {
@@ -43,6 +41,6 @@ describe("Excute Add Table Entity Pane", () => {
// Since default entity row doesn't have delete option, so added row then delete for test cases. // Since default entity row doesn't have delete option, so added row then delete for test cases.
wrapper.find(".addButtonEntiy").last().simulate("click"); wrapper.find(".addButtonEntiy").last().simulate("click");
wrapper.find("#deleteEntity").last().simulate("click"); wrapper.find("#deleteEntity").last().simulate("click");
expect(wrapper.find("input[type='text']")).toHaveLength(1); expect(wrapper.find("input[type='text']")).toHaveLength(0);
}); });
}); });

View File

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

View File

@@ -4,7 +4,7 @@ import React from "react";
import TableListViewModal from "../../Tables/DataTable/TableEntityListViewModel"; import TableListViewModal from "../../Tables/DataTable/TableEntityListViewModel";
import * as Entities from "../../Tables/Entities"; import * as Entities from "../../Tables/Entities";
import { CassandraAPIDataClient, TablesAPIDataClient } from "../../Tables/TableDataClient"; import { CassandraAPIDataClient, TablesAPIDataClient } from "../../Tables/TableDataClient";
import QueryTablesTab from "../../Tabs/QueryTablesTab/QueryTablesTab"; import QueryTablesTab from "../../Tabs/QueryTablesTab";
import { EditTableEntityPanel } from "./EditTableEntityPanel"; import { EditTableEntityPanel } from "./EditTableEntityPanel";
describe("Excute Edit Table Entity Pane", () => { describe("Excute Edit Table Entity Pane", () => {
@@ -15,14 +15,11 @@ describe("Excute Edit Table Entity Pane", () => {
fakeTableEntityListViewModel.headers = []; fakeTableEntityListViewModel.headers = [];
fakeTableEntityListViewModel.selected = ko.observableArray<Entities.ITableEntity>([{}]); fakeTableEntityListViewModel.selected = ko.observableArray<Entities.ITableEntity>([{}]);
const fakeSelectedItem = [{ PartitionKey: { _: "test", $: "String" } }];
const props = { const props = {
tableDataClient: new TablesAPIDataClient(), tableDataClient: new TablesAPIDataClient(),
queryTablesTab: fakeQueryTablesTab, queryTablesTab: fakeQueryTablesTab,
tableEntityListViewModel: fakeTableEntityListViewModel, tableEntityListViewModel: fakeTableEntityListViewModel,
cassandraApiClient: fakeCassandraApiClient, cassandraApiClient: fakeCassandraApiClient,
selectedEntity: fakeSelectedItem,
reloadEntities: () => "{}",
}; };
it("should render Default properly", () => { it("should render Default properly", () => {
@@ -32,13 +29,13 @@ describe("Excute Edit Table Entity Pane", () => {
it("initially display 4 input field, 2 properties and 1 entity values", () => { it("initially display 4 input field, 2 properties and 1 entity values", () => {
const wrapper = mount(<EditTableEntityPanel {...props} />); const wrapper = mount(<EditTableEntityPanel {...props} />);
expect(wrapper.find("input[type='text']")).toHaveLength(1); expect(wrapper.find("input[type='text']")).toHaveLength(0);
}); });
it("add a new entity row", () => { it("add a new entity row", () => {
const wrapper = mount(<EditTableEntityPanel {...props} />); const wrapper = mount(<EditTableEntityPanel {...props} />);
wrapper.find(".addButtonEntiy").last().simulate("click"); wrapper.find(".addButtonEntiy").last().simulate("click");
expect(wrapper.find("input[type='text']")).toHaveLength(2); expect(wrapper.find("input[type='text']")).toHaveLength(1);
}); });
it("remove a entity field", () => { it("remove a entity field", () => {
@@ -46,6 +43,6 @@ describe("Excute Edit Table Entity Pane", () => {
// Since default entity row doesn't have delete option, so added row then delete for test cases. // Since default entity row doesn't have delete option, so added row then delete for test cases.
wrapper.find(".addButtonEntiy").last().simulate("click"); wrapper.find(".addButtonEntiy").last().simulate("click");
wrapper.find("#deleteEntity").last().simulate("click"); wrapper.find("#deleteEntity").last().simulate("click");
expect(wrapper.find("input[type='text']")).toHaveLength(1); expect(wrapper.find("input[type='text']")).toHaveLength(0);
}); });
}); });

View File

@@ -6,14 +6,14 @@ import AddPropertyIcon from "../../../../images/Add-property.svg";
import RevertBackIcon from "../../../../images/RevertBack.svg"; import RevertBackIcon from "../../../../images/RevertBack.svg";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
import { TableEntity } from "../../../Common/TableEntity"; import { TableEntity } from "../../../Common/TableEntity";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import * as TableConstants from "../../Tables/Constants"; import * as TableConstants from "../../Tables/Constants";
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel"; import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
import * as Entities from "../../Tables/Entities"; import * as Entities from "../../Tables/Entities";
import { CassandraAPIDataClient, TableDataClient } from "../../Tables/TableDataClient"; import { CassandraAPIDataClient, TableDataClient } from "../../Tables/TableDataClient";
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor"; import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
import NewQueryTablesTab from "../../Tabs/QueryTablesTab/QueryTablesTab"; import QueryTablesTab from "../../Tabs/QueryTablesTab";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import { import {
attributeNameLabel, attributeNameLabel,
@@ -34,11 +34,9 @@ import {
interface EditTableEntityPanelProps { interface EditTableEntityPanelProps {
tableDataClient: TableDataClient; tableDataClient: TableDataClient;
queryTablesTab: NewQueryTablesTab; queryTablesTab: QueryTablesTab;
tableEntityListViewModel: TableEntityListViewModel; tableEntityListViewModel: TableEntityListViewModel;
cassandraApiClient: CassandraAPIDataClient; cassandraApiClient: CassandraAPIDataClient;
selectedEntity: Entities.ITableEntity[];
reloadEntities: () => void;
} }
interface EntityRowType { interface EntityRowType {
@@ -59,10 +57,7 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
queryTablesTab, queryTablesTab,
tableEntityListViewModel, tableEntityListViewModel,
cassandraApiClient, cassandraApiClient,
selectedEntity,
reloadEntities,
}: EditTableEntityPanelProps): JSX.Element => { }: EditTableEntityPanelProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [entities, setEntities] = useState<EntityRowType[]>([]); const [entities, setEntities] = useState<EntityRowType[]>([]);
const [selectedRow, setSelectedRow] = useState<number>(0); const [selectedRow, setSelectedRow] = useState<number>(0);
const [entityAttributeValue, setEntityAttributeValue] = useState<string>(""); const [entityAttributeValue, setEntityAttributeValue] = useState<string>("");
@@ -80,8 +75,8 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
let originalDocument: { [key: string]: any } = {}; let originalDocument: { [key: string]: any } = {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const entityAttribute: any = selectedEntity; const entityAttribute: any = tableEntityListViewModel.selected();
const entityFormattedAttribute = constructDisplayedAttributes(entityAttribute && entityAttribute[0]); const entityFormattedAttribute = constructDisplayedAttributes(entityAttribute[0]);
setEntities(entityFormattedAttribute); setEntities(entityFormattedAttribute);
if (userContext.apiType === "Tables") { if (userContext.apiType === "Tables") {
@@ -91,7 +86,6 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
originalDocument = entityAttribute; originalDocument = entityAttribute;
} }
setOriginalDocument(originalDocument); setOriginalDocument(originalDocument);
//eslint-disable-next-line
}, []); }, []);
const constructDisplayedAttributes = (entity: Entities.ITableEntity): EntityRowType[] => { const constructDisplayedAttributes = (entity: Entities.ITableEntity): EntityRowType[] => {
@@ -222,8 +216,9 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
entity entity
); );
await tableEntityListViewModel.updateCachedEntity(newEntity); await tableEntityListViewModel.updateCachedEntity(newEntity);
reloadEntities(); if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
closeSidePanel(); tableEntityListViewModel.redrawTableThrottled();
}
tableEntityListViewModel.selected.removeAll(); tableEntityListViewModel.selected.removeAll();
tableEntityListViewModel.selected.push(newEntity); tableEntityListViewModel.selected.push(newEntity);
} catch (error) { } catch (error) {
@@ -235,6 +230,34 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
} }
}; };
const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
let newHeaders: string[] = [];
const keys = Object.keys(newEntity);
keys &&
keys.forEach((key: string) => {
if (
!_.contains(viewModel.headers, key) &&
key !== TableEntityProcessor.keyProperties.attachments &&
key !== TableEntityProcessor.keyProperties.etag &&
key !== TableEntityProcessor.keyProperties.resourceId &&
key !== TableEntityProcessor.keyProperties.self &&
(!(userContext.apiType === "Cassandra") || key !== TableConstants.EntityKeyNames.RowKey)
) {
newHeaders.push(key);
}
});
let newHeadersInserted = false;
if (newHeaders.length) {
if (!DataTableUtilities.checkForDefaultHeader(viewModel.headers)) {
newHeaders = viewModel.headers.concat(newHeaders);
}
viewModel.updateHeaders(newHeaders, /* notifyColumnChanges */ true, /* enablePrompt */ false);
newHeadersInserted = true;
}
return newHeadersInserted;
};
// Add new entity row // Add new entity row
const addNewEntity = (): void => { const addNewEntity = (): void => {
const cloneEntities = [...entities]; const cloneEntities = [...entities];

View File

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

View File

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

View File

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

View File

@@ -368,7 +368,6 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
</Text> </Text>
<StyledTextFieldBase <StyledTextFieldBase
ariaLabel="Confirm by typing the database id" ariaLabel="Confirm by typing the database id"
autoFocus={true}
id="confirmDatabaseId" id="confirmDatabaseId"
onChange={[Function]} onChange={[Function]}
styles={ styles={
@@ -381,7 +380,6 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
> >
<TextFieldBase <TextFieldBase
ariaLabel="Confirm by typing the database id" ariaLabel="Confirm by typing the database id"
autoFocus={true}
deferredValidationTime={200} deferredValidationTime={200}
id="confirmDatabaseId" id="confirmDatabaseId"
onChange={[Function]} onChange={[Function]}
@@ -674,7 +672,6 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
<input <input
aria-invalid={false} aria-invalid={false}
aria-label="Confirm by typing the database id" aria-label="Confirm by typing the database id"
autoFocus={true}
className="ms-TextField-field field-60" className="ms-TextField-field field-60"
id="confirmDatabaseId" id="confirmDatabaseId"
onBlur={[Function]} onBlur={[Function]}

View File

@@ -108,7 +108,8 @@
} }
.oneLineContent { .oneLineContent {
margin-top: 4px; margin-top: 6px;
margin-left: 6px;
} }
.twoLineContent { .twoLineContent {
@@ -133,18 +134,18 @@
.flex-display(); .flex-display();
.flex-direction(); .flex-direction();
>.title { .title {
color: @BaseDark; color: @BaseDark;
padding: 0px; padding: 0px;
font-size: 12px; font-size: 12px;
} }
>.description { .description {
color: @BaseDark; color: @BaseDark;
} }
&:not(:hover):not(:focus) { &:not(:hover):not(:focus) {
background-color: @BaseLow; background-color: @BaseLow;
} }
} }
&.commonTasks { &.commonTasks {

View File

@@ -128,18 +128,18 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
<div className="title">Common Tasks</div> <div className="title">Common Tasks</div>
<ul> <ul>
{commonTaskItems.map((item) => ( {commonTaskItems.map((item) => (
<li <li className="focusable" key={`${item.title}${item.description}`}>
className="focusable" <div
key={`${item.title}${item.description}`} onClick={item.onClick}
onClick={item.onClick} onKeyPress={(event: React.KeyboardEvent) => this.onSplashScreenItemKeyPress(event, item.onClick)}
onKeyPress={(event: React.KeyboardEvent) => this.onSplashScreenItemKeyPress(event, item.onClick)} tabIndex={0}
tabIndex={0} role="button"
role="button" >
> <img src={item.iconSrc} alt="" />
<img src={item.iconSrc} alt="" /> <span className="oneLineContent" title={item.info}>
<span className="oneLineContent" title={item.info}> {item.title}
{item.title} </span>
</span> </div>
</li> </li>
))} ))}
</ul> </ul>
@@ -165,22 +165,23 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
<div className="title">Tips</div> <div className="title">Tips</div>
<ul> <ul>
{tipsItems.map((item) => ( {tipsItems.map((item) => (
<li <li className="tipContainer focusable" key={`${item.title}${item.description}`}>
className="tipContainer focusable" <div
key={`${item.title}${item.description}`} onClick={item.onClick}
onClick={item.onClick} onKeyPress={(event: React.KeyboardEvent) => this.onSplashScreenItemKeyPress(event, item.onClick)}
onKeyPress={(event: React.KeyboardEvent) => this.onSplashScreenItemKeyPress(event, item.onClick)} tabIndex={0}
tabIndex={0} role="button"
role="link" className="tipsWrapper"
> >
<div className="title" title={item.info}> <div className="title" title={item.info}>
{item.title} {item.title}
</div>
<div className="description">{item.description}</div>
</div> </div>
<div className="description">{item.description}</div>
</li> </li>
))} ))}
<li> <li>
<a role="link" href={SplashScreen.seeMoreItemUrl} rel="noreferrer" target="_blank" tabIndex={0}> <a href={SplashScreen.seeMoreItemUrl} rel="noreferrer" target="_blank" tabIndex={0}>
{SplashScreen.seeMoreItemTitle} {SplashScreen.seeMoreItemTitle}
</a> </a>
</li> </li>

View File

@@ -0,0 +1,393 @@
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

@@ -0,0 +1,52 @@
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

@@ -0,0 +1,300 @@
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

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

View File

@@ -1,12 +1,14 @@
import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import * as ko from "knockout"; import * as ko from "knockout";
import * as _ from "underscore"; import * as _ from "underscore";
import * as CommonConstants from "../../../Common/Constants";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import NewQueryTablesTab from "../../Tabs/QueryTablesTab/QueryTablesTab";
import * as Entities from "../Entities";
import CacheBase from "./CacheBase"; 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 * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
// This is the format of the data we will have to pass to Datatable render callback, // This is the format of the data we will have to pass to Datatable render callback,
// and property names are defined by Datatable as well. // and property names are defined by Datatable as well.
@@ -45,11 +47,19 @@ abstract class DataTableViewModel {
private pendingRedraw = false; private pendingRedraw = false;
private lastRedrawTime = new Date().getTime(); private lastRedrawTime = new Date().getTime();
public queryTablesTab: NewQueryTablesTab; private dataTableOperationManager: IDataTableOperation;
public queryTablesTab: QueryTablesTab;
constructor() { constructor() {
this.items([]); this.items([]);
this.selected([]); this.selected([]);
// Late bound
this.dataTableOperationManager = null;
}
public bind(dataTableOperationManager: IDataTableOperation): void {
this.dataTableOperationManager = dataTableOperationManager;
} }
public clearLastSelected(): void { public clearLastSelected(): void {
@@ -91,6 +101,10 @@ abstract class DataTableViewModel {
} }
} }
public focusDataTable(): void {
this.dataTableOperationManager.focusTable();
}
public getItemFromSelectedItems(itemKeys: Entities.IProperty[]): Entities.ITableEntity { public getItemFromSelectedItems(itemKeys: Entities.IProperty[]): Entities.ITableEntity {
return _.find(this.selected(), (item: Entities.ITableEntity) => { return _.find(this.selected(), (item: Entities.ITableEntity) => {
return this.matchesKeys(item, itemKeys); return this.matchesKeys(item, itemKeys);
@@ -156,12 +170,35 @@ abstract class DataTableViewModel {
this.cache.sortOrder = sortOrder; this.cache.sortOrder = sortOrder;
} }
protected renderPage(startIndex: number, pageSize: number) { 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
var endIndex = pageSize < 0 ? this.cache.length : startIndex + pageSize; var endIndex = pageSize < 0 ? this.cache.length : startIndex + pageSize;
var renderData = this.cache.data.slice(startIndex, endIndex); var renderData = this.cache.data.slice(startIndex, endIndex);
this.items(renderData); this.items(renderData);
var render: IDataTableRenderData = {
draw: draw,
aaData: renderData,
recordsTotal: this.cache.length,
recordsFiltered: this.cache.length,
};
if (!!postRenderTasks) {
postRenderTasks(startIndex, pageSize).then(() => {
this.table.rows().invalidate();
});
}
renderCallBack(render);
if (this.queryTablesTab.onLoadStartKey != null && this.queryTablesTab.onLoadStartKey != undefined) { if (this.queryTablesTab.onLoadStartKey != null && this.queryTablesTab.onLoadStartKey != undefined) {
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.Tab, Action.Tab,
@@ -180,6 +217,16 @@ abstract class DataTableViewModel {
protected matchesKeys(item: Entities.ITableEntity, itemKeys: Entities.IProperty[]): boolean { protected matchesKeys(item: Entities.ITableEntity, itemKeys: Entities.IProperty[]): boolean {
return itemKeys.every((property: Entities.IProperty) => { return itemKeys.every((property: Entities.IProperty) => {
var itemValue = item[property.key]; var itemValue = item[property.key];
// if (itemValue && property.subkey) {
// itemValue = itemValue._[property.subkey];
// if (!itemValue) {
// itemValue = "";
// }
// } else if (property.subkey) {
// itemValue = "";
// }
return this.stringCompare(itemValue._, property.value); return this.stringCompare(itemValue._, property.value);
}); });
} }
@@ -191,6 +238,27 @@ abstract class DataTableViewModel {
protected stringCompare(s1: string, s2: string): boolean { protected stringCompare(s1: string, s2: string): boolean {
return s1 === s2; return s1 === s2;
} }
private updatePaginationControls(oSettings: any) {
var pageInfo = this.table.page.info();
var pageSize = pageInfo.length;
var paginateElement = $(oSettings.nTableWrapper).find(Constants.htmlSelectors.paginateSelector);
if (this.allDownloaded) {
if (this.cache.length <= pageSize) {
// Hide pagination controls if everything fits in one page!.
paginateElement.hide();
} else {
// Enable pagination controls.
paginateElement.show();
oSettings.oLanguage.oPaginate.sLast = DataTableViewModel.lastPageLabel;
}
} else {
// Enable pagination controls and show load more button.
paginateElement.show();
oSettings.oLanguage.oPaginate.sLast = DataTableViewModel.loadMoreLabel;
}
}
} }
interface IDataTableOperation { interface IDataTableOperation {

View File

@@ -94,4 +94,8 @@ export default class TableCommands {
return null; 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 { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import NewQueryTablesTab from "../../Tabs/QueryTablesTab/QueryTablesTab"; import QueryTablesTab from "../../Tabs/QueryTablesTab";
import * as Constants from "../Constants"; import * as Constants from "../Constants";
import { getQuotedCqlIdentifier } from "../CqlUtilities"; import { getQuotedCqlIdentifier } from "../CqlUtilities";
import * as Entities from "../Entities"; import * as Entities from "../Entities";
@@ -101,8 +101,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
public useSetting: boolean = true; public useSetting: boolean = true;
//public tableExplorerContext: TableExplorerContext; //public tableExplorerContext: TableExplorerContext;
public notifyColumnChanges: (enablePrompt: boolean, queryTablesTab: NewQueryTablesTab) => void; public notifyColumnChanges: (enablePrompt: boolean, queryTablesTab: QueryTablesTab) => void;
public tablePageStartIndex: number; public tablePageStartIndex: number;
public tableQuery: Entities.ITableQuery = {}; public tableQuery: Entities.ITableQuery = {};
public cqlQuery: ko.Observable<string>; public cqlQuery: ko.Observable<string>;
@@ -113,7 +112,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
public queryErrorMessage: ko.Observable<string>; public queryErrorMessage: ko.Observable<string>;
public id: string; public id: string;
constructor(tableCommands: TableCommands, queryTablesTab: NewQueryTablesTab) { constructor(tableCommands: TableCommands, queryTablesTab: QueryTablesTab) {
super(); super();
this.cache = new TableEntityCache(); this.cache = new TableEntityCache();
this.queryErrorMessage = ko.observable<string>(); this.queryErrorMessage = ko.observable<string>();
@@ -132,8 +131,24 @@ export default class TableEntityListViewModel extends DataTableViewModel {
return [{ key: Constants.EntityKeyNames.RowKey, value: rowKey }]; return [{ key: Constants.EntityKeyNames.RowKey, value: rowKey }];
} }
public reloadTable(useSetting: boolean = true, resetHeaders: boolean = true): DataTables.DataTable {
this.clearCache();
this.clearSelection();
this.isCancelled = false;
this.useSetting = useSetting;
if (resetHeaders) {
this.updateHeaders([Constants.defaultHeader]);
}
return this.table.ajax.reload();
}
public updateHeaders(newHeaders: string[], notifyColumnChanges: boolean = false, enablePrompt: boolean = true): void { public updateHeaders(newHeaders: string[], notifyColumnChanges: boolean = false, enablePrompt: boolean = true): void {
this.headers = newHeaders; this.headers = newHeaders;
if (notifyColumnChanges) {
this.clearSelection();
this.notifyColumnChanges(enablePrompt, this.queryTablesTab);
}
} }
/** /**
@@ -143,21 +158,40 @@ export default class TableEntityListViewModel extends DataTableViewModel {
* fnCallback - is the render callback with data to render. * fnCallback - is the render callback with data to render.
* oSetting: current settings used for table initialization. * oSetting: current settings used for table initialization.
*/ */
public renderNextPageAndupdateCache(sSource: any, aoData: any, fnCallback: any, oSettings: any) {
public async renderNextPageAndupdateCache(): Promise<Entities.ITableEntity[]> {
var tablePageSize: number; var tablePageSize: number;
var draw: number;
var prefetchNeeded = true; var prefetchNeeded = true;
var columnSortOrder: any;
// Threshold(pages) for triggering cache prefetch. // Threshold(pages) for triggering cache prefetch.
// If number remaining pages in cache falls below prefetchThreshold prefetch will be triggered. // If number remaining pages in cache falls below prefetchThreshold prefetch will be triggered.
var prefetchThreshold = 10; var prefetchThreshold = 10;
var tableQuery = this.tableQuery; var tableQuery = this.tableQuery;
for (var index in aoData) {
var data = aoData[index];
if (data.name === "length") {
tablePageSize = data.value;
}
if (data.name === "start") {
this.tablePageStartIndex = data.value;
}
if (data.name === "draw") {
draw = data.value;
}
if (data.name === "order") {
columnSortOrder = data.value;
}
}
// Try cache if valid. // Try cache if valid.
if (this.isCacheValid(tableQuery)) { if (this.isCacheValid(tableQuery)) {
// Check if prefetch needed. // Check if prefetch needed.
if (this.tablePageStartIndex + tablePageSize <= this.cache.length || this.allDownloaded) { if (this.tablePageStartIndex + tablePageSize <= this.cache.length || this.allDownloaded) {
prefetchNeeded = false; prefetchNeeded = false;
this.tablePageStartIndex = 0; if (columnSortOrder && (!this.cache.sortOrder || !_.isEqual(this.cache.sortOrder, columnSortOrder))) {
this.renderPage(this.tablePageStartIndex, this.cache.length); this.sortColumns(columnSortOrder, oSettings);
}
this.renderPage(fnCallback, draw, this.tablePageStartIndex, tablePageSize, oSettings);
if ( if (
!this.allDownloaded && !this.allDownloaded &&
this.tablePageStartIndex > 0 && // This is a case now that we can hit this as we re-construct table when we update column this.tablePageStartIndex > 0 && // This is a case now that we can hit this as we re-construct table when we update column
@@ -174,21 +208,41 @@ export default class TableEntityListViewModel extends DataTableViewModel {
if (prefetchNeeded) { if (prefetchNeeded) {
var downloadSize = tableQuery.top || this.downloadSize; var downloadSize = tableQuery.top || this.downloadSize;
return await this.prefetchAndRender(tableQuery, 0, tablePageSize, downloadSize); this.prefetchAndRender(
} else { tableQuery,
return this.cache.data; this.tablePageStartIndex,
tablePageSize,
downloadSize,
draw,
fnCallback,
oSettings,
columnSortOrder
);
} }
} }
public addEntityToCache(entity: Entities.ITableEntity): Q.Promise<any> { public addEntityToCache(entity: Entities.ITableEntity): Q.Promise<any> {
// Delay the add operation if we are fetching data from server, so as to avoid race condition. // Delay the add operation if we are fetching data from server, so as to avoid race condition.
if (this.cache.serverCallInProgress) { if (this.cache.serverCallInProgress) {
Utilities.delay(this.pollingInterval).then(() => { return Utilities.delay(this.pollingInterval).then(() => {
this.updateCachedEntity(entity); return this.updateCachedEntity(entity);
}); });
} }
this.cache.data.splice(this.cache.length, 0, 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);
// Finally, select newly added entity // Finally, select newly added entity
this.clearSelection(); this.clearSelection();
@@ -200,8 +254,8 @@ export default class TableEntityListViewModel extends DataTableViewModel {
public updateCachedEntity(entity: Entities.ITableEntity): Q.Promise<any> { public updateCachedEntity(entity: Entities.ITableEntity): Q.Promise<any> {
// Delay the add operation if we are fetching data from server, so as to avoid race condition. // Delay the add operation if we are fetching data from server, so as to avoid race condition.
if (this.cache.serverCallInProgress) { if (this.cache.serverCallInProgress) {
Utilities.delay(this.pollingInterval).then(() => { return Utilities.delay(this.pollingInterval).then(() => {
this.updateCachedEntity(entity); return this.updateCachedEntity(entity);
}); });
} }
var oldEntityIndex: number = _.findIndex( var oldEntityIndex: number = _.findIndex(
@@ -221,8 +275,8 @@ export default class TableEntityListViewModel extends DataTableViewModel {
// Delay the remove operation if we are fetching data from server, so as to avoid race condition. // Delay the remove operation if we are fetching data from server, so as to avoid race condition.
if (this.cache.serverCallInProgress) { if (this.cache.serverCallInProgress) {
Utilities.delay(this.pollingInterval).then(() => { return Utilities.delay(this.pollingInterval).then(() => {
this.removeEntitiesFromCache(entities); return this.removeEntitiesFromCache(entities);
}); });
} }
@@ -238,6 +292,14 @@ export default class TableEntityListViewModel extends DataTableViewModel {
}); });
this.clearSelection(); this.clearSelection();
// Show last available page if there is not enough data
var pageInfo = this.table.page.info();
if (this.cache.length <= pageInfo.start) {
var availablePages = Math.ceil(this.cache.length / pageInfo.length);
var pageToShow = availablePages > 0 ? availablePages - 1 : 0;
this.table.page(pageToShow);
}
return Q.resolve(null); return Q.resolve(null);
} }
@@ -330,75 +392,86 @@ export default class TableEntityListViewModel extends DataTableViewModel {
}); });
} }
private async prefetchAndRender( private prefetchAndRender(
tableQuery: Entities.ITableQuery, tableQuery: Entities.ITableQuery,
tablePageStartIndex: number, tablePageStartIndex: number,
tablePageSize: number, tablePageSize: number,
downloadSize: number downloadSize: number,
): Promise<Entities.ITableEntity[]> { draw: number,
renderCallBack: Function,
oSettings: any,
columnSortOrder: any
): void {
this.queryErrorMessage(null); this.queryErrorMessage(null);
if (this.cache.serverCallInProgress) { if (this.cache.serverCallInProgress) {
return undefined; return;
} }
try { this.prefetchData(tableQuery, downloadSize, /* currentRetry */ 0)
const result = await this.prefetchData(tableQuery, downloadSize, /* currentRetry */ 0); .then((result: IListTableEntitiesSegmentedResult) => {
if (!result) { if (!result) {
return undefined; return;
}
// 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);
return result; var entities = this.cache.data;
} catch (error) { if (userContext.apiType === "Cassandra" && DataTableUtilities.checkForDefaultHeader(this.headers)) {
const parsedErrors = parseError(error); (<CassandraAPIDataClient>this.queryTablesTab.container.tableDataClient)
var errors = parsedErrors.map((error) => { .getTableSchema(this.queryTablesTab.collection)
return <ViewModels.QueryError>{ .then((headers: CassandraTableKey[]) => {
message: error.message, this.updateHeaders(
start: error.location ? error.location.start : undefined, headers.map((header) => header.property),
end: error.location ? error.location.end : undefined, true
code: error.code, );
severity: error.severity, });
}; } else {
var selectedHeadersUnion: string[] = DataTableUtilities.getPropertyIntersectionFromTableEntities(
entities,
userContext.apiType === "Cassandra"
);
var newHeaders: string[] = _.difference(selectedHeadersUnion, this.headers);
if (newHeaders.length > 0) {
// Any new columns found will be added into headers array, which will trigger a re-render of the DataTable.
// So there is no need to call it here.
this.updateHeaders(newHeaders, /* notifyColumnChanges */ true);
} else {
if (columnSortOrder) {
this.sortColumns(columnSortOrder, oSettings);
}
this.renderPage(renderCallBack, draw, tablePageStartIndex, tablePageSize, oSettings);
}
}
if (result.ExceedMaximumRetries) {
var message: string = "We are having trouble getting your data. Please try again."; // localize
}
})
.catch((error: any) => {
const parsedErrors = parseError(error);
var errors = parsedErrors.map((error) => {
return <ViewModels.QueryError>{
message: error.message,
start: error.location ? error.location.start : undefined,
end: error.location ? error.location.end : undefined,
code: error.code,
severity: error.severity,
};
});
this.queryErrorMessage(errors[0].message);
if (this.queryTablesTab.onLoadStartKey != null && this.queryTablesTab.onLoadStartKey != undefined) {
TelemetryProcessor.traceFailure(
Action.Tab,
{
databaseName: this.queryTablesTab.collection.databaseId,
collectionName: this.queryTablesTab.collection.id(),
dataExplorerArea: Areas.Tab,
tabTitle: this.queryTablesTab.tabTitle(),
error: error,
},
this.queryTablesTab.onLoadStartKey
);
this.queryTablesTab.onLoadStartKey = null;
}
DataTableUtilities.turnOffProgressIndicator();
}); });
this.queryErrorMessage(errors[0].message);
if (this.queryTablesTab.onLoadStartKey != null && this.queryTablesTab.onLoadStartKey != undefined) {
TelemetryProcessor.traceFailure(
Action.Tab,
{
databaseName: this.queryTablesTab.collection.databaseId,
collectionName: this.queryTablesTab.collection.id(),
dataExplorerArea: Areas.Tab,
tabTitle: this.queryTablesTab.tabTitle(),
error: error,
},
this.queryTablesTab.onLoadStartKey
);
this.queryTablesTab.onLoadStartKey = null;
}
}
return undefined;
} }
/** /**
@@ -412,52 +485,51 @@ export default class TableEntityListViewModel extends DataTableViewModel {
* Note that this also means that we can get less entities than the requested download size in a successful call. * Note that this also means that we can get less entities than the requested download size in a successful call.
* See Microsoft Azure API Documentation at: https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx * See Microsoft Azure API Documentation at: https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx
*/ */
private prefetchData(
private async prefetchData(
tableQuery: Entities.ITableQuery, tableQuery: Entities.ITableQuery,
downloadSize: number, downloadSize: number,
currentRetry: number = 0 currentRetry: number = 0
): Promise<any> { ): Q.Promise<any> {
var entities: any;
if (!this.cache.serverCallInProgress) { if (!this.cache.serverCallInProgress) {
this.cache.serverCallInProgress = true; this.cache.serverCallInProgress = true;
this.allDownloaded = false; this.allDownloaded = false;
this.lastPrefetchTime = new Date().getTime(); this.lastPrefetchTime = new Date().getTime();
var time = this.lastPrefetchTime; var time = this.lastPrefetchTime;
try { var promise: Q.Promise<IListTableEntitiesSegmentedResult>;
if (this._documentIterator && this.continuationToken) { if (this._documentIterator && this.continuationToken) {
// TODO handle Cassandra case // TODO handle Cassandra case
const fetchNext = await this._documentIterator.fetchNext();
let fetchNextEntities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities( promise = Q(this._documentIterator.fetchNext().then((response) => response.resources)).then(
fetchNext.resources (documents: any[]) => {
); let entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(documents);
let finalEntities: IListTableEntitiesSegmentedResult = <IListTableEntitiesSegmentedResult>{ let finalEntities: IListTableEntitiesSegmentedResult = <IListTableEntitiesSegmentedResult>{
Results: fetchNextEntities, Results: entities,
ContinuationToken: this._documentIterator.hasMoreResults(), ContinuationToken: this._documentIterator.hasMoreResults(),
}; };
entities = finalEntities; return Q.resolve(finalEntities);
} else if (this.continuationToken && userContext.apiType === "Cassandra") { }
entities = await this.queryTablesTab.container.tableDataClient.queryDocuments( );
} else if (this.continuationToken && userContext.apiType === "Cassandra") {
promise = Q(
this.queryTablesTab.container.tableDataClient.queryDocuments(
this.queryTablesTab.collection, this.queryTablesTab.collection,
this.cqlQuery(), this.cqlQuery(),
true, true,
this.continuationToken this.continuationToken
); )
} else { );
let query = this.sqlQuery(); } else {
if (userContext.apiType === "Cassandra") { let query = this.sqlQuery();
query = this.cqlQuery(); if (userContext.apiType === "Cassandra") {
} query = this.cqlQuery();
entities = await this.queryTablesTab.container.tableDataClient.queryDocuments(
this.queryTablesTab.collection,
query,
true
);
} }
promise = Q(
const result = entities; this.queryTablesTab.container.tableDataClient.queryDocuments(this.queryTablesTab.collection, query, true)
if (result) { );
}
return promise
.then((result: IListTableEntitiesSegmentedResult) => {
if (!this._documentIterator) { if (!this._documentIterator) {
this._documentIterator = result.iterator; this._documentIterator = result.iterator;
} }
@@ -467,14 +539,14 @@ export default class TableEntityListViewModel extends DataTableViewModel {
// And as another service call is during process, we don't set serverCallInProgress to false here. // And as another service call is during process, we don't set serverCallInProgress to false here.
// Thus, end the prefetch. // Thus, end the prefetch.
if (this.lastPrefetchTime !== time) { if (this.lastPrefetchTime !== time) {
return Promise.resolve(undefined); return Q.resolve(null);
} }
var entities = result.Results; var entities = result.Results;
actualDownloadSize = entities.length; actualDownloadSize = entities.length;
// Queries can fetch no results and still return a continuation header. See prefetchAndRender() method. // Queries can fetch no results and still return a continuation header. See prefetchAndRender() method.
this.continuationToken = this.isCancelled ? undefined : result.ContinuationToken; this.continuationToken = this.isCancelled ? null : result.ContinuationToken;
if (!this.continuationToken) { if (!this.continuationToken) {
this.allDownloaded = true; this.allDownloaded = true;
@@ -496,20 +568,30 @@ export default class TableEntityListViewModel extends DataTableViewModel {
this.allDownloaded = true; this.allDownloaded = true;
} }
// There are three possible results for a prefetch:
// 1. Continuation token is null or fetched items' size reaches predefined.
// 2. Continuation token is not null and fetched items' size hasn't reach predefined.
// 2.1 Retry times has reached predefined maximum.
// 2.2 Retry times hasn't reached predefined maximum.
// Correspondingly,
// For #1, end prefetch.
// For #2.1, set prefetch exceeds maximum retry number and end prefetch.
// For #2.2, go to next round prefetch.
if (this.allDownloaded || nextDownloadSize === 0) { if (this.allDownloaded || nextDownloadSize === 0) {
return Promise.resolve(this.cache.data); return Q.resolve(result);
} }
if (currentRetry >= TableEntityListViewModel._maximumNumberOfPrefetchRetries) { if (currentRetry >= TableEntityListViewModel._maximumNumberOfPrefetchRetries) {
result.ExceedMaximumRetries = true; result.ExceedMaximumRetries = true;
return Promise.resolve(this.cache.data); return Q.resolve(result);
} }
return this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1); return this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1);
} })
} catch (error) { .catch((error: Error) => {
this.cache.serverCallInProgress = false; this.cache.serverCallInProgress = false;
return Promise.reject(error); return Q.reject(error);
} });
} }
return null;
} }
} }

View File

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

View File

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

View File

@@ -966,10 +966,15 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
<> <>
<span className="queryResultDivider">|</span> <span className="queryResultDivider">|</span>
<span className="queryResultNextEnable"> <span className="queryResultNextEnable">
<a onClick={this.onFetchNextPageClick.bind(this)}> <div
<span>Load more</span> onClick={this.onFetchNextPageClick.bind(this)}
role="button"
tabIndex={0}
onKeyPress={this.onFetchNextPageClick.bind(this)}
>
<span className="moreOption">Load more</span>
<img className="queryResultnextImg" src={QueryEditorNext} alt="Fetch next page" /> <img className="queryResultnextImg" src={QueryEditorNext} alt="Fetch next page" />
</a> </div>
</span> </span>
</> </>
)} )}
@@ -1015,7 +1020,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
</div> </div>
{this.state.isQueryMetricsEnabled && ( {this.state.isQueryMetricsEnabled && (
<div className="downloadMetricsLinkContainer"> <div className="downloadMetricsLinkContainer">
<a <span
id="downloadMetricsLink" id="downloadMetricsLink"
role="button" role="button"
tabIndex={0} tabIndex={0}
@@ -1030,7 +1035,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
alt="download query metrics csv" alt="download query metrics csv"
/> />
<span>Per-partition query metrics (CSV)</span> <span>Per-partition query metrics (CSV)</span>
</a> </span>
</div> </div>
)} )}
</div> </div>
@@ -1044,7 +1049,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
<div className="errorContent"> <div className="errorContent">
<span className="errorMessage">{this.state.error}</span> <span className="errorMessage">{this.state.error}</span>
<span className="errorDetailsLink"> <span className="errorDetailsLink">
<a <span
onClick={() => this.onErrorDetailsClick()} onClick={() => this.onErrorDetailsClick()}
onKeyPress={(event: React.KeyboardEvent<HTMLAnchorElement>) => onKeyPress={(event: React.KeyboardEvent<HTMLAnchorElement>) =>
this.onErrorDetailsKeyPress(event) this.onErrorDetailsKeyPress(event)
@@ -1052,9 +1057,11 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
id="error-display" id="error-display"
tabIndex={0} tabIndex={0}
aria-label="Error details link" aria-label="Error details link"
role="button"
className="moreOption"
> >
More details More details
</a> </span>
</span> </span>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,273 @@
<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

@@ -0,0 +1,285 @@
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

@@ -1,169 +0,0 @@
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

@@ -1,104 +0,0 @@
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

@@ -1,43 +0,0 @@
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

@@ -579,13 +579,16 @@ export default class StoredProcedureTabComponent extends React.Component<
<div className="errorContent"> <div className="errorContent">
<span className="errorMessage">{this.state.error}</span> <span className="errorMessage">{this.state.error}</span>
<span className="errorDetailsLink"> <span className="errorDetailsLink">
<a <span
role="button"
tabIndex={0}
className="moreOption"
aria-label="Error details link" aria-label="Error details link"
onClick={() => this.onErrorDetailsClick()} onClick={() => this.onErrorDetailsClick()}
onKeyPress={(event: React.KeyboardEvent<HTMLAnchorElement>) => this.onErrorDetailsKeyPress(event)} onKeyPress={(event: React.KeyboardEvent<HTMLAnchorElement>) => this.onErrorDetailsKeyPress(event)}
> >
More details More details
</a> </span>
</span> </span>
</div> </div>
</div> </div>

View File

@@ -53,6 +53,7 @@ function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
tabIndex={0} tabIndex={0}
role="tab" role="tab"
ref={focusTab} ref={focusTab}
onFocus={() => setHovering(true)}
> >
<span className="tabNavContentContainer"> <span className="tabNavContentContainer">
<a data-toggle="tab" href={"#" + tab.tabId} tabIndex={-1}> <a data-toggle="tab" href={"#" + tab.tabId} tabIndex={-1}>

View File

@@ -295,8 +295,6 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
className="trigger-field" className="trigger-field"
label="Trigger Id" label="Trigger Id"
id="entityTimeId" id="entityTimeId"
autoFocus
required
type="text" type="text"
pattern="[^/?#\\]*[^/?# \\]" pattern="[^/?#\\]*[^/?# \\]"
placeholder="Enter the new trigger id" placeholder="Enter the new trigger id"

View File

@@ -276,7 +276,6 @@ export default class UserDefinedFunctionTabContent extends Component<
className="trigger-field" className="trigger-field"
label="User Defined Function Id" label="User Defined Function Id"
id="entityTimeId" id="entityTimeId"
autoFocus
required required
readOnly={!isUdfIdEditable} readOnly={!isUdfIdEditable}
type="text" type="text"

View File

@@ -32,8 +32,7 @@ import MongoDocumentsTab from "../Tabs/MongoDocumentsTab";
import { NewMongoQueryTab } from "../Tabs/MongoQueryTab/MongoQueryTab"; import { NewMongoQueryTab } from "../Tabs/MongoQueryTab/MongoQueryTab";
import { NewMongoShellTab } from "../Tabs/MongoShellTab/MongoShellTab"; import { NewMongoShellTab } from "../Tabs/MongoShellTab/MongoShellTab";
import { NewQueryTab } from "../Tabs/QueryTab/QueryTab"; 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 { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2";
import { useDatabases } from "../useDatabases"; import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
@@ -392,13 +391,13 @@ export default class Collection implements ViewModels.Collection {
}); });
} }
const queryTablesTabs: NewQueryTablesTab[] = useTabs const queryTablesTabs: QueryTablesTab[] = useTabs
.getState() .getState()
.getTabs( .getTabs(
ViewModels.CollectionTabKind.QueryTables, ViewModels.CollectionTabKind.QueryTables,
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
) as NewQueryTablesTab[]; ) as QueryTablesTab[];
let queryTablesTab: NewQueryTablesTab = queryTablesTabs && queryTablesTabs[0]; let queryTablesTab: QueryTablesTab = queryTablesTabs && queryTablesTabs[0];
if (queryTablesTab) { if (queryTablesTab) {
useTabs.getState().activateTab(queryTablesTab); useTabs.getState().activateTab(queryTablesTab);
@@ -416,57 +415,14 @@ export default class Collection implements ViewModels.Collection {
tabTitle: title, tabTitle: title,
}); });
queryTablesTab = new NewQueryTablesTab( queryTablesTab = new QueryTablesTab({
{ tabKind: ViewModels.CollectionTabKind.QueryTables,
tabKind: ViewModels.CollectionTabKind.QueryTables, title: title,
title: title, tabPath: "",
tabPath: "", collection: this,
collection: this, node: this,
node: this, onLoadStartKey: startKey,
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); useTabs.getState().activateNewTab(queryTablesTab);
} }

View File

@@ -89,6 +89,8 @@ const App: React.FunctionComponent = () => {
onClick={() => window.open("https://portal.azure.com", "_blank")} onClick={() => window.open("https://portal.azure.com", "_blank")}
tabIndex={0} tabIndex={0}
title="Go to Azure Portal" title="Go to Azure Portal"
role="button"
onKeyDown={() => window.open("https://portal.azure.com", "_blank")}
> >
Microsoft Azure Microsoft Azure
</span> </span>

View File

@@ -33,13 +33,23 @@ const Index = (): JSX.Element => {
<div <div
id="Quickstart" id="Quickstart"
onClick={quickstart_click} onClick={quickstart_click}
onKeyPress={quickstart_click}
role="button"
tabIndex={0}
className={navigationSelection === "quickstart" ? "topSelected" : ""} className={navigationSelection === "quickstart" ? "topSelected" : ""}
> >
<img id="imgiconwidth1" src={Quickstart} alt="Open Quick Start" /> <img id="imgiconwidth1" src={Quickstart} alt="Open Quick Start" />
<span className="menuQuickStart">Quickstart</span> <span className="menuQuickStart">Quickstart</span>
</div> </div>
<div id="Explorer" onClick={explorer_click} className={navigationSelection === "explorer" ? "topSelected" : ""}> <div
id="Explorer"
onClick={explorer_click}
className={navigationSelection === "explorer" ? "topSelected" : ""}
role="button"
tabIndex={0}
onKeyPress={explorer_click}
>
<img id="imgiconwidth1" src={Explorer} alt="Open Data Explorer" /> <img id="imgiconwidth1" src={Explorer} alt="Open Data Explorer" />
<span className="menuExplorer">Explorer</span> <span className="menuExplorer">Explorer</span>
</div> </div>
@@ -53,11 +63,11 @@ const Index = (): JSX.Element => {
</nav> </nav>
{navigationSelection === "quickstart" && ( {navigationSelection === "quickstart" && (
<iframe name="quickstart" className="iframe" src="quickstart.html"></iframe> <iframe name="quickstart" className="iframe" src="quickstart.html" title="Quick Start"></iframe>
)} )}
{navigationSelection === "explorer" && ( {navigationSelection === "explorer" && (
<iframe name="explorer" className="iframe" src="explorer.html?platform=Emulator"></iframe> <iframe name="explorer" className="iframe" src="explorer.html?platform=Emulator" title="Explorer"></iframe>
)} )}
</React.Fragment> </React.Fragment>
); );

View File

@@ -77,16 +77,16 @@ export const ConnectExplorer: React.FunctionComponent<Props> = ({
<p className="connectExplorerContent"> <p className="connectExplorerContent">
<input className="filterbtnstyle" type="submit" value="Connect" /> <input className="filterbtnstyle" type="submit" value="Connect" />
</p> </p>
<p className="switchConnectTypeText" onClick={login}> <div className="switchConnectTypeText" onClick={login} onKeyDown={login} role="button" tabIndex={0}>
Sign In with Azure Account Sign In with Azure Account
</p> </div>
</form> </form>
) : ( ) : (
<div id="connectWithAad"> <div id="connectWithAad">
<input className="filterbtnstyle" type="button" value="Sign In" onClick={login} /> <input className="filterbtnstyle" type="button" value="Sign In" onClick={login} />
<p className="switchConnectTypeText" onClick={showForm}> <div className="switchConnectTypeText" onClick={showForm} onKeyDown={showForm} role="button" tabIndex={0}>
Connect to your account with connection string Connect to your account with connection string
</p> </div>
</div> </div>
)} )}
</div> </div>

View File

@@ -1,61 +0,0 @@
<!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>

View File

@@ -8,6 +8,7 @@
"noUnusedParameters": true "noUnusedParameters": true
}, },
"files": [ "files": [
"./src/SelfServe/GraphAPICompute/GraphAPICompute.types.ts",
"./src/AuthType.ts", "./src/AuthType.ts",
"./src/Bindings/ReactBindingHandler.ts", "./src/Bindings/ReactBindingHandler.ts",
"./src/Common/ArrayHashMap.ts", "./src/Common/ArrayHashMap.ts",
@@ -165,4 +166,4 @@
"src/Terminal/**/*", "src/Terminal/**/*",
"src/Utils/arm/**/*" "src/Utils/arm/**/*"
] ]
} }