Compare commits

...

51 Commits

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

@@ -6,7 +6,7 @@
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}]*/
@@ -34,7 +34,7 @@
} }
.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;
@@ -45,7 +45,7 @@
/*[{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}]*/
} }
@@ -70,14 +70,14 @@
} }
.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,
@@ -94,12 +94,15 @@
overflow: hidden; overflow: hidden;
.flex-display(); .flex-display();
.flex-direction(); .flex-direction();
position: relative;
} }
.tablesQueryTab{ .tablesQueryTab {
padding-left: @MediumSpace; padding-left: @MediumSpace;
width: 100%; width: 100%;
margin-bottom:@LargeSpace; margin-bottom: 60px;
position: relative;
height: 100%;
} }
.entity-error-Img { .entity-error-Img {
@@ -120,7 +123,7 @@
.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;
@@ -169,7 +172,7 @@
margin-left: 2px; margin-left: 2px;
} }
.advanced-options-panel{ .advanced-options-panel {
margin-bottom: @DefaultSpace; margin-bottom: @DefaultSpace;
} }
@@ -201,9 +204,9 @@ input::-webkit-inner-spin-button {
.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;
@@ -257,9 +260,9 @@ input::-webkit-inner-spin-button {
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;
@@ -268,7 +271,7 @@ input::-webkit-inner-spin-button {
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;
@@ -295,13 +298,31 @@ input::-webkit-inner-spin-button {
.and-or-svg { .and-or-svg {
margin-top: -8px; margin-top: -8px;
margin-right: -5px; margin-right: -26px;
}
.and-or-label {
margin-left: 52px;
}
.field-label {
margin-left: 69px;
}
.data-type-label {
margin-left: 54px;
}
.operator-label {
margin-left: 80px;
}
.value-label {
margin-left: 62px;
} }
.scroll-box { .scroll-box {
border-bottom: 1px transparent #DDD; border-bottom: 1px transparent #ddd;
/*[1px solid {plugin-table-border-color}]*/ /*[1px solid {plugin-table-border-color}]*/
border-top: 1px transparent #DDD; border-top: 1px transparent #ddd;
/*[1px solid {plugin-table-border-color}]*/ /*[1px solid {plugin-table-border-color}]*/
max-height: 20vh; max-height: 20vh;
width: 100%; width: 100%;
@@ -366,7 +387,7 @@ input::-webkit-inner-spin-button {
.group-indicator-table { .group-indicator-table {
border-spacing: 0px; border-spacing: 0px;
min-height: 24px min-height: 24px;
} }
.group-indicator-column { .group-indicator-column {
@@ -396,7 +417,6 @@ input::-webkit-inner-spin-button {
background-color: #ffffff; background-color: #ffffff;
} }
/*.type-header { /*.type-header {
padding-right: 4px; padding-right: 4px;
} }
@@ -410,9 +430,9 @@ 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}]*/
} }
@@ -462,19 +482,26 @@ input::-webkit-inner-spin-button {
.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 {
@@ -498,7 +525,7 @@ input::-webkit-inner-spin-button {
} }
.query-builder-isDisabled { .query-builder-isDisabled {
border: 1px solid #CCCEDB; border: 1px solid #cccedb;
color: #ccc; color: #ccc;
} }
@@ -515,6 +542,53 @@ input::-webkit-inner-spin-button {
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) {

34145
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,5 @@
import * as ko from "knockout"; import * as 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

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

View File

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

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

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -1,393 +0,0 @@
import * as ko from "knockout";
import * as _ from "underscore";
import * as Constants from "../Constants";
import * as ViewModels from "../../../Contracts/ViewModels";
import * as DataTableBuilder from "./DataTableBuilder";
import DataTableOperationManager from "./DataTableOperationManager";
import * as DataTableOperations from "./DataTableOperations";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import TableEntityListViewModel from "./TableEntityListViewModel";
import * as Utilities from "../Utilities";
import * as Entities from "../Entities";
/**
* Custom binding manager of datatable
*/
var tableEntityListViewModelMap: {
[key: string]: {
tableViewModel: TableEntityListViewModel;
operationManager: DataTableOperationManager;
$dataTable: JQuery;
};
} = {};
function bindDataTable(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) {
var tableEntityListViewModel = bindingContext.$data;
tableEntityListViewModel.notifyColumnChanges = onTableColumnChange;
var $dataTable = $(element);
var queryTablesTab = bindingContext.$parent;
var operationManager = new DataTableOperationManager(
$dataTable,
tableEntityListViewModel,
queryTablesTab.tableCommands
);
tableEntityListViewModelMap[queryTablesTab.tabId] = {
tableViewModel: tableEntityListViewModel,
operationManager: operationManager,
$dataTable: $dataTable,
};
createDataTable(0, tableEntityListViewModel, queryTablesTab); // Fake a DataTable to start.
$(window).resize(updateTableScrollableRegionMetrics);
operationManager.focusTable(); // Also selects the first row if needed.
}
function onTableColumnChange(enablePrompt: boolean = true, queryTablesTab: QueryTablesTab) {
var columnsFilter: boolean[] = null;
var tableEntityListViewModel = tableEntityListViewModelMap[queryTablesTab.tabId].tableViewModel;
if (queryTablesTab.queryViewModel()) {
queryTablesTab.queryViewModel().queryBuilderViewModel().updateColumnOptions();
}
createDataTable(
tableEntityListViewModel.tablePageStartIndex,
tableEntityListViewModel,
queryTablesTab,
true,
columnsFilter
);
}
function createDataTable(
startIndex: number,
tableEntityListViewModel: TableEntityListViewModel,
queryTablesTab: QueryTablesTab,
destroy: boolean = false,
columnsFilter: boolean[] = null
): void {
var $dataTable = tableEntityListViewModelMap[queryTablesTab.tabId].$dataTable;
if (destroy) {
// Find currently displayed columns.
var currentColumns: string[] = tableEntityListViewModel.headers;
// Calculate how many more columns need to added to the current table.
var columnsToAdd: number = _.difference(tableEntityListViewModel.headers, currentColumns).length;
// This is needed as current solution of adding column is more like a workaround
// The official support for dynamically add column is not yet there
// Please track github issue https://github.com/DataTables/DataTables/issues/273 for its offical support
for (var i = 0; i < columnsToAdd; i++) {
$(".dataTables_scrollHead table thead tr th").eq(0).after("<th></th>");
}
tableEntityListViewModel.table.destroy();
$dataTable.empty();
}
var jsonColTable = [];
for (var i = 0; i < tableEntityListViewModel.headers.length; i++) {
jsonColTable.push({
sTitle: tableEntityListViewModel.headers[i],
data: tableEntityListViewModel.headers[i],
aTargets: [i],
mRender: bindColumn,
visible: !!columnsFilter ? columnsFilter[i] : true,
});
}
tableEntityListViewModel.table = DataTableBuilder.createDataTable($dataTable, <DataTables.Settings>{
// WARNING!!! SECURITY: If you add new columns, make sure you encode them if they are user strings from Azure (see encodeText)
// so that they don't get interpreted as HTML in our page.
colReorder: true,
aoColumnDefs: jsonColTable,
stateSave: false,
dom: "RZlfrtip",
oColReorder: {
iFixedColumns: 1,
},
displayStart: startIndex,
bPaginate: true,
pagingType: "full_numbers",
bProcessing: true,
oLanguage: {
sInfo: "Results _START_ - _END_ of _TOTAL_",
oPaginate: {
sFirst: "<<",
sNext: ">",
sPrevious: "<",
sLast: ">>",
},
sProcessing: '<img style="width: 28px; height: 6px; " src="images/LoadingIndicator_3Squares.gif">',
oAria: {
sSortAscending: "",
sSortDescending: "",
},
},
destroy: destroy,
bInfo: true,
bLength: false,
bLengthChange: false,
scrollX: true,
scrollCollapse: true,
iDisplayLength: 100,
serverSide: true,
ajax: queryTablesTab.tabId, // Using this settings to make sure for getServerData we update the table based on the appropriate tab
fnServerData: getServerData,
fnRowCallback: bindClientId,
fnInitComplete: initializeTable,
fnDrawCallback: updateSelectionStatus,
});
(tableEntityListViewModel.table.table(0).container() as Element)
.querySelectorAll(Constants.htmlSelectors.dataTableHeaderTableSelector)
.forEach((table) => {
table.setAttribute(
"summary",
`Header for sorting results for container ${tableEntityListViewModel.queryTablesTab.collection.id()}`
);
});
(tableEntityListViewModel.table.table(0).container() as Element)
.querySelectorAll(Constants.htmlSelectors.dataTableBodyTableSelector)
.forEach((table) => {
table.setAttribute("summary", `Results for container ${tableEntityListViewModel.queryTablesTab.collection.id()}`);
});
}
function bindColumn(data: any, type: string, full: any) {
var displayedValue: any = null;
if (data) {
displayedValue = data._;
// SECURITY: Make sure we don't allow cross-site scripting by interpreting the values as HTML
displayedValue = Utilities.htmlEncode(displayedValue);
// Css' empty psuedo class can only tell the difference of whether a cell has values.
// A cell has no values no matter it's empty or it has no such a property.
// To distinguish between an empty cell and a non-existing property cell,
// we add a whitespace to the empty cell so that css will treat it as a cell with values.
if (displayedValue === "" && data.$ === Constants.TableType.String) {
displayedValue = " ";
}
}
return displayedValue;
}
function getServerData(sSource: any, aoData: any, fnCallback: any, oSettings: any) {
tableEntityListViewModelMap[oSettings.ajax].tableViewModel.renderNextPageAndupdateCache(
sSource,
aoData,
fnCallback,
oSettings
);
}
/**
* Bind table data information to row element so that we can track back to the table data
* from UI elements.
*/
function bindClientId(nRow: Node, aData: Entities.ITableEntity) {
$(nRow).attr(Constants.htmlAttributeNames.dataTableRowKeyAttr, aData.RowKey._);
return nRow;
}
function selectionChanged(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) {
$(".dataTable tr.selected").attr("tabindex", "-1").removeClass("selected");
const selected =
bindingContext && bindingContext.$data && bindingContext.$data.selected && bindingContext.$data.selected();
selected &&
selected.forEach((b: Entities.ITableEntity) => {
var sel = DataTableOperations.getRowSelector([
{
key: Constants.htmlAttributeNames.dataTableRowKeyAttr,
value: b.RowKey && b.RowKey._ && b.RowKey._.toString(),
},
]);
$(sel).attr("tabindex", "0").focus().addClass("selected");
});
//selected = bindingContext.$data.selected();
}
function dataChanged(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) {
// do nothing for now
}
function initializeTable(): void {
updateTableScrollableRegionMetrics();
initializeEventHandlers();
}
function updateTableScrollableRegionMetrics(): void {
updateTableScrollableRegionHeight();
updateTableScrollableRegionWidth();
}
/*
* Update the table's scrollable region height. So the pagination control is always shown at the bottom of the page.
*/
function updateTableScrollableRegionHeight(): void {
$(".tab-pane").each(function (index, tabElement) {
if (!$(tabElement).hasClass("tableContainer")) {
return;
}
// Add some padding to the table so it doesn't get too close to the container border.
var dataTablePaddingBottom = 10;
var bodyHeight = $(window).height();
var dataTablesScrollBodyPosY = $(tabElement).find(Constants.htmlSelectors.dataTableScrollBodySelector).offset().top;
var dataTablesInfoElem = $(tabElement).find(".dataTables_info");
var dataTablesPaginateElem = $(tabElement).find(".dataTables_paginate");
const notificationConsoleHeight = 32; /** Header height **/
var scrollHeight =
bodyHeight -
dataTablesScrollBodyPosY -
dataTablesPaginateElem.outerHeight(true) -
dataTablePaddingBottom -
notificationConsoleHeight;
//info and paginate control are stacked
if (dataTablesInfoElem.offset().top < dataTablesPaginateElem.offset().top) {
scrollHeight -= dataTablesInfoElem.outerHeight(true);
}
// TODO This is a work around for setting the outerheight since we don't have access to the JQuery.outerheight(numberValue)
// in the current version of JQuery we are using. Ideally, we would upgrade JQuery and use this line instead:
// $(Constants.htmlSelectors.dataTableScrollBodySelector).outerHeight(scrollHeight);
var element = $(tabElement).find(Constants.htmlSelectors.dataTableScrollBodySelector)[0];
var style = getComputedStyle(element);
var actualHeight = parseInt(style.height);
var change = element.offsetHeight - scrollHeight;
$(tabElement)
.find(Constants.htmlSelectors.dataTableScrollBodySelector)
.height(actualHeight - change);
});
}
/*
* Update the table's scrollable region width to make efficient use of the remaining space.
*/
function updateTableScrollableRegionWidth(): void {
$(".tab-pane").each(function (index, tabElement) {
if (!$(tabElement).hasClass("tableContainer")) {
return;
}
var bodyWidth = $(window).width();
var dataTablesScrollBodyPosLeft = $(tabElement).find(Constants.htmlSelectors.dataTableScrollBodySelector).offset()
.left;
var scrollWidth = bodyWidth - dataTablesScrollBodyPosLeft;
// jquery datatables automatically sets width:100% to both the header and the body when we use it's column autoWidth feature.
// We work around that by setting the height for it's container instead.
$(tabElement).find(Constants.htmlSelectors.dataTableScrollContainerSelector).width(scrollWidth);
});
}
function initializeEventHandlers(): void {
var $headers: JQuery = $(Constants.htmlSelectors.dataTableHeaderTypeSelector);
var $firstHeader: JQuery = $headers.first();
var firstIndex: string = $firstHeader.attr(Constants.htmlAttributeNames.dataTableHeaderIndex);
$headers
.on("keydown", (event: JQueryEventObject) => {
Utilities.onEnter(event, ($sourceElement: JQuery) => {
$sourceElement.css("background-color", Constants.cssColors.commonControlsButtonActive);
});
// Bind shift+tab from first header back to search input field
Utilities.onTab(
event,
($sourceElement: JQuery) => {
var sourceIndex: string = $sourceElement.attr(Constants.htmlAttributeNames.dataTableHeaderIndex);
if (sourceIndex === firstIndex) {
event.preventDefault();
}
},
/* metaKey */ null,
/* shiftKey */ true,
/* altKey */ null
);
// Also reset color if [shift-] tabbing away from button while holding down 'enter'
Utilities.onTab(event, ($sourceElement: JQuery) => {
$sourceElement.css("background-color", "");
});
})
.on("keyup", (event: JQueryEventObject) => {
Utilities.onEnter(event, ($sourceElement: JQuery) => {
$sourceElement.css("background-color", "");
});
});
}
function updateSelectionStatus(oSettings: any): void {
var $dataTableRows: JQuery = $(Constants.htmlSelectors.dataTableAllRowsSelector);
if ($dataTableRows) {
for (var i = 0; i < $dataTableRows.length; i++) {
var $row: JQuery = $dataTableRows.eq(i);
var rowKey: string = $row.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr);
var table = tableEntityListViewModelMap[oSettings.ajax].tableViewModel;
if (table.isItemSelected(table.getTableEntityKeys(rowKey))) {
$row.attr("tabindex", "0");
}
}
}
updateDataTableFocus(oSettings.ajax);
DataTableOperations.setPaginationButtonEventHandlers();
}
// TODO consider centralizing this "post-command" logic into some sort of Command Manager entity.
// See VSO:166520: "[Storage Explorer] Consider adding a 'command manager' to track command post-effects."
function updateDataTableFocus(queryTablesTabId: string): void {
var $activeElement: JQuery = $(document.activeElement);
var isFocusLost: boolean = $activeElement.is("body"); // When focus is lost, "body" becomes the active element.
var storageExplorerFrameHasFocus: boolean = document.hasFocus();
var operationManager = tableEntityListViewModelMap[queryTablesTabId].operationManager;
if (operationManager) {
if (isFocusLost && storageExplorerFrameHasFocus) {
// We get here when no control is active, meaning that the table update was triggered
// from a dialog, the context menu or by clicking on a toolbar control or header.
// Note that giving focus to the table also selects the first row if needed.
// The document.hasFocus() ensures that the table will only get focus when the
// focus was lost (i.e. "body has the focus") within the Storage Explorer frame
// i.e. not when the focus is lost because it is in another frame
// e.g. a daytona dialog or in the Activity Log.
operationManager.focusTable();
}
if ($activeElement.is(".sorting_asc") || $activeElement.is(".sorting_desc")) {
// If table header is selected, focus is shifted to the selected element as part of accessibility
$activeElement && $activeElement.focus();
} else {
// If some control is active, we don't give focus back to the table,
// just select the first row if needed (empty selection).
operationManager.selectFirstIfNeeded();
}
}
}
(<any>ko.bindingHandlers).tableSource = {
init: bindDataTable,
update: dataChanged,
};
(<any>ko.bindingHandlers).tableSelection = {
update: selectionChanged,
};
(<any>ko.bindingHandlers).readOnly = {
update: function (element: any, valueAccessor: any) {
var value = ko.utils.unwrapObservable(valueAccessor());
if (value) {
element.setAttribute("readOnly", true);
} else {
element.removeAttribute("readOnly");
}
},
};

View File

@@ -1,52 +0,0 @@
import * as Utilities from "../Utilities";
/**
* Wrapper function for creating data tables. Call this method, not the
* data tables constructor when you want to create a data table. This
* function makes sure that content without a render function is properly
* encoded to prevent XSS.
* @param{$dataTableElem} JQuery data table element
* @param{$settings} Settings to use when creating the data table
*/
export function createDataTable($dataTableElem: JQuery, settings: any): DataTables.DataTable {
return $dataTableElem.DataTable(applyDefaultRendering(settings));
}
/**
* Go through the settings for a data table and apply a simple HTML encode to any column
* without a render function to prevent XSS.
* @param{settings} The settings to check
* @return The given settings with all columns having a rendering function
*/
function applyDefaultRendering(settings: any): DataTables.SettingsLegacy {
var tableColumns: DataTables.ColumnLegacy[] = null;
if (settings.aoColumns) {
tableColumns = settings.aoColumns;
} else if (settings.aoColumnDefs) {
// for tables we use aoColumnDefs instead of aoColumns
tableColumns = settings.aoColumnDefs;
}
// either the settings had no columns defined, or they were called
// by a property name which we have not used before
if (!tableColumns) {
return settings;
}
for (var i = 0; i < tableColumns.length; i++) {
// the column does not have a render function
if (!tableColumns[i].mRender) {
tableColumns[i].mRender = defaultDataRender;
}
}
return settings;
}
/**
* Default data render function, whatever is done to data in here
* will be done to any data which we do not specify a render for.
*/
function defaultDataRender(data: any, type: string, full: any) {
return Utilities.htmlEncode(data);
}

View File

@@ -1,300 +0,0 @@
import ko from "knockout";
import * as DataTableOperations from "./DataTableOperations";
import * as Constants from "../Constants";
import TableCommands from "./TableCommands";
import TableEntityListViewModel from "./TableEntityListViewModel";
import * as Utilities from "../Utilities";
import * as Entities from "../Entities";
/*
* Base class for data table row selection.
*/
export default class DataTableOperationManager {
private _tableEntityListViewModel: TableEntityListViewModel;
private _tableCommands: TableCommands;
private dataTable: JQuery;
constructor(table: JQuery, viewModel: TableEntityListViewModel, tableCommands: TableCommands) {
this.dataTable = table;
this._tableEntityListViewModel = viewModel;
this._tableCommands = tableCommands;
this.bind();
this._tableEntityListViewModel.bind(this);
}
private click = (event: JQueryEventObject) => {
var elem: JQuery = $(event.currentTarget);
this.updateLastSelectedItem(elem, event.shiftKey);
if (Utilities.isEnvironmentCtrlPressed(event)) {
this.applyCtrlSelection(elem);
} else if (event.shiftKey) {
this.applyShiftSelection(elem);
} else {
this.applySingleSelection(elem);
}
};
private doubleClick = (event: JQueryEventObject) => {
this.tryOpenEditor();
};
private keyDown = (event: JQueryEventObject): boolean => {
var isUpArrowKey: boolean = event.keyCode === Constants.keyCodes.UpArrow,
isDownArrowKey: boolean = event.keyCode === Constants.keyCodes.DownArrow,
handled: boolean = false;
if (isUpArrowKey || isDownArrowKey) {
var lastSelectedItem: Entities.ITableEntity = this._tableEntityListViewModel.lastSelectedItem;
var dataTableRows: JQuery = $(Constants.htmlSelectors.dataTableAllRowsSelector);
var maximumIndex = dataTableRows.length - 1;
// If can't find an index for lastSelectedItem, then either no item is previously selected or it goes across page.
// Simply select the first item in this case.
var lastSelectedItemIndex = lastSelectedItem
? this._tableEntityListViewModel.getItemIndexFromCurrentPage(
this._tableEntityListViewModel.getTableEntityKeys(lastSelectedItem.RowKey._)
)
: -1;
var nextIndex: number = isUpArrowKey ? lastSelectedItemIndex - 1 : lastSelectedItemIndex + 1;
var safeIndex: number = Utilities.ensureBetweenBounds(nextIndex, 0, maximumIndex);
var selectedRowElement: JQuery = dataTableRows.eq(safeIndex);
if (selectedRowElement) {
if (event.shiftKey) {
this.applyShiftSelection(selectedRowElement);
} else {
this.applySingleSelection(selectedRowElement);
}
this.updateLastSelectedItem(selectedRowElement, event.shiftKey);
handled = true;
DataTableOperations.scrollToRowIfNeeded(dataTableRows, safeIndex, isUpArrowKey);
}
} else if (
Utilities.isEnvironmentCtrlPressed(event) &&
!Utilities.isEnvironmentShiftPressed(event) &&
!Utilities.isEnvironmentAltPressed(event) &&
event.keyCode === Constants.keyCodes.A
) {
this.applySelectAll();
handled = true;
}
return !handled;
};
// Note: There is one key up event each time a key is pressed;
// in contrast, there may be more than one key down and key
// pressed events.
private keyUp = (event: JQueryEventObject): boolean => {
var handled: boolean = false;
switch (event.keyCode) {
case Constants.keyCodes.Enter:
handled = this.tryOpenEditor();
break;
case Constants.keyCodes.Delete:
handled = this.tryHandleDeleteSelected();
break;
}
return !handled;
};
private itemDropped = (event: JQueryEventObject): boolean => {
var handled: boolean = false;
var items = (<any>event.originalEvent).dataTransfer.items;
if (!items) {
// On browsers outside of Chromium
// we can't discern between dirs and files
// so we will disable drag & drop for now
return null;
}
for (var i = 0; i < items.length; i++) {
var item = items[i];
var entry = item.webkitGetAsEntry();
if (entry.isFile) {
// TODO: parse the file and insert content as entities
}
}
return !handled;
};
private tryOpenEditor(): boolean {
return this._tableCommands.tryOpenEntityEditor(this._tableEntityListViewModel);
}
private tryHandleDeleteSelected(): boolean {
var selectedEntities: Entities.ITableEntity[] = this._tableEntityListViewModel.selected();
var handled: boolean = false;
if (selectedEntities && selectedEntities.length) {
this._tableCommands.deleteEntitiesCommand(this._tableEntityListViewModel);
handled = true;
}
return handled;
}
private getEntityIdentity($elem: JQuery): Entities.ITableEntityIdentity {
return {
RowKey: $elem.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr),
};
}
private updateLastSelectedItem($elem: JQuery, isShiftSelect: boolean) {
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
var entity = this._tableEntityListViewModel.getItemFromCurrentPage(
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
);
this._tableEntityListViewModel.lastSelectedItem = entity;
if (!isShiftSelect) {
this._tableEntityListViewModel.lastSelectedAnchorItem = entity;
}
}
private applySingleSelection($elem: JQuery) {
if ($elem) {
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
this._tableEntityListViewModel.clearSelection();
this.addToSelection(entityIdentity.RowKey);
}
}
private applySelectAll() {
this._tableEntityListViewModel.clearSelection();
ko.utils.arrayPushAll<Entities.ITableEntity>(
this._tableEntityListViewModel.selected,
this._tableEntityListViewModel.getAllItemsInCurrentPage()
);
}
private applyCtrlSelection($elem: JQuery): void {
var koSelected: ko.ObservableArray<Entities.ITableEntity> = this._tableEntityListViewModel
? this._tableEntityListViewModel.selected
: null;
if (koSelected) {
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
if (
!this._tableEntityListViewModel.isItemSelected(
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
)
) {
// Adding item not previously in selection
this.addToSelection(entityIdentity.RowKey);
} else {
koSelected.remove((item: Entities.ITableEntity) => item.RowKey._ === entityIdentity.RowKey);
}
}
}
private applyShiftSelection($elem: JQuery): void {
var anchorItem = this._tableEntityListViewModel.lastSelectedAnchorItem;
// If anchor item doesn't exist, use the first available item of current page instead
if (!anchorItem && this._tableEntityListViewModel.items().length > 0) {
anchorItem = this._tableEntityListViewModel.items()[0];
}
if (anchorItem) {
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
var elementIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
);
var anchorIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
this._tableEntityListViewModel.getTableEntityKeys(anchorItem.RowKey._)
);
var startIndex = Math.min(elementIndex, anchorIndex);
var endIndex = Math.max(elementIndex, anchorIndex);
this._tableEntityListViewModel.clearSelection();
ko.utils.arrayPushAll<Entities.ITableEntity>(
this._tableEntityListViewModel.selected,
this._tableEntityListViewModel.getItemsFromAllPagesWithinRange(startIndex, endIndex + 1)
);
}
}
private applyContextMenuSelection($elem: JQuery) {
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
if (
!this._tableEntityListViewModel.isItemSelected(
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
)
) {
if (this._tableEntityListViewModel.selected().length) {
this._tableEntityListViewModel.clearSelection();
}
this.addToSelection(entityIdentity.RowKey);
}
}
private addToSelection(rowKey: string) {
var selectedEntity: Entities.ITableEntity = this._tableEntityListViewModel.getItemFromCurrentPage(
this._tableEntityListViewModel.getTableEntityKeys(rowKey)
);
if (selectedEntity != null) {
this._tableEntityListViewModel.selected.push(selectedEntity);
}
}
// Selecting first row if the selection is empty.
public selectFirstIfNeeded(): void {
var koSelected: ko.ObservableArray<Entities.ITableEntity> = this._tableEntityListViewModel
? this._tableEntityListViewModel.selected
: null;
var koEntities: ko.ObservableArray<Entities.ITableEntity> = this._tableEntityListViewModel
? this._tableEntityListViewModel.items
: null;
if (!koSelected().length && koEntities().length) {
var firstEntity: Entities.ITableEntity = koEntities()[0];
// Clear last selection: lastSelectedItem and lastSelectedAnchorItem
this._tableEntityListViewModel.clearLastSelected();
this.addToSelection(firstEntity.RowKey._);
// Update last selection
this._tableEntityListViewModel.lastSelectedItem = firstEntity;
// Finally, make sure first row is visible
DataTableOperations.scrollToTopIfNeeded();
}
}
public bind() {
this.dataTable.on("click", "tr", this.click);
this.dataTable.on("dblclick", "tr", this.doubleClick);
this.dataTable.on("keydown", "td", this.keyDown);
this.dataTable.on("keyup", "td", this.keyUp);
// Keyboard navigation - selecting first row if the selection is empty when the table gains focus.
this.dataTable.on("focus", () => {
this.selectFirstIfNeeded();
return true;
});
// Bind drag & drop behavior
$("body").on("drop", this.itemDropped);
}
public focusTable(): void {
this.dataTable.focus();
}
}

View File

@@ -1,192 +0,0 @@
import _ from "underscore";
import Q from "q";
import * as Entities from "../Entities";
import * as QueryBuilderConstants from "../Constants";
import * as Utilities from "../Utilities";
export function getRowSelector(selectorSchema: Entities.IProperty[]): string {
var selector: string = "";
selectorSchema &&
selectorSchema.forEach((p: Entities.IProperty) => {
selector += "[" + p.key + '="' + Utilities.jQuerySelectorEscape(p.value) + '"]';
});
return QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector + selector;
}
export function isRowVisible(dataTableScrollBodyQuery: JQuery, element: HTMLElement): boolean {
var isVisible = false;
if (dataTableScrollBodyQuery.length && element) {
var elementRect: ClientRect = element.getBoundingClientRect(),
dataTableScrollBodyRect: ClientRect = dataTableScrollBodyQuery.get(0).getBoundingClientRect();
isVisible = elementRect.bottom <= dataTableScrollBodyRect.bottom && dataTableScrollBodyRect.top <= elementRect.top;
}
return isVisible;
}
export function scrollToRowIfNeeded(dataTableRows: JQuery, currentIndex: number, isScrollUp: boolean): void {
if (dataTableRows.length) {
var dataTableScrollBodyQuery: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector),
selectedRowElement: HTMLElement = dataTableRows.get(currentIndex);
if (dataTableScrollBodyQuery.length && selectedRowElement) {
var isVisible: boolean = isRowVisible(dataTableScrollBodyQuery, selectedRowElement);
if (!isVisible) {
var selectedRowQuery: JQuery = $(selectedRowElement),
scrollPosition: number = dataTableScrollBodyQuery.scrollTop(),
selectedElementPosition: number = selectedRowQuery.position().top,
newScrollPosition: number = 0;
if (isScrollUp) {
newScrollPosition = scrollPosition + selectedElementPosition;
} else {
newScrollPosition =
scrollPosition + (selectedElementPosition + selectedRowQuery.height() - dataTableScrollBodyQuery.height());
}
dataTableScrollBodyQuery.scrollTop(newScrollPosition);
}
}
}
}
export function scrollToTopIfNeeded(): void {
var $dataTableRows: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector),
$dataTableScrollBody: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector);
if ($dataTableRows.length && $dataTableScrollBody.length) {
$dataTableScrollBody.scrollTop(0);
}
}
export function setPaginationButtonEventHandlers(): void {
$(QueryBuilderConstants.htmlSelectors.dataTablePaginationButtonSelector)
.on("mousedown", (event: JQueryEventObject) => {
// Prevents the table contents from briefly jumping when clicking on "Load more"
event.preventDefault();
})
.attr("role", "button");
}
export function filterColumns(table: DataTables.DataTable, settings: boolean[]): void {
settings &&
settings.forEach((value: boolean, index: number) => {
table.column(index).visible(value, false);
});
table.columns.adjust().draw(false);
}
/**
* Reorder columns based on current order.
* If no current order is specified, reorder the columns based on intial order.
*/
export function reorderColumns(
table: DataTables.DataTable,
targetOrder: number[],
currentOrder?: number[]
): Q.Promise<any> {
var columnsCount: number = targetOrder.length;
var isCurrentOrderPassedIn: boolean = !!currentOrder;
if (!isCurrentOrderPassedIn) {
currentOrder = getInitialOrder(columnsCount);
}
var isSameOrder: boolean = Utilities.isEqual(currentOrder, targetOrder);
// if the targetOrder is the same as current order, do nothing.
if (!isSameOrder) {
// Otherwise, calculate the transformation order.
// If current order not specified, then it'll be set to initial order,
// i.e., either no reorder happened before or reordering to its initial order,
// Then the transformation order will be the same as target order.
// If current order is specified, then a transformation order is calculated.
// Refer to calculateTransformationOrder for details about transformation order.
var transformationOrder: number[] = isCurrentOrderPassedIn
? calculateTransformationOrder(currentOrder, targetOrder)
: targetOrder;
try {
$.fn.dataTable.ColReorder(table).fnOrder(transformationOrder);
} catch (err) {
return Q.reject(err);
}
}
return Q.resolve(null);
}
export function resetColumns(table: DataTables.DataTable): void {
$.fn.dataTable.ColReorder(table).fnReset();
}
/**
* A table's initial order is described in the form of a natural ascending order.
* E.g., for a table with 9 columns, the initial order will be: [0, 1, 2, 3, 4, 5, 6, 7, 8]
*/
export function getInitialOrder(columnsCount: number): number[] {
return _.range(columnsCount);
}
/**
* Get current table's column order which is described based on initial table. E.g.,
* Initial order: I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8}
* Current order: C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8}
*/
export function getCurrentOrder(table: DataTables.DataTable): number[] {
return $.fn.dataTable.ColReorder(table).fnOrder();
}
/**
* Switch the index and value for each element of an array. e.g.,
* InputArray: [0, 1, 2, 6, 7, 3, 4, 5, 8]
* Result: [0, 1, 2, 5, 6, 7, 3, 4, 8]
*/
export function invertIndexValues(inputArray: number[]): number[] {
var invertedArray: number[] = [];
if (inputArray) {
inputArray.forEach((value: number, index: number) => {
invertedArray[inputArray[index]] = index;
});
}
return invertedArray;
}
/**
* DataTable fnOrder API is based on the current table. So we need to map the order targeting original table to targeting current table.
* An detailed example for this. Assume the table has 9 columns.
* Initial order (order of the initial table): I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8}
* Current order (order of the current table): C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8}
* Target order (order of the targeting table): T = [0, 1, 2, 5, 6, 7, 8, 3, 4] <----> {prop0, prop1, prop2, prop5, prop6, prop7, prop8, prop3, prop4}
* Transformation order: an order passed to fnOrder API that transforms table from current order to target order.
* When the table is constructed, it has the intial order. After an reordering with current order array, now the table is shown in current order, e.g.,
* column 3 in the current table is actually column C[3]=6 in the intial table, both indicate the column with header prop6.
* Now we want to continue to do another reorder to make the target table in the target order. Directly invoking API with the new order won't work as
* the API only do reorder based on the current table like the first time we invoke the API. So an order based on the current table needs to be calulated.
* Here is an example of how to calculate the transformation order:
* In target table, column 3 should be column T[3]=5 in the intial table with header prop5, while in current table, column with header prop5 is column 7 as C[7]=5.
* As a result, in transformation order, column 3 in the target table should be column 7 in the current table, Trans[3] = 7. In the same manner, we can get the
* transformation order: Trans = [0, 1, 2, 7, 3, 4, 8, 5, 6]
*/
export function calculateTransformationOrder(currentOrder: number[], targetOrder: number[]): number[] {
var transformationOrder: number[] = [];
if (currentOrder && targetOrder && currentOrder.length === targetOrder.length) {
var invertedCurrentOrder: number[] = invertIndexValues(currentOrder);
transformationOrder = targetOrder.map((value: number) => invertedCurrentOrder[value]);
}
return transformationOrder;
}
export function getDataTableHeaders(table: DataTables.DataTable): string[] {
var columns: DataTables.ColumnsMethods = table.columns();
var headers: string[] = [];
if (columns) {
// table.columns() return ColumnsMethods which is an array of arrays
var columnIndexes: number[] = (<any>columns)[0];
if (columnIndexes) {
headers = columnIndexes.map((value: number) => $(table.columns(value).header()).html());
}
}
return headers;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,273 +0,0 @@
<div class="tab-pane tableContainer" data-bind="attr:{id: tabId}" role="tabpanel">
<!-- Tables Query Tab Query Builder - Start-->
<div
class="query-builder"
data-bind="with: queryViewModel, attr: {
id: queryViewModel.id
}"
>
<!-- Tables Query Tab Errors - Start-->
<div class="error-bar">
<div class="error-message" aria-label="Error Message" data-bind="visible: hasQueryError">
<span><img class="entity-error-Img" src="/error_red.svg" /></span>
<span class="error-text" role="alert" data-bind="text: queryErrorMessage"></span>
</div>
</div>
<!-- Tables Query Tab Errors - End-->
<!-- Tables Query Tab Query Text - Start-->
<div class="query-editor-panel" data-bind="visible: isEditorActive">
<div>
<textarea
class="query-editor-text"
data-bind="textInput: queryText,
css: { 'query-editor-text-invalid': hasQueryError },
readOnly: true"
name="query-editor"
rows="5"
cols="100"
></textarea>
</div>
</div>
<!-- Tables Query Tab Query Text - End-->
<!-- Tables Query Tab Query Helper - Start-->
<div data-bind="visible: isHelperActive" style="padding-left: 13px">
<div class="clause-table" data-bind="with: queryBuilderViewModel ">
<div class="scroll-box scrollable" id="scroll">
<table class="clause-table">
<thead>
<tr class="clause-table-row">
<th class="clause-table-cell header-background action-header">
<span data-bind="text: actionLabel"></span>
</th>
<th class="clause-table-cell header-background group-control-header">
<button
type="button"
data-bind="enable: canGroupClauses, attr:{title: groupSelectedClauses}, click: groupClauses"
>
<img class="and-or-svg" src="/And-Or.svg" alt="Group selected clauses" />
</button>
</th>
<th class="clause-table-cell header-background">
<!-- Grouping indicator -->
</th>
<th class="clause-table-cell header-background and-or-header">
<span data-bind="text: andLabel"></span>
</th>
<th class="clause-table-cell header-background field-header">
<span data-bind="text: fieldLabel"></span>
</th>
<th class="clause-table-cell header-background type-header">
<span data-bind="text: dataTypeLabel"></span>
</th>
<th class="clause-table-cell header-background operator-header">
<span data-bind="text: operatorLabel"></span>
</th>
<th class="clause-table-cell header-background value-header">
<span data-bind="text: valueLabel"></span>
</th>
</tr>
</thead>
<tbody data-bind="template: { name: 'queryClause-template', foreach: clauseArray, as: 'clause' }"></tbody>
</table>
</div>
<div
class="addClause"
role="button"
data-bind="click: addNewClause, event: { keydown: onAddNewClauseKeyDown }, attr: { title: addNewClauseLine }"
tabindex="0"
>
<div class="addClause-heading">
<span class="clause-table addClause-title">
<img
class="addclauseProperty-Img"
style="margin-bottom: 5px"
src="/Add-property.svg"
alt="Add new clause"
/>
<span style="margin-left: 5px" data-bind="text: addNewClauseLine"></span>
</span>
</div>
</div>
</div>
</div>
<!-- Tables Query Tab Query Helper - End-->
<!-- Tables Query Tab Advanced Options - Start-->
<div class="advanced-options-panel">
<div class="advanced-heading">
<span
class="advanced-title"
role="button"
data-bind="click:toggleAdvancedOptions, event: { keydown: ontoggleAdvancedOptionsKeyDown }, attr:{ 'aria-expanded': isExpanded() ? 'true' : 'false' }"
tabindex="0"
>
<!-- ko template: { ifnot: isExpanded} -->
<div class="themed-images" type="text/html" id="ExpandChevronRight" data-bind="hasFocus: focusExpandIcon">
<img class="imgiconwidth expand-triangle expand-triangle-right" src="/Triangle-right.svg" alt="toggle" />
</div>
<!-- /ko -->
<!-- ko template: { if: isExpanded} -->
<div class="themed-images" type="text/html" id="ExpandChevronDown">
<img class="imgiconwidth expand-triangle" src="/Triangle-down.svg" alt="toggle" />
</div>
<!-- /ko -->
<span>Advanced Options</span>
</span>
</div>
<div class="advanced-options" data-bind="visible: isExpanded">
<div class="top">
<span>Show top results:</span>
<input
class="top-input"
type="number"
data-bind="hasFocus: focusTopResult, textInput: topValue, attr: { title: topValueLimitMessage }"
role="textbox"
aria-label="Show top results"
/>
<div role="alert" aria-atomic="true" class="inline-div" data-bind="visible: isExceedingLimit">
<img class="advanced-options-icon" src="/QueryBuilder/StatusWarning_16x.png" />
<span data-bind="text: topValueLimitMessage"></span>
</div>
</div>
<div class="select">
<span> Select fields for query: </span>
<div data-bind="visible: isSelected">
<img class="advanced-options-icon" src="/QueryBuilder/QueryInformation_16x.png" />
<span class="select-options-text" data-bind="text: selectMessage" />
</div>
<a
class="select-options-link"
data-bind="click: selectQueryOptions, event: { keydown: onselectQueryOptionsKeyDown }"
tabindex="0"
role="button"
>
<span>Choose Columns... </span>
</a>
</div>
</div>
</div>
<!-- Tables Query Tab Advanced Options - End-->
</div>
<!-- Tables Query Tab Query Builder - End-->
<div
class="tablesQueryTab tableContainer"
data-bind="with: tableEntityListViewModel, attr: {
id: tableEntityListViewModel.id
}"
>
<!-- Keyboard navigation - tabindex is required to make the table focusable. -->
<table
id="storageTable"
class="storage azure-table show-gridlines"
tabindex="0"
data-bind="tableSource: items, tableSelection: selected"
></table>
</div>
</div>
<!-- Script for each clause in the tables query builder -->
<script type="text/html" id="queryClause-template">
<tr class="clause-table-row">
<td class="clause-table-cell action-column">
<span
class="entity-Add-Cancel"
role="button"
tabindex="0"
data-bind="click: $parent.addClauseIndex.bind($data, $index()), event: { keydown: $parent.onAddClauseKeyDown.bind($data, $index()) }, attr:{title: $parent.insertNewFilterLine}"
>
<img class="querybuilder-addpropertyImg" src="/Add-property.svg" alt="Add clause" />
</span>
<span
class="entity-Add-Cancel"
role="button"
tabindex="0"
data-bind="hasFocus: isDeleteButtonFocused, click: $parent.deleteClause.bind($data, $index()), event: { keydown: $parent.onDeleteClauseKeyDown.bind($data, $index()) }, attr:{title: $parent.removeThisFilterLine}"
>
<img class="querybuilder-cancelImg" src="/Entity_cancel.svg" alt="Delete clause" />
</span>
</td>
<td class="clause-table-cell group-control-column">
<input type="checkbox" aria-label="And/Or" data-bind="checked: checkedForGrouping" />
</td>
<td>
<table class="group-indicator-table">
<tbody>
<tr
data-bind="template: { name: 'groupIndicator-template', foreach: $parent.getClauseGroupViewModels($data), as: 'gi' }"
></tr>
</tbody>
</table>
</td>
<td class="clause-table-cell and-or-column">
<select
class="clause-table-field and-or-column"
data-bind="hasFocus: isAndOrFocused, options: $parent.clauseRules, value: and_or, visible: canAnd, attr:{ 'aria-label': and_or }"
></select>
</td>
<td class="clause-table-cell field-column" data-bind="click: $parent.updateColumnOptions">
<select
class="clause-table-field field-column"
data-bind="options: $parent.columnOptions, value: field, attr:{ 'aria-label': field }"
></select>
</td>
<td class="clause-table-cell type-column">
<select
class="clause-table-field type-column"
data-bind="
options: $parent.edmTypes,
enable: isTypeEditable,
value: type,
css: {'query-builder-isDisabled': !isTypeEditable()},
attr: { 'aria-label': type }"
></select>
</td>
<td class="clause-table-cell operator-column">
<select
class="clause-table-field operator-column"
data-bind="
options: $parent.operators,
enable: isOperaterEditable,
value: operator,
css: {'query-builder-isDisabled': !isOperaterEditable()},
attr: { 'aria-label': operator }"
></select>
</td>
<td class="clause-table-cell value-column">
<!-- ko template: {if: isValue} -->
<input
type="text"
class="clause-table-field value-column"
type="search"
data-bind="textInput: value, attr: {'aria-label': $parent.valueLabel}"
/>
<!-- /ko -->
<!-- ko template: {if: isTimestamp} -->
<select
class="clause-table-field time-column"
data-bind="options: $parent.timeOptions, value: timeValue"
></select>
<!-- /ko -->
<!-- ko template: {if: isCustomLastTimestamp} -->
<input class="clause-table-field time-column" data-bind="value: customTimeValue, click: customTimestampDialog" />
<!-- /ko -->
<!-- ko template: {if: isCustomRangeTimestamp} -->
<input class="clause-table-field time-column" type="datetime-local" step="1" data-bind="value: customTimeValue" />
<!-- /ko -->
</td>
</tr>
</script>
<!-- Script for each clause group in the tables query builder -->
<script type="text/html" id="groupIndicator-template">
<td
class="group-indicator-column"
data-bind="style: {backgroundColor: gi.backgroundColor, borderTop: gi.showTopBorder.peek() ? 'solid thin #CCCCCC' : 'none', borderLeft: gi.showLeftBorder.peek() ? 'solid thin #CCCCCC' : 'none', borderBottom: gi.showBottomBorder.peek() ? 'solid thin #CCCCCC' : gi.borderBackgroundColor}"
>
<!-- ko template: {if: gi.canUngroup} -->
<button type="button" data-bind="click: ungroupClauses, attr: {title: ungroupClausesLabel}">
<img src="/QueryBuilder/UngroupClause_16x.png" alt="Ungroup clauses" />
</button>
<!-- /ko -->
</td>
</script>

View File

@@ -1,285 +0,0 @@
import * as ko from "knockout";
import React from "react";
import AddEntityIcon from "../../../images/AddEntity.svg";
import DeleteEntitiesIcon from "../../../images/DeleteEntities.svg";
import EditEntityIcon from "../../../images/Edit-entity.svg";
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
import QueryBuilderIcon from "../../../images/Query-Builder.svg";
import QueryTextIcon from "../../../images/Query-Text.svg";
import * as ViewModels from "../../Contracts/ViewModels";
import { useSidePanel } from "../../hooks/useSidePanel";
import { userContext } from "../../UserContext";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer";
import { AddTableEntityPanel } from "../Panes/Tables/AddTableEntityPanel";
import { EditTableEntityPanel } from "../Panes/Tables/EditTableEntityPanel";
import TableCommands from "../Tables/DataTable/TableCommands";
import TableEntityListViewModel from "../Tables/DataTable/TableEntityListViewModel";
import QueryViewModel from "../Tables/QueryBuilder/QueryViewModel";
import { CassandraAPIDataClient, TableDataClient } from "../Tables/TableDataClient";
import template from "./QueryTablesTab.html";
import TabsBase from "./TabsBase";
// Will act as table explorer class
export default class QueryTablesTab extends TabsBase {
public readonly html = template;
public collection: ViewModels.Collection;
public tableEntityListViewModel = ko.observable<TableEntityListViewModel>();
public queryViewModel = ko.observable<QueryViewModel>();
public tableCommands: TableCommands;
public tableDataClient: TableDataClient;
public queryText = ko.observable("PartitionKey eq 'partitionKey1'"); // Start out with an example they can modify
public selectedQueryText = ko.observable("").extend({ notify: "always" });
public executeQueryButton: ViewModels.Button;
public addEntityButton: ViewModels.Button;
public editEntityButton: ViewModels.Button;
public deleteEntityButton: ViewModels.Button;
public queryBuilderButton: ViewModels.Button;
public queryTextButton: ViewModels.Button;
public container: Explorer;
constructor(options: ViewModels.TabOptions) {
super(options);
this.container = options.collection && options.collection.container;
this.tableCommands = new TableCommands(this.container);
this.tableDataClient = this.container.tableDataClient;
this.tableEntityListViewModel(new TableEntityListViewModel(this.tableCommands, this));
this.tableEntityListViewModel().queryTablesTab = this;
this.queryViewModel(new QueryViewModel(this));
const sampleQuerySubscription = this.tableEntityListViewModel().items.subscribe(() => {
if (this.tableEntityListViewModel().items().length > 0 && userContext.apiType === "Tables") {
this.queryViewModel().queryBuilderViewModel().setExample();
}
sampleQuerySubscription.dispose();
});
this.executeQueryButton = {
enabled: ko.computed<boolean>(() => {
return true;
}),
visible: ko.computed<boolean>(() => {
return true;
}),
};
this.queryBuilderButton = {
enabled: ko.computed<boolean>(() => {
return true;
}),
visible: ko.computed<boolean>(() => {
return true;
}),
isSelected: ko.computed<boolean>(() => {
return this.queryViewModel() ? this.queryViewModel().isHelperActive() : false;
}),
};
this.queryTextButton = {
enabled: ko.computed<boolean>(() => {
return true;
}),
visible: ko.computed<boolean>(() => {
return true;
}),
isSelected: ko.computed<boolean>(() => {
return this.queryViewModel() ? this.queryViewModel().isEditorActive() : false;
}),
};
this.addEntityButton = {
enabled: ko.computed<boolean>(() => {
return true;
}),
visible: ko.computed<boolean>(() => {
return true;
}),
};
this.editEntityButton = {
enabled: ko.computed<boolean>(() => {
return this.tableCommands.isEnabled(
TableCommands.editEntityCommand,
this.tableEntityListViewModel().selected()
);
}),
visible: ko.computed<boolean>(() => {
return true;
}),
};
this.deleteEntityButton = {
enabled: ko.computed<boolean>(() => {
return this.tableCommands.isEnabled(
TableCommands.deleteEntitiesCommand,
this.tableEntityListViewModel().selected()
);
}),
visible: ko.computed<boolean>(() => {
return true;
}),
};
this.buildCommandBarOptions();
}
public onAddEntityClick = (): void => {
useSidePanel
.getState()
.openSidePanel(
"Add Table Row",
<AddTableEntityPanel
tableDataClient={this.tableDataClient}
queryTablesTab={this}
tableEntityListViewModel={this.tableEntityListViewModel()}
cassandraApiClient={new CassandraAPIDataClient()}
/>,
"700px"
);
};
public onEditEntityClick = (): void => {
useSidePanel
.getState()
.openSidePanel(
"Edit Table Entity",
<EditTableEntityPanel
tableDataClient={this.tableDataClient}
queryTablesTab={this}
tableEntityListViewModel={this.tableEntityListViewModel()}
cassandraApiClient={new CassandraAPIDataClient()}
/>,
"700px"
);
};
public onDeleteEntityClick = (): void => {
this.tableCommands.deleteEntitiesCommand(this.tableEntityListViewModel());
};
public onActivate(): void {
super.onActivate();
const columns =
!!this.tableEntityListViewModel() &&
!!this.tableEntityListViewModel().table &&
this.tableEntityListViewModel().table.columns;
if (columns) {
columns.adjust();
$(window).resize();
}
}
protected getTabsButtons(): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
if (this.queryBuilderButton.visible()) {
const label = userContext.apiType === "Cassandra" ? "CQL Query Builder" : "Query Builder";
buttons.push({
iconSrc: QueryBuilderIcon,
iconAlt: label,
onCommandClick: () => this.queryViewModel().selectHelper(),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.queryBuilderButton.enabled(),
isSelected: this.queryBuilderButton.isSelected(),
});
}
if (this.queryTextButton.visible()) {
const label = userContext.apiType === "Cassandra" ? "CQL Query Text" : "Query Text";
buttons.push({
iconSrc: QueryTextIcon,
iconAlt: label,
onCommandClick: () => this.queryViewModel().selectEditor(),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.queryTextButton.enabled(),
isSelected: this.queryTextButton.isSelected(),
});
}
if (this.executeQueryButton.visible()) {
const label = "Run Query";
buttons.push({
iconSrc: ExecuteQueryIcon,
iconAlt: label,
onCommandClick: () => this.queryViewModel().runQuery(),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.executeQueryButton.enabled(),
});
}
if (this.addEntityButton.visible()) {
const label = userContext.apiType === "Cassandra" ? "Add Row" : "Add Entity";
buttons.push({
iconSrc: AddEntityIcon,
iconAlt: label,
onCommandClick: this.onAddEntityClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: !this.addEntityButton.enabled(),
});
}
if (this.editEntityButton.visible()) {
const label = userContext.apiType === "Cassandra" ? "Edit Row" : "Edit Entity";
buttons.push({
iconSrc: EditEntityIcon,
iconAlt: label,
onCommandClick: this.onEditEntityClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: !this.editEntityButton.enabled(),
});
}
if (this.deleteEntityButton.visible()) {
const label = userContext.apiType === "Cassandra" ? "Delete Rows" : "Delete Entities";
buttons.push({
iconSrc: DeleteEntitiesIcon,
iconAlt: label,
onCommandClick: this.onDeleteEntityClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: !this.deleteEntityButton.enabled(),
});
}
return buttons;
}
protected buildCommandBarOptions(): void {
ko.computed(() =>
ko.toJSON([
this.queryBuilderButton.visible,
this.queryBuilderButton.enabled,
this.queryTextButton.visible,
this.queryTextButton.enabled,
this.executeQueryButton.visible,
this.executeQueryButton.enabled,
this.addEntityButton.visible,
this.addEntityButton.enabled,
this.editEntityButton.visible,
this.editEntityButton.enabled,
this.deleteEntityButton.visible,
this.deleteEntityButton.enabled,
])
).subscribe(() => this.updateNavbarWithTabsButtons());
this.updateNavbarWithTabsButtons();
}
}

View File

@@ -0,0 +1,169 @@
import {
Checkbox,
Dropdown,
IDropdownOption,
IDropdownStyles,
IImageProps,
Image,
IStackTokens,
Stack,
TextField,
TooltipHost,
} from "@fluentui/react";
import React, { FunctionComponent } from "react";
import AddIcon from "../../../../images/Add-property.svg";
import CancelIcon from "../../../../images/Entity_cancel.svg";
import { userContext } from "../../../UserContext";
import { IOption } from "./QueryTableTabUtils";
const dropdownStyles: Partial<IDropdownStyles> = { dropdown: { width: 100 } };
export interface IQueryTableEntityClauseProps {
index: number;
entityValue: string;
entityValuePlaceHolder?: string;
selectedOperator: string;
selectedOperation: string;
operatorOptions: IOption[];
operationOptions: IOption[];
isQueryTableEntityChecked: boolean;
selectedField: string;
fieldOptions: IOption[];
entityTypeOptions: IOption[];
selectedEntityType: string;
isTimeStampSelected?: boolean;
selectedTimestamp: string;
timestampOptions: IOption[];
onAddNewClause?: () => void;
onDeleteClause?: () => void;
onQueryTableEntityCheck: (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => void;
onDropdownChange: (selectedOption: IDropdownOption, selectedOptionType: string) => void;
onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
onAddNewClauseKeyDown?: (ev: React.KeyboardEvent<HTMLImageElement>) => void;
onDeleteCaluseKeyDown?: (ev: React.KeyboardEvent<HTMLImageElement>) => void;
}
export const QueryTableEntityClause: FunctionComponent<IQueryTableEntityClauseProps> = ({
index,
entityValue,
entityValuePlaceHolder,
selectedOperator,
operatorOptions,
selectedField,
isQueryTableEntityChecked,
fieldOptions,
entityTypeOptions,
selectedEntityType,
selectedOperation,
operationOptions,
isTimeStampSelected,
selectedTimestamp,
timestampOptions,
onQueryTableEntityCheck,
onAddNewClause,
onDeleteClause,
onDropdownChange,
onEntityValueChange,
onAddNewClauseKeyDown,
onDeleteCaluseKeyDown,
}: IQueryTableEntityClauseProps): JSX.Element => {
const cancelImageProps: IImageProps = {
className: "querybuilder-cancelImg",
};
const addImageProps: IImageProps = {
className: "querybuilder-addpropertyImg",
};
const sectionStackTokens: IStackTokens = { childrenGap: 12 };
const validateEntityTypeOption = (): boolean => {
if (userContext.apiType === "Cassandra") {
return true;
} else if (selectedField === "PartitionKey" || selectedField === "RowKey" || selectedField === "Timestamp") {
return true;
}
return false;
};
return (
<>
<Stack horizontal tokens={sectionStackTokens}>
<TooltipHost content="Add new clause" id="addNewClause">
<Image
{...addImageProps}
src={AddIcon}
alt="Add new clause"
id="addNewClause"
onClick={onAddNewClause}
onKeyDown={onAddNewClauseKeyDown}
tabIndex={0}
/>
</TooltipHost>
<TooltipHost content="Delete clause" id="deleteClause">
<Image
{...cancelImageProps}
src={CancelIcon}
alt="delete clause"
id="deleteClause"
onClick={onDeleteClause}
onKeyDown={onDeleteCaluseKeyDown}
tabIndex={0}
/>
</TooltipHost>
<Checkbox checked={isQueryTableEntityChecked} onChange={onQueryTableEntityCheck} />
<Dropdown
style={{ visibility: index > 0 ? "visible" : "hidden" }}
selectedKey={selectedOperation}
onChange={(_event: React.FormEvent<HTMLElement>, selectedOption: IDropdownOption) =>
onDropdownChange(selectedOption, "selectedOperation")
}
options={operationOptions}
styles={dropdownStyles}
/>
<Dropdown
selectedKey={selectedField}
onChange={(_event: React.FormEvent<HTMLElement>, selectedOption: IDropdownOption) =>
onDropdownChange(selectedOption, "selectedField")
}
options={fieldOptions}
styles={dropdownStyles}
/>
<Dropdown
selectedKey={selectedEntityType}
onChange={(_event: React.FormEvent<HTMLElement>, selectedOption: IDropdownOption) =>
onDropdownChange(selectedOption, "selectedEntityType")
}
options={entityTypeOptions}
disabled={validateEntityTypeOption()}
styles={dropdownStyles}
/>
<Dropdown
selectedKey={selectedOperator}
onChange={(_event: React.FormEvent<HTMLElement>, selectedOption: IDropdownOption) =>
onDropdownChange(selectedOption, "selectedOperator")
}
options={operatorOptions}
styles={dropdownStyles}
/>
{isTimeStampSelected ? (
<Dropdown
selectedKey={selectedTimestamp}
onChange={(_event: React.FormEvent<HTMLElement>, selectedOption: IDropdownOption) =>
onDropdownChange(selectedOption, "selectedTimestamp")
}
options={timestampOptions}
styles={dropdownStyles}
/>
) : (
<TextField
autoFocus
placeholder={entityValuePlaceHolder}
value={entityValue}
onChange={onEntityValueChange}
required
/>
)}
</Stack>
</>
);
};

View File

@@ -0,0 +1,104 @@
import { IColumn, Selection } from "@fluentui/react";
import * as ViewModels from "../../../Contracts/ViewModels";
import Explorer from "../../Explorer";
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
import * as Entities from "../../Tables/Entities";
import ClauseGroup from "../../Tables/QueryBuilder/ClauseGroup";
import QueryViewModel from "../../Tables/QueryBuilder/QueryViewModel";
import TabsBase from "../TabsBase";
import NewQueryTablesTab from "./QueryTablesTab";
export interface Button {
visible: boolean;
enabled: boolean;
isSelected?: boolean;
}
export interface IOption {
key: string;
text: string;
}
export interface IDocument {
partitionKey: string;
rowKey: string;
timeStamp: string;
}
export interface IQueryTablesTabComponentProps {
tabKind: ViewModels.CollectionTabKind;
title: string;
tabPath: string;
collection: ViewModels.CollectionBase;
node: ViewModels.TreeNode;
onLoadStartKey: number;
container: Explorer;
tabsBaseInstance: TabsBase;
queryTablesTab: NewQueryTablesTab;
}
export interface IQueryTablesTabComponentStates {
tableEntityListViewModel: TableEntityListViewModel;
queryViewModel: QueryViewModel;
queryText: string;
selectedQueryText: string;
executeQueryButton: Button;
queryBuilderButton: Button;
queryTextButton: Button;
addEntityButton: Button;
editEntityButton: Button;
deleteEntityButton: Button;
isHelperActive: boolean;
columns: IColumn[];
items: IDocument[];
isExpanded: boolean;
isEditorActive: boolean;
selectedItems: Entities.ITableEntity[];
isValue: boolean;
isTimestamp: boolean;
isCustomLastTimestamp: boolean;
isCustomRangeTimestamp: boolean;
operators: string[];
selectMessage: string;
queryTableRows: IQueryTableRowsType[];
originalItems: IDocument[];
rowSelected: boolean;
selection: Selection;
entities: Entities.ITableEntity[];
headers: string[];
isLoading: boolean;
queryErrorMessage: string;
hasQueryError: boolean;
currentPage: number;
currentStartIndex: number;
fromDocument: number;
toDocument: number;
selectedItem: number;
}
export interface IQueryTableRowsType {
isQueryTableEntityChecked: boolean;
isTimeStampSelected: boolean;
selectedOperator: string;
selectedField: string;
entityValue: string;
selectedEntityType: string;
selectedOperation: string;
selectedTimestamp: string;
fieldOptions: IOption[];
operatorOptions: IOption[];
entityTypeOptions: IOption[];
operationOptions: IOption[];
timestampOptions: IOption[];
id: string;
clauseGroup: ClauseGroup;
isLocal: boolean;
isTimestamp: boolean;
isValue: boolean;
isCustomRangeTimestamp: boolean;
customTimeValue: string;
timeValue: string;
}
export const getformattedOptions = (options: Array<string>): IOption[] => {
return options.map((option) => {
return { key: option, text: option };
});
};

View File

@@ -0,0 +1,43 @@
import React from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import Explorer from "../../Explorer";
import TableCommands from "../../Tables/DataTable/TableCommands";
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
import TabsBase from "../TabsBase";
import QueryTablesTabComponent from "./QueryTablesTabComponent";
import { IQueryTablesTabComponentProps } from "./QueryTableTabUtils";
interface QueryTablesTabProps {
container: Explorer;
}
class NewQueryTablesTab extends TabsBase {
public iQueryTablesTabCompProps: IQueryTablesTabComponentProps;
public collection: ViewModels.Collection;
public tableEntityListViewModel: TableEntityListViewModel;
public tableCommands: TableCommands;
public container: Explorer;
constructor(options: ViewModels.TabOptions, props: QueryTablesTabProps) {
super(options);
this.container = props.container;
this.tableCommands = new TableCommands(props.container);
this.tableEntityListViewModel = new TableEntityListViewModel(this.tableCommands, this);
this.iQueryTablesTabCompProps = {
tabKind: options.tabKind,
title: options.title,
tabPath: options.tabPath,
collection: options.collection,
node: options.node,
onLoadStartKey: options.onLoadStartKey,
container: props.container,
tabsBaseInstance: this,
queryTablesTab: this,
};
}
public render(): JSX.Element {
return <QueryTablesTabComponent {...this.iQueryTablesTabCompProps} />;
}
}
export default NewQueryTablesTab;

File diff suppressed because it is too large Load Diff

View File

@@ -32,7 +32,8 @@ import MongoDocumentsTab from "../Tabs/MongoDocumentsTab";
import { NewMongoQueryTab } from "../Tabs/MongoQueryTab/MongoQueryTab"; import { 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";
@@ -391,13 +392,13 @@ export default class Collection implements ViewModels.Collection {
}); });
} }
const queryTablesTabs: QueryTablesTab[] = useTabs const queryTablesTabs: NewQueryTablesTab[] = 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 QueryTablesTab[]; ) as NewQueryTablesTab[];
let queryTablesTab: QueryTablesTab = queryTablesTabs && queryTablesTabs[0]; let queryTablesTab: NewQueryTablesTab = queryTablesTabs && queryTablesTabs[0];
if (queryTablesTab) { if (queryTablesTab) {
useTabs.getState().activateTab(queryTablesTab); useTabs.getState().activateTab(queryTablesTab);
@@ -415,14 +416,57 @@ export default class Collection implements ViewModels.Collection {
tabTitle: title, tabTitle: title,
}); });
queryTablesTab = new QueryTablesTab({ queryTablesTab = new NewQueryTablesTab(
{
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);
} }

61
src/indexO.html Normal file
View File

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