mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-19 17:01:13 +00:00
Prettier 2.0 (#393)
This commit is contained in:
@@ -1,171 +1,171 @@
|
||||
export var TableType = {
|
||||
String: "String",
|
||||
Boolean: "Boolean",
|
||||
Binary: "Binary",
|
||||
DateTime: "DateTime",
|
||||
Double: "Double",
|
||||
Guid: "Guid",
|
||||
Int32: "Int32",
|
||||
Int64: "Int64"
|
||||
};
|
||||
|
||||
export var CassandraType = {
|
||||
Ascii: "Ascii",
|
||||
Bigint: "Bigint",
|
||||
Blob: "Blob",
|
||||
Boolean: "Boolean",
|
||||
Decimal: "Decimal",
|
||||
Double: "Double",
|
||||
Float: "Float",
|
||||
Int: "Int",
|
||||
Text: "Text",
|
||||
Uuid: "Uuid",
|
||||
Varchar: "Varchar",
|
||||
Varint: "Varint",
|
||||
Inet: "Inet",
|
||||
Smallint: "Smallint",
|
||||
Tinyint: "Tinyint"
|
||||
};
|
||||
|
||||
export var ClauseRule = {
|
||||
And: "And",
|
||||
Or: "Or"
|
||||
};
|
||||
|
||||
export var Operator = {
|
||||
EqualTo: "==",
|
||||
GreaterThan: ">",
|
||||
GreaterThanOrEqualTo: ">=",
|
||||
LessThan: "<",
|
||||
LessThanOrEqualTo: "<=",
|
||||
NotEqualTo: "<>",
|
||||
Equal: "="
|
||||
};
|
||||
|
||||
export var ODataOperator = {
|
||||
EqualTo: "eq",
|
||||
GreaterThan: "gt",
|
||||
GreaterThanOrEqualTo: "ge",
|
||||
LessThan: "lt",
|
||||
LessThanOrEqualTo: "le",
|
||||
NotEqualTo: "ne"
|
||||
};
|
||||
|
||||
export var timeOptions = {
|
||||
lastHour: "Last hour",
|
||||
last24Hours: "Last 24 hours",
|
||||
last7Days: "Last 7 days",
|
||||
last31Days: "Last 31 days",
|
||||
last365Days: "Last 365 days",
|
||||
currentMonth: "Current month",
|
||||
currentYear: "Current year",
|
||||
custom: "Custom..."
|
||||
};
|
||||
|
||||
export var htmlSelectors = {
|
||||
dataTableSelector: "#storageTable",
|
||||
dataTableAllRowsSelector: "#storageTable tbody tr",
|
||||
dataTableHeadRowSelector: ".dataTable thead tr",
|
||||
dataTableBodyRowSelector: ".dataTable tbody tr",
|
||||
dataTableScrollBodySelector: ".dataTables_scrollBody",
|
||||
dataTableScrollContainerSelector: ".dataTables_scroll",
|
||||
dataTableHeaderTypeSelector: "table thead th",
|
||||
dataTablePaginationButtonSelector: ".paginate_button",
|
||||
dataTableHeaderTableSelector: "#storageTable_wrapper .dataTables_scrollHeadInner table",
|
||||
dataTableBodyTableSelector: "#storageTable_wrapper .dataTables_scrollBody table",
|
||||
searchInputField: ".search-input",
|
||||
uploadDropdownSelector: "#upload-dropdown",
|
||||
navigationDropdownSelector: "#navigation-dropdown",
|
||||
addressBarInputSelector: "#address-bar",
|
||||
breadCrumbsSelector: "#breadcrumb-list",
|
||||
breadCrumbItemsSelector: ".breadcrumb li a",
|
||||
paginateSelector: "#storageTable_paginate",
|
||||
dataTablesInfoSelector: "#storageTable_info",
|
||||
selectAllDropdownSelector: "#select-all-dropdown"
|
||||
};
|
||||
|
||||
export var defaultHeader = " ";
|
||||
|
||||
export var EntityKeyNames = {
|
||||
PartitionKey: "PartitionKey",
|
||||
RowKey: "RowKey",
|
||||
Timestamp: "Timestamp",
|
||||
Metadata: ".metadata",
|
||||
Etag: "etag"
|
||||
};
|
||||
|
||||
export var htmlAttributeNames = {
|
||||
dataTableNameAttr: "name_attr",
|
||||
dataTableContentTypeAttr: "contentType_attr",
|
||||
dataTableSnapshotAttr: "snapshot_attr",
|
||||
dataTableRowKeyAttr: "rowKey_attr",
|
||||
dataTableMessageIdAttr: "messageId_attr",
|
||||
dataTableHeaderIndex: "data-column-index"
|
||||
};
|
||||
|
||||
export var cssColors = {
|
||||
commonControlsButtonActive: "#B4C7DC" /* A darker shade of [{common-controls-button-hover-background}] */
|
||||
};
|
||||
|
||||
export var clauseGroupColors = ["#ffe1ff", "#fffacd", "#f0ffff", "#ffefd5", "#f0fff0"];
|
||||
export var transparentColor = "transparent";
|
||||
|
||||
export var keyCodes = {
|
||||
RightClick: 3,
|
||||
Enter: 13,
|
||||
Esc: 27,
|
||||
Tab: 9,
|
||||
LeftArrow: 37,
|
||||
UpArrow: 38,
|
||||
RightArrow: 39,
|
||||
DownArrow: 40,
|
||||
Delete: 46,
|
||||
A: 65,
|
||||
B: 66,
|
||||
C: 67,
|
||||
D: 68,
|
||||
E: 69,
|
||||
F: 70,
|
||||
G: 71,
|
||||
H: 72,
|
||||
I: 73,
|
||||
J: 74,
|
||||
K: 75,
|
||||
L: 76,
|
||||
M: 77,
|
||||
N: 78,
|
||||
O: 79,
|
||||
P: 80,
|
||||
Q: 81,
|
||||
R: 82,
|
||||
S: 83,
|
||||
T: 84,
|
||||
U: 85,
|
||||
V: 86,
|
||||
W: 87,
|
||||
X: 88,
|
||||
Y: 89,
|
||||
Z: 90,
|
||||
Period: 190,
|
||||
DecimalPoint: 110,
|
||||
F1: 112,
|
||||
F2: 113,
|
||||
F3: 114,
|
||||
F4: 115,
|
||||
F5: 116,
|
||||
F6: 117,
|
||||
F7: 118,
|
||||
F8: 119,
|
||||
F9: 120,
|
||||
F10: 121,
|
||||
F11: 122,
|
||||
F12: 123,
|
||||
Dash: 189
|
||||
};
|
||||
|
||||
export var InputType = {
|
||||
Text: "text",
|
||||
// Chrome doesn't support datetime, instead, datetime-local is supported.
|
||||
DateTime: "datetime-local",
|
||||
Number: "number"
|
||||
};
|
||||
export var TableType = {
|
||||
String: "String",
|
||||
Boolean: "Boolean",
|
||||
Binary: "Binary",
|
||||
DateTime: "DateTime",
|
||||
Double: "Double",
|
||||
Guid: "Guid",
|
||||
Int32: "Int32",
|
||||
Int64: "Int64",
|
||||
};
|
||||
|
||||
export var CassandraType = {
|
||||
Ascii: "Ascii",
|
||||
Bigint: "Bigint",
|
||||
Blob: "Blob",
|
||||
Boolean: "Boolean",
|
||||
Decimal: "Decimal",
|
||||
Double: "Double",
|
||||
Float: "Float",
|
||||
Int: "Int",
|
||||
Text: "Text",
|
||||
Uuid: "Uuid",
|
||||
Varchar: "Varchar",
|
||||
Varint: "Varint",
|
||||
Inet: "Inet",
|
||||
Smallint: "Smallint",
|
||||
Tinyint: "Tinyint",
|
||||
};
|
||||
|
||||
export var ClauseRule = {
|
||||
And: "And",
|
||||
Or: "Or",
|
||||
};
|
||||
|
||||
export var Operator = {
|
||||
EqualTo: "==",
|
||||
GreaterThan: ">",
|
||||
GreaterThanOrEqualTo: ">=",
|
||||
LessThan: "<",
|
||||
LessThanOrEqualTo: "<=",
|
||||
NotEqualTo: "<>",
|
||||
Equal: "=",
|
||||
};
|
||||
|
||||
export var ODataOperator = {
|
||||
EqualTo: "eq",
|
||||
GreaterThan: "gt",
|
||||
GreaterThanOrEqualTo: "ge",
|
||||
LessThan: "lt",
|
||||
LessThanOrEqualTo: "le",
|
||||
NotEqualTo: "ne",
|
||||
};
|
||||
|
||||
export var timeOptions = {
|
||||
lastHour: "Last hour",
|
||||
last24Hours: "Last 24 hours",
|
||||
last7Days: "Last 7 days",
|
||||
last31Days: "Last 31 days",
|
||||
last365Days: "Last 365 days",
|
||||
currentMonth: "Current month",
|
||||
currentYear: "Current year",
|
||||
custom: "Custom...",
|
||||
};
|
||||
|
||||
export var htmlSelectors = {
|
||||
dataTableSelector: "#storageTable",
|
||||
dataTableAllRowsSelector: "#storageTable tbody tr",
|
||||
dataTableHeadRowSelector: ".dataTable thead tr",
|
||||
dataTableBodyRowSelector: ".dataTable tbody tr",
|
||||
dataTableScrollBodySelector: ".dataTables_scrollBody",
|
||||
dataTableScrollContainerSelector: ".dataTables_scroll",
|
||||
dataTableHeaderTypeSelector: "table thead th",
|
||||
dataTablePaginationButtonSelector: ".paginate_button",
|
||||
dataTableHeaderTableSelector: "#storageTable_wrapper .dataTables_scrollHeadInner table",
|
||||
dataTableBodyTableSelector: "#storageTable_wrapper .dataTables_scrollBody table",
|
||||
searchInputField: ".search-input",
|
||||
uploadDropdownSelector: "#upload-dropdown",
|
||||
navigationDropdownSelector: "#navigation-dropdown",
|
||||
addressBarInputSelector: "#address-bar",
|
||||
breadCrumbsSelector: "#breadcrumb-list",
|
||||
breadCrumbItemsSelector: ".breadcrumb li a",
|
||||
paginateSelector: "#storageTable_paginate",
|
||||
dataTablesInfoSelector: "#storageTable_info",
|
||||
selectAllDropdownSelector: "#select-all-dropdown",
|
||||
};
|
||||
|
||||
export var defaultHeader = " ";
|
||||
|
||||
export var EntityKeyNames = {
|
||||
PartitionKey: "PartitionKey",
|
||||
RowKey: "RowKey",
|
||||
Timestamp: "Timestamp",
|
||||
Metadata: ".metadata",
|
||||
Etag: "etag",
|
||||
};
|
||||
|
||||
export var htmlAttributeNames = {
|
||||
dataTableNameAttr: "name_attr",
|
||||
dataTableContentTypeAttr: "contentType_attr",
|
||||
dataTableSnapshotAttr: "snapshot_attr",
|
||||
dataTableRowKeyAttr: "rowKey_attr",
|
||||
dataTableMessageIdAttr: "messageId_attr",
|
||||
dataTableHeaderIndex: "data-column-index",
|
||||
};
|
||||
|
||||
export var cssColors = {
|
||||
commonControlsButtonActive: "#B4C7DC" /* A darker shade of [{common-controls-button-hover-background}] */,
|
||||
};
|
||||
|
||||
export var clauseGroupColors = ["#ffe1ff", "#fffacd", "#f0ffff", "#ffefd5", "#f0fff0"];
|
||||
export var transparentColor = "transparent";
|
||||
|
||||
export var keyCodes = {
|
||||
RightClick: 3,
|
||||
Enter: 13,
|
||||
Esc: 27,
|
||||
Tab: 9,
|
||||
LeftArrow: 37,
|
||||
UpArrow: 38,
|
||||
RightArrow: 39,
|
||||
DownArrow: 40,
|
||||
Delete: 46,
|
||||
A: 65,
|
||||
B: 66,
|
||||
C: 67,
|
||||
D: 68,
|
||||
E: 69,
|
||||
F: 70,
|
||||
G: 71,
|
||||
H: 72,
|
||||
I: 73,
|
||||
J: 74,
|
||||
K: 75,
|
||||
L: 76,
|
||||
M: 77,
|
||||
N: 78,
|
||||
O: 79,
|
||||
P: 80,
|
||||
Q: 81,
|
||||
R: 82,
|
||||
S: 83,
|
||||
T: 84,
|
||||
U: 85,
|
||||
V: 86,
|
||||
W: 87,
|
||||
X: 88,
|
||||
Y: 89,
|
||||
Z: 90,
|
||||
Period: 190,
|
||||
DecimalPoint: 110,
|
||||
F1: 112,
|
||||
F2: 113,
|
||||
F3: 114,
|
||||
F4: 115,
|
||||
F5: 116,
|
||||
F6: 117,
|
||||
F7: 118,
|
||||
F8: 119,
|
||||
F9: 120,
|
||||
F10: 121,
|
||||
F11: 122,
|
||||
F12: 123,
|
||||
Dash: 189,
|
||||
};
|
||||
|
||||
export var InputType = {
|
||||
Text: "text",
|
||||
// Chrome doesn't support datetime, instead, datetime-local is supported.
|
||||
DateTime: "datetime-local",
|
||||
Number: "number",
|
||||
};
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
abstract class CacheBase<T> {
|
||||
public data: T[] | null;
|
||||
public sortOrder: any;
|
||||
public serverCallInProgress: boolean;
|
||||
|
||||
constructor() {
|
||||
this.data = null;
|
||||
this.sortOrder = null;
|
||||
this.serverCallInProgress = false;
|
||||
}
|
||||
|
||||
public get length(): number {
|
||||
return this.data ? this.data.length : 0;
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.preClear();
|
||||
this.data = null;
|
||||
this.sortOrder = null;
|
||||
this.serverCallInProgress = false;
|
||||
}
|
||||
|
||||
protected abstract preClear(): void;
|
||||
}
|
||||
|
||||
export default CacheBase;
|
||||
abstract class CacheBase<T> {
|
||||
public data: T[] | null;
|
||||
public sortOrder: any;
|
||||
public serverCallInProgress: boolean;
|
||||
|
||||
constructor() {
|
||||
this.data = null;
|
||||
this.sortOrder = null;
|
||||
this.serverCallInProgress = false;
|
||||
}
|
||||
|
||||
public get length(): number {
|
||||
return this.data ? this.data.length : 0;
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.preClear();
|
||||
this.data = null;
|
||||
this.sortOrder = null;
|
||||
this.serverCallInProgress = false;
|
||||
}
|
||||
|
||||
protected abstract preClear(): void;
|
||||
}
|
||||
|
||||
export default CacheBase;
|
||||
|
||||
@@ -1,411 +1,396 @@
|
||||
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 explorer = window.dataExplorer;
|
||||
const notificationConsoleHeight = explorer.isNotificationConsoleExpanded()
|
||||
? 252 /** 32px(header) + 220px(content height) **/
|
||||
: 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");
|
||||
}
|
||||
}
|
||||
};
|
||||
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 explorer = window.dataExplorer;
|
||||
const notificationConsoleHeight = explorer.isNotificationConsoleExpanded()
|
||||
? 252 /** 32px(header) + 220px(content height) **/
|
||||
: 32; /** Header height **/
|
||||
|
||||
var scrollHeight =
|
||||
bodyHeight -
|
||||
dataTablesScrollBodyPosY -
|
||||
dataTablesPaginateElem.outerHeight(true) -
|
||||
dataTablePaddingBottom -
|
||||
notificationConsoleHeight;
|
||||
|
||||
//info and paginate control are stacked
|
||||
if (dataTablesInfoElem.offset().top < dataTablesPaginateElem.offset().top) {
|
||||
scrollHeight -= dataTablesInfoElem.outerHeight(true);
|
||||
}
|
||||
|
||||
// TODO This is a work around for setting the outerheight since we don't have access to the JQuery.outerheight(numberValue)
|
||||
// in the current version of JQuery we are using. Ideally, we would upgrade JQuery and use this line instead:
|
||||
// $(Constants.htmlSelectors.dataTableScrollBodySelector).outerHeight(scrollHeight);
|
||||
var element = $(tabElement).find(Constants.htmlSelectors.dataTableScrollBodySelector)[0];
|
||||
var style = getComputedStyle(element);
|
||||
var actualHeight = parseInt(style.height);
|
||||
var change = element.offsetHeight - scrollHeight;
|
||||
$(tabElement)
|
||||
.find(Constants.htmlSelectors.dataTableScrollBodySelector)
|
||||
.height(actualHeight - change);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Update the table's scrollable region width to make efficient use of the remaining space.
|
||||
*/
|
||||
function updateTableScrollableRegionWidth(): void {
|
||||
$(".tab-pane").each(function (index, tabElement) {
|
||||
if (!$(tabElement).hasClass("tableContainer")) {
|
||||
return;
|
||||
}
|
||||
|
||||
var bodyWidth = $(window).width();
|
||||
var dataTablesScrollBodyPosLeft = $(tabElement).find(Constants.htmlSelectors.dataTableScrollBodySelector).offset()
|
||||
.left;
|
||||
var scrollWidth = bodyWidth - dataTablesScrollBodyPosLeft;
|
||||
|
||||
// jquery datatables automatically sets width:100% to both the header and the body when we use it's column autoWidth feature.
|
||||
// We work around that by setting the height for it's container instead.
|
||||
$(tabElement).find(Constants.htmlSelectors.dataTableScrollContainerSelector).width(scrollWidth);
|
||||
});
|
||||
}
|
||||
|
||||
function initializeEventHandlers(): void {
|
||||
var $headers: JQuery = $(Constants.htmlSelectors.dataTableHeaderTypeSelector);
|
||||
var $firstHeader: JQuery = $headers.first();
|
||||
var firstIndex: string = $firstHeader.attr(Constants.htmlAttributeNames.dataTableHeaderIndex);
|
||||
|
||||
$headers
|
||||
.on("keydown", (event: JQueryEventObject) => {
|
||||
Utilities.onEnter(event, ($sourceElement: JQuery) => {
|
||||
$sourceElement.css("background-color", Constants.cssColors.commonControlsButtonActive);
|
||||
});
|
||||
|
||||
// Bind shift+tab from first header back to search input field
|
||||
Utilities.onTab(
|
||||
event,
|
||||
($sourceElement: JQuery) => {
|
||||
var sourceIndex: string = $sourceElement.attr(Constants.htmlAttributeNames.dataTableHeaderIndex);
|
||||
|
||||
if (sourceIndex === firstIndex) {
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
/* metaKey */ null,
|
||||
/* shiftKey */ true,
|
||||
/* altKey */ null
|
||||
);
|
||||
|
||||
// Also reset color if [shift-] tabbing away from button while holding down 'enter'
|
||||
Utilities.onTab(event, ($sourceElement: JQuery) => {
|
||||
$sourceElement.css("background-color", "");
|
||||
});
|
||||
})
|
||||
.on("keyup", (event: JQueryEventObject) => {
|
||||
Utilities.onEnter(event, ($sourceElement: JQuery) => {
|
||||
$sourceElement.css("background-color", "");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateSelectionStatus(oSettings: any): void {
|
||||
var $dataTableRows: JQuery = $(Constants.htmlSelectors.dataTableAllRowsSelector);
|
||||
if ($dataTableRows) {
|
||||
for (var i = 0; i < $dataTableRows.length; i++) {
|
||||
var $row: JQuery = $dataTableRows.eq(i);
|
||||
var rowKey: string = $row.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr);
|
||||
var table = tableEntityListViewModelMap[oSettings.ajax].tableViewModel;
|
||||
if (table.isItemSelected(table.getTableEntityKeys(rowKey))) {
|
||||
$row.attr("tabindex", "0");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateDataTableFocus(oSettings.ajax);
|
||||
|
||||
DataTableOperations.setPaginationButtonEventHandlers();
|
||||
}
|
||||
|
||||
// TODO consider centralizing this "post-command" logic into some sort of Command Manager entity.
|
||||
// See VSO:166520: "[Storage Explorer] Consider adding a 'command manager' to track command post-effects."
|
||||
function updateDataTableFocus(queryTablesTabId: string): void {
|
||||
var $activeElement: JQuery = $(document.activeElement);
|
||||
var isFocusLost: boolean = $activeElement.is("body"); // When focus is lost, "body" becomes the active element.
|
||||
var storageExplorerFrameHasFocus: boolean = document.hasFocus();
|
||||
var operationManager = tableEntityListViewModelMap[queryTablesTabId].operationManager;
|
||||
if (operationManager) {
|
||||
if (isFocusLost && storageExplorerFrameHasFocus) {
|
||||
// We get here when no control is active, meaning that the table update was triggered
|
||||
// from a dialog, the context menu or by clicking on a toolbar control or header.
|
||||
// Note that giving focus to the table also selects the first row if needed.
|
||||
// The document.hasFocus() ensures that the table will only get focus when the
|
||||
// focus was lost (i.e. "body has the focus") within the Storage Explorer frame
|
||||
// i.e. not when the focus is lost because it is in another frame
|
||||
// e.g. a daytona dialog or in the Activity Log.
|
||||
operationManager.focusTable();
|
||||
}
|
||||
if ($activeElement.is(".sorting_asc") || $activeElement.is(".sorting_desc")) {
|
||||
// If table header is selected, focus is shifted to the selected element as part of accessibility
|
||||
$activeElement && $activeElement.focus();
|
||||
} else {
|
||||
// If some control is active, we don't give focus back to the table,
|
||||
// just select the first row if needed (empty selection).
|
||||
operationManager.selectFirstIfNeeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(<any>ko.bindingHandlers).tableSource = {
|
||||
init: bindDataTable,
|
||||
update: dataChanged,
|
||||
};
|
||||
|
||||
(<any>ko.bindingHandlers).tableSelection = {
|
||||
update: selectionChanged,
|
||||
};
|
||||
|
||||
(<any>ko.bindingHandlers).readOnly = {
|
||||
update: function (element: any, valueAccessor: any) {
|
||||
var value = ko.utils.unwrapObservable(valueAccessor());
|
||||
if (value) {
|
||||
element.setAttribute("readOnly", true);
|
||||
} else {
|
||||
element.removeAttribute("readOnly");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
import * as Utilities from "../Utilities";
|
||||
|
||||
/**
|
||||
* Wrapper function for creating data tables. Call this method, not the
|
||||
* data tables constructor when you want to create a data table. This
|
||||
* function makes sure that content without a render function is properly
|
||||
* encoded to prevent XSS.
|
||||
* @param{$dataTableElem} JQuery data table element
|
||||
* @param{$settings} Settings to use when creating the data table
|
||||
*/
|
||||
export function createDataTable($dataTableElem: JQuery, settings: any): DataTables.DataTable {
|
||||
return $dataTableElem.DataTable(applyDefaultRendering(settings));
|
||||
}
|
||||
|
||||
/**
|
||||
* Go through the settings for a data table and apply a simple HTML encode to any column
|
||||
* without a render function to prevent XSS.
|
||||
* @param{settings} The settings to check
|
||||
* @return The given settings with all columns having a rendering function
|
||||
*/
|
||||
function applyDefaultRendering(settings: any): DataTables.SettingsLegacy {
|
||||
var tableColumns: DataTables.ColumnLegacy[] = null;
|
||||
|
||||
if (settings.aoColumns) {
|
||||
tableColumns = settings.aoColumns;
|
||||
} else if (settings.aoColumnDefs) {
|
||||
// for tables we use aoColumnDefs instead of aoColumns
|
||||
tableColumns = settings.aoColumnDefs;
|
||||
}
|
||||
|
||||
// either the settings had no columns defined, or they were called
|
||||
// by a property name which we have not used before
|
||||
if (!tableColumns) {
|
||||
return settings;
|
||||
}
|
||||
|
||||
for (var i = 0; i < tableColumns.length; i++) {
|
||||
// the column does not have a render function
|
||||
if (!tableColumns[i].mRender) {
|
||||
tableColumns[i].mRender = defaultDataRender;
|
||||
}
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default data render function, whatever is done to data in here
|
||||
* will be done to any data which we do not specify a render for.
|
||||
*/
|
||||
function defaultDataRender(data: any, type: string, full: any) {
|
||||
return Utilities.htmlEncode(data);
|
||||
}
|
||||
import * as Utilities from "../Utilities";
|
||||
|
||||
/**
|
||||
* Wrapper function for creating data tables. Call this method, not the
|
||||
* data tables constructor when you want to create a data table. This
|
||||
* function makes sure that content without a render function is properly
|
||||
* encoded to prevent XSS.
|
||||
* @param{$dataTableElem} JQuery data table element
|
||||
* @param{$settings} Settings to use when creating the data table
|
||||
*/
|
||||
export function createDataTable($dataTableElem: JQuery, settings: any): DataTables.DataTable {
|
||||
return $dataTableElem.DataTable(applyDefaultRendering(settings));
|
||||
}
|
||||
|
||||
/**
|
||||
* Go through the settings for a data table and apply a simple HTML encode to any column
|
||||
* without a render function to prevent XSS.
|
||||
* @param{settings} The settings to check
|
||||
* @return The given settings with all columns having a rendering function
|
||||
*/
|
||||
function applyDefaultRendering(settings: any): DataTables.SettingsLegacy {
|
||||
var tableColumns: DataTables.ColumnLegacy[] = null;
|
||||
|
||||
if (settings.aoColumns) {
|
||||
tableColumns = settings.aoColumns;
|
||||
} else if (settings.aoColumnDefs) {
|
||||
// for tables we use aoColumnDefs instead of aoColumns
|
||||
tableColumns = settings.aoColumnDefs;
|
||||
}
|
||||
|
||||
// either the settings had no columns defined, or they were called
|
||||
// by a property name which we have not used before
|
||||
if (!tableColumns) {
|
||||
return settings;
|
||||
}
|
||||
|
||||
for (var i = 0; i < tableColumns.length; i++) {
|
||||
// the column does not have a render function
|
||||
if (!tableColumns[i].mRender) {
|
||||
tableColumns[i].mRender = defaultDataRender;
|
||||
}
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default data render function, whatever is done to data in here
|
||||
* will be done to any data which we do not specify a render for.
|
||||
*/
|
||||
function defaultDataRender(data: any, type: string, full: any) {
|
||||
return Utilities.htmlEncode(data);
|
||||
}
|
||||
|
||||
@@ -1,300 +1,300 @@
|
||||
import ko from "knockout";
|
||||
|
||||
import * as DataTableOperations from "./DataTableOperations";
|
||||
import * as Constants from "../Constants";
|
||||
import TableCommands from "./TableCommands";
|
||||
import TableEntityListViewModel from "./TableEntityListViewModel";
|
||||
import * as Utilities from "../Utilities";
|
||||
import * as Entities from "../Entities";
|
||||
|
||||
/*
|
||||
* Base class for data table row selection.
|
||||
*/
|
||||
export default class DataTableOperationManager {
|
||||
private _tableEntityListViewModel: TableEntityListViewModel;
|
||||
private _tableCommands: TableCommands;
|
||||
private dataTable: JQuery;
|
||||
|
||||
constructor(table: JQuery, viewModel: TableEntityListViewModel, tableCommands: TableCommands) {
|
||||
this.dataTable = table;
|
||||
this._tableEntityListViewModel = viewModel;
|
||||
this._tableCommands = tableCommands;
|
||||
|
||||
this.bind();
|
||||
this._tableEntityListViewModel.bind(this);
|
||||
}
|
||||
|
||||
private click = (event: JQueryEventObject) => {
|
||||
var elem: JQuery = $(event.currentTarget);
|
||||
this.updateLastSelectedItem(elem, event.shiftKey);
|
||||
|
||||
if (Utilities.isEnvironmentCtrlPressed(event)) {
|
||||
this.applyCtrlSelection(elem);
|
||||
} else if (event.shiftKey) {
|
||||
this.applyShiftSelection(elem);
|
||||
} else {
|
||||
this.applySingleSelection(elem);
|
||||
}
|
||||
};
|
||||
|
||||
private doubleClick = (event: JQueryEventObject) => {
|
||||
this.tryOpenEditor();
|
||||
};
|
||||
|
||||
private keyDown = (event: JQueryEventObject): boolean => {
|
||||
var isUpArrowKey: boolean = event.keyCode === Constants.keyCodes.UpArrow,
|
||||
isDownArrowKey: boolean = event.keyCode === Constants.keyCodes.DownArrow,
|
||||
handled: boolean = false;
|
||||
|
||||
if (isUpArrowKey || isDownArrowKey) {
|
||||
var lastSelectedItem: Entities.ITableEntity = this._tableEntityListViewModel.lastSelectedItem;
|
||||
var dataTableRows: JQuery = $(Constants.htmlSelectors.dataTableAllRowsSelector);
|
||||
var maximumIndex = dataTableRows.length - 1;
|
||||
|
||||
// If can't find an index for lastSelectedItem, then either no item is previously selected or it goes across page.
|
||||
// Simply select the first item in this case.
|
||||
var lastSelectedItemIndex = lastSelectedItem
|
||||
? this._tableEntityListViewModel.getItemIndexFromCurrentPage(
|
||||
this._tableEntityListViewModel.getTableEntityKeys(lastSelectedItem.RowKey._)
|
||||
)
|
||||
: -1;
|
||||
var nextIndex: number = isUpArrowKey ? lastSelectedItemIndex - 1 : lastSelectedItemIndex + 1;
|
||||
var safeIndex: number = Utilities.ensureBetweenBounds(nextIndex, 0, maximumIndex);
|
||||
var selectedRowElement: JQuery = dataTableRows.eq(safeIndex);
|
||||
|
||||
if (selectedRowElement) {
|
||||
if (event.shiftKey) {
|
||||
this.applyShiftSelection(selectedRowElement);
|
||||
} else {
|
||||
this.applySingleSelection(selectedRowElement);
|
||||
}
|
||||
|
||||
this.updateLastSelectedItem(selectedRowElement, event.shiftKey);
|
||||
handled = true;
|
||||
DataTableOperations.scrollToRowIfNeeded(dataTableRows, safeIndex, isUpArrowKey);
|
||||
}
|
||||
} else if (
|
||||
Utilities.isEnvironmentCtrlPressed(event) &&
|
||||
!Utilities.isEnvironmentShiftPressed(event) &&
|
||||
!Utilities.isEnvironmentAltPressed(event) &&
|
||||
event.keyCode === Constants.keyCodes.A
|
||||
) {
|
||||
this.applySelectAll();
|
||||
handled = true;
|
||||
}
|
||||
|
||||
return !handled;
|
||||
};
|
||||
|
||||
// Note: There is one key up event each time a key is pressed;
|
||||
// in contrast, there may be more than one key down and key
|
||||
// pressed events.
|
||||
private keyUp = (event: JQueryEventObject): boolean => {
|
||||
var handled: boolean = false;
|
||||
|
||||
switch (event.keyCode) {
|
||||
case Constants.keyCodes.Enter:
|
||||
handled = this.tryOpenEditor();
|
||||
break;
|
||||
case Constants.keyCodes.Delete:
|
||||
handled = this.tryHandleDeleteSelected();
|
||||
break;
|
||||
}
|
||||
|
||||
return !handled;
|
||||
};
|
||||
|
||||
private itemDropped = (event: JQueryEventObject): boolean => {
|
||||
var handled: boolean = false;
|
||||
var items = (<any>event.originalEvent).dataTransfer.items;
|
||||
|
||||
if (!items) {
|
||||
// On browsers outside of Chromium
|
||||
// we can't discern between dirs and files
|
||||
// so we will disable drag & drop for now
|
||||
return null;
|
||||
}
|
||||
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var item = items[i];
|
||||
var entry = item.webkitGetAsEntry();
|
||||
|
||||
if (entry.isFile) {
|
||||
// TODO: parse the file and insert content as entities
|
||||
}
|
||||
}
|
||||
|
||||
return !handled;
|
||||
};
|
||||
|
||||
private tryOpenEditor(): boolean {
|
||||
return this._tableCommands.tryOpenEntityEditor(this._tableEntityListViewModel);
|
||||
}
|
||||
|
||||
private tryHandleDeleteSelected(): boolean {
|
||||
var selectedEntities: Entities.ITableEntity[] = this._tableEntityListViewModel.selected();
|
||||
var handled: boolean = false;
|
||||
|
||||
if (selectedEntities && selectedEntities.length) {
|
||||
this._tableCommands.deleteEntitiesCommand(this._tableEntityListViewModel);
|
||||
handled = true;
|
||||
}
|
||||
|
||||
return handled;
|
||||
}
|
||||
|
||||
private getEntityIdentity($elem: JQuery): Entities.ITableEntityIdentity {
|
||||
return {
|
||||
RowKey: $elem.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr)
|
||||
};
|
||||
}
|
||||
|
||||
private updateLastSelectedItem($elem: JQuery, isShiftSelect: boolean) {
|
||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
||||
var entity = this._tableEntityListViewModel.getItemFromCurrentPage(
|
||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
|
||||
);
|
||||
|
||||
this._tableEntityListViewModel.lastSelectedItem = entity;
|
||||
|
||||
if (!isShiftSelect) {
|
||||
this._tableEntityListViewModel.lastSelectedAnchorItem = entity;
|
||||
}
|
||||
}
|
||||
|
||||
private applySingleSelection($elem: JQuery) {
|
||||
if ($elem) {
|
||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
||||
|
||||
this._tableEntityListViewModel.clearSelection();
|
||||
this.addToSelection(entityIdentity.RowKey);
|
||||
}
|
||||
}
|
||||
|
||||
private applySelectAll() {
|
||||
this._tableEntityListViewModel.clearSelection();
|
||||
ko.utils.arrayPushAll<Entities.ITableEntity>(
|
||||
this._tableEntityListViewModel.selected,
|
||||
this._tableEntityListViewModel.getAllItemsInCurrentPage()
|
||||
);
|
||||
}
|
||||
|
||||
private applyCtrlSelection($elem: JQuery): void {
|
||||
var koSelected: ko.ObservableArray<Entities.ITableEntity> = this._tableEntityListViewModel
|
||||
? this._tableEntityListViewModel.selected
|
||||
: null;
|
||||
|
||||
if (koSelected) {
|
||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
||||
|
||||
if (
|
||||
!this._tableEntityListViewModel.isItemSelected(
|
||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
|
||||
)
|
||||
) {
|
||||
// Adding item not previously in selection
|
||||
this.addToSelection(entityIdentity.RowKey);
|
||||
} else {
|
||||
koSelected.remove((item: Entities.ITableEntity) => item.RowKey._ === entityIdentity.RowKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private applyShiftSelection($elem: JQuery): void {
|
||||
var anchorItem = this._tableEntityListViewModel.lastSelectedAnchorItem;
|
||||
|
||||
// If anchor item doesn't exist, use the first available item of current page instead
|
||||
if (!anchorItem && this._tableEntityListViewModel.items().length > 0) {
|
||||
anchorItem = this._tableEntityListViewModel.items()[0];
|
||||
}
|
||||
|
||||
if (anchorItem) {
|
||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
||||
var elementIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
|
||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
|
||||
);
|
||||
var anchorIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
|
||||
this._tableEntityListViewModel.getTableEntityKeys(anchorItem.RowKey._)
|
||||
);
|
||||
|
||||
var startIndex = Math.min(elementIndex, anchorIndex);
|
||||
var endIndex = Math.max(elementIndex, anchorIndex);
|
||||
|
||||
this._tableEntityListViewModel.clearSelection();
|
||||
ko.utils.arrayPushAll<Entities.ITableEntity>(
|
||||
this._tableEntityListViewModel.selected,
|
||||
this._tableEntityListViewModel.getItemsFromAllPagesWithinRange(startIndex, endIndex + 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private applyContextMenuSelection($elem: JQuery) {
|
||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
||||
|
||||
if (
|
||||
!this._tableEntityListViewModel.isItemSelected(
|
||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
|
||||
)
|
||||
) {
|
||||
if (this._tableEntityListViewModel.selected().length) {
|
||||
this._tableEntityListViewModel.clearSelection();
|
||||
}
|
||||
this.addToSelection(entityIdentity.RowKey);
|
||||
}
|
||||
}
|
||||
|
||||
private addToSelection(rowKey: string) {
|
||||
var selectedEntity: Entities.ITableEntity = this._tableEntityListViewModel.getItemFromCurrentPage(
|
||||
this._tableEntityListViewModel.getTableEntityKeys(rowKey)
|
||||
);
|
||||
|
||||
if (selectedEntity != null) {
|
||||
this._tableEntityListViewModel.selected.push(selectedEntity);
|
||||
}
|
||||
}
|
||||
|
||||
// Selecting first row if the selection is empty.
|
||||
public selectFirstIfNeeded(): void {
|
||||
var koSelected: ko.ObservableArray<Entities.ITableEntity> = this._tableEntityListViewModel
|
||||
? this._tableEntityListViewModel.selected
|
||||
: null;
|
||||
var koEntities: ko.ObservableArray<Entities.ITableEntity> = this._tableEntityListViewModel
|
||||
? this._tableEntityListViewModel.items
|
||||
: null;
|
||||
|
||||
if (!koSelected().length && koEntities().length) {
|
||||
var firstEntity: Entities.ITableEntity = koEntities()[0];
|
||||
|
||||
// Clear last selection: lastSelectedItem and lastSelectedAnchorItem
|
||||
this._tableEntityListViewModel.clearLastSelected();
|
||||
|
||||
this.addToSelection(firstEntity.RowKey._);
|
||||
|
||||
// Update last selection
|
||||
this._tableEntityListViewModel.lastSelectedItem = firstEntity;
|
||||
|
||||
// Finally, make sure first row is visible
|
||||
DataTableOperations.scrollToTopIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
public bind() {
|
||||
this.dataTable.on("click", "tr", this.click);
|
||||
this.dataTable.on("dblclick", "tr", this.doubleClick);
|
||||
this.dataTable.on("keydown", "td", this.keyDown);
|
||||
this.dataTable.on("keyup", "td", this.keyUp);
|
||||
|
||||
// Keyboard navigation - selecting first row if the selection is empty when the table gains focus.
|
||||
this.dataTable.on("focus", () => {
|
||||
this.selectFirstIfNeeded();
|
||||
return true;
|
||||
});
|
||||
|
||||
// Bind drag & drop behavior
|
||||
$("body").on("drop", this.itemDropped);
|
||||
}
|
||||
|
||||
public focusTable(): void {
|
||||
this.dataTable.focus();
|
||||
}
|
||||
}
|
||||
import ko from "knockout";
|
||||
|
||||
import * as DataTableOperations from "./DataTableOperations";
|
||||
import * as Constants from "../Constants";
|
||||
import TableCommands from "./TableCommands";
|
||||
import TableEntityListViewModel from "./TableEntityListViewModel";
|
||||
import * as Utilities from "../Utilities";
|
||||
import * as Entities from "../Entities";
|
||||
|
||||
/*
|
||||
* Base class for data table row selection.
|
||||
*/
|
||||
export default class DataTableOperationManager {
|
||||
private _tableEntityListViewModel: TableEntityListViewModel;
|
||||
private _tableCommands: TableCommands;
|
||||
private dataTable: JQuery;
|
||||
|
||||
constructor(table: JQuery, viewModel: TableEntityListViewModel, tableCommands: TableCommands) {
|
||||
this.dataTable = table;
|
||||
this._tableEntityListViewModel = viewModel;
|
||||
this._tableCommands = tableCommands;
|
||||
|
||||
this.bind();
|
||||
this._tableEntityListViewModel.bind(this);
|
||||
}
|
||||
|
||||
private click = (event: JQueryEventObject) => {
|
||||
var elem: JQuery = $(event.currentTarget);
|
||||
this.updateLastSelectedItem(elem, event.shiftKey);
|
||||
|
||||
if (Utilities.isEnvironmentCtrlPressed(event)) {
|
||||
this.applyCtrlSelection(elem);
|
||||
} else if (event.shiftKey) {
|
||||
this.applyShiftSelection(elem);
|
||||
} else {
|
||||
this.applySingleSelection(elem);
|
||||
}
|
||||
};
|
||||
|
||||
private doubleClick = (event: JQueryEventObject) => {
|
||||
this.tryOpenEditor();
|
||||
};
|
||||
|
||||
private keyDown = (event: JQueryEventObject): boolean => {
|
||||
var isUpArrowKey: boolean = event.keyCode === Constants.keyCodes.UpArrow,
|
||||
isDownArrowKey: boolean = event.keyCode === Constants.keyCodes.DownArrow,
|
||||
handled: boolean = false;
|
||||
|
||||
if (isUpArrowKey || isDownArrowKey) {
|
||||
var lastSelectedItem: Entities.ITableEntity = this._tableEntityListViewModel.lastSelectedItem;
|
||||
var dataTableRows: JQuery = $(Constants.htmlSelectors.dataTableAllRowsSelector);
|
||||
var maximumIndex = dataTableRows.length - 1;
|
||||
|
||||
// If can't find an index for lastSelectedItem, then either no item is previously selected or it goes across page.
|
||||
// Simply select the first item in this case.
|
||||
var lastSelectedItemIndex = lastSelectedItem
|
||||
? this._tableEntityListViewModel.getItemIndexFromCurrentPage(
|
||||
this._tableEntityListViewModel.getTableEntityKeys(lastSelectedItem.RowKey._)
|
||||
)
|
||||
: -1;
|
||||
var nextIndex: number = isUpArrowKey ? lastSelectedItemIndex - 1 : lastSelectedItemIndex + 1;
|
||||
var safeIndex: number = Utilities.ensureBetweenBounds(nextIndex, 0, maximumIndex);
|
||||
var selectedRowElement: JQuery = dataTableRows.eq(safeIndex);
|
||||
|
||||
if (selectedRowElement) {
|
||||
if (event.shiftKey) {
|
||||
this.applyShiftSelection(selectedRowElement);
|
||||
} else {
|
||||
this.applySingleSelection(selectedRowElement);
|
||||
}
|
||||
|
||||
this.updateLastSelectedItem(selectedRowElement, event.shiftKey);
|
||||
handled = true;
|
||||
DataTableOperations.scrollToRowIfNeeded(dataTableRows, safeIndex, isUpArrowKey);
|
||||
}
|
||||
} else if (
|
||||
Utilities.isEnvironmentCtrlPressed(event) &&
|
||||
!Utilities.isEnvironmentShiftPressed(event) &&
|
||||
!Utilities.isEnvironmentAltPressed(event) &&
|
||||
event.keyCode === Constants.keyCodes.A
|
||||
) {
|
||||
this.applySelectAll();
|
||||
handled = true;
|
||||
}
|
||||
|
||||
return !handled;
|
||||
};
|
||||
|
||||
// Note: There is one key up event each time a key is pressed;
|
||||
// in contrast, there may be more than one key down and key
|
||||
// pressed events.
|
||||
private keyUp = (event: JQueryEventObject): boolean => {
|
||||
var handled: boolean = false;
|
||||
|
||||
switch (event.keyCode) {
|
||||
case Constants.keyCodes.Enter:
|
||||
handled = this.tryOpenEditor();
|
||||
break;
|
||||
case Constants.keyCodes.Delete:
|
||||
handled = this.tryHandleDeleteSelected();
|
||||
break;
|
||||
}
|
||||
|
||||
return !handled;
|
||||
};
|
||||
|
||||
private itemDropped = (event: JQueryEventObject): boolean => {
|
||||
var handled: boolean = false;
|
||||
var items = (<any>event.originalEvent).dataTransfer.items;
|
||||
|
||||
if (!items) {
|
||||
// On browsers outside of Chromium
|
||||
// we can't discern between dirs and files
|
||||
// so we will disable drag & drop for now
|
||||
return null;
|
||||
}
|
||||
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var item = items[i];
|
||||
var entry = item.webkitGetAsEntry();
|
||||
|
||||
if (entry.isFile) {
|
||||
// TODO: parse the file and insert content as entities
|
||||
}
|
||||
}
|
||||
|
||||
return !handled;
|
||||
};
|
||||
|
||||
private tryOpenEditor(): boolean {
|
||||
return this._tableCommands.tryOpenEntityEditor(this._tableEntityListViewModel);
|
||||
}
|
||||
|
||||
private tryHandleDeleteSelected(): boolean {
|
||||
var selectedEntities: Entities.ITableEntity[] = this._tableEntityListViewModel.selected();
|
||||
var handled: boolean = false;
|
||||
|
||||
if (selectedEntities && selectedEntities.length) {
|
||||
this._tableCommands.deleteEntitiesCommand(this._tableEntityListViewModel);
|
||||
handled = true;
|
||||
}
|
||||
|
||||
return handled;
|
||||
}
|
||||
|
||||
private getEntityIdentity($elem: JQuery): Entities.ITableEntityIdentity {
|
||||
return {
|
||||
RowKey: $elem.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr),
|
||||
};
|
||||
}
|
||||
|
||||
private updateLastSelectedItem($elem: JQuery, isShiftSelect: boolean) {
|
||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
||||
var entity = this._tableEntityListViewModel.getItemFromCurrentPage(
|
||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
|
||||
);
|
||||
|
||||
this._tableEntityListViewModel.lastSelectedItem = entity;
|
||||
|
||||
if (!isShiftSelect) {
|
||||
this._tableEntityListViewModel.lastSelectedAnchorItem = entity;
|
||||
}
|
||||
}
|
||||
|
||||
private applySingleSelection($elem: JQuery) {
|
||||
if ($elem) {
|
||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
||||
|
||||
this._tableEntityListViewModel.clearSelection();
|
||||
this.addToSelection(entityIdentity.RowKey);
|
||||
}
|
||||
}
|
||||
|
||||
private applySelectAll() {
|
||||
this._tableEntityListViewModel.clearSelection();
|
||||
ko.utils.arrayPushAll<Entities.ITableEntity>(
|
||||
this._tableEntityListViewModel.selected,
|
||||
this._tableEntityListViewModel.getAllItemsInCurrentPage()
|
||||
);
|
||||
}
|
||||
|
||||
private applyCtrlSelection($elem: JQuery): void {
|
||||
var koSelected: ko.ObservableArray<Entities.ITableEntity> = this._tableEntityListViewModel
|
||||
? this._tableEntityListViewModel.selected
|
||||
: null;
|
||||
|
||||
if (koSelected) {
|
||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
||||
|
||||
if (
|
||||
!this._tableEntityListViewModel.isItemSelected(
|
||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
|
||||
)
|
||||
) {
|
||||
// Adding item not previously in selection
|
||||
this.addToSelection(entityIdentity.RowKey);
|
||||
} else {
|
||||
koSelected.remove((item: Entities.ITableEntity) => item.RowKey._ === entityIdentity.RowKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private applyShiftSelection($elem: JQuery): void {
|
||||
var anchorItem = this._tableEntityListViewModel.lastSelectedAnchorItem;
|
||||
|
||||
// If anchor item doesn't exist, use the first available item of current page instead
|
||||
if (!anchorItem && this._tableEntityListViewModel.items().length > 0) {
|
||||
anchorItem = this._tableEntityListViewModel.items()[0];
|
||||
}
|
||||
|
||||
if (anchorItem) {
|
||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
||||
var elementIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
|
||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
|
||||
);
|
||||
var anchorIndex = this._tableEntityListViewModel.getItemIndexFromAllPages(
|
||||
this._tableEntityListViewModel.getTableEntityKeys(anchorItem.RowKey._)
|
||||
);
|
||||
|
||||
var startIndex = Math.min(elementIndex, anchorIndex);
|
||||
var endIndex = Math.max(elementIndex, anchorIndex);
|
||||
|
||||
this._tableEntityListViewModel.clearSelection();
|
||||
ko.utils.arrayPushAll<Entities.ITableEntity>(
|
||||
this._tableEntityListViewModel.selected,
|
||||
this._tableEntityListViewModel.getItemsFromAllPagesWithinRange(startIndex, endIndex + 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private applyContextMenuSelection($elem: JQuery) {
|
||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
||||
|
||||
if (
|
||||
!this._tableEntityListViewModel.isItemSelected(
|
||||
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey)
|
||||
)
|
||||
) {
|
||||
if (this._tableEntityListViewModel.selected().length) {
|
||||
this._tableEntityListViewModel.clearSelection();
|
||||
}
|
||||
this.addToSelection(entityIdentity.RowKey);
|
||||
}
|
||||
}
|
||||
|
||||
private addToSelection(rowKey: string) {
|
||||
var selectedEntity: Entities.ITableEntity = this._tableEntityListViewModel.getItemFromCurrentPage(
|
||||
this._tableEntityListViewModel.getTableEntityKeys(rowKey)
|
||||
);
|
||||
|
||||
if (selectedEntity != null) {
|
||||
this._tableEntityListViewModel.selected.push(selectedEntity);
|
||||
}
|
||||
}
|
||||
|
||||
// Selecting first row if the selection is empty.
|
||||
public selectFirstIfNeeded(): void {
|
||||
var koSelected: ko.ObservableArray<Entities.ITableEntity> = this._tableEntityListViewModel
|
||||
? this._tableEntityListViewModel.selected
|
||||
: null;
|
||||
var koEntities: ko.ObservableArray<Entities.ITableEntity> = this._tableEntityListViewModel
|
||||
? this._tableEntityListViewModel.items
|
||||
: null;
|
||||
|
||||
if (!koSelected().length && koEntities().length) {
|
||||
var firstEntity: Entities.ITableEntity = koEntities()[0];
|
||||
|
||||
// Clear last selection: lastSelectedItem and lastSelectedAnchorItem
|
||||
this._tableEntityListViewModel.clearLastSelected();
|
||||
|
||||
this.addToSelection(firstEntity.RowKey._);
|
||||
|
||||
// Update last selection
|
||||
this._tableEntityListViewModel.lastSelectedItem = firstEntity;
|
||||
|
||||
// Finally, make sure first row is visible
|
||||
DataTableOperations.scrollToTopIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
public bind() {
|
||||
this.dataTable.on("click", "tr", this.click);
|
||||
this.dataTable.on("dblclick", "tr", this.doubleClick);
|
||||
this.dataTable.on("keydown", "td", this.keyDown);
|
||||
this.dataTable.on("keyup", "td", this.keyUp);
|
||||
|
||||
// Keyboard navigation - selecting first row if the selection is empty when the table gains focus.
|
||||
this.dataTable.on("focus", () => {
|
||||
this.selectFirstIfNeeded();
|
||||
return true;
|
||||
});
|
||||
|
||||
// Bind drag & drop behavior
|
||||
$("body").on("drop", this.itemDropped);
|
||||
}
|
||||
|
||||
public focusTable(): void {
|
||||
this.dataTable.focus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,192 +1,192 @@
|
||||
import _ from "underscore";
|
||||
import Q from "q";
|
||||
|
||||
import * as Entities from "../Entities";
|
||||
import * as QueryBuilderConstants from "../Constants";
|
||||
import * as Utilities from "../Utilities";
|
||||
|
||||
export function getRowSelector(selectorSchema: Entities.IProperty[]): string {
|
||||
var selector: string = "";
|
||||
selectorSchema &&
|
||||
selectorSchema.forEach((p: Entities.IProperty) => {
|
||||
selector += "[" + p.key + '="' + Utilities.jQuerySelectorEscape(p.value) + '"]';
|
||||
});
|
||||
return QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector + selector;
|
||||
}
|
||||
|
||||
export function isRowVisible(dataTableScrollBodyQuery: JQuery, element: HTMLElement): boolean {
|
||||
var isVisible = false;
|
||||
|
||||
if (dataTableScrollBodyQuery.length && element) {
|
||||
var elementRect: ClientRect = element.getBoundingClientRect(),
|
||||
dataTableScrollBodyRect: ClientRect = dataTableScrollBodyQuery.get(0).getBoundingClientRect();
|
||||
|
||||
isVisible = elementRect.bottom <= dataTableScrollBodyRect.bottom && dataTableScrollBodyRect.top <= elementRect.top;
|
||||
}
|
||||
|
||||
return isVisible;
|
||||
}
|
||||
|
||||
export function scrollToRowIfNeeded(dataTableRows: JQuery, currentIndex: number, isScrollUp: boolean): void {
|
||||
if (dataTableRows.length) {
|
||||
var dataTableScrollBodyQuery: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector),
|
||||
selectedRowElement: HTMLElement = dataTableRows.get(currentIndex);
|
||||
|
||||
if (dataTableScrollBodyQuery.length && selectedRowElement) {
|
||||
var isVisible: boolean = isRowVisible(dataTableScrollBodyQuery, selectedRowElement);
|
||||
|
||||
if (!isVisible) {
|
||||
var selectedRowQuery: JQuery = $(selectedRowElement),
|
||||
scrollPosition: number = dataTableScrollBodyQuery.scrollTop(),
|
||||
selectedElementPosition: number = selectedRowQuery.position().top,
|
||||
newScrollPosition: number = 0;
|
||||
|
||||
if (isScrollUp) {
|
||||
newScrollPosition = scrollPosition + selectedElementPosition;
|
||||
} else {
|
||||
newScrollPosition =
|
||||
scrollPosition + (selectedElementPosition + selectedRowQuery.height() - dataTableScrollBodyQuery.height());
|
||||
}
|
||||
|
||||
dataTableScrollBodyQuery.scrollTop(newScrollPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function scrollToTopIfNeeded(): void {
|
||||
var $dataTableRows: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector),
|
||||
$dataTableScrollBody: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector);
|
||||
|
||||
if ($dataTableRows.length && $dataTableScrollBody.length) {
|
||||
$dataTableScrollBody.scrollTop(0);
|
||||
}
|
||||
}
|
||||
|
||||
export function setPaginationButtonEventHandlers(): void {
|
||||
$(QueryBuilderConstants.htmlSelectors.dataTablePaginationButtonSelector)
|
||||
.on("mousedown", (event: JQueryEventObject) => {
|
||||
// Prevents the table contents from briefly jumping when clicking on "Load more"
|
||||
event.preventDefault();
|
||||
})
|
||||
.attr("role", "button");
|
||||
}
|
||||
|
||||
export function filterColumns(table: DataTables.DataTable, settings: boolean[]): void {
|
||||
settings &&
|
||||
settings.forEach((value: boolean, index: number) => {
|
||||
table.column(index).visible(value, false);
|
||||
});
|
||||
table.columns.adjust().draw(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder columns based on current order.
|
||||
* If no current order is specified, reorder the columns based on intial order.
|
||||
*/
|
||||
export function reorderColumns(
|
||||
table: DataTables.DataTable,
|
||||
targetOrder: number[],
|
||||
currentOrder?: number[]
|
||||
): Q.Promise<any> {
|
||||
var columnsCount: number = targetOrder.length;
|
||||
var isCurrentOrderPassedIn: boolean = !!currentOrder;
|
||||
if (!isCurrentOrderPassedIn) {
|
||||
currentOrder = getInitialOrder(columnsCount);
|
||||
}
|
||||
var isSameOrder: boolean = Utilities.isEqual(currentOrder, targetOrder);
|
||||
|
||||
// if the targetOrder is the same as current order, do nothing.
|
||||
if (!isSameOrder) {
|
||||
// Otherwise, calculate the transformation order.
|
||||
// If current order not specified, then it'll be set to initial order,
|
||||
// i.e., either no reorder happened before or reordering to its initial order,
|
||||
// Then the transformation order will be the same as target order.
|
||||
// If current order is specified, then a transformation order is calculated.
|
||||
// Refer to calculateTransformationOrder for details about transformation order.
|
||||
var transformationOrder: number[] = isCurrentOrderPassedIn
|
||||
? calculateTransformationOrder(currentOrder, targetOrder)
|
||||
: targetOrder;
|
||||
try {
|
||||
$.fn.dataTable.ColReorder(table).fnOrder(transformationOrder);
|
||||
} catch (err) {
|
||||
return Q.reject(err);
|
||||
}
|
||||
}
|
||||
return Q.resolve(null);
|
||||
}
|
||||
|
||||
export function resetColumns(table: DataTables.DataTable): void {
|
||||
$.fn.dataTable.ColReorder(table).fnReset();
|
||||
}
|
||||
|
||||
/**
|
||||
* A table's initial order is described in the form of a natural ascending order.
|
||||
* E.g., for a table with 9 columns, the initial order will be: [0, 1, 2, 3, 4, 5, 6, 7, 8]
|
||||
*/
|
||||
export function getInitialOrder(columnsCount: number): number[] {
|
||||
return _.range(columnsCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current table's column order which is described based on initial table. E.g.,
|
||||
* Initial order: I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8}
|
||||
* Current order: C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8}
|
||||
*/
|
||||
export function getCurrentOrder(table: DataTables.DataTable): number[] {
|
||||
return $.fn.dataTable.ColReorder(table).fnOrder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the index and value for each element of an array. e.g.,
|
||||
* InputArray: [0, 1, 2, 6, 7, 3, 4, 5, 8]
|
||||
* Result: [0, 1, 2, 5, 6, 7, 3, 4, 8]
|
||||
*/
|
||||
export function invertIndexValues(inputArray: number[]): number[] {
|
||||
var invertedArray: number[] = [];
|
||||
if (inputArray) {
|
||||
inputArray.forEach((value: number, index: number) => {
|
||||
invertedArray[inputArray[index]] = index;
|
||||
});
|
||||
}
|
||||
|
||||
return invertedArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* DataTable fnOrder API is based on the current table. So we need to map the order targeting original table to targeting current table.
|
||||
* An detailed example for this. Assume the table has 9 columns.
|
||||
* Initial order (order of the initial table): I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8}
|
||||
* Current order (order of the current table): C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8}
|
||||
* Target order (order of the targeting table): T = [0, 1, 2, 5, 6, 7, 8, 3, 4] <----> {prop0, prop1, prop2, prop5, prop6, prop7, prop8, prop3, prop4}
|
||||
* Transformation order: an order passed to fnOrder API that transforms table from current order to target order.
|
||||
* When the table is constructed, it has the intial order. After an reordering with current order array, now the table is shown in current order, e.g.,
|
||||
* column 3 in the current table is actually column C[3]=6 in the intial table, both indicate the column with header prop6.
|
||||
* Now we want to continue to do another reorder to make the target table in the target order. Directly invoking API with the new order won't work as
|
||||
* the API only do reorder based on the current table like the first time we invoke the API. So an order based on the current table needs to be calulated.
|
||||
* Here is an example of how to calculate the transformation order:
|
||||
* In target table, column 3 should be column T[3]=5 in the intial table with header prop5, while in current table, column with header prop5 is column 7 as C[7]=5.
|
||||
* As a result, in transformation order, column 3 in the target table should be column 7 in the current table, Trans[3] = 7. In the same manner, we can get the
|
||||
* transformation order: Trans = [0, 1, 2, 7, 3, 4, 8, 5, 6]
|
||||
*/
|
||||
export function calculateTransformationOrder(currentOrder: number[], targetOrder: number[]): number[] {
|
||||
var transformationOrder: number[] = [];
|
||||
if (currentOrder && targetOrder && currentOrder.length === targetOrder.length) {
|
||||
var invertedCurrentOrder: number[] = invertIndexValues(currentOrder);
|
||||
transformationOrder = targetOrder.map((value: number) => invertedCurrentOrder[value]);
|
||||
}
|
||||
return transformationOrder;
|
||||
}
|
||||
|
||||
export function getDataTableHeaders(table: DataTables.DataTable): string[] {
|
||||
var columns: DataTables.ColumnsMethods = table.columns();
|
||||
var headers: string[] = [];
|
||||
if (columns) {
|
||||
// table.columns() return ColumnsMethods which is an array of arrays
|
||||
var columnIndexes: number[] = (<any>columns)[0];
|
||||
if (columnIndexes) {
|
||||
headers = columnIndexes.map((value: number) => $(table.columns(value).header()).html());
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,148 +1,148 @@
|
||||
import * as _ from "underscore";
|
||||
import * as Constants from "../Constants";
|
||||
import * as Entities from "../Entities";
|
||||
import * as TableEntityProcessor from "../TableEntityProcessor";
|
||||
|
||||
export enum IconState {
|
||||
default,
|
||||
hoverState,
|
||||
toggleOn
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an html input element shown in context menu.
|
||||
* name: the input name
|
||||
* type: the input type, e.g., "text", "checkbox", "radio", etc.
|
||||
* selected: optional. Used when the input type is checkbox. True means checkbox is selected. Otherwise, unselected.
|
||||
*/
|
||||
export interface IContextMenuInputItem {
|
||||
name: string;
|
||||
type: string;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
export interface IContextMenuOption {
|
||||
[key: string]: IContextMenuInputItem;
|
||||
}
|
||||
|
||||
export function containMultipleItems<T>(items: T[]): boolean {
|
||||
return items && items.length > 1;
|
||||
}
|
||||
|
||||
export function containSingleItem<T>(items: T[]): boolean {
|
||||
return items && items.length === 1;
|
||||
}
|
||||
|
||||
export function containItems<T>(items: T[]): boolean {
|
||||
return items && items.length > 0;
|
||||
}
|
||||
|
||||
// export function setTargetIcon(idToIconHandlerMap: CloudHub.Common.IToolbarElementIdIconMap, $sourceElement: JQuery, toIconState: IconState): void {
|
||||
// if (idToIconHandlerMap) {
|
||||
// var iconId: string = $sourceElement.attr("id");
|
||||
// var iconHandler = idToIconHandlerMap[iconId];
|
||||
// switch (toIconState) {
|
||||
// case IconState.default:
|
||||
// iconHandler.observable(iconHandler.default);
|
||||
// break;
|
||||
// case IconState.hoverState:
|
||||
// iconHandler.observable(iconHandler.hoverState);
|
||||
// break;
|
||||
// default:
|
||||
// window.console.log("error");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
export function addCssClass($sourceElement: JQuery, cssClassName: string): void {
|
||||
if (!$sourceElement.hasClass(cssClassName)) {
|
||||
$sourceElement.addClass(cssClassName);
|
||||
}
|
||||
}
|
||||
|
||||
export function removeCssClass($sourceElement: JQuery, cssClassName: string): void {
|
||||
if ($sourceElement.hasClass(cssClassName)) {
|
||||
$sourceElement.removeClass(cssClassName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the property union of input entities.
|
||||
* Example:
|
||||
* Input:
|
||||
* Entities: [{ PrimaryKey, id, Prop1, Prop2 }, { PrimaryKey, id, Prop2, Prop3, Prop4 }]
|
||||
* Return:
|
||||
* Union: [PrimaryKey, id, Prop1, Prop2, Prop3, Prop4]
|
||||
*/
|
||||
export function getPropertyIntersectionFromTableEntities(
|
||||
entities: Entities.ITableEntity[],
|
||||
isCassandraApi: boolean
|
||||
): string[] {
|
||||
var headerUnion: string[] = [];
|
||||
entities &&
|
||||
entities.forEach((row: any) => {
|
||||
const keys = Object.keys(row);
|
||||
keys &&
|
||||
keys.forEach((key: string) => {
|
||||
if (
|
||||
key !== ".metadata" &&
|
||||
!_.contains(headerUnion, key) &&
|
||||
key !== TableEntityProcessor.keyProperties.attachments &&
|
||||
key !== TableEntityProcessor.keyProperties.etag &&
|
||||
key !== TableEntityProcessor.keyProperties.resourceId &&
|
||||
key !== TableEntityProcessor.keyProperties.self &&
|
||||
(!isCassandraApi || key !== Constants.EntityKeyNames.RowKey)
|
||||
) {
|
||||
headerUnion.push(key);
|
||||
}
|
||||
});
|
||||
});
|
||||
return headerUnion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the names of two Azure table columns and returns a number indicating which comes before the other.
|
||||
* System-defined properties come before custom properties. Otherwise they are compared using string comparison.
|
||||
*/
|
||||
export function compareTableColumns(a: string, b: string): number {
|
||||
if (a === "PartitionKey") {
|
||||
if (b !== "PartitionKey") {
|
||||
return -1;
|
||||
}
|
||||
} else if (a === "RowKey") {
|
||||
if (b === "PartitionKey") {
|
||||
return 1;
|
||||
} else if (b !== "RowKey") {
|
||||
return -1;
|
||||
}
|
||||
} else if (a === "Timestamp") {
|
||||
if (b === "PartitionKey" || b === "RowKey") {
|
||||
return 1;
|
||||
} else if (b !== "Timestamp") {
|
||||
return -1;
|
||||
}
|
||||
} else if (b === "PartitionKey" || b === "RowKey" || b === "Timestamp") {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
|
||||
export function checkForDefaultHeader(headers: string[]): boolean {
|
||||
return headers[0] === Constants.defaultHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* DataTableBindingManager registers an event handler of body.resize and recalculates the data table size.
|
||||
* This method forces the event to happen.
|
||||
*/
|
||||
export function forceRecalculateTableSize(): void {
|
||||
$("body").trigger("resize");
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns off the spinning progress indicator on the data table.
|
||||
*/
|
||||
export function turnOffProgressIndicator(): void {
|
||||
$("div.dataTables_processing").hide();
|
||||
}
|
||||
import * as _ from "underscore";
|
||||
import * as Constants from "../Constants";
|
||||
import * as Entities from "../Entities";
|
||||
import * as TableEntityProcessor from "../TableEntityProcessor";
|
||||
|
||||
export enum IconState {
|
||||
default,
|
||||
hoverState,
|
||||
toggleOn,
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an html input element shown in context menu.
|
||||
* name: the input name
|
||||
* type: the input type, e.g., "text", "checkbox", "radio", etc.
|
||||
* selected: optional. Used when the input type is checkbox. True means checkbox is selected. Otherwise, unselected.
|
||||
*/
|
||||
export interface IContextMenuInputItem {
|
||||
name: string;
|
||||
type: string;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
export interface IContextMenuOption {
|
||||
[key: string]: IContextMenuInputItem;
|
||||
}
|
||||
|
||||
export function containMultipleItems<T>(items: T[]): boolean {
|
||||
return items && items.length > 1;
|
||||
}
|
||||
|
||||
export function containSingleItem<T>(items: T[]): boolean {
|
||||
return items && items.length === 1;
|
||||
}
|
||||
|
||||
export function containItems<T>(items: T[]): boolean {
|
||||
return items && items.length > 0;
|
||||
}
|
||||
|
||||
// export function setTargetIcon(idToIconHandlerMap: CloudHub.Common.IToolbarElementIdIconMap, $sourceElement: JQuery, toIconState: IconState): void {
|
||||
// if (idToIconHandlerMap) {
|
||||
// var iconId: string = $sourceElement.attr("id");
|
||||
// var iconHandler = idToIconHandlerMap[iconId];
|
||||
// switch (toIconState) {
|
||||
// case IconState.default:
|
||||
// iconHandler.observable(iconHandler.default);
|
||||
// break;
|
||||
// case IconState.hoverState:
|
||||
// iconHandler.observable(iconHandler.hoverState);
|
||||
// break;
|
||||
// default:
|
||||
// window.console.log("error");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
export function addCssClass($sourceElement: JQuery, cssClassName: string): void {
|
||||
if (!$sourceElement.hasClass(cssClassName)) {
|
||||
$sourceElement.addClass(cssClassName);
|
||||
}
|
||||
}
|
||||
|
||||
export function removeCssClass($sourceElement: JQuery, cssClassName: string): void {
|
||||
if ($sourceElement.hasClass(cssClassName)) {
|
||||
$sourceElement.removeClass(cssClassName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the property union of input entities.
|
||||
* Example:
|
||||
* Input:
|
||||
* Entities: [{ PrimaryKey, id, Prop1, Prop2 }, { PrimaryKey, id, Prop2, Prop3, Prop4 }]
|
||||
* Return:
|
||||
* Union: [PrimaryKey, id, Prop1, Prop2, Prop3, Prop4]
|
||||
*/
|
||||
export function getPropertyIntersectionFromTableEntities(
|
||||
entities: Entities.ITableEntity[],
|
||||
isCassandraApi: boolean
|
||||
): string[] {
|
||||
var headerUnion: string[] = [];
|
||||
entities &&
|
||||
entities.forEach((row: any) => {
|
||||
const keys = Object.keys(row);
|
||||
keys &&
|
||||
keys.forEach((key: string) => {
|
||||
if (
|
||||
key !== ".metadata" &&
|
||||
!_.contains(headerUnion, key) &&
|
||||
key !== TableEntityProcessor.keyProperties.attachments &&
|
||||
key !== TableEntityProcessor.keyProperties.etag &&
|
||||
key !== TableEntityProcessor.keyProperties.resourceId &&
|
||||
key !== TableEntityProcessor.keyProperties.self &&
|
||||
(!isCassandraApi || key !== Constants.EntityKeyNames.RowKey)
|
||||
) {
|
||||
headerUnion.push(key);
|
||||
}
|
||||
});
|
||||
});
|
||||
return headerUnion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the names of two Azure table columns and returns a number indicating which comes before the other.
|
||||
* System-defined properties come before custom properties. Otherwise they are compared using string comparison.
|
||||
*/
|
||||
export function compareTableColumns(a: string, b: string): number {
|
||||
if (a === "PartitionKey") {
|
||||
if (b !== "PartitionKey") {
|
||||
return -1;
|
||||
}
|
||||
} else if (a === "RowKey") {
|
||||
if (b === "PartitionKey") {
|
||||
return 1;
|
||||
} else if (b !== "RowKey") {
|
||||
return -1;
|
||||
}
|
||||
} else if (a === "Timestamp") {
|
||||
if (b === "PartitionKey" || b === "RowKey") {
|
||||
return 1;
|
||||
} else if (b !== "Timestamp") {
|
||||
return -1;
|
||||
}
|
||||
} else if (b === "PartitionKey" || b === "RowKey" || b === "Timestamp") {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
|
||||
export function checkForDefaultHeader(headers: string[]): boolean {
|
||||
return headers[0] === Constants.defaultHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* DataTableBindingManager registers an event handler of body.resize and recalculates the data table size.
|
||||
* This method forces the event to happen.
|
||||
*/
|
||||
export function forceRecalculateTableSize(): void {
|
||||
$("body").trigger("resize");
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns off the spinning progress indicator on the data table.
|
||||
*/
|
||||
export function turnOffProgressIndicator(): void {
|
||||
$("div.dataTables_processing").hide();
|
||||
}
|
||||
|
||||
@@ -1,270 +1,270 @@
|
||||
import * as ko from "knockout";
|
||||
import * as _ from "underscore";
|
||||
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import CacheBase from "./CacheBase";
|
||||
import * as CommonConstants from "../../../Common/Constants";
|
||||
import * as Constants from "../Constants";
|
||||
import * as Entities from "../Entities";
|
||||
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
|
||||
|
||||
// This is the format of the data we will have to pass to Datatable render callback,
|
||||
// and property names are defined by Datatable as well.
|
||||
export interface IDataTableRenderData {
|
||||
draw: number;
|
||||
aaData: any;
|
||||
recordsTotal: number;
|
||||
recordsFiltered: number;
|
||||
}
|
||||
|
||||
abstract class DataTableViewModel {
|
||||
private static lastPageLabel = ">>"; // Localize
|
||||
private static loadMoreLabel = "Load more"; // Localize
|
||||
|
||||
/* Observables */
|
||||
public items = ko.observableArray<Entities.ITableEntity>();
|
||||
public selected = ko.observableArray<Entities.ITableEntity>();
|
||||
|
||||
public table: DataTables.DataTable;
|
||||
|
||||
// The anchor item is for shift selection. i.e., select all items between anchor item and a give item.
|
||||
public lastSelectedAnchorItem: Entities.ITableEntity;
|
||||
public lastSelectedItem: Entities.ITableEntity;
|
||||
|
||||
public cache: CacheBase<Entities.ITableEntity>;
|
||||
|
||||
protected continuationToken: any;
|
||||
protected allDownloaded: boolean;
|
||||
protected lastPrefetchTime: number;
|
||||
protected downloadSize = 300;
|
||||
protected _documentIterator: QueryIterator<ItemDefinition & Resource>;
|
||||
|
||||
// Used by table redraw throttling
|
||||
protected pollingInterval = 1000;
|
||||
private redrawInterval = 500;
|
||||
private pendingRedraw = false;
|
||||
private lastRedrawTime = new Date().getTime();
|
||||
|
||||
private dataTableOperationManager: IDataTableOperation;
|
||||
|
||||
public queryTablesTab: QueryTablesTab;
|
||||
|
||||
constructor() {
|
||||
this.items([]);
|
||||
this.selected([]);
|
||||
// Late bound
|
||||
this.dataTableOperationManager = null;
|
||||
}
|
||||
|
||||
public bind(dataTableOperationManager: IDataTableOperation): void {
|
||||
this.dataTableOperationManager = dataTableOperationManager;
|
||||
}
|
||||
|
||||
public clearLastSelected(): void {
|
||||
this.lastSelectedItem = null;
|
||||
this.lastSelectedAnchorItem = null;
|
||||
}
|
||||
|
||||
public clearCache(): void {
|
||||
this.cache.clear();
|
||||
this._documentIterator = null;
|
||||
this.continuationToken = null;
|
||||
this.allDownloaded = false;
|
||||
}
|
||||
|
||||
public clearSelection(): void {
|
||||
this.selected.removeAll();
|
||||
}
|
||||
|
||||
// Redraws the table, but guarantees that multiple sequential calls will not incur
|
||||
// another redraw until a certain time interval has passed.
|
||||
public redrawTableThrottled() {
|
||||
if (!this.pendingRedraw) {
|
||||
this.pendingRedraw = true;
|
||||
|
||||
var current = new Date().getTime();
|
||||
var timeSinceLastRedraw = current - this.lastRedrawTime;
|
||||
var redraw = () => {
|
||||
this.table.draw(false /*reset*/);
|
||||
this.lastRedrawTime = new Date().getTime();
|
||||
this.pendingRedraw = false;
|
||||
};
|
||||
|
||||
if (timeSinceLastRedraw > this.redrawInterval) {
|
||||
redraw();
|
||||
} else {
|
||||
var timeUntilNextRedraw = this.redrawInterval - timeSinceLastRedraw;
|
||||
setTimeout(() => redraw(), timeUntilNextRedraw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public focusDataTable(): void {
|
||||
this.dataTableOperationManager.focusTable();
|
||||
}
|
||||
|
||||
public getItemFromSelectedItems(itemKeys: Entities.IProperty[]): Entities.ITableEntity {
|
||||
return _.find(this.selected(), (item: Entities.ITableEntity) => {
|
||||
return this.matchesKeys(item, itemKeys);
|
||||
});
|
||||
}
|
||||
|
||||
public getItemFromCurrentPage(itemKeys: Entities.IProperty[]): Entities.ITableEntity {
|
||||
return _.find(this.items(), (item: Entities.ITableEntity) => {
|
||||
return this.matchesKeys(item, itemKeys);
|
||||
});
|
||||
}
|
||||
|
||||
public getItemIndexFromCurrentPage(itemKeys: Entities.IProperty[]): number {
|
||||
return _.findIndex(this.items(), (item: Entities.ITableEntity) => {
|
||||
return this.matchesKeys(item, itemKeys);
|
||||
});
|
||||
}
|
||||
|
||||
public getItemIndexFromAllPages(itemKeys: Entities.IProperty[]): number {
|
||||
return _.findIndex(this.cache.data, (item: Entities.ITableEntity) => {
|
||||
return this.matchesKeys(item, itemKeys);
|
||||
});
|
||||
}
|
||||
|
||||
public getItemsFromAllPagesWithinRange(start: number, end: number): Entities.ITableEntity[] {
|
||||
return this.cache.data.slice(start, end);
|
||||
}
|
||||
|
||||
public isItemSelected(itemKeys: Entities.IProperty[]): boolean {
|
||||
return _.some(this.selected(), (item: Entities.ITableEntity) => {
|
||||
return this.matchesKeys(item, itemKeys);
|
||||
});
|
||||
}
|
||||
|
||||
public isItemCached(itemKeys: Entities.IProperty[]): boolean {
|
||||
return _.some(this.cache.data, (item: Entities.ITableEntity) => {
|
||||
return this.matchesKeys(item, itemKeys);
|
||||
});
|
||||
}
|
||||
|
||||
public getAllItemsInCurrentPage(): Entities.ITableEntity[] {
|
||||
return this.items();
|
||||
}
|
||||
|
||||
public getAllItemsInCache(): Entities.ITableEntity[] {
|
||||
return this.cache.data;
|
||||
}
|
||||
|
||||
protected abstract dataComparer(
|
||||
item1: Entities.ITableEntity,
|
||||
item2: Entities.ITableEntity,
|
||||
sortOrder: any,
|
||||
oSettings: any
|
||||
): number;
|
||||
protected abstract isCacheValid(validator: any): boolean;
|
||||
|
||||
protected sortColumns(sortOrder: any, oSettings: any) {
|
||||
var self = this;
|
||||
this.clearSelection();
|
||||
this.cache.data.sort(function(a: any, b: any) {
|
||||
return self.dataComparer(a, b, sortOrder, oSettings);
|
||||
});
|
||||
this.cache.sortOrder = sortOrder;
|
||||
}
|
||||
|
||||
protected renderPage(
|
||||
renderCallBack: any,
|
||||
draw: number,
|
||||
startIndex: number,
|
||||
pageSize: number,
|
||||
oSettings: any,
|
||||
postRenderTasks: (startIndex: number, pageSize: number) => Promise<void> = null
|
||||
) {
|
||||
this.updatePaginationControls(oSettings);
|
||||
|
||||
// pageSize < 0 means to show all data
|
||||
var endIndex = pageSize < 0 ? this.cache.length : startIndex + pageSize;
|
||||
var renderData = this.cache.data.slice(startIndex, endIndex);
|
||||
|
||||
this.items(renderData);
|
||||
|
||||
var render: IDataTableRenderData = {
|
||||
draw: draw,
|
||||
aaData: renderData,
|
||||
recordsTotal: this.cache.length,
|
||||
recordsFiltered: this.cache.length
|
||||
};
|
||||
|
||||
if (!!postRenderTasks) {
|
||||
postRenderTasks(startIndex, pageSize).then(() => {
|
||||
this.table.rows().invalidate();
|
||||
});
|
||||
}
|
||||
renderCallBack(render);
|
||||
if (this.queryTablesTab.onLoadStartKey != null && this.queryTablesTab.onLoadStartKey != undefined) {
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.Tab,
|
||||
{
|
||||
databaseAccountName: this.queryTablesTab.collection.container.databaseAccount().name,
|
||||
databaseName: this.queryTablesTab.collection.databaseId,
|
||||
collectionName: this.queryTablesTab.collection.id(),
|
||||
defaultExperience: this.queryTablesTab.collection.container.defaultExperience(),
|
||||
dataExplorerArea: CommonConstants.Areas.Tab,
|
||||
tabTitle: this.queryTablesTab.tabTitle()
|
||||
},
|
||||
this.queryTablesTab.onLoadStartKey
|
||||
);
|
||||
this.queryTablesTab.onLoadStartKey = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected matchesKeys(item: Entities.ITableEntity, itemKeys: Entities.IProperty[]): boolean {
|
||||
return itemKeys.every((property: Entities.IProperty) => {
|
||||
var itemValue = item[property.key];
|
||||
|
||||
// if (itemValue && property.subkey) {
|
||||
// itemValue = itemValue._[property.subkey];
|
||||
// if (!itemValue) {
|
||||
// itemValue = "";
|
||||
// }
|
||||
// } else if (property.subkey) {
|
||||
// itemValue = "";
|
||||
// }
|
||||
|
||||
return this.stringCompare(itemValue._, property.value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Default string comparison is case sensitive as most Azure resources' names are case sensitive.
|
||||
* Override this if a name, i.e., Azure File/Directory name, is case insensitive.
|
||||
*/
|
||||
protected stringCompare(s1: string, s2: string): boolean {
|
||||
return s1 === s2;
|
||||
}
|
||||
|
||||
private updatePaginationControls(oSettings: any) {
|
||||
var pageInfo = this.table.page.info();
|
||||
var pageSize = pageInfo.length;
|
||||
var paginateElement = $(oSettings.nTableWrapper).find(Constants.htmlSelectors.paginateSelector);
|
||||
|
||||
if (this.allDownloaded) {
|
||||
if (this.cache.length <= pageSize) {
|
||||
// Hide pagination controls if everything fits in one page!.
|
||||
paginateElement.hide();
|
||||
} else {
|
||||
// Enable pagination controls.
|
||||
paginateElement.show();
|
||||
oSettings.oLanguage.oPaginate.sLast = DataTableViewModel.lastPageLabel;
|
||||
}
|
||||
} else {
|
||||
// Enable pagination controls and show load more button.
|
||||
paginateElement.show();
|
||||
oSettings.oLanguage.oPaginate.sLast = DataTableViewModel.loadMoreLabel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface IDataTableOperation {
|
||||
focusTable(): void;
|
||||
}
|
||||
|
||||
export default DataTableViewModel;
|
||||
import * as ko from "knockout";
|
||||
import * as _ from "underscore";
|
||||
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import CacheBase from "./CacheBase";
|
||||
import * as CommonConstants from "../../../Common/Constants";
|
||||
import * as Constants from "../Constants";
|
||||
import * as Entities from "../Entities";
|
||||
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
|
||||
|
||||
// This is the format of the data we will have to pass to Datatable render callback,
|
||||
// and property names are defined by Datatable as well.
|
||||
export interface IDataTableRenderData {
|
||||
draw: number;
|
||||
aaData: any;
|
||||
recordsTotal: number;
|
||||
recordsFiltered: number;
|
||||
}
|
||||
|
||||
abstract class DataTableViewModel {
|
||||
private static lastPageLabel = ">>"; // Localize
|
||||
private static loadMoreLabel = "Load more"; // Localize
|
||||
|
||||
/* Observables */
|
||||
public items = ko.observableArray<Entities.ITableEntity>();
|
||||
public selected = ko.observableArray<Entities.ITableEntity>();
|
||||
|
||||
public table: DataTables.DataTable;
|
||||
|
||||
// The anchor item is for shift selection. i.e., select all items between anchor item and a give item.
|
||||
public lastSelectedAnchorItem: Entities.ITableEntity;
|
||||
public lastSelectedItem: Entities.ITableEntity;
|
||||
|
||||
public cache: CacheBase<Entities.ITableEntity>;
|
||||
|
||||
protected continuationToken: any;
|
||||
protected allDownloaded: boolean;
|
||||
protected lastPrefetchTime: number;
|
||||
protected downloadSize = 300;
|
||||
protected _documentIterator: QueryIterator<ItemDefinition & Resource>;
|
||||
|
||||
// Used by table redraw throttling
|
||||
protected pollingInterval = 1000;
|
||||
private redrawInterval = 500;
|
||||
private pendingRedraw = false;
|
||||
private lastRedrawTime = new Date().getTime();
|
||||
|
||||
private dataTableOperationManager: IDataTableOperation;
|
||||
|
||||
public queryTablesTab: QueryTablesTab;
|
||||
|
||||
constructor() {
|
||||
this.items([]);
|
||||
this.selected([]);
|
||||
// Late bound
|
||||
this.dataTableOperationManager = null;
|
||||
}
|
||||
|
||||
public bind(dataTableOperationManager: IDataTableOperation): void {
|
||||
this.dataTableOperationManager = dataTableOperationManager;
|
||||
}
|
||||
|
||||
public clearLastSelected(): void {
|
||||
this.lastSelectedItem = null;
|
||||
this.lastSelectedAnchorItem = null;
|
||||
}
|
||||
|
||||
public clearCache(): void {
|
||||
this.cache.clear();
|
||||
this._documentIterator = null;
|
||||
this.continuationToken = null;
|
||||
this.allDownloaded = false;
|
||||
}
|
||||
|
||||
public clearSelection(): void {
|
||||
this.selected.removeAll();
|
||||
}
|
||||
|
||||
// Redraws the table, but guarantees that multiple sequential calls will not incur
|
||||
// another redraw until a certain time interval has passed.
|
||||
public redrawTableThrottled() {
|
||||
if (!this.pendingRedraw) {
|
||||
this.pendingRedraw = true;
|
||||
|
||||
var current = new Date().getTime();
|
||||
var timeSinceLastRedraw = current - this.lastRedrawTime;
|
||||
var redraw = () => {
|
||||
this.table.draw(false /*reset*/);
|
||||
this.lastRedrawTime = new Date().getTime();
|
||||
this.pendingRedraw = false;
|
||||
};
|
||||
|
||||
if (timeSinceLastRedraw > this.redrawInterval) {
|
||||
redraw();
|
||||
} else {
|
||||
var timeUntilNextRedraw = this.redrawInterval - timeSinceLastRedraw;
|
||||
setTimeout(() => redraw(), timeUntilNextRedraw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public focusDataTable(): void {
|
||||
this.dataTableOperationManager.focusTable();
|
||||
}
|
||||
|
||||
public getItemFromSelectedItems(itemKeys: Entities.IProperty[]): Entities.ITableEntity {
|
||||
return _.find(this.selected(), (item: Entities.ITableEntity) => {
|
||||
return this.matchesKeys(item, itemKeys);
|
||||
});
|
||||
}
|
||||
|
||||
public getItemFromCurrentPage(itemKeys: Entities.IProperty[]): Entities.ITableEntity {
|
||||
return _.find(this.items(), (item: Entities.ITableEntity) => {
|
||||
return this.matchesKeys(item, itemKeys);
|
||||
});
|
||||
}
|
||||
|
||||
public getItemIndexFromCurrentPage(itemKeys: Entities.IProperty[]): number {
|
||||
return _.findIndex(this.items(), (item: Entities.ITableEntity) => {
|
||||
return this.matchesKeys(item, itemKeys);
|
||||
});
|
||||
}
|
||||
|
||||
public getItemIndexFromAllPages(itemKeys: Entities.IProperty[]): number {
|
||||
return _.findIndex(this.cache.data, (item: Entities.ITableEntity) => {
|
||||
return this.matchesKeys(item, itemKeys);
|
||||
});
|
||||
}
|
||||
|
||||
public getItemsFromAllPagesWithinRange(start: number, end: number): Entities.ITableEntity[] {
|
||||
return this.cache.data.slice(start, end);
|
||||
}
|
||||
|
||||
public isItemSelected(itemKeys: Entities.IProperty[]): boolean {
|
||||
return _.some(this.selected(), (item: Entities.ITableEntity) => {
|
||||
return this.matchesKeys(item, itemKeys);
|
||||
});
|
||||
}
|
||||
|
||||
public isItemCached(itemKeys: Entities.IProperty[]): boolean {
|
||||
return _.some(this.cache.data, (item: Entities.ITableEntity) => {
|
||||
return this.matchesKeys(item, itemKeys);
|
||||
});
|
||||
}
|
||||
|
||||
public getAllItemsInCurrentPage(): Entities.ITableEntity[] {
|
||||
return this.items();
|
||||
}
|
||||
|
||||
public getAllItemsInCache(): Entities.ITableEntity[] {
|
||||
return this.cache.data;
|
||||
}
|
||||
|
||||
protected abstract dataComparer(
|
||||
item1: Entities.ITableEntity,
|
||||
item2: Entities.ITableEntity,
|
||||
sortOrder: any,
|
||||
oSettings: any
|
||||
): number;
|
||||
protected abstract isCacheValid(validator: any): boolean;
|
||||
|
||||
protected sortColumns(sortOrder: any, oSettings: any) {
|
||||
var self = this;
|
||||
this.clearSelection();
|
||||
this.cache.data.sort(function (a: any, b: any) {
|
||||
return self.dataComparer(a, b, sortOrder, oSettings);
|
||||
});
|
||||
this.cache.sortOrder = sortOrder;
|
||||
}
|
||||
|
||||
protected renderPage(
|
||||
renderCallBack: any,
|
||||
draw: number,
|
||||
startIndex: number,
|
||||
pageSize: number,
|
||||
oSettings: any,
|
||||
postRenderTasks: (startIndex: number, pageSize: number) => Promise<void> = null
|
||||
) {
|
||||
this.updatePaginationControls(oSettings);
|
||||
|
||||
// pageSize < 0 means to show all data
|
||||
var endIndex = pageSize < 0 ? this.cache.length : startIndex + pageSize;
|
||||
var renderData = this.cache.data.slice(startIndex, endIndex);
|
||||
|
||||
this.items(renderData);
|
||||
|
||||
var render: IDataTableRenderData = {
|
||||
draw: draw,
|
||||
aaData: renderData,
|
||||
recordsTotal: this.cache.length,
|
||||
recordsFiltered: this.cache.length,
|
||||
};
|
||||
|
||||
if (!!postRenderTasks) {
|
||||
postRenderTasks(startIndex, pageSize).then(() => {
|
||||
this.table.rows().invalidate();
|
||||
});
|
||||
}
|
||||
renderCallBack(render);
|
||||
if (this.queryTablesTab.onLoadStartKey != null && this.queryTablesTab.onLoadStartKey != undefined) {
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.Tab,
|
||||
{
|
||||
databaseAccountName: this.queryTablesTab.collection.container.databaseAccount().name,
|
||||
databaseName: this.queryTablesTab.collection.databaseId,
|
||||
collectionName: this.queryTablesTab.collection.id(),
|
||||
defaultExperience: this.queryTablesTab.collection.container.defaultExperience(),
|
||||
dataExplorerArea: CommonConstants.Areas.Tab,
|
||||
tabTitle: this.queryTablesTab.tabTitle(),
|
||||
},
|
||||
this.queryTablesTab.onLoadStartKey
|
||||
);
|
||||
this.queryTablesTab.onLoadStartKey = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected matchesKeys(item: Entities.ITableEntity, itemKeys: Entities.IProperty[]): boolean {
|
||||
return itemKeys.every((property: Entities.IProperty) => {
|
||||
var itemValue = item[property.key];
|
||||
|
||||
// if (itemValue && property.subkey) {
|
||||
// itemValue = itemValue._[property.subkey];
|
||||
// if (!itemValue) {
|
||||
// itemValue = "";
|
||||
// }
|
||||
// } else if (property.subkey) {
|
||||
// itemValue = "";
|
||||
// }
|
||||
|
||||
return this.stringCompare(itemValue._, property.value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Default string comparison is case sensitive as most Azure resources' names are case sensitive.
|
||||
* Override this if a name, i.e., Azure File/Directory name, is case insensitive.
|
||||
*/
|
||||
protected stringCompare(s1: string, s2: string): boolean {
|
||||
return s1 === s2;
|
||||
}
|
||||
|
||||
private updatePaginationControls(oSettings: any) {
|
||||
var pageInfo = this.table.page.info();
|
||||
var pageSize = pageInfo.length;
|
||||
var paginateElement = $(oSettings.nTableWrapper).find(Constants.htmlSelectors.paginateSelector);
|
||||
|
||||
if (this.allDownloaded) {
|
||||
if (this.cache.length <= pageSize) {
|
||||
// Hide pagination controls if everything fits in one page!.
|
||||
paginateElement.hide();
|
||||
} else {
|
||||
// Enable pagination controls.
|
||||
paginateElement.show();
|
||||
oSettings.oLanguage.oPaginate.sLast = DataTableViewModel.lastPageLabel;
|
||||
}
|
||||
} else {
|
||||
// Enable pagination controls and show load more button.
|
||||
paginateElement.show();
|
||||
oSettings.oLanguage.oPaginate.sLast = DataTableViewModel.loadMoreLabel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface IDataTableOperation {
|
||||
focusTable(): void;
|
||||
}
|
||||
|
||||
export default DataTableViewModel;
|
||||
|
||||
@@ -1,156 +1,156 @@
|
||||
import _ from "underscore";
|
||||
import Q from "q";
|
||||
import * as DataTableUtilities from "./DataTableUtilities";
|
||||
import * as DataTableOperations from "./DataTableOperations";
|
||||
import TableEntityListViewModel from "./TableEntityListViewModel";
|
||||
import * as Entities from "../Entities";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import * as TableColumnOptionsPane from "../../Panes/Tables/TableColumnOptionsPane";
|
||||
import Explorer from "../../Explorer";
|
||||
|
||||
export default class TableCommands {
|
||||
// Command Ids
|
||||
public static editEntityCommand: string = "edit";
|
||||
public static deleteEntitiesCommand: string = "delete";
|
||||
public static reorderColumnsCommand: string = "reorder";
|
||||
public static resetColumnsCommand: string = "reset";
|
||||
public static customizeColumnsCommand: string = "customizeColumns";
|
||||
|
||||
private _container: Explorer;
|
||||
|
||||
constructor(container: Explorer) {
|
||||
this._container = container;
|
||||
}
|
||||
|
||||
public isEnabled(commandName: string, selectedEntites: Entities.ITableEntity[]): boolean {
|
||||
var singleItemSelected: boolean = DataTableUtilities.containSingleItem(selectedEntites);
|
||||
var atLeastOneItemSelected: boolean = DataTableUtilities.containItems(selectedEntites);
|
||||
switch (commandName) {
|
||||
case TableCommands.editEntityCommand:
|
||||
return singleItemSelected;
|
||||
case TableCommands.deleteEntitiesCommand:
|
||||
case TableCommands.reorderColumnsCommand:
|
||||
return atLeastOneItemSelected;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public tryOpenEntityEditor(viewModel: TableEntityListViewModel): boolean {
|
||||
if (this.isEnabled(TableCommands.editEntityCommand, viewModel.selected())) {
|
||||
this.editEntityCommand(viewModel);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit entity
|
||||
*/
|
||||
public editEntityCommand(viewModel: TableEntityListViewModel): Q.Promise<any> {
|
||||
if (!viewModel) {
|
||||
return null; // Error
|
||||
}
|
||||
|
||||
if (!DataTableUtilities.containSingleItem(viewModel.selected())) {
|
||||
return null; // Erorr
|
||||
}
|
||||
|
||||
var entityToUpdate: Entities.ITableEntity = viewModel.selected()[0];
|
||||
var originalNumberOfProperties = entityToUpdate ? 0 : Object.keys(entityToUpdate).length - 1; // .metadata is always a property for etag
|
||||
|
||||
this._container.editTableEntityPane.originEntity = entityToUpdate;
|
||||
this._container.editTableEntityPane.tableViewModel = viewModel;
|
||||
this._container.editTableEntityPane.originalNumberOfProperties = originalNumberOfProperties;
|
||||
this._container.editTableEntityPane.open();
|
||||
return null;
|
||||
}
|
||||
|
||||
public deleteEntitiesCommand(viewModel: TableEntityListViewModel): Q.Promise<any> {
|
||||
if (!viewModel) {
|
||||
return null; // Error
|
||||
}
|
||||
if (!DataTableUtilities.containItems(viewModel.selected())) {
|
||||
return null; // Error
|
||||
}
|
||||
var entitiesToDelete: Entities.ITableEntity[] = viewModel.selected();
|
||||
let deleteMessage: string = "Are you sure you want to delete the selected entities?";
|
||||
if (viewModel.queryTablesTab.container.isPreferredApiCassandra()) {
|
||||
deleteMessage = "Are you sure you want to delete the selected rows?";
|
||||
}
|
||||
if (window.confirm(deleteMessage)) {
|
||||
viewModel.queryTablesTab.container.tableDataClient
|
||||
.deleteDocuments(viewModel.queryTablesTab.collection, entitiesToDelete)
|
||||
.then((results: any) => {
|
||||
return viewModel.removeEntitiesFromCache(entitiesToDelete).then(() => {
|
||||
viewModel.redrawTableThrottled();
|
||||
});
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public customizeColumnsCommand(viewModel: TableEntityListViewModel): Q.Promise<any> {
|
||||
var table: DataTables.DataTable = viewModel.table;
|
||||
var displayedColumnNames: string[] = DataTableOperations.getDataTableHeaders(table);
|
||||
var columnsCount: number = displayedColumnNames.length;
|
||||
var currentOrder: number[] = DataTableOperations.getInitialOrder(columnsCount);
|
||||
//Debug.assert(!!table && !!currentOrder && displayedColumnNames.length === currentOrder.length);
|
||||
|
||||
var currentSettings: boolean[];
|
||||
try {
|
||||
currentSettings = currentOrder.map((value: number, index: number) => {
|
||||
return table.column(index).visible();
|
||||
});
|
||||
} catch (err) {
|
||||
// Error
|
||||
}
|
||||
|
||||
let parameters: TableColumnOptionsPane.IColumnSetting = <TableColumnOptionsPane.IColumnSetting>{
|
||||
columnNames: displayedColumnNames,
|
||||
order: currentOrder,
|
||||
visible: currentSettings
|
||||
};
|
||||
|
||||
this._container.tableColumnOptionsPane.tableViewModel = viewModel;
|
||||
this._container.tableColumnOptionsPane.parameters = parameters;
|
||||
this._container.tableColumnOptionsPane.open();
|
||||
return null;
|
||||
}
|
||||
|
||||
public reorderColumnsBasedOnSelectedEntities(viewModel: TableEntityListViewModel): Q.Promise<boolean> {
|
||||
var selected = viewModel.selected();
|
||||
if (!selected || !selected.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var table = viewModel.table;
|
||||
var currentColumnNames: string[] = DataTableOperations.getDataTableHeaders(table);
|
||||
var headersCount: number = currentColumnNames.length;
|
||||
|
||||
var headersUnion: string[] = DataTableUtilities.getPropertyIntersectionFromTableEntities(
|
||||
selected,
|
||||
viewModel.queryTablesTab.container.isPreferredApiCassandra()
|
||||
);
|
||||
|
||||
// An array with elements representing indexes of selected entities' header union out of initial headers.
|
||||
var orderOfLeftHeaders: number[] = headersUnion.map((item: string) => currentColumnNames.indexOf(item));
|
||||
|
||||
// An array with elements representing initial order of the table.
|
||||
var initialOrder: number[] = DataTableOperations.getInitialOrder(headersCount);
|
||||
|
||||
// An array with elements representing indexes of headers not present in selected entities' header union.
|
||||
var orderOfRightHeaders: number[] = _.difference(initialOrder, orderOfLeftHeaders);
|
||||
|
||||
// This will be the target order, with headers in selected entities on the left while others on the right, both in the initial order, respectively.
|
||||
var targetOrder: number[] = orderOfLeftHeaders.concat(orderOfRightHeaders);
|
||||
|
||||
return DataTableOperations.reorderColumns(table, targetOrder);
|
||||
}
|
||||
|
||||
public resetColumns(viewModel: TableEntityListViewModel): void {
|
||||
viewModel.reloadTable();
|
||||
}
|
||||
}
|
||||
import _ from "underscore";
|
||||
import Q from "q";
|
||||
import * as DataTableUtilities from "./DataTableUtilities";
|
||||
import * as DataTableOperations from "./DataTableOperations";
|
||||
import TableEntityListViewModel from "./TableEntityListViewModel";
|
||||
import * as Entities from "../Entities";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import * as TableColumnOptionsPane from "../../Panes/Tables/TableColumnOptionsPane";
|
||||
import Explorer from "../../Explorer";
|
||||
|
||||
export default class TableCommands {
|
||||
// Command Ids
|
||||
public static editEntityCommand: string = "edit";
|
||||
public static deleteEntitiesCommand: string = "delete";
|
||||
public static reorderColumnsCommand: string = "reorder";
|
||||
public static resetColumnsCommand: string = "reset";
|
||||
public static customizeColumnsCommand: string = "customizeColumns";
|
||||
|
||||
private _container: Explorer;
|
||||
|
||||
constructor(container: Explorer) {
|
||||
this._container = container;
|
||||
}
|
||||
|
||||
public isEnabled(commandName: string, selectedEntites: Entities.ITableEntity[]): boolean {
|
||||
var singleItemSelected: boolean = DataTableUtilities.containSingleItem(selectedEntites);
|
||||
var atLeastOneItemSelected: boolean = DataTableUtilities.containItems(selectedEntites);
|
||||
switch (commandName) {
|
||||
case TableCommands.editEntityCommand:
|
||||
return singleItemSelected;
|
||||
case TableCommands.deleteEntitiesCommand:
|
||||
case TableCommands.reorderColumnsCommand:
|
||||
return atLeastOneItemSelected;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public tryOpenEntityEditor(viewModel: TableEntityListViewModel): boolean {
|
||||
if (this.isEnabled(TableCommands.editEntityCommand, viewModel.selected())) {
|
||||
this.editEntityCommand(viewModel);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit entity
|
||||
*/
|
||||
public editEntityCommand(viewModel: TableEntityListViewModel): Q.Promise<any> {
|
||||
if (!viewModel) {
|
||||
return null; // Error
|
||||
}
|
||||
|
||||
if (!DataTableUtilities.containSingleItem(viewModel.selected())) {
|
||||
return null; // Erorr
|
||||
}
|
||||
|
||||
var entityToUpdate: Entities.ITableEntity = viewModel.selected()[0];
|
||||
var originalNumberOfProperties = entityToUpdate ? 0 : Object.keys(entityToUpdate).length - 1; // .metadata is always a property for etag
|
||||
|
||||
this._container.editTableEntityPane.originEntity = entityToUpdate;
|
||||
this._container.editTableEntityPane.tableViewModel = viewModel;
|
||||
this._container.editTableEntityPane.originalNumberOfProperties = originalNumberOfProperties;
|
||||
this._container.editTableEntityPane.open();
|
||||
return null;
|
||||
}
|
||||
|
||||
public deleteEntitiesCommand(viewModel: TableEntityListViewModel): Q.Promise<any> {
|
||||
if (!viewModel) {
|
||||
return null; // Error
|
||||
}
|
||||
if (!DataTableUtilities.containItems(viewModel.selected())) {
|
||||
return null; // Error
|
||||
}
|
||||
var entitiesToDelete: Entities.ITableEntity[] = viewModel.selected();
|
||||
let deleteMessage: string = "Are you sure you want to delete the selected entities?";
|
||||
if (viewModel.queryTablesTab.container.isPreferredApiCassandra()) {
|
||||
deleteMessage = "Are you sure you want to delete the selected rows?";
|
||||
}
|
||||
if (window.confirm(deleteMessage)) {
|
||||
viewModel.queryTablesTab.container.tableDataClient
|
||||
.deleteDocuments(viewModel.queryTablesTab.collection, entitiesToDelete)
|
||||
.then((results: any) => {
|
||||
return viewModel.removeEntitiesFromCache(entitiesToDelete).then(() => {
|
||||
viewModel.redrawTableThrottled();
|
||||
});
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public customizeColumnsCommand(viewModel: TableEntityListViewModel): Q.Promise<any> {
|
||||
var table: DataTables.DataTable = viewModel.table;
|
||||
var displayedColumnNames: string[] = DataTableOperations.getDataTableHeaders(table);
|
||||
var columnsCount: number = displayedColumnNames.length;
|
||||
var currentOrder: number[] = DataTableOperations.getInitialOrder(columnsCount);
|
||||
//Debug.assert(!!table && !!currentOrder && displayedColumnNames.length === currentOrder.length);
|
||||
|
||||
var currentSettings: boolean[];
|
||||
try {
|
||||
currentSettings = currentOrder.map((value: number, index: number) => {
|
||||
return table.column(index).visible();
|
||||
});
|
||||
} catch (err) {
|
||||
// Error
|
||||
}
|
||||
|
||||
let parameters: TableColumnOptionsPane.IColumnSetting = <TableColumnOptionsPane.IColumnSetting>{
|
||||
columnNames: displayedColumnNames,
|
||||
order: currentOrder,
|
||||
visible: currentSettings,
|
||||
};
|
||||
|
||||
this._container.tableColumnOptionsPane.tableViewModel = viewModel;
|
||||
this._container.tableColumnOptionsPane.parameters = parameters;
|
||||
this._container.tableColumnOptionsPane.open();
|
||||
return null;
|
||||
}
|
||||
|
||||
public reorderColumnsBasedOnSelectedEntities(viewModel: TableEntityListViewModel): Q.Promise<boolean> {
|
||||
var selected = viewModel.selected();
|
||||
if (!selected || !selected.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var table = viewModel.table;
|
||||
var currentColumnNames: string[] = DataTableOperations.getDataTableHeaders(table);
|
||||
var headersCount: number = currentColumnNames.length;
|
||||
|
||||
var headersUnion: string[] = DataTableUtilities.getPropertyIntersectionFromTableEntities(
|
||||
selected,
|
||||
viewModel.queryTablesTab.container.isPreferredApiCassandra()
|
||||
);
|
||||
|
||||
// An array with elements representing indexes of selected entities' header union out of initial headers.
|
||||
var orderOfLeftHeaders: number[] = headersUnion.map((item: string) => currentColumnNames.indexOf(item));
|
||||
|
||||
// An array with elements representing initial order of the table.
|
||||
var initialOrder: number[] = DataTableOperations.getInitialOrder(headersCount);
|
||||
|
||||
// An array with elements representing indexes of headers not present in selected entities' header union.
|
||||
var orderOfRightHeaders: number[] = _.difference(initialOrder, orderOfLeftHeaders);
|
||||
|
||||
// This will be the target order, with headers in selected entities on the left while others on the right, both in the initial order, respectively.
|
||||
var targetOrder: number[] = orderOfLeftHeaders.concat(orderOfRightHeaders);
|
||||
|
||||
return DataTableOperations.reorderColumns(table, targetOrder);
|
||||
}
|
||||
|
||||
public resetColumns(viewModel: TableEntityListViewModel): void {
|
||||
viewModel.reloadTable();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import * as Utilities from "../Utilities";
|
||||
import * as Entities from "../Entities";
|
||||
import CacheBase from "./CacheBase";
|
||||
|
||||
export default class TableEntityCache extends CacheBase<Entities.ITableEntity> {
|
||||
private _tableQuery: Entities.ITableQuery;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.data = null;
|
||||
this._tableQuery = null;
|
||||
this.serverCallInProgress = false;
|
||||
this.sortOrder = null;
|
||||
}
|
||||
|
||||
public get tableQuery(): Entities.ITableQuery {
|
||||
return Utilities.copyTableQuery(this._tableQuery);
|
||||
}
|
||||
|
||||
public set tableQuery(tableQuery: Entities.ITableQuery) {
|
||||
this._tableQuery = Utilities.copyTableQuery(tableQuery);
|
||||
}
|
||||
|
||||
public preClear() {
|
||||
this.tableQuery = null;
|
||||
}
|
||||
}
|
||||
import * as Utilities from "../Utilities";
|
||||
import * as Entities from "../Entities";
|
||||
import CacheBase from "./CacheBase";
|
||||
|
||||
export default class TableEntityCache extends CacheBase<Entities.ITableEntity> {
|
||||
private _tableQuery: Entities.ITableQuery;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.data = null;
|
||||
this._tableQuery = null;
|
||||
this.serverCallInProgress = false;
|
||||
this.sortOrder = null;
|
||||
}
|
||||
|
||||
public get tableQuery(): Entities.ITableQuery {
|
||||
return Utilities.copyTableQuery(this._tableQuery);
|
||||
}
|
||||
|
||||
public set tableQuery(tableQuery: Entities.ITableQuery) {
|
||||
this._tableQuery = Utilities.copyTableQuery(tableQuery);
|
||||
}
|
||||
|
||||
public preClear() {
|
||||
this.tableQuery = null;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,38 +1,38 @@
|
||||
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
|
||||
|
||||
export interface ITableEntity {
|
||||
[property: string]: ITableEntityAttribute;
|
||||
}
|
||||
|
||||
export interface ITableEntityForTablesAPI extends ITableEntity {
|
||||
PartitionKey: ITableEntityAttribute;
|
||||
RowKey: ITableEntityAttribute;
|
||||
Timestamp: ITableEntityAttribute;
|
||||
}
|
||||
|
||||
export interface ITableEntityAttribute {
|
||||
_: string; // Value of a property
|
||||
$?: string; // Edm Type
|
||||
}
|
||||
|
||||
export interface IListTableEntitiesResult {
|
||||
Results: ITableEntity[];
|
||||
ContinuationToken: any;
|
||||
iterator?: QueryIterator<ItemDefinition & Resource>;
|
||||
}
|
||||
|
||||
export interface IProperty {
|
||||
key: string;
|
||||
subkey?: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ITableQuery {
|
||||
select?: string[];
|
||||
filter?: string;
|
||||
top?: number;
|
||||
}
|
||||
|
||||
export interface ITableEntityIdentity {
|
||||
RowKey: string;
|
||||
}
|
||||
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
|
||||
|
||||
export interface ITableEntity {
|
||||
[property: string]: ITableEntityAttribute;
|
||||
}
|
||||
|
||||
export interface ITableEntityForTablesAPI extends ITableEntity {
|
||||
PartitionKey: ITableEntityAttribute;
|
||||
RowKey: ITableEntityAttribute;
|
||||
Timestamp: ITableEntityAttribute;
|
||||
}
|
||||
|
||||
export interface ITableEntityAttribute {
|
||||
_: string; // Value of a property
|
||||
$?: string; // Edm Type
|
||||
}
|
||||
|
||||
export interface IListTableEntitiesResult {
|
||||
Results: ITableEntity[];
|
||||
ContinuationToken: any;
|
||||
iterator?: QueryIterator<ItemDefinition & Resource>;
|
||||
}
|
||||
|
||||
export interface IProperty {
|
||||
key: string;
|
||||
subkey?: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ITableQuery {
|
||||
select?: string[];
|
||||
filter?: string;
|
||||
top?: number;
|
||||
}
|
||||
|
||||
export interface ITableEntityIdentity {
|
||||
RowKey: string;
|
||||
}
|
||||
|
||||
@@ -1,327 +1,327 @@
|
||||
import QueryClauseViewModel from "./QueryClauseViewModel";
|
||||
import * as Utilities from "../Utilities";
|
||||
|
||||
export default class ClauseGroup {
|
||||
public isRootGroup: boolean;
|
||||
public children = new Array();
|
||||
public parentGroup: ClauseGroup;
|
||||
private _id: string;
|
||||
|
||||
constructor(isRootGroup: boolean, parentGroup: ClauseGroup, id?: string) {
|
||||
this.isRootGroup = isRootGroup;
|
||||
this.parentGroup = parentGroup;
|
||||
this._id = id ? id : Utilities.guid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens the clause tree into an array, depth-first, left to right.
|
||||
*/
|
||||
public flattenClauses(targetArray: ko.ObservableArray<QueryClauseViewModel>): void {
|
||||
var tempArray = new Array<QueryClauseViewModel>();
|
||||
|
||||
this.flattenClausesImpl(this, tempArray);
|
||||
targetArray.removeAll();
|
||||
tempArray.forEach(element => {
|
||||
targetArray.push(element);
|
||||
});
|
||||
}
|
||||
|
||||
public insertClauseBefore(newClause: QueryClauseViewModel, insertBefore?: QueryClauseViewModel): void {
|
||||
if (!insertBefore) {
|
||||
newClause.clauseGroup = this;
|
||||
this.children.push(newClause);
|
||||
} else {
|
||||
var targetGroup = insertBefore.clauseGroup;
|
||||
|
||||
if (targetGroup) {
|
||||
var insertBeforeIndex = targetGroup.children.indexOf(insertBefore);
|
||||
newClause.clauseGroup = targetGroup;
|
||||
targetGroup.children.splice(insertBeforeIndex, 0, newClause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public deleteClause(clause: QueryClauseViewModel): void {
|
||||
var targetGroup = clause.clauseGroup;
|
||||
|
||||
if (targetGroup) {
|
||||
var index = targetGroup.children.indexOf(clause);
|
||||
targetGroup.children.splice(index, 1);
|
||||
clause.dispose();
|
||||
|
||||
if (targetGroup.children.length <= 1 && !targetGroup.isRootGroup) {
|
||||
var parent = targetGroup.parentGroup;
|
||||
var targetGroupIndex = parent.children.indexOf(targetGroup);
|
||||
|
||||
if (targetGroup.children.length === 1) {
|
||||
var orphan = targetGroup.children.shift();
|
||||
|
||||
if (orphan instanceof QueryClauseViewModel) {
|
||||
(<QueryClauseViewModel>orphan).clauseGroup = parent;
|
||||
} else if (orphan instanceof ClauseGroup) {
|
||||
(<ClauseGroup>orphan).parentGroup = parent;
|
||||
}
|
||||
|
||||
parent.children.splice(targetGroupIndex, 1, orphan);
|
||||
} else {
|
||||
parent.children.splice(targetGroupIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public removeAll(): void {
|
||||
var allClauses: QueryClauseViewModel[] = new Array<QueryClauseViewModel>();
|
||||
|
||||
this.flattenClausesImpl(this, allClauses);
|
||||
|
||||
while (allClauses.length > 0) {
|
||||
allClauses.shift().dispose();
|
||||
}
|
||||
|
||||
this.children = new Array<any>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups selected items. Returns True if a new group was created, otherwise False.
|
||||
*/
|
||||
public groupSelectedItems(): boolean {
|
||||
// Find the selection start & end, also check for gaps between selected items (if found, cannot proceed).
|
||||
var selection = this.getCheckedItemsInfo();
|
||||
|
||||
if (selection.canGroup) {
|
||||
var newGroup = new ClauseGroup(false, this);
|
||||
// Replace the selected items with the new group, and then move the selected items into the new group.
|
||||
var groupedItems = this.children.splice(selection.begin, selection.end - selection.begin + 1, newGroup);
|
||||
|
||||
groupedItems &&
|
||||
groupedItems.forEach(element => {
|
||||
newGroup.children.push(element);
|
||||
|
||||
if (element instanceof QueryClauseViewModel) {
|
||||
(<QueryClauseViewModel>element).clauseGroup = newGroup;
|
||||
} else if (element instanceof ClauseGroup) {
|
||||
(<ClauseGroup>element).parentGroup = newGroup;
|
||||
}
|
||||
});
|
||||
|
||||
this.unselectAll();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public ungroup(): void {
|
||||
if (this.isRootGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
var parentGroup = this.parentGroup;
|
||||
var index = parentGroup.children.indexOf(this);
|
||||
|
||||
if (index >= 0) {
|
||||
parentGroup.children.splice(index, 1);
|
||||
|
||||
var toPromote = this.children.splice(0, this.children.length);
|
||||
|
||||
// Move all children one level up.
|
||||
toPromote &&
|
||||
toPromote.forEach(element => {
|
||||
if (element instanceof ClauseGroup) {
|
||||
(<ClauseGroup>element).parentGroup = parentGroup;
|
||||
} else if (element instanceof QueryClauseViewModel) {
|
||||
(<QueryClauseViewModel>element).clauseGroup = parentGroup;
|
||||
}
|
||||
|
||||
parentGroup.children.splice(index, 0, element);
|
||||
index++;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public canGroupSelectedItems(): boolean {
|
||||
return this.getCheckedItemsInfo().canGroup;
|
||||
}
|
||||
|
||||
public findDeepestGroupInChildren(skipIndex?: number): ClauseGroup {
|
||||
var deepest: ClauseGroup = this;
|
||||
var level: number = 0;
|
||||
var func = (currentGroup: ClauseGroup): void => {
|
||||
level++;
|
||||
if (currentGroup.getCurrentGroupDepth() > deepest.getCurrentGroupDepth()) {
|
||||
deepest = currentGroup;
|
||||
}
|
||||
|
||||
for (var i = 0; i < currentGroup.children.length; i++) {
|
||||
var currentItem = currentGroup.children[i];
|
||||
|
||||
if ((i !== skipIndex || level > 1) && currentItem instanceof ClauseGroup) {
|
||||
func(currentItem);
|
||||
}
|
||||
}
|
||||
level--;
|
||||
};
|
||||
|
||||
func(this);
|
||||
|
||||
return deepest;
|
||||
}
|
||||
|
||||
private getCheckedItemsInfo(): { canGroup: boolean; begin: number; end: number } {
|
||||
var beginIndex = -1;
|
||||
var endIndex = -1;
|
||||
// In order to perform group, all selected items must be next to each other.
|
||||
// If one or more items are not selected between the first and the last selected item, the gapFlag will be set to True, meaning cannot perform group.
|
||||
var gapFlag = false;
|
||||
var count = 0;
|
||||
|
||||
for (var i = 0; i < this.children.length; i++) {
|
||||
var currentItem = this.children[i];
|
||||
var subGroupSelectionState: { allSelected: boolean; partiallySelected: boolean; nonSelected: boolean };
|
||||
|
||||
if (currentItem instanceof ClauseGroup) {
|
||||
subGroupSelectionState = (<ClauseGroup>currentItem).getSelectionState();
|
||||
|
||||
if (subGroupSelectionState.partiallySelected) {
|
||||
gapFlag = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
beginIndex < 0 &&
|
||||
endIndex < 0 &&
|
||||
((currentItem instanceof QueryClauseViewModel && currentItem.checkedForGrouping.peek()) ||
|
||||
(currentItem instanceof ClauseGroup && subGroupSelectionState.allSelected))
|
||||
) {
|
||||
beginIndex = i;
|
||||
}
|
||||
|
||||
if (
|
||||
beginIndex >= 0 &&
|
||||
endIndex < 0 &&
|
||||
((currentItem instanceof QueryClauseViewModel && !currentItem.checkedForGrouping.peek()) ||
|
||||
(currentItem instanceof ClauseGroup && !subGroupSelectionState.allSelected))
|
||||
) {
|
||||
endIndex = i - 1;
|
||||
}
|
||||
|
||||
if (beginIndex >= 0 && endIndex < 0) {
|
||||
count++;
|
||||
}
|
||||
|
||||
if (
|
||||
beginIndex >= 0 &&
|
||||
endIndex >= 0 &&
|
||||
((currentItem instanceof QueryClauseViewModel && currentItem.checkedForGrouping.peek()) ||
|
||||
(currentItem instanceof ClauseGroup && !subGroupSelectionState.nonSelected))
|
||||
) {
|
||||
gapFlag = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!gapFlag && endIndex < 0) {
|
||||
endIndex = this.children.length - 1;
|
||||
}
|
||||
|
||||
return {
|
||||
canGroup: beginIndex >= 0 && !gapFlag && count > 1,
|
||||
begin: beginIndex,
|
||||
end: endIndex
|
||||
};
|
||||
}
|
||||
|
||||
private getSelectionState(): { allSelected: boolean; partiallySelected: boolean; nonSelected: boolean } {
|
||||
var selectedCount = 0;
|
||||
|
||||
for (var i = 0; i < this.children.length; i++) {
|
||||
var currentItem = this.children[i];
|
||||
|
||||
if (currentItem instanceof ClauseGroup && (<ClauseGroup>currentItem).getSelectionState().allSelected) {
|
||||
selectedCount++;
|
||||
}
|
||||
|
||||
if (
|
||||
currentItem instanceof QueryClauseViewModel &&
|
||||
(<QueryClauseViewModel>currentItem).checkedForGrouping.peek()
|
||||
) {
|
||||
selectedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
allSelected: selectedCount === this.children.length,
|
||||
partiallySelected: selectedCount > 0 && selectedCount < this.children.length,
|
||||
nonSelected: selectedCount === 0
|
||||
};
|
||||
}
|
||||
|
||||
private unselectAll(): void {
|
||||
for (var i = 0; i < this.children.length; i++) {
|
||||
var currentItem = this.children[i];
|
||||
|
||||
if (currentItem instanceof ClauseGroup) {
|
||||
(<ClauseGroup>currentItem).unselectAll();
|
||||
}
|
||||
|
||||
if (currentItem instanceof QueryClauseViewModel) {
|
||||
(<QueryClauseViewModel>currentItem).checkedForGrouping(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private flattenClausesImpl(queryGroup: ClauseGroup, targetArray: QueryClauseViewModel[]): void {
|
||||
if (queryGroup.isRootGroup) {
|
||||
targetArray.splice(0, targetArray.length);
|
||||
}
|
||||
|
||||
for (var i = 0; i < queryGroup.children.length; i++) {
|
||||
var currentItem = queryGroup.children[i];
|
||||
|
||||
if (currentItem instanceof ClauseGroup) {
|
||||
this.flattenClausesImpl(currentItem, targetArray);
|
||||
}
|
||||
|
||||
if (currentItem instanceof QueryClauseViewModel) {
|
||||
targetArray.push(currentItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getTreeDepth(): number {
|
||||
var currentDepth = this.getCurrentGroupDepth();
|
||||
|
||||
for (var i = 0; i < this.children.length; i++) {
|
||||
var currentItem = this.children[i];
|
||||
|
||||
if (currentItem instanceof ClauseGroup) {
|
||||
var newDepth = (<ClauseGroup>currentItem).getTreeDepth();
|
||||
|
||||
if (newDepth > currentDepth) {
|
||||
currentDepth = newDepth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return currentDepth;
|
||||
}
|
||||
|
||||
public getCurrentGroupDepth(): number {
|
||||
var group = <ClauseGroup>this;
|
||||
var depth = 0;
|
||||
|
||||
while (!group.isRootGroup) {
|
||||
depth++;
|
||||
group = group.parentGroup;
|
||||
}
|
||||
|
||||
return depth;
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return this._id;
|
||||
}
|
||||
}
|
||||
import QueryClauseViewModel from "./QueryClauseViewModel";
|
||||
import * as Utilities from "../Utilities";
|
||||
|
||||
export default class ClauseGroup {
|
||||
public isRootGroup: boolean;
|
||||
public children = new Array();
|
||||
public parentGroup: ClauseGroup;
|
||||
private _id: string;
|
||||
|
||||
constructor(isRootGroup: boolean, parentGroup: ClauseGroup, id?: string) {
|
||||
this.isRootGroup = isRootGroup;
|
||||
this.parentGroup = parentGroup;
|
||||
this._id = id ? id : Utilities.guid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens the clause tree into an array, depth-first, left to right.
|
||||
*/
|
||||
public flattenClauses(targetArray: ko.ObservableArray<QueryClauseViewModel>): void {
|
||||
var tempArray = new Array<QueryClauseViewModel>();
|
||||
|
||||
this.flattenClausesImpl(this, tempArray);
|
||||
targetArray.removeAll();
|
||||
tempArray.forEach((element) => {
|
||||
targetArray.push(element);
|
||||
});
|
||||
}
|
||||
|
||||
public insertClauseBefore(newClause: QueryClauseViewModel, insertBefore?: QueryClauseViewModel): void {
|
||||
if (!insertBefore) {
|
||||
newClause.clauseGroup = this;
|
||||
this.children.push(newClause);
|
||||
} else {
|
||||
var targetGroup = insertBefore.clauseGroup;
|
||||
|
||||
if (targetGroup) {
|
||||
var insertBeforeIndex = targetGroup.children.indexOf(insertBefore);
|
||||
newClause.clauseGroup = targetGroup;
|
||||
targetGroup.children.splice(insertBeforeIndex, 0, newClause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public deleteClause(clause: QueryClauseViewModel): void {
|
||||
var targetGroup = clause.clauseGroup;
|
||||
|
||||
if (targetGroup) {
|
||||
var index = targetGroup.children.indexOf(clause);
|
||||
targetGroup.children.splice(index, 1);
|
||||
clause.dispose();
|
||||
|
||||
if (targetGroup.children.length <= 1 && !targetGroup.isRootGroup) {
|
||||
var parent = targetGroup.parentGroup;
|
||||
var targetGroupIndex = parent.children.indexOf(targetGroup);
|
||||
|
||||
if (targetGroup.children.length === 1) {
|
||||
var orphan = targetGroup.children.shift();
|
||||
|
||||
if (orphan instanceof QueryClauseViewModel) {
|
||||
(<QueryClauseViewModel>orphan).clauseGroup = parent;
|
||||
} else if (orphan instanceof ClauseGroup) {
|
||||
(<ClauseGroup>orphan).parentGroup = parent;
|
||||
}
|
||||
|
||||
parent.children.splice(targetGroupIndex, 1, orphan);
|
||||
} else {
|
||||
parent.children.splice(targetGroupIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public removeAll(): void {
|
||||
var allClauses: QueryClauseViewModel[] = new Array<QueryClauseViewModel>();
|
||||
|
||||
this.flattenClausesImpl(this, allClauses);
|
||||
|
||||
while (allClauses.length > 0) {
|
||||
allClauses.shift().dispose();
|
||||
}
|
||||
|
||||
this.children = new Array<any>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups selected items. Returns True if a new group was created, otherwise False.
|
||||
*/
|
||||
public groupSelectedItems(): boolean {
|
||||
// Find the selection start & end, also check for gaps between selected items (if found, cannot proceed).
|
||||
var selection = this.getCheckedItemsInfo();
|
||||
|
||||
if (selection.canGroup) {
|
||||
var newGroup = new ClauseGroup(false, this);
|
||||
// Replace the selected items with the new group, and then move the selected items into the new group.
|
||||
var groupedItems = this.children.splice(selection.begin, selection.end - selection.begin + 1, newGroup);
|
||||
|
||||
groupedItems &&
|
||||
groupedItems.forEach((element) => {
|
||||
newGroup.children.push(element);
|
||||
|
||||
if (element instanceof QueryClauseViewModel) {
|
||||
(<QueryClauseViewModel>element).clauseGroup = newGroup;
|
||||
} else if (element instanceof ClauseGroup) {
|
||||
(<ClauseGroup>element).parentGroup = newGroup;
|
||||
}
|
||||
});
|
||||
|
||||
this.unselectAll();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public ungroup(): void {
|
||||
if (this.isRootGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
var parentGroup = this.parentGroup;
|
||||
var index = parentGroup.children.indexOf(this);
|
||||
|
||||
if (index >= 0) {
|
||||
parentGroup.children.splice(index, 1);
|
||||
|
||||
var toPromote = this.children.splice(0, this.children.length);
|
||||
|
||||
// Move all children one level up.
|
||||
toPromote &&
|
||||
toPromote.forEach((element) => {
|
||||
if (element instanceof ClauseGroup) {
|
||||
(<ClauseGroup>element).parentGroup = parentGroup;
|
||||
} else if (element instanceof QueryClauseViewModel) {
|
||||
(<QueryClauseViewModel>element).clauseGroup = parentGroup;
|
||||
}
|
||||
|
||||
parentGroup.children.splice(index, 0, element);
|
||||
index++;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public canGroupSelectedItems(): boolean {
|
||||
return this.getCheckedItemsInfo().canGroup;
|
||||
}
|
||||
|
||||
public findDeepestGroupInChildren(skipIndex?: number): ClauseGroup {
|
||||
var deepest: ClauseGroup = this;
|
||||
var level: number = 0;
|
||||
var func = (currentGroup: ClauseGroup): void => {
|
||||
level++;
|
||||
if (currentGroup.getCurrentGroupDepth() > deepest.getCurrentGroupDepth()) {
|
||||
deepest = currentGroup;
|
||||
}
|
||||
|
||||
for (var i = 0; i < currentGroup.children.length; i++) {
|
||||
var currentItem = currentGroup.children[i];
|
||||
|
||||
if ((i !== skipIndex || level > 1) && currentItem instanceof ClauseGroup) {
|
||||
func(currentItem);
|
||||
}
|
||||
}
|
||||
level--;
|
||||
};
|
||||
|
||||
func(this);
|
||||
|
||||
return deepest;
|
||||
}
|
||||
|
||||
private getCheckedItemsInfo(): { canGroup: boolean; begin: number; end: number } {
|
||||
var beginIndex = -1;
|
||||
var endIndex = -1;
|
||||
// In order to perform group, all selected items must be next to each other.
|
||||
// If one or more items are not selected between the first and the last selected item, the gapFlag will be set to True, meaning cannot perform group.
|
||||
var gapFlag = false;
|
||||
var count = 0;
|
||||
|
||||
for (var i = 0; i < this.children.length; i++) {
|
||||
var currentItem = this.children[i];
|
||||
var subGroupSelectionState: { allSelected: boolean; partiallySelected: boolean; nonSelected: boolean };
|
||||
|
||||
if (currentItem instanceof ClauseGroup) {
|
||||
subGroupSelectionState = (<ClauseGroup>currentItem).getSelectionState();
|
||||
|
||||
if (subGroupSelectionState.partiallySelected) {
|
||||
gapFlag = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
beginIndex < 0 &&
|
||||
endIndex < 0 &&
|
||||
((currentItem instanceof QueryClauseViewModel && currentItem.checkedForGrouping.peek()) ||
|
||||
(currentItem instanceof ClauseGroup && subGroupSelectionState.allSelected))
|
||||
) {
|
||||
beginIndex = i;
|
||||
}
|
||||
|
||||
if (
|
||||
beginIndex >= 0 &&
|
||||
endIndex < 0 &&
|
||||
((currentItem instanceof QueryClauseViewModel && !currentItem.checkedForGrouping.peek()) ||
|
||||
(currentItem instanceof ClauseGroup && !subGroupSelectionState.allSelected))
|
||||
) {
|
||||
endIndex = i - 1;
|
||||
}
|
||||
|
||||
if (beginIndex >= 0 && endIndex < 0) {
|
||||
count++;
|
||||
}
|
||||
|
||||
if (
|
||||
beginIndex >= 0 &&
|
||||
endIndex >= 0 &&
|
||||
((currentItem instanceof QueryClauseViewModel && currentItem.checkedForGrouping.peek()) ||
|
||||
(currentItem instanceof ClauseGroup && !subGroupSelectionState.nonSelected))
|
||||
) {
|
||||
gapFlag = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!gapFlag && endIndex < 0) {
|
||||
endIndex = this.children.length - 1;
|
||||
}
|
||||
|
||||
return {
|
||||
canGroup: beginIndex >= 0 && !gapFlag && count > 1,
|
||||
begin: beginIndex,
|
||||
end: endIndex,
|
||||
};
|
||||
}
|
||||
|
||||
private getSelectionState(): { allSelected: boolean; partiallySelected: boolean; nonSelected: boolean } {
|
||||
var selectedCount = 0;
|
||||
|
||||
for (var i = 0; i < this.children.length; i++) {
|
||||
var currentItem = this.children[i];
|
||||
|
||||
if (currentItem instanceof ClauseGroup && (<ClauseGroup>currentItem).getSelectionState().allSelected) {
|
||||
selectedCount++;
|
||||
}
|
||||
|
||||
if (
|
||||
currentItem instanceof QueryClauseViewModel &&
|
||||
(<QueryClauseViewModel>currentItem).checkedForGrouping.peek()
|
||||
) {
|
||||
selectedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
allSelected: selectedCount === this.children.length,
|
||||
partiallySelected: selectedCount > 0 && selectedCount < this.children.length,
|
||||
nonSelected: selectedCount === 0,
|
||||
};
|
||||
}
|
||||
|
||||
private unselectAll(): void {
|
||||
for (var i = 0; i < this.children.length; i++) {
|
||||
var currentItem = this.children[i];
|
||||
|
||||
if (currentItem instanceof ClauseGroup) {
|
||||
(<ClauseGroup>currentItem).unselectAll();
|
||||
}
|
||||
|
||||
if (currentItem instanceof QueryClauseViewModel) {
|
||||
(<QueryClauseViewModel>currentItem).checkedForGrouping(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private flattenClausesImpl(queryGroup: ClauseGroup, targetArray: QueryClauseViewModel[]): void {
|
||||
if (queryGroup.isRootGroup) {
|
||||
targetArray.splice(0, targetArray.length);
|
||||
}
|
||||
|
||||
for (var i = 0; i < queryGroup.children.length; i++) {
|
||||
var currentItem = queryGroup.children[i];
|
||||
|
||||
if (currentItem instanceof ClauseGroup) {
|
||||
this.flattenClausesImpl(currentItem, targetArray);
|
||||
}
|
||||
|
||||
if (currentItem instanceof QueryClauseViewModel) {
|
||||
targetArray.push(currentItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getTreeDepth(): number {
|
||||
var currentDepth = this.getCurrentGroupDepth();
|
||||
|
||||
for (var i = 0; i < this.children.length; i++) {
|
||||
var currentItem = this.children[i];
|
||||
|
||||
if (currentItem instanceof ClauseGroup) {
|
||||
var newDepth = (<ClauseGroup>currentItem).getTreeDepth();
|
||||
|
||||
if (newDepth > currentDepth) {
|
||||
currentDepth = newDepth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return currentDepth;
|
||||
}
|
||||
|
||||
public getCurrentGroupDepth(): number {
|
||||
var group = <ClauseGroup>this;
|
||||
var depth = 0;
|
||||
|
||||
while (!group.isRootGroup) {
|
||||
depth++;
|
||||
group = group.parentGroup;
|
||||
}
|
||||
|
||||
return depth;
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return this._id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
import * as ko from "knockout";
|
||||
import ClauseGroup from "./ClauseGroup";
|
||||
import QueryBuilderViewModel from "./QueryBuilderViewModel";
|
||||
import * as Constants from "../Constants";
|
||||
|
||||
/**
|
||||
* View model for showing group indicators on UI, contains information such as group color and border styles.
|
||||
*/
|
||||
export default class ClauseGroupViewModel {
|
||||
public ungroupClausesLabel = "Ungroup clauses"; // localize
|
||||
|
||||
public backgroundColor: ko.Observable<string>;
|
||||
public canUngroup: ko.Observable<boolean>;
|
||||
public showTopBorder: ko.Observable<boolean>;
|
||||
public showLeftBorder: ko.Observable<boolean>;
|
||||
public showBottomBorder: ko.Observable<boolean>;
|
||||
public depth: ko.Observable<number>; // for debugging purpose only, now showing the number on UI.
|
||||
public borderBackgroundColor: ko.Observable<string>;
|
||||
|
||||
private _clauseGroup: ClauseGroup;
|
||||
private _queryBuilderViewModel: QueryBuilderViewModel;
|
||||
|
||||
constructor(clauseGroup: ClauseGroup, canUngroup: boolean, queryBuilderViewModel: QueryBuilderViewModel) {
|
||||
this._clauseGroup = clauseGroup;
|
||||
this._queryBuilderViewModel = queryBuilderViewModel;
|
||||
this.backgroundColor = ko.observable<string>(this.getGroupBackgroundColor(clauseGroup));
|
||||
this.canUngroup = ko.observable<boolean>(canUngroup);
|
||||
this.showTopBorder = ko.observable<boolean>(false);
|
||||
this.showLeftBorder = ko.observable<boolean>(false);
|
||||
this.showBottomBorder = ko.observable<boolean>(false);
|
||||
this.depth = ko.observable<number>(clauseGroup.getCurrentGroupDepth());
|
||||
this.borderBackgroundColor = ko.observable<string>("solid thin " + this.getGroupBackgroundColor(clauseGroup));
|
||||
}
|
||||
|
||||
public ungroupClauses = (): void => {
|
||||
this._clauseGroup.ungroup();
|
||||
this._queryBuilderViewModel.updateClauseArray();
|
||||
};
|
||||
|
||||
private getGroupBackgroundColor(group: ClauseGroup): string {
|
||||
var colorCount = Constants.clauseGroupColors.length;
|
||||
|
||||
if (group.isRootGroup) {
|
||||
return Constants.transparentColor;
|
||||
} else {
|
||||
return Constants.clauseGroupColors[group.getCurrentGroupDepth() % colorCount];
|
||||
}
|
||||
}
|
||||
}
|
||||
import * as ko from "knockout";
|
||||
import ClauseGroup from "./ClauseGroup";
|
||||
import QueryBuilderViewModel from "./QueryBuilderViewModel";
|
||||
import * as Constants from "../Constants";
|
||||
|
||||
/**
|
||||
* View model for showing group indicators on UI, contains information such as group color and border styles.
|
||||
*/
|
||||
export default class ClauseGroupViewModel {
|
||||
public ungroupClausesLabel = "Ungroup clauses"; // localize
|
||||
|
||||
public backgroundColor: ko.Observable<string>;
|
||||
public canUngroup: ko.Observable<boolean>;
|
||||
public showTopBorder: ko.Observable<boolean>;
|
||||
public showLeftBorder: ko.Observable<boolean>;
|
||||
public showBottomBorder: ko.Observable<boolean>;
|
||||
public depth: ko.Observable<number>; // for debugging purpose only, now showing the number on UI.
|
||||
public borderBackgroundColor: ko.Observable<string>;
|
||||
|
||||
private _clauseGroup: ClauseGroup;
|
||||
private _queryBuilderViewModel: QueryBuilderViewModel;
|
||||
|
||||
constructor(clauseGroup: ClauseGroup, canUngroup: boolean, queryBuilderViewModel: QueryBuilderViewModel) {
|
||||
this._clauseGroup = clauseGroup;
|
||||
this._queryBuilderViewModel = queryBuilderViewModel;
|
||||
this.backgroundColor = ko.observable<string>(this.getGroupBackgroundColor(clauseGroup));
|
||||
this.canUngroup = ko.observable<boolean>(canUngroup);
|
||||
this.showTopBorder = ko.observable<boolean>(false);
|
||||
this.showLeftBorder = ko.observable<boolean>(false);
|
||||
this.showBottomBorder = ko.observable<boolean>(false);
|
||||
this.depth = ko.observable<number>(clauseGroup.getCurrentGroupDepth());
|
||||
this.borderBackgroundColor = ko.observable<string>("solid thin " + this.getGroupBackgroundColor(clauseGroup));
|
||||
}
|
||||
|
||||
public ungroupClauses = (): void => {
|
||||
this._clauseGroup.ungroup();
|
||||
this._queryBuilderViewModel.updateClauseArray();
|
||||
};
|
||||
|
||||
private getGroupBackgroundColor(group: ClauseGroup): string {
|
||||
var colorCount = Constants.clauseGroupColors.length;
|
||||
|
||||
if (group.isRootGroup) {
|
||||
return Constants.transparentColor;
|
||||
} else {
|
||||
return Constants.clauseGroupColors[group.getCurrentGroupDepth() % colorCount];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,377 +1,377 @@
|
||||
import QueryBuilderViewModel from "./QueryBuilderViewModel";
|
||||
import QueryClauseViewModel from "./QueryClauseViewModel";
|
||||
import * as DateTimeUtilities from "./DateTimeUtilities";
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
export var utc = "utc";
|
||||
export var local = "local";
|
||||
|
||||
export interface ITimestampQuery {
|
||||
queryType: string; // valid values are "last" and "range"
|
||||
lastNumber: number; // number value of a custom timestamp using the last option
|
||||
lastTimeUnit: string; // timeunit of a custom timestamp using the last option
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
timeZone: string; // timezone of custom range timestamp, valid values are "local" and "utc"
|
||||
}
|
||||
|
||||
export interface ILastQuery {
|
||||
lastNumber: number;
|
||||
lastTimeUnit: string;
|
||||
}
|
||||
|
||||
export enum TimeUnit {
|
||||
Seconds,
|
||||
Minutes,
|
||||
Hours,
|
||||
Days
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting helpers
|
||||
*/
|
||||
|
||||
export function addRangeTimestamp(
|
||||
timestamp: ITimestampQuery,
|
||||
queryBuilderViewModel: QueryBuilderViewModel,
|
||||
queryClauseViewModel: QueryClauseViewModel
|
||||
): void {
|
||||
queryBuilderViewModel.addCustomRange(timestamp, queryClauseViewModel);
|
||||
}
|
||||
|
||||
export function getDefaultStart(localTime: boolean, durationHours: number = 24): string {
|
||||
var startTimestamp: string;
|
||||
|
||||
var utcNowString: string = new Date().toISOString();
|
||||
var yesterday: Date = new Date(utcNowString);
|
||||
|
||||
yesterday.setHours(yesterday.getHours() - durationHours);
|
||||
startTimestamp = yesterday.toISOString();
|
||||
|
||||
if (localTime) {
|
||||
startTimestamp = localFromUtcDateString(startTimestamp);
|
||||
}
|
||||
|
||||
return startTimestamp;
|
||||
}
|
||||
|
||||
export function getDefaultEnd(localTime: boolean): string {
|
||||
var endTimestamp: string;
|
||||
|
||||
var utcNowString: string = new Date().toISOString();
|
||||
|
||||
endTimestamp = utcNowString;
|
||||
|
||||
if (localTime) {
|
||||
endTimestamp = localFromUtcDateString(endTimestamp);
|
||||
}
|
||||
|
||||
return endTimestamp;
|
||||
}
|
||||
|
||||
export function parseDate(dateString: string, isUTC: boolean): Date {
|
||||
// TODO validate dateString
|
||||
var date: Date = null;
|
||||
|
||||
if (dateString) {
|
||||
try {
|
||||
// Date string is assumed to be UTC in Storage Explorer Standalone.
|
||||
// Behavior may vary in other browsers.
|
||||
// Here's an example of how the string looks like "2015-10-24T21:44:12"
|
||||
var millisecondTime = Date.parse(dateString),
|
||||
parsed: Date = new Date(millisecondTime);
|
||||
|
||||
if (isUTC) {
|
||||
date = parsed;
|
||||
} else {
|
||||
// Since we parsed in UTC, accessors are flipped - we get local time from the getUTC* group
|
||||
// Reinstating, the date is parsed above as UTC, and here we are creating a new date object
|
||||
// in local time.
|
||||
var year = parsed.getUTCFullYear(),
|
||||
month = parsed.getUTCMonth(),
|
||||
day = parsed.getUTCDate(),
|
||||
hours = parsed.getUTCHours(),
|
||||
minutes = parsed.getUTCMinutes(),
|
||||
seconds = parsed.getUTCSeconds(),
|
||||
milliseconds = parsed.getUTCMilliseconds();
|
||||
|
||||
date = new Date(year, month, day, hours, minutes, seconds, milliseconds);
|
||||
}
|
||||
} catch (error) {
|
||||
//Debug.error("Error parsing date string: ", dateString, error);
|
||||
}
|
||||
}
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
export function utcFromLocalDateString(localDateString: string): string {
|
||||
// TODO validate localDateString
|
||||
var localDate = parseDate(localDateString, false),
|
||||
utcDateString: string = null;
|
||||
|
||||
if (localDate) {
|
||||
utcDateString = localDate.toISOString();
|
||||
}
|
||||
|
||||
return utcDateString;
|
||||
}
|
||||
|
||||
function padIfNeeded(value: number): string {
|
||||
var padded: string = String(value);
|
||||
|
||||
if (0 <= value && value < 10) {
|
||||
padded = "0" + padded;
|
||||
}
|
||||
|
||||
return padded;
|
||||
}
|
||||
|
||||
function toLocalDateString(date: Date): string {
|
||||
var localDateString: string = null;
|
||||
|
||||
if (date) {
|
||||
localDateString =
|
||||
date.getFullYear() +
|
||||
"-" +
|
||||
padIfNeeded(date.getMonth() + 1) +
|
||||
"-" +
|
||||
padIfNeeded(date.getDate()) +
|
||||
"T" +
|
||||
padIfNeeded(date.getHours()) +
|
||||
":" +
|
||||
padIfNeeded(date.getMinutes()) +
|
||||
":" +
|
||||
padIfNeeded(date.getSeconds());
|
||||
}
|
||||
|
||||
return localDateString;
|
||||
}
|
||||
|
||||
export function localFromUtcDateString(utcDateString: string): string {
|
||||
// TODO validate utcDateString
|
||||
var utcDate: Date = parseDate(utcDateString, true),
|
||||
localDateString: string = null;
|
||||
|
||||
if (utcDate) {
|
||||
localDateString = toLocalDateString(utcDate);
|
||||
}
|
||||
|
||||
return localDateString;
|
||||
}
|
||||
|
||||
export function tryChangeTimestampTimeZone(koTimestamp: ko.Observable<string>, toUTC: boolean): void {
|
||||
if (koTimestamp) {
|
||||
var currentDateString: string = koTimestamp(),
|
||||
newDateString: string;
|
||||
|
||||
if (currentDateString) {
|
||||
if (toUTC) {
|
||||
newDateString = utcFromLocalDateString(currentDateString);
|
||||
// removing last character because cannot format it to html binding with the 'Z' at the end
|
||||
newDateString = newDateString.substring(0, newDateString.length - 1);
|
||||
} else {
|
||||
newDateString = localFromUtcDateString(currentDateString);
|
||||
}
|
||||
|
||||
// utcFromLocalDateString and localFromUtcDateString could return null if currentDateString is invalid.
|
||||
// Hence, only set koTimestamp if newDateString is not null.
|
||||
if (newDateString) {
|
||||
koTimestamp(newDateString);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Input validation helpers
|
||||
*/
|
||||
|
||||
export var noTooltip = "",
|
||||
invalidStartTimeTooltip = "Please provide a valid start time.", // localize
|
||||
invalidExpiryTimeRequiredTooltip = "Required field. Please provide a valid expiry time.", // localize
|
||||
invalidExpiryTimeGreaterThanStartTimeTooltip = "The expiry time must be greater than the start time."; // localize
|
||||
|
||||
export function isDateString(dateString: string): boolean {
|
||||
var success: boolean = false;
|
||||
|
||||
if (dateString) {
|
||||
var date: number = Date.parse(dateString);
|
||||
|
||||
success = $.isNumeric(date);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
// Is date string and earlier than expiry time; or is empty
|
||||
// export function isInvalidStartTimeInput(startTimestamp: string, expiryTimestamp: string, isUTC: boolean): DialogsCommon.IValidationResult {
|
||||
// var tooltip: string = noTooltip,
|
||||
// isValid: boolean = isDateString(startTimestamp),
|
||||
// startDate: Date,
|
||||
// expiryDate: Date;
|
||||
|
||||
// if (!isValid) {
|
||||
// isValid = (startTimestamp === "");
|
||||
// }
|
||||
|
||||
// if (!isValid) {
|
||||
// tooltip = invalidStartTimeTooltip;
|
||||
// }
|
||||
|
||||
// if (isValid && !!startTimestamp && isDateString(expiryTimestamp)) {
|
||||
// startDate = parseDate(startTimestamp, isUTC);
|
||||
// expiryDate = parseDate(expiryTimestamp, isUTC);
|
||||
|
||||
// isValid = (startDate < expiryDate);
|
||||
|
||||
// if (!isValid) {
|
||||
// tooltip = invalidExpiryTimeGreaterThanStartTimeTooltip;
|
||||
// }
|
||||
// }
|
||||
|
||||
// return { isInvalid: !isValid, help: tooltip };
|
||||
// }
|
||||
|
||||
// Is date string, and later than start time (if any)
|
||||
// export function isInvalidExpiryTimeInput(startTimestamp: string, expiryTimestamp: string, isUTC: boolean): DialogsCommon.IValidationResult {
|
||||
// var isValid: boolean = isDateString(expiryTimestamp),
|
||||
// tooltip: string = isValid ? noTooltip : invalidExpiryTimeRequiredTooltip,
|
||||
// startDate: Date,
|
||||
// expiryDate: Date;
|
||||
|
||||
// if (isValid && startTimestamp) {
|
||||
// if (isDateString(startTimestamp)) {
|
||||
// startDate = parseDate(startTimestamp, isUTC);
|
||||
// expiryDate = parseDate(expiryTimestamp, isUTC);
|
||||
// isValid = (startDate < expiryDate);
|
||||
|
||||
// if (!isValid) {
|
||||
// tooltip = invalidExpiryTimeGreaterThanStartTimeTooltip;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return { isInvalid: !isValid, help: tooltip };
|
||||
// }
|
||||
|
||||
/**
|
||||
* Functions to calculate DateTime Strings
|
||||
*/
|
||||
|
||||
function _getLocalIsoDateTimeString(time: Date): string {
|
||||
// yyyy-mm-ddThh:mm:ss.sss
|
||||
// Not using the timezone offset (or 'Z'), which will make the
|
||||
// date/time represent local time by default.
|
||||
// var formatted = _string.sprintf(
|
||||
// "%sT%02d:%02d:%02d.%03d",
|
||||
// _getLocalIsoDateString(time),
|
||||
// time.getHours(),
|
||||
// time.getMinutes(),
|
||||
// time.getSeconds(),
|
||||
// time.getMilliseconds()
|
||||
// );
|
||||
// return formatted;
|
||||
return (
|
||||
_getLocalIsoDateString(time) +
|
||||
"T" +
|
||||
DateTimeUtilities.ensureDoubleDigits(time.getHours()) +
|
||||
":" +
|
||||
DateTimeUtilities.ensureDoubleDigits(time.getMinutes()) +
|
||||
":" +
|
||||
DateTimeUtilities.ensureDoubleDigits(time.getSeconds()) +
|
||||
"." +
|
||||
DateTimeUtilities.ensureTripleDigits(time.getMilliseconds())
|
||||
);
|
||||
}
|
||||
|
||||
function _getLocalIsoDateString(date: Date): string {
|
||||
return _getLocalIsoDateStringFromParts(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
}
|
||||
|
||||
function _getLocalIsoDateStringFromParts(
|
||||
fullYear: number,
|
||||
month: number /* 0..11 */,
|
||||
date: number /* 1..31 */
|
||||
): string {
|
||||
month = month + 1;
|
||||
return (
|
||||
fullYear + "-" + DateTimeUtilities.ensureDoubleDigits(month) + "-" + DateTimeUtilities.ensureDoubleDigits(date)
|
||||
);
|
||||
// return _string.sprintf(
|
||||
// "%04d-%02d-%02d",
|
||||
// fullYear,
|
||||
// month + 1, // JS month is 0..11
|
||||
// date); // but date is 1..31
|
||||
}
|
||||
|
||||
function _addDaysHours(time: Date, days: number, hours: number): Date {
|
||||
var msPerHour = 1000 * 60 * 60;
|
||||
var daysMs = days * msPerHour * 24;
|
||||
var hoursMs = hours * msPerHour;
|
||||
var newTimeMs = time.getTime() + daysMs + hoursMs;
|
||||
return new Date(newTimeMs);
|
||||
}
|
||||
|
||||
function _daysHoursBeforeNow(days: number, hours: number): Date {
|
||||
return _addDaysHours(new Date(), -days, -hours);
|
||||
}
|
||||
|
||||
export function _queryLastDaysHours(days: number, hours: number): string {
|
||||
/* tslint:disable: no-unused-variable */
|
||||
var daysHoursAgo = _getLocalIsoDateTimeString(_daysHoursBeforeNow(days, hours));
|
||||
daysHoursAgo = DateTimeUtilities.getUTCDateTime(daysHoursAgo);
|
||||
|
||||
return daysHoursAgo;
|
||||
/* tslint:enable: no-unused-variable */
|
||||
}
|
||||
|
||||
export function _queryCurrentMonthLocal(): string {
|
||||
var now = new Date();
|
||||
var start = _getLocalIsoDateStringFromParts(now.getFullYear(), now.getMonth(), 1);
|
||||
start = DateTimeUtilities.getUTCDateTime(start);
|
||||
return start;
|
||||
}
|
||||
|
||||
export function _queryCurrentYearLocal(): string {
|
||||
var now = new Date();
|
||||
var start = _getLocalIsoDateStringFromParts(now.getFullYear(), 0, 1); // Month is 0..11, date is 1..31
|
||||
start = DateTimeUtilities.getUTCDateTime(start);
|
||||
return start;
|
||||
}
|
||||
|
||||
function _addTime(time: Date, lastNumber: number, timeUnit: string): Date {
|
||||
var timeMS: number;
|
||||
switch (TimeUnit[Number(timeUnit)]) {
|
||||
case TimeUnit.Days.toString():
|
||||
timeMS = lastNumber * 1000 * 60 * 60 * 24;
|
||||
break;
|
||||
case TimeUnit.Hours.toString():
|
||||
timeMS = lastNumber * 1000 * 60 * 60;
|
||||
break;
|
||||
case TimeUnit.Minutes.toString():
|
||||
timeMS = lastNumber * 1000 * 60;
|
||||
break;
|
||||
case TimeUnit.Seconds.toString():
|
||||
timeMS = lastNumber * 1000;
|
||||
break;
|
||||
default:
|
||||
//throw new Errors.ArgumentOutOfRangeError(timeUnit);
|
||||
}
|
||||
var newTimeMS = time.getTime() + timeMS;
|
||||
return new Date(newTimeMS);
|
||||
}
|
||||
|
||||
function _timeBeforeNow(lastNumber: number, timeUnit: string): Date {
|
||||
return _addTime(new Date(), -lastNumber, timeUnit);
|
||||
}
|
||||
|
||||
export function _queryLastTime(lastNumber: number, timeUnit: string): string {
|
||||
/* tslint:disable: no-unused-variable */
|
||||
var daysHoursAgo = _getLocalIsoDateTimeString(_timeBeforeNow(lastNumber, timeUnit));
|
||||
daysHoursAgo = DateTimeUtilities.getUTCDateTime(daysHoursAgo);
|
||||
return daysHoursAgo;
|
||||
/* tslint:enable: no-unused-variable */
|
||||
}
|
||||
import QueryBuilderViewModel from "./QueryBuilderViewModel";
|
||||
import QueryClauseViewModel from "./QueryClauseViewModel";
|
||||
import * as DateTimeUtilities from "./DateTimeUtilities";
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
export var utc = "utc";
|
||||
export var local = "local";
|
||||
|
||||
export interface ITimestampQuery {
|
||||
queryType: string; // valid values are "last" and "range"
|
||||
lastNumber: number; // number value of a custom timestamp using the last option
|
||||
lastTimeUnit: string; // timeunit of a custom timestamp using the last option
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
timeZone: string; // timezone of custom range timestamp, valid values are "local" and "utc"
|
||||
}
|
||||
|
||||
export interface ILastQuery {
|
||||
lastNumber: number;
|
||||
lastTimeUnit: string;
|
||||
}
|
||||
|
||||
export enum TimeUnit {
|
||||
Seconds,
|
||||
Minutes,
|
||||
Hours,
|
||||
Days,
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting helpers
|
||||
*/
|
||||
|
||||
export function addRangeTimestamp(
|
||||
timestamp: ITimestampQuery,
|
||||
queryBuilderViewModel: QueryBuilderViewModel,
|
||||
queryClauseViewModel: QueryClauseViewModel
|
||||
): void {
|
||||
queryBuilderViewModel.addCustomRange(timestamp, queryClauseViewModel);
|
||||
}
|
||||
|
||||
export function getDefaultStart(localTime: boolean, durationHours: number = 24): string {
|
||||
var startTimestamp: string;
|
||||
|
||||
var utcNowString: string = new Date().toISOString();
|
||||
var yesterday: Date = new Date(utcNowString);
|
||||
|
||||
yesterday.setHours(yesterday.getHours() - durationHours);
|
||||
startTimestamp = yesterday.toISOString();
|
||||
|
||||
if (localTime) {
|
||||
startTimestamp = localFromUtcDateString(startTimestamp);
|
||||
}
|
||||
|
||||
return startTimestamp;
|
||||
}
|
||||
|
||||
export function getDefaultEnd(localTime: boolean): string {
|
||||
var endTimestamp: string;
|
||||
|
||||
var utcNowString: string = new Date().toISOString();
|
||||
|
||||
endTimestamp = utcNowString;
|
||||
|
||||
if (localTime) {
|
||||
endTimestamp = localFromUtcDateString(endTimestamp);
|
||||
}
|
||||
|
||||
return endTimestamp;
|
||||
}
|
||||
|
||||
export function parseDate(dateString: string, isUTC: boolean): Date {
|
||||
// TODO validate dateString
|
||||
var date: Date = null;
|
||||
|
||||
if (dateString) {
|
||||
try {
|
||||
// Date string is assumed to be UTC in Storage Explorer Standalone.
|
||||
// Behavior may vary in other browsers.
|
||||
// Here's an example of how the string looks like "2015-10-24T21:44:12"
|
||||
var millisecondTime = Date.parse(dateString),
|
||||
parsed: Date = new Date(millisecondTime);
|
||||
|
||||
if (isUTC) {
|
||||
date = parsed;
|
||||
} else {
|
||||
// Since we parsed in UTC, accessors are flipped - we get local time from the getUTC* group
|
||||
// Reinstating, the date is parsed above as UTC, and here we are creating a new date object
|
||||
// in local time.
|
||||
var year = parsed.getUTCFullYear(),
|
||||
month = parsed.getUTCMonth(),
|
||||
day = parsed.getUTCDate(),
|
||||
hours = parsed.getUTCHours(),
|
||||
minutes = parsed.getUTCMinutes(),
|
||||
seconds = parsed.getUTCSeconds(),
|
||||
milliseconds = parsed.getUTCMilliseconds();
|
||||
|
||||
date = new Date(year, month, day, hours, minutes, seconds, milliseconds);
|
||||
}
|
||||
} catch (error) {
|
||||
//Debug.error("Error parsing date string: ", dateString, error);
|
||||
}
|
||||
}
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
export function utcFromLocalDateString(localDateString: string): string {
|
||||
// TODO validate localDateString
|
||||
var localDate = parseDate(localDateString, false),
|
||||
utcDateString: string = null;
|
||||
|
||||
if (localDate) {
|
||||
utcDateString = localDate.toISOString();
|
||||
}
|
||||
|
||||
return utcDateString;
|
||||
}
|
||||
|
||||
function padIfNeeded(value: number): string {
|
||||
var padded: string = String(value);
|
||||
|
||||
if (0 <= value && value < 10) {
|
||||
padded = "0" + padded;
|
||||
}
|
||||
|
||||
return padded;
|
||||
}
|
||||
|
||||
function toLocalDateString(date: Date): string {
|
||||
var localDateString: string = null;
|
||||
|
||||
if (date) {
|
||||
localDateString =
|
||||
date.getFullYear() +
|
||||
"-" +
|
||||
padIfNeeded(date.getMonth() + 1) +
|
||||
"-" +
|
||||
padIfNeeded(date.getDate()) +
|
||||
"T" +
|
||||
padIfNeeded(date.getHours()) +
|
||||
":" +
|
||||
padIfNeeded(date.getMinutes()) +
|
||||
":" +
|
||||
padIfNeeded(date.getSeconds());
|
||||
}
|
||||
|
||||
return localDateString;
|
||||
}
|
||||
|
||||
export function localFromUtcDateString(utcDateString: string): string {
|
||||
// TODO validate utcDateString
|
||||
var utcDate: Date = parseDate(utcDateString, true),
|
||||
localDateString: string = null;
|
||||
|
||||
if (utcDate) {
|
||||
localDateString = toLocalDateString(utcDate);
|
||||
}
|
||||
|
||||
return localDateString;
|
||||
}
|
||||
|
||||
export function tryChangeTimestampTimeZone(koTimestamp: ko.Observable<string>, toUTC: boolean): void {
|
||||
if (koTimestamp) {
|
||||
var currentDateString: string = koTimestamp(),
|
||||
newDateString: string;
|
||||
|
||||
if (currentDateString) {
|
||||
if (toUTC) {
|
||||
newDateString = utcFromLocalDateString(currentDateString);
|
||||
// removing last character because cannot format it to html binding with the 'Z' at the end
|
||||
newDateString = newDateString.substring(0, newDateString.length - 1);
|
||||
} else {
|
||||
newDateString = localFromUtcDateString(currentDateString);
|
||||
}
|
||||
|
||||
// utcFromLocalDateString and localFromUtcDateString could return null if currentDateString is invalid.
|
||||
// Hence, only set koTimestamp if newDateString is not null.
|
||||
if (newDateString) {
|
||||
koTimestamp(newDateString);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Input validation helpers
|
||||
*/
|
||||
|
||||
export var noTooltip = "",
|
||||
invalidStartTimeTooltip = "Please provide a valid start time.", // localize
|
||||
invalidExpiryTimeRequiredTooltip = "Required field. Please provide a valid expiry time.", // localize
|
||||
invalidExpiryTimeGreaterThanStartTimeTooltip = "The expiry time must be greater than the start time."; // localize
|
||||
|
||||
export function isDateString(dateString: string): boolean {
|
||||
var success: boolean = false;
|
||||
|
||||
if (dateString) {
|
||||
var date: number = Date.parse(dateString);
|
||||
|
||||
success = $.isNumeric(date);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
// Is date string and earlier than expiry time; or is empty
|
||||
// export function isInvalidStartTimeInput(startTimestamp: string, expiryTimestamp: string, isUTC: boolean): DialogsCommon.IValidationResult {
|
||||
// var tooltip: string = noTooltip,
|
||||
// isValid: boolean = isDateString(startTimestamp),
|
||||
// startDate: Date,
|
||||
// expiryDate: Date;
|
||||
|
||||
// if (!isValid) {
|
||||
// isValid = (startTimestamp === "");
|
||||
// }
|
||||
|
||||
// if (!isValid) {
|
||||
// tooltip = invalidStartTimeTooltip;
|
||||
// }
|
||||
|
||||
// if (isValid && !!startTimestamp && isDateString(expiryTimestamp)) {
|
||||
// startDate = parseDate(startTimestamp, isUTC);
|
||||
// expiryDate = parseDate(expiryTimestamp, isUTC);
|
||||
|
||||
// isValid = (startDate < expiryDate);
|
||||
|
||||
// if (!isValid) {
|
||||
// tooltip = invalidExpiryTimeGreaterThanStartTimeTooltip;
|
||||
// }
|
||||
// }
|
||||
|
||||
// return { isInvalid: !isValid, help: tooltip };
|
||||
// }
|
||||
|
||||
// Is date string, and later than start time (if any)
|
||||
// export function isInvalidExpiryTimeInput(startTimestamp: string, expiryTimestamp: string, isUTC: boolean): DialogsCommon.IValidationResult {
|
||||
// var isValid: boolean = isDateString(expiryTimestamp),
|
||||
// tooltip: string = isValid ? noTooltip : invalidExpiryTimeRequiredTooltip,
|
||||
// startDate: Date,
|
||||
// expiryDate: Date;
|
||||
|
||||
// if (isValid && startTimestamp) {
|
||||
// if (isDateString(startTimestamp)) {
|
||||
// startDate = parseDate(startTimestamp, isUTC);
|
||||
// expiryDate = parseDate(expiryTimestamp, isUTC);
|
||||
// isValid = (startDate < expiryDate);
|
||||
|
||||
// if (!isValid) {
|
||||
// tooltip = invalidExpiryTimeGreaterThanStartTimeTooltip;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return { isInvalid: !isValid, help: tooltip };
|
||||
// }
|
||||
|
||||
/**
|
||||
* Functions to calculate DateTime Strings
|
||||
*/
|
||||
|
||||
function _getLocalIsoDateTimeString(time: Date): string {
|
||||
// yyyy-mm-ddThh:mm:ss.sss
|
||||
// Not using the timezone offset (or 'Z'), which will make the
|
||||
// date/time represent local time by default.
|
||||
// var formatted = _string.sprintf(
|
||||
// "%sT%02d:%02d:%02d.%03d",
|
||||
// _getLocalIsoDateString(time),
|
||||
// time.getHours(),
|
||||
// time.getMinutes(),
|
||||
// time.getSeconds(),
|
||||
// time.getMilliseconds()
|
||||
// );
|
||||
// return formatted;
|
||||
return (
|
||||
_getLocalIsoDateString(time) +
|
||||
"T" +
|
||||
DateTimeUtilities.ensureDoubleDigits(time.getHours()) +
|
||||
":" +
|
||||
DateTimeUtilities.ensureDoubleDigits(time.getMinutes()) +
|
||||
":" +
|
||||
DateTimeUtilities.ensureDoubleDigits(time.getSeconds()) +
|
||||
"." +
|
||||
DateTimeUtilities.ensureTripleDigits(time.getMilliseconds())
|
||||
);
|
||||
}
|
||||
|
||||
function _getLocalIsoDateString(date: Date): string {
|
||||
return _getLocalIsoDateStringFromParts(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
}
|
||||
|
||||
function _getLocalIsoDateStringFromParts(
|
||||
fullYear: number,
|
||||
month: number /* 0..11 */,
|
||||
date: number /* 1..31 */
|
||||
): string {
|
||||
month = month + 1;
|
||||
return (
|
||||
fullYear + "-" + DateTimeUtilities.ensureDoubleDigits(month) + "-" + DateTimeUtilities.ensureDoubleDigits(date)
|
||||
);
|
||||
// return _string.sprintf(
|
||||
// "%04d-%02d-%02d",
|
||||
// fullYear,
|
||||
// month + 1, // JS month is 0..11
|
||||
// date); // but date is 1..31
|
||||
}
|
||||
|
||||
function _addDaysHours(time: Date, days: number, hours: number): Date {
|
||||
var msPerHour = 1000 * 60 * 60;
|
||||
var daysMs = days * msPerHour * 24;
|
||||
var hoursMs = hours * msPerHour;
|
||||
var newTimeMs = time.getTime() + daysMs + hoursMs;
|
||||
return new Date(newTimeMs);
|
||||
}
|
||||
|
||||
function _daysHoursBeforeNow(days: number, hours: number): Date {
|
||||
return _addDaysHours(new Date(), -days, -hours);
|
||||
}
|
||||
|
||||
export function _queryLastDaysHours(days: number, hours: number): string {
|
||||
/* tslint:disable: no-unused-variable */
|
||||
var daysHoursAgo = _getLocalIsoDateTimeString(_daysHoursBeforeNow(days, hours));
|
||||
daysHoursAgo = DateTimeUtilities.getUTCDateTime(daysHoursAgo);
|
||||
|
||||
return daysHoursAgo;
|
||||
/* tslint:enable: no-unused-variable */
|
||||
}
|
||||
|
||||
export function _queryCurrentMonthLocal(): string {
|
||||
var now = new Date();
|
||||
var start = _getLocalIsoDateStringFromParts(now.getFullYear(), now.getMonth(), 1);
|
||||
start = DateTimeUtilities.getUTCDateTime(start);
|
||||
return start;
|
||||
}
|
||||
|
||||
export function _queryCurrentYearLocal(): string {
|
||||
var now = new Date();
|
||||
var start = _getLocalIsoDateStringFromParts(now.getFullYear(), 0, 1); // Month is 0..11, date is 1..31
|
||||
start = DateTimeUtilities.getUTCDateTime(start);
|
||||
return start;
|
||||
}
|
||||
|
||||
function _addTime(time: Date, lastNumber: number, timeUnit: string): Date {
|
||||
var timeMS: number;
|
||||
switch (TimeUnit[Number(timeUnit)]) {
|
||||
case TimeUnit.Days.toString():
|
||||
timeMS = lastNumber * 1000 * 60 * 60 * 24;
|
||||
break;
|
||||
case TimeUnit.Hours.toString():
|
||||
timeMS = lastNumber * 1000 * 60 * 60;
|
||||
break;
|
||||
case TimeUnit.Minutes.toString():
|
||||
timeMS = lastNumber * 1000 * 60;
|
||||
break;
|
||||
case TimeUnit.Seconds.toString():
|
||||
timeMS = lastNumber * 1000;
|
||||
break;
|
||||
default:
|
||||
//throw new Errors.ArgumentOutOfRangeError(timeUnit);
|
||||
}
|
||||
var newTimeMS = time.getTime() + timeMS;
|
||||
return new Date(newTimeMS);
|
||||
}
|
||||
|
||||
function _timeBeforeNow(lastNumber: number, timeUnit: string): Date {
|
||||
return _addTime(new Date(), -lastNumber, timeUnit);
|
||||
}
|
||||
|
||||
export function _queryLastTime(lastNumber: number, timeUnit: string): string {
|
||||
/* tslint:disable: no-unused-variable */
|
||||
var daysHoursAgo = _getLocalIsoDateTimeString(_timeBeforeNow(lastNumber, timeUnit));
|
||||
daysHoursAgo = DateTimeUtilities.getUTCDateTime(daysHoursAgo);
|
||||
return daysHoursAgo;
|
||||
/* tslint:enable: no-unused-variable */
|
||||
}
|
||||
|
||||
@@ -1,67 +1,67 @@
|
||||
const epochTicks = 621355968000000000;
|
||||
const ticksPerMillisecond = 10000;
|
||||
|
||||
export function getLocalDateTime(dateTime: string): string {
|
||||
var dateTimeObject: Date = new Date(dateTime);
|
||||
var year: number = dateTimeObject.getFullYear();
|
||||
var month: string = ensureDoubleDigits(dateTimeObject.getMonth() + 1); // Month ranges from 0 to 11
|
||||
var day: string = ensureDoubleDigits(dateTimeObject.getDate());
|
||||
var hours: string = ensureDoubleDigits(dateTimeObject.getHours());
|
||||
var minutes: string = ensureDoubleDigits(dateTimeObject.getMinutes());
|
||||
var seconds: string = ensureDoubleDigits(dateTimeObject.getSeconds());
|
||||
var milliseconds: string = ensureTripleDigits(dateTimeObject.getMilliseconds());
|
||||
|
||||
var localDateTime: string = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${milliseconds}`;
|
||||
return localDateTime;
|
||||
}
|
||||
|
||||
export function getUTCDateTime(dateTime: string): string {
|
||||
var dateTimeObject: Date = new Date(dateTime);
|
||||
return dateTimeObject.toISOString();
|
||||
}
|
||||
|
||||
export function ensureDoubleDigits(num: number): string {
|
||||
var doubleDigitsString: string = num.toString();
|
||||
if (num < 10) {
|
||||
doubleDigitsString = `0${doubleDigitsString}`;
|
||||
} else if (num > 99) {
|
||||
doubleDigitsString = doubleDigitsString.substring(0, 2);
|
||||
}
|
||||
return doubleDigitsString;
|
||||
}
|
||||
|
||||
export function ensureTripleDigits(num: number): string {
|
||||
var tripleDigitsString: string = num.toString();
|
||||
if (num < 10) {
|
||||
tripleDigitsString = `00${tripleDigitsString}`;
|
||||
} else if (num < 100) {
|
||||
tripleDigitsString = `0${tripleDigitsString}`;
|
||||
} else if (num > 999) {
|
||||
tripleDigitsString = tripleDigitsString.substring(0, 3);
|
||||
}
|
||||
return tripleDigitsString;
|
||||
}
|
||||
|
||||
export function convertUnixToJSDate(unixTime: number): Date {
|
||||
return new Date(unixTime * 1000);
|
||||
}
|
||||
|
||||
export function convertJSDateToUnix(dateTime: string): number {
|
||||
return Number((new Date(dateTime).getTime() / 1000).toFixed(0));
|
||||
}
|
||||
|
||||
export function convertTicksToJSDate(ticks: string): Date {
|
||||
var ticksJSBased = Number(ticks) - epochTicks;
|
||||
var timeInMillisecond = ticksJSBased / ticksPerMillisecond;
|
||||
return new Date(timeInMillisecond);
|
||||
}
|
||||
|
||||
export function convertJSDateToTicksWithPadding(dateTime: string): string {
|
||||
var ticks = epochTicks + new Date(dateTime).getTime() * ticksPerMillisecond;
|
||||
return padDateTicksWithZeros(ticks.toString());
|
||||
}
|
||||
|
||||
function padDateTicksWithZeros(value: string): string {
|
||||
var s = "0000000000000000000" + value;
|
||||
return s.substr(s.length - 20);
|
||||
}
|
||||
const epochTicks = 621355968000000000;
|
||||
const ticksPerMillisecond = 10000;
|
||||
|
||||
export function getLocalDateTime(dateTime: string): string {
|
||||
var dateTimeObject: Date = new Date(dateTime);
|
||||
var year: number = dateTimeObject.getFullYear();
|
||||
var month: string = ensureDoubleDigits(dateTimeObject.getMonth() + 1); // Month ranges from 0 to 11
|
||||
var day: string = ensureDoubleDigits(dateTimeObject.getDate());
|
||||
var hours: string = ensureDoubleDigits(dateTimeObject.getHours());
|
||||
var minutes: string = ensureDoubleDigits(dateTimeObject.getMinutes());
|
||||
var seconds: string = ensureDoubleDigits(dateTimeObject.getSeconds());
|
||||
var milliseconds: string = ensureTripleDigits(dateTimeObject.getMilliseconds());
|
||||
|
||||
var localDateTime: string = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${milliseconds}`;
|
||||
return localDateTime;
|
||||
}
|
||||
|
||||
export function getUTCDateTime(dateTime: string): string {
|
||||
var dateTimeObject: Date = new Date(dateTime);
|
||||
return dateTimeObject.toISOString();
|
||||
}
|
||||
|
||||
export function ensureDoubleDigits(num: number): string {
|
||||
var doubleDigitsString: string = num.toString();
|
||||
if (num < 10) {
|
||||
doubleDigitsString = `0${doubleDigitsString}`;
|
||||
} else if (num > 99) {
|
||||
doubleDigitsString = doubleDigitsString.substring(0, 2);
|
||||
}
|
||||
return doubleDigitsString;
|
||||
}
|
||||
|
||||
export function ensureTripleDigits(num: number): string {
|
||||
var tripleDigitsString: string = num.toString();
|
||||
if (num < 10) {
|
||||
tripleDigitsString = `00${tripleDigitsString}`;
|
||||
} else if (num < 100) {
|
||||
tripleDigitsString = `0${tripleDigitsString}`;
|
||||
} else if (num > 999) {
|
||||
tripleDigitsString = tripleDigitsString.substring(0, 3);
|
||||
}
|
||||
return tripleDigitsString;
|
||||
}
|
||||
|
||||
export function convertUnixToJSDate(unixTime: number): Date {
|
||||
return new Date(unixTime * 1000);
|
||||
}
|
||||
|
||||
export function convertJSDateToUnix(dateTime: string): number {
|
||||
return Number((new Date(dateTime).getTime() / 1000).toFixed(0));
|
||||
}
|
||||
|
||||
export function convertTicksToJSDate(ticks: string): Date {
|
||||
var ticksJSBased = Number(ticks) - epochTicks;
|
||||
var timeInMillisecond = ticksJSBased / ticksPerMillisecond;
|
||||
return new Date(timeInMillisecond);
|
||||
}
|
||||
|
||||
export function convertJSDateToTicksWithPadding(dateTime: string): string {
|
||||
var ticks = epochTicks + new Date(dateTime).getTime() * ticksPerMillisecond;
|
||||
return padDateTicksWithZeros(ticks.toString());
|
||||
}
|
||||
|
||||
function padDateTicksWithZeros(value: string): string {
|
||||
var s = "0000000000000000000" + value;
|
||||
return s.substr(s.length - 20);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,285 +1,285 @@
|
||||
import * as ko from "knockout";
|
||||
import _ from "underscore";
|
||||
import * as QueryBuilderConstants from "../Constants";
|
||||
import QueryBuilderViewModel from "./QueryBuilderViewModel";
|
||||
import ClauseGroup from "./ClauseGroup";
|
||||
import * as Utilities from "../Utilities";
|
||||
|
||||
export default class QueryClauseViewModel {
|
||||
public checkedForGrouping: ko.Observable<boolean>;
|
||||
public isFirstInGroup: ko.Observable<boolean>;
|
||||
public clauseGroup: ClauseGroup;
|
||||
public and_or: ko.Observable<string>;
|
||||
public field: ko.Observable<string>;
|
||||
public type: ko.Observable<string>;
|
||||
public operator: ko.Observable<string>;
|
||||
public value: ko.Observable<any>;
|
||||
public timeValue: ko.Observable<string>;
|
||||
public customTimeValue: ko.Observable<string>;
|
||||
public canAnd: ko.Observable<boolean>;
|
||||
public timestampType: ko.Observable<string>;
|
||||
//public customLastTimestamp: ko.Observable<CustomTimestampHelper.ILastQuery>;
|
||||
public isLocal: ko.Observable<boolean>;
|
||||
public isOperaterEditable: ko.PureComputed<boolean>;
|
||||
public isTypeEditable: ko.PureComputed<boolean>;
|
||||
public isValue: ko.Observable<boolean>;
|
||||
public isTimestamp: ko.Observable<boolean>;
|
||||
public isCustomLastTimestamp: ko.Observable<boolean>;
|
||||
public isCustomRangeTimestamp: ko.Observable<boolean>;
|
||||
private _queryBuilderViewModel: QueryBuilderViewModel;
|
||||
private _groupCheckSubscription: ko.Subscription;
|
||||
private _id: string;
|
||||
public isAndOrFocused: ko.Observable<boolean>;
|
||||
public isDeleteButtonFocused: ko.Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
queryBuilderViewModel: QueryBuilderViewModel,
|
||||
and_or: string,
|
||||
field: string,
|
||||
type: string,
|
||||
operator: string,
|
||||
value: any,
|
||||
canAnd: boolean,
|
||||
timeValue: string,
|
||||
customTimeValue: string,
|
||||
timestampType: string,
|
||||
//customLastTimestamp: CustomTimestampHelper.ILastQuery,
|
||||
isLocal: boolean,
|
||||
id?: string
|
||||
) {
|
||||
this._queryBuilderViewModel = queryBuilderViewModel;
|
||||
this.checkedForGrouping = ko.observable<boolean>(false);
|
||||
this.isFirstInGroup = ko.observable<boolean>(false);
|
||||
this.and_or = ko.observable<string>(and_or);
|
||||
this.field = ko.observable<string>(field);
|
||||
this.type = ko.observable<string>(type);
|
||||
this.operator = ko.observable<string>(operator);
|
||||
this.value = ko.observable<string>(value);
|
||||
this.timeValue = ko.observable<string>(timeValue);
|
||||
this.customTimeValue = ko.observable<string>(customTimeValue);
|
||||
this.canAnd = ko.observable<boolean>(canAnd);
|
||||
this.isLocal = ko.observable<boolean>(isLocal);
|
||||
this._id = id ? id : Utilities.guid();
|
||||
|
||||
//this.customLastTimestamp = ko.observable<CustomTimestampHelper.ILastQuery>(customLastTimestamp);
|
||||
//this.setCustomLastTimestamp();
|
||||
|
||||
this.timestampType = ko.observable<string>(timestampType);
|
||||
this.getValueType();
|
||||
|
||||
this.isOperaterEditable = ko.pureComputed<boolean>(() => {
|
||||
const isPreferredApiCassandra = this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra();
|
||||
const cassandraKeys = isPreferredApiCassandra
|
||||
? this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys.map(
|
||||
key => key.property
|
||||
)
|
||||
: [];
|
||||
return (
|
||||
(this.isValue() || this.isCustomRangeTimestamp()) &&
|
||||
(!isPreferredApiCassandra || !_.contains(cassandraKeys, this.field()))
|
||||
);
|
||||
});
|
||||
this.isTypeEditable = ko.pureComputed<boolean>(
|
||||
() =>
|
||||
this.field() !== "Timestamp" &&
|
||||
this.field() !== "PartitionKey" &&
|
||||
this.field() !== "RowKey" &&
|
||||
!this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra()
|
||||
);
|
||||
|
||||
this.and_or.subscribe(value => {
|
||||
this._queryBuilderViewModel.checkIfClauseChanged(this);
|
||||
});
|
||||
this.field.subscribe(value => {
|
||||
this.changeField();
|
||||
});
|
||||
this.type.subscribe(value => {
|
||||
this.changeType();
|
||||
});
|
||||
this.timeValue.subscribe(value => {
|
||||
// if (this.timeValue() === QueryBuilderConstants.timeOptions.custom) {
|
||||
// this.customTimestampDialog();
|
||||
// }
|
||||
});
|
||||
this.customTimeValue.subscribe(value => {
|
||||
this._queryBuilderViewModel.checkIfClauseChanged(this);
|
||||
});
|
||||
this.value.subscribe(value => {
|
||||
this._queryBuilderViewModel.checkIfClauseChanged(this);
|
||||
});
|
||||
this.operator.subscribe(value => {
|
||||
this._queryBuilderViewModel.checkIfClauseChanged(this);
|
||||
});
|
||||
this._groupCheckSubscription = this.checkedForGrouping.subscribe(value => {
|
||||
this._queryBuilderViewModel.updateCanGroupClauses();
|
||||
});
|
||||
this.isAndOrFocused = ko.observable<boolean>(false);
|
||||
this.isDeleteButtonFocused = ko.observable<boolean>(false);
|
||||
}
|
||||
|
||||
// private setCustomLastTimestamp() : void {
|
||||
// if (this.customLastTimestamp() === null) {
|
||||
// var lastNumberandType: CustomTimestampHelper.ILastQuery = {
|
||||
// lastNumber: 7,
|
||||
// lastTimeUnit: "Days"
|
||||
// };
|
||||
// this.customLastTimestamp(lastNumberandType);
|
||||
// }
|
||||
// }
|
||||
|
||||
private getValueType(): void {
|
||||
switch (this.timestampType()) {
|
||||
case "time":
|
||||
this.isValue = ko.observable<boolean>(false);
|
||||
this.isTimestamp = ko.observable<boolean>(true);
|
||||
this.isCustomLastTimestamp = ko.observable<boolean>(false);
|
||||
this.isCustomRangeTimestamp = ko.observable<boolean>(false);
|
||||
break;
|
||||
case "last":
|
||||
this.isValue = ko.observable<boolean>(false);
|
||||
this.isTimestamp = ko.observable<boolean>(false);
|
||||
this.isCustomLastTimestamp = ko.observable<boolean>(true);
|
||||
this.isCustomRangeTimestamp = ko.observable<boolean>(false);
|
||||
break;
|
||||
case "range":
|
||||
this.isValue = ko.observable<boolean>(false);
|
||||
this.isTimestamp = ko.observable<boolean>(false);
|
||||
this.isCustomLastTimestamp = ko.observable<boolean>(false);
|
||||
this.isCustomRangeTimestamp = ko.observable<boolean>(true);
|
||||
break;
|
||||
default:
|
||||
this.isValue = ko.observable<boolean>(true);
|
||||
this.isTimestamp = ko.observable<boolean>(false);
|
||||
this.isCustomLastTimestamp = ko.observable<boolean>(false);
|
||||
this.isCustomRangeTimestamp = ko.observable<boolean>(false);
|
||||
}
|
||||
}
|
||||
|
||||
private changeField(): void {
|
||||
this.isCustomLastTimestamp(false);
|
||||
this.isCustomRangeTimestamp(false);
|
||||
|
||||
if (this.field() === "Timestamp") {
|
||||
this.isValue(false);
|
||||
this.isTimestamp(true);
|
||||
this.type(QueryBuilderConstants.TableType.DateTime);
|
||||
this.operator(QueryBuilderConstants.Operator.GreaterThanOrEqualTo);
|
||||
this.timestampType("time");
|
||||
} else if (this.field() === "PartitionKey" || this.field() === "RowKey") {
|
||||
this.resetFromTimestamp();
|
||||
this.type(QueryBuilderConstants.TableType.String);
|
||||
} else {
|
||||
this.resetFromTimestamp();
|
||||
if (this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra()) {
|
||||
const cassandraSchema = this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.collection
|
||||
.cassandraSchema;
|
||||
for (let i = 0, len = cassandraSchema.length; i < len; i++) {
|
||||
if (cassandraSchema[i].property === this.field()) {
|
||||
this.type(cassandraSchema[i].type);
|
||||
i = len;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.type(QueryBuilderConstants.TableType.String);
|
||||
}
|
||||
}
|
||||
this._queryBuilderViewModel.checkIfClauseChanged(this);
|
||||
}
|
||||
|
||||
private resetFromTimestamp(): void {
|
||||
this.isValue(true);
|
||||
this.isTimestamp(false);
|
||||
this.operator(QueryBuilderConstants.Operator.Equal);
|
||||
this.value("");
|
||||
this.timestampType("");
|
||||
this.timeValue("");
|
||||
this.customTimeValue("");
|
||||
}
|
||||
|
||||
private changeType(): void {
|
||||
this.isCustomLastTimestamp(false);
|
||||
this.isCustomRangeTimestamp(false);
|
||||
|
||||
if (this.type() === QueryBuilderConstants.TableType.DateTime) {
|
||||
this.isValue(false);
|
||||
this.isTimestamp(true);
|
||||
this.operator(QueryBuilderConstants.Operator.GreaterThanOrEqualTo);
|
||||
this.timestampType("time");
|
||||
} else {
|
||||
this.isValue(true);
|
||||
this.isTimestamp(false);
|
||||
this.timeValue("");
|
||||
this.operator(QueryBuilderConstants.Operator.EqualTo);
|
||||
this.value("");
|
||||
this.timestampType("");
|
||||
this.timeValue("");
|
||||
this.customTimeValue("");
|
||||
}
|
||||
this._queryBuilderViewModel.checkIfClauseChanged(this);
|
||||
}
|
||||
|
||||
// private customTimestampDialog(): Promise<any> {
|
||||
// var lastNumber = this.customLastTimestamp().lastNumber;
|
||||
// var lastTimeUnit = this.customLastTimestamp().lastTimeUnit;
|
||||
|
||||
// return this._host.executeOperation("Environment.openDialog", [{
|
||||
// id: AzureConstants.registeredDialogs.customTimestampQueryDialog,
|
||||
// width: 500,
|
||||
// height: 300,
|
||||
// parameters: { lastNumber, lastTimeUnit }
|
||||
// }]).then((timestamp: CustomTimestampHelper.ITimestampQuery) => {
|
||||
// if (timestamp) {
|
||||
// this.isValue(false);
|
||||
// this.isTimestamp(false);
|
||||
// this.timestampType(timestamp.queryType);
|
||||
|
||||
// if (timestamp.queryType === "last") {
|
||||
// this.isCustomLastTimestamp(true);
|
||||
// this.isCustomRangeTimestamp(false);
|
||||
|
||||
// var lastNumberandType: CustomTimestampHelper.ILastQuery = {
|
||||
// lastNumber: timestamp.lastNumber,
|
||||
// lastTimeUnit: timestamp.lastTimeUnit
|
||||
// };
|
||||
|
||||
// this.customLastTimestamp(lastNumberandType);
|
||||
// this.customTimeValue(`Last ${timestamp.lastNumber} ${timestamp.lastTimeUnit}`);
|
||||
|
||||
// } else {
|
||||
// if (timestamp.timeZone === "local") {
|
||||
// this.isLocal = ko.observable(true);
|
||||
// } else {
|
||||
// this.isLocal = ko.observable(false);
|
||||
// }
|
||||
// this.isCustomLastTimestamp(false);
|
||||
// this.isCustomRangeTimestamp(true);
|
||||
// this.customTimeValue(timestamp.startTime);
|
||||
// CustomTimestampHelper.addRangeTimestamp(timestamp, this._queryBuilderViewModel, this);
|
||||
// }
|
||||
// } else {
|
||||
// this.timeValue(QueryBuilderConstants.timeOptions.lastHour);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
public getId(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
public get groupDepth(): number {
|
||||
if (this.clauseGroup) {
|
||||
return this.clauseGroup.getCurrentGroupDepth();
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._groupCheckSubscription) {
|
||||
this._groupCheckSubscription.dispose();
|
||||
}
|
||||
|
||||
this.clauseGroup = null;
|
||||
this._queryBuilderViewModel = null;
|
||||
}
|
||||
}
|
||||
import * as ko from "knockout";
|
||||
import _ from "underscore";
|
||||
import * as QueryBuilderConstants from "../Constants";
|
||||
import QueryBuilderViewModel from "./QueryBuilderViewModel";
|
||||
import ClauseGroup from "./ClauseGroup";
|
||||
import * as Utilities from "../Utilities";
|
||||
|
||||
export default class QueryClauseViewModel {
|
||||
public checkedForGrouping: ko.Observable<boolean>;
|
||||
public isFirstInGroup: ko.Observable<boolean>;
|
||||
public clauseGroup: ClauseGroup;
|
||||
public and_or: ko.Observable<string>;
|
||||
public field: ko.Observable<string>;
|
||||
public type: ko.Observable<string>;
|
||||
public operator: ko.Observable<string>;
|
||||
public value: ko.Observable<any>;
|
||||
public timeValue: ko.Observable<string>;
|
||||
public customTimeValue: ko.Observable<string>;
|
||||
public canAnd: ko.Observable<boolean>;
|
||||
public timestampType: ko.Observable<string>;
|
||||
//public customLastTimestamp: ko.Observable<CustomTimestampHelper.ILastQuery>;
|
||||
public isLocal: ko.Observable<boolean>;
|
||||
public isOperaterEditable: ko.PureComputed<boolean>;
|
||||
public isTypeEditable: ko.PureComputed<boolean>;
|
||||
public isValue: ko.Observable<boolean>;
|
||||
public isTimestamp: ko.Observable<boolean>;
|
||||
public isCustomLastTimestamp: ko.Observable<boolean>;
|
||||
public isCustomRangeTimestamp: ko.Observable<boolean>;
|
||||
private _queryBuilderViewModel: QueryBuilderViewModel;
|
||||
private _groupCheckSubscription: ko.Subscription;
|
||||
private _id: string;
|
||||
public isAndOrFocused: ko.Observable<boolean>;
|
||||
public isDeleteButtonFocused: ko.Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
queryBuilderViewModel: QueryBuilderViewModel,
|
||||
and_or: string,
|
||||
field: string,
|
||||
type: string,
|
||||
operator: string,
|
||||
value: any,
|
||||
canAnd: boolean,
|
||||
timeValue: string,
|
||||
customTimeValue: string,
|
||||
timestampType: string,
|
||||
//customLastTimestamp: CustomTimestampHelper.ILastQuery,
|
||||
isLocal: boolean,
|
||||
id?: string
|
||||
) {
|
||||
this._queryBuilderViewModel = queryBuilderViewModel;
|
||||
this.checkedForGrouping = ko.observable<boolean>(false);
|
||||
this.isFirstInGroup = ko.observable<boolean>(false);
|
||||
this.and_or = ko.observable<string>(and_or);
|
||||
this.field = ko.observable<string>(field);
|
||||
this.type = ko.observable<string>(type);
|
||||
this.operator = ko.observable<string>(operator);
|
||||
this.value = ko.observable<string>(value);
|
||||
this.timeValue = ko.observable<string>(timeValue);
|
||||
this.customTimeValue = ko.observable<string>(customTimeValue);
|
||||
this.canAnd = ko.observable<boolean>(canAnd);
|
||||
this.isLocal = ko.observable<boolean>(isLocal);
|
||||
this._id = id ? id : Utilities.guid();
|
||||
|
||||
//this.customLastTimestamp = ko.observable<CustomTimestampHelper.ILastQuery>(customLastTimestamp);
|
||||
//this.setCustomLastTimestamp();
|
||||
|
||||
this.timestampType = ko.observable<string>(timestampType);
|
||||
this.getValueType();
|
||||
|
||||
this.isOperaterEditable = ko.pureComputed<boolean>(() => {
|
||||
const isPreferredApiCassandra = this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra();
|
||||
const cassandraKeys = isPreferredApiCassandra
|
||||
? this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys.map(
|
||||
(key) => key.property
|
||||
)
|
||||
: [];
|
||||
return (
|
||||
(this.isValue() || this.isCustomRangeTimestamp()) &&
|
||||
(!isPreferredApiCassandra || !_.contains(cassandraKeys, this.field()))
|
||||
);
|
||||
});
|
||||
this.isTypeEditable = ko.pureComputed<boolean>(
|
||||
() =>
|
||||
this.field() !== "Timestamp" &&
|
||||
this.field() !== "PartitionKey" &&
|
||||
this.field() !== "RowKey" &&
|
||||
!this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra()
|
||||
);
|
||||
|
||||
this.and_or.subscribe((value) => {
|
||||
this._queryBuilderViewModel.checkIfClauseChanged(this);
|
||||
});
|
||||
this.field.subscribe((value) => {
|
||||
this.changeField();
|
||||
});
|
||||
this.type.subscribe((value) => {
|
||||
this.changeType();
|
||||
});
|
||||
this.timeValue.subscribe((value) => {
|
||||
// if (this.timeValue() === QueryBuilderConstants.timeOptions.custom) {
|
||||
// this.customTimestampDialog();
|
||||
// }
|
||||
});
|
||||
this.customTimeValue.subscribe((value) => {
|
||||
this._queryBuilderViewModel.checkIfClauseChanged(this);
|
||||
});
|
||||
this.value.subscribe((value) => {
|
||||
this._queryBuilderViewModel.checkIfClauseChanged(this);
|
||||
});
|
||||
this.operator.subscribe((value) => {
|
||||
this._queryBuilderViewModel.checkIfClauseChanged(this);
|
||||
});
|
||||
this._groupCheckSubscription = this.checkedForGrouping.subscribe((value) => {
|
||||
this._queryBuilderViewModel.updateCanGroupClauses();
|
||||
});
|
||||
this.isAndOrFocused = ko.observable<boolean>(false);
|
||||
this.isDeleteButtonFocused = ko.observable<boolean>(false);
|
||||
}
|
||||
|
||||
// private setCustomLastTimestamp() : void {
|
||||
// if (this.customLastTimestamp() === null) {
|
||||
// var lastNumberandType: CustomTimestampHelper.ILastQuery = {
|
||||
// lastNumber: 7,
|
||||
// lastTimeUnit: "Days"
|
||||
// };
|
||||
// this.customLastTimestamp(lastNumberandType);
|
||||
// }
|
||||
// }
|
||||
|
||||
private getValueType(): void {
|
||||
switch (this.timestampType()) {
|
||||
case "time":
|
||||
this.isValue = ko.observable<boolean>(false);
|
||||
this.isTimestamp = ko.observable<boolean>(true);
|
||||
this.isCustomLastTimestamp = ko.observable<boolean>(false);
|
||||
this.isCustomRangeTimestamp = ko.observable<boolean>(false);
|
||||
break;
|
||||
case "last":
|
||||
this.isValue = ko.observable<boolean>(false);
|
||||
this.isTimestamp = ko.observable<boolean>(false);
|
||||
this.isCustomLastTimestamp = ko.observable<boolean>(true);
|
||||
this.isCustomRangeTimestamp = ko.observable<boolean>(false);
|
||||
break;
|
||||
case "range":
|
||||
this.isValue = ko.observable<boolean>(false);
|
||||
this.isTimestamp = ko.observable<boolean>(false);
|
||||
this.isCustomLastTimestamp = ko.observable<boolean>(false);
|
||||
this.isCustomRangeTimestamp = ko.observable<boolean>(true);
|
||||
break;
|
||||
default:
|
||||
this.isValue = ko.observable<boolean>(true);
|
||||
this.isTimestamp = ko.observable<boolean>(false);
|
||||
this.isCustomLastTimestamp = ko.observable<boolean>(false);
|
||||
this.isCustomRangeTimestamp = ko.observable<boolean>(false);
|
||||
}
|
||||
}
|
||||
|
||||
private changeField(): void {
|
||||
this.isCustomLastTimestamp(false);
|
||||
this.isCustomRangeTimestamp(false);
|
||||
|
||||
if (this.field() === "Timestamp") {
|
||||
this.isValue(false);
|
||||
this.isTimestamp(true);
|
||||
this.type(QueryBuilderConstants.TableType.DateTime);
|
||||
this.operator(QueryBuilderConstants.Operator.GreaterThanOrEqualTo);
|
||||
this.timestampType("time");
|
||||
} else if (this.field() === "PartitionKey" || this.field() === "RowKey") {
|
||||
this.resetFromTimestamp();
|
||||
this.type(QueryBuilderConstants.TableType.String);
|
||||
} else {
|
||||
this.resetFromTimestamp();
|
||||
if (this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra()) {
|
||||
const cassandraSchema = this._queryBuilderViewModel.tableEntityListViewModel.queryTablesTab.collection
|
||||
.cassandraSchema;
|
||||
for (let i = 0, len = cassandraSchema.length; i < len; i++) {
|
||||
if (cassandraSchema[i].property === this.field()) {
|
||||
this.type(cassandraSchema[i].type);
|
||||
i = len;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.type(QueryBuilderConstants.TableType.String);
|
||||
}
|
||||
}
|
||||
this._queryBuilderViewModel.checkIfClauseChanged(this);
|
||||
}
|
||||
|
||||
private resetFromTimestamp(): void {
|
||||
this.isValue(true);
|
||||
this.isTimestamp(false);
|
||||
this.operator(QueryBuilderConstants.Operator.Equal);
|
||||
this.value("");
|
||||
this.timestampType("");
|
||||
this.timeValue("");
|
||||
this.customTimeValue("");
|
||||
}
|
||||
|
||||
private changeType(): void {
|
||||
this.isCustomLastTimestamp(false);
|
||||
this.isCustomRangeTimestamp(false);
|
||||
|
||||
if (this.type() === QueryBuilderConstants.TableType.DateTime) {
|
||||
this.isValue(false);
|
||||
this.isTimestamp(true);
|
||||
this.operator(QueryBuilderConstants.Operator.GreaterThanOrEqualTo);
|
||||
this.timestampType("time");
|
||||
} else {
|
||||
this.isValue(true);
|
||||
this.isTimestamp(false);
|
||||
this.timeValue("");
|
||||
this.operator(QueryBuilderConstants.Operator.EqualTo);
|
||||
this.value("");
|
||||
this.timestampType("");
|
||||
this.timeValue("");
|
||||
this.customTimeValue("");
|
||||
}
|
||||
this._queryBuilderViewModel.checkIfClauseChanged(this);
|
||||
}
|
||||
|
||||
// private customTimestampDialog(): Promise<any> {
|
||||
// var lastNumber = this.customLastTimestamp().lastNumber;
|
||||
// var lastTimeUnit = this.customLastTimestamp().lastTimeUnit;
|
||||
|
||||
// return this._host.executeOperation("Environment.openDialog", [{
|
||||
// id: AzureConstants.registeredDialogs.customTimestampQueryDialog,
|
||||
// width: 500,
|
||||
// height: 300,
|
||||
// parameters: { lastNumber, lastTimeUnit }
|
||||
// }]).then((timestamp: CustomTimestampHelper.ITimestampQuery) => {
|
||||
// if (timestamp) {
|
||||
// this.isValue(false);
|
||||
// this.isTimestamp(false);
|
||||
// this.timestampType(timestamp.queryType);
|
||||
|
||||
// if (timestamp.queryType === "last") {
|
||||
// this.isCustomLastTimestamp(true);
|
||||
// this.isCustomRangeTimestamp(false);
|
||||
|
||||
// var lastNumberandType: CustomTimestampHelper.ILastQuery = {
|
||||
// lastNumber: timestamp.lastNumber,
|
||||
// lastTimeUnit: timestamp.lastTimeUnit
|
||||
// };
|
||||
|
||||
// this.customLastTimestamp(lastNumberandType);
|
||||
// this.customTimeValue(`Last ${timestamp.lastNumber} ${timestamp.lastTimeUnit}`);
|
||||
|
||||
// } else {
|
||||
// if (timestamp.timeZone === "local") {
|
||||
// this.isLocal = ko.observable(true);
|
||||
// } else {
|
||||
// this.isLocal = ko.observable(false);
|
||||
// }
|
||||
// this.isCustomLastTimestamp(false);
|
||||
// this.isCustomRangeTimestamp(true);
|
||||
// this.customTimeValue(timestamp.startTime);
|
||||
// CustomTimestampHelper.addRangeTimestamp(timestamp, this._queryBuilderViewModel, this);
|
||||
// }
|
||||
// } else {
|
||||
// this.timeValue(QueryBuilderConstants.timeOptions.lastHour);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
public getId(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
public get groupDepth(): number {
|
||||
if (this.clauseGroup) {
|
||||
return this.clauseGroup.getCurrentGroupDepth();
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._groupCheckSubscription) {
|
||||
this._groupCheckSubscription.dispose();
|
||||
}
|
||||
|
||||
this.clauseGroup = null;
|
||||
this._queryBuilderViewModel = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,237 +1,237 @@
|
||||
import * as ko from "knockout";
|
||||
import * as _ from "underscore";
|
||||
|
||||
import QueryBuilderViewModel from "./QueryBuilderViewModel";
|
||||
import QueryClauseViewModel from "./QueryClauseViewModel";
|
||||
import TableEntityListViewModel from "../DataTable/TableEntityListViewModel";
|
||||
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
||||
import * as DataTableUtilities from "../DataTable/DataTableUtilities";
|
||||
import { KeyCodes } from "../../../Common/Constants";
|
||||
import { getQuotedCqlIdentifier } from "../CqlUtilities";
|
||||
|
||||
export default class QueryViewModel {
|
||||
public topValueLimitMessage: string = "Please input a number between 0 and 1000.";
|
||||
public queryBuilderViewModel = ko.observable<QueryBuilderViewModel>();
|
||||
public isHelperActive = ko.observable<boolean>(true);
|
||||
public isEditorActive = ko.observable<boolean>(false);
|
||||
public isExpanded = ko.observable<boolean>(false);
|
||||
public isWarningBox = ko.observable<boolean>();
|
||||
public hasQueryError: ko.Computed<boolean>;
|
||||
public queryErrorMessage: ko.Computed<string>;
|
||||
public isSaveEnabled: ko.PureComputed<boolean>;
|
||||
public isExceedingLimit: ko.Computed<boolean>;
|
||||
public canRunQuery: ko.Computed<boolean>;
|
||||
public queryTextIsReadOnly: ko.Computed<boolean>;
|
||||
public queryText = ko.observable<string>();
|
||||
public topValue = ko.observable<number>();
|
||||
public selectText = ko.observableArray<string>();
|
||||
public unchangedText = ko.observable<string>();
|
||||
public unchangedSaveText = ko.observable<string>();
|
||||
public unchangedSaveTop = ko.observable<number>();
|
||||
public unchangedSaveSelect = ko.observableArray<string>();
|
||||
public focusTopResult: ko.Observable<boolean>;
|
||||
public focusExpandIcon: ko.Observable<boolean>;
|
||||
|
||||
public savedQueryName = ko.observable<string>();
|
||||
public selectMessage = ko.observable<string>();
|
||||
|
||||
public columnOptions: ko.ObservableArray<string>;
|
||||
|
||||
public queryTablesTab: QueryTablesTab;
|
||||
public id: string;
|
||||
private _tableEntityListViewModel: TableEntityListViewModel;
|
||||
|
||||
constructor(queryTablesTab: QueryTablesTab) {
|
||||
this.queryTablesTab = queryTablesTab;
|
||||
this.id = `queryViewModel${this.queryTablesTab.tabId}`;
|
||||
this._tableEntityListViewModel = queryTablesTab.tableEntityListViewModel();
|
||||
|
||||
this.queryTextIsReadOnly = ko.computed<boolean>(() => {
|
||||
return !this.queryTablesTab.container.isPreferredApiCassandra();
|
||||
});
|
||||
let initialOptions = this._tableEntityListViewModel.headers;
|
||||
this.columnOptions = ko.observableArray<string>(initialOptions);
|
||||
this.focusTopResult = ko.observable<boolean>(false);
|
||||
this.focusExpandIcon = ko.observable<boolean>(false);
|
||||
|
||||
this.queryBuilderViewModel(new QueryBuilderViewModel(this, this._tableEntityListViewModel));
|
||||
|
||||
this.isSaveEnabled = ko.pureComputed<boolean>(
|
||||
() =>
|
||||
this.queryText() !== this.unchangedSaveText() ||
|
||||
this.selectText() !== this.unchangedSaveSelect() ||
|
||||
this.topValue() !== this.unchangedSaveTop()
|
||||
);
|
||||
|
||||
this.queryBuilderViewModel().clauseArray.subscribe(value => {
|
||||
this.setFilter();
|
||||
});
|
||||
|
||||
this.isExceedingLimit = ko.computed<boolean>(() => {
|
||||
var currentTopValue: number = this.topValue();
|
||||
return currentTopValue < 0 || currentTopValue > 1000;
|
||||
});
|
||||
|
||||
this.canRunQuery = ko.computed<boolean>(() => {
|
||||
return !this.isExceedingLimit();
|
||||
});
|
||||
|
||||
this.hasQueryError = ko.computed<boolean>(() => {
|
||||
return !!this._tableEntityListViewModel.queryErrorMessage();
|
||||
});
|
||||
|
||||
this.queryErrorMessage = ko.computed<string>(() => {
|
||||
return this._tableEntityListViewModel.queryErrorMessage();
|
||||
});
|
||||
}
|
||||
|
||||
public selectHelper = (): void => {
|
||||
this.isHelperActive(true);
|
||||
this.isEditorActive(false);
|
||||
DataTableUtilities.forceRecalculateTableSize();
|
||||
};
|
||||
|
||||
public selectEditor = (): void => {
|
||||
this.setFilter();
|
||||
if (!this.isEditorActive()) {
|
||||
this.unchangedText(this.queryText());
|
||||
}
|
||||
this.isEditorActive(true);
|
||||
this.isHelperActive(false);
|
||||
DataTableUtilities.forceRecalculateTableSize();
|
||||
};
|
||||
|
||||
public toggleAdvancedOptions = () => {
|
||||
this.isExpanded(!this.isExpanded());
|
||||
if (this.isExpanded()) {
|
||||
this.focusTopResult(true);
|
||||
} else {
|
||||
this.focusExpandIcon(true);
|
||||
}
|
||||
DataTableUtilities.forceRecalculateTableSize(); // Fix for 261924, forces the resize event so DataTableBindingManager will redo the calculation on table size.
|
||||
};
|
||||
|
||||
public ontoggleAdvancedOptionsKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||
this.toggleAdvancedOptions();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
private _getSelectedResults = (): Array<string> => {
|
||||
return this.selectText();
|
||||
};
|
||||
|
||||
private setFilter = (): string => {
|
||||
var queryString = this.isEditorActive()
|
||||
? this.queryText()
|
||||
: this.queryTablesTab.container.isPreferredApiCassandra()
|
||||
? this.queryBuilderViewModel().getCqlFilterFromClauses()
|
||||
: this.queryBuilderViewModel().getODataFilterFromClauses();
|
||||
var filter = queryString;
|
||||
this.queryText(filter);
|
||||
return this.queryText();
|
||||
};
|
||||
|
||||
private setSqlFilter = (): string => {
|
||||
var filter = this.queryBuilderViewModel().getSqlFilterFromClauses();
|
||||
return filter;
|
||||
};
|
||||
|
||||
private setCqlFilter = (): string => {
|
||||
var filter = this.queryBuilderViewModel().getCqlFilterFromClauses();
|
||||
return filter;
|
||||
};
|
||||
|
||||
public isHelperEnabled = ko
|
||||
.computed<boolean>(() => {
|
||||
return (
|
||||
this.queryText() === this.unchangedText() ||
|
||||
this.queryText() === null ||
|
||||
this.queryText() === "" ||
|
||||
this.isHelperActive()
|
||||
);
|
||||
})
|
||||
.extend({
|
||||
notify: "always"
|
||||
});
|
||||
|
||||
public runQuery = (): DataTables.DataTable => {
|
||||
var filter = this.setFilter();
|
||||
if (filter && !this.queryTablesTab.container.isPreferredApiCassandra()) {
|
||||
filter = filter.replace(/"/g, "'");
|
||||
}
|
||||
var top = this.topValue();
|
||||
var selectOptions = this._getSelectedResults();
|
||||
var select = selectOptions;
|
||||
this._tableEntityListViewModel.tableQuery.filter = filter;
|
||||
this._tableEntityListViewModel.tableQuery.top = top;
|
||||
this._tableEntityListViewModel.tableQuery.select = select;
|
||||
this._tableEntityListViewModel.oDataQuery(filter);
|
||||
this._tableEntityListViewModel.sqlQuery(this.setSqlFilter());
|
||||
this._tableEntityListViewModel.cqlQuery(filter);
|
||||
|
||||
return this._tableEntityListViewModel.reloadTable(/*useSetting*/ false, /*resetHeaders*/ false);
|
||||
};
|
||||
|
||||
public clearQuery = (): DataTables.DataTable => {
|
||||
this.queryText(null);
|
||||
this.topValue(null);
|
||||
this.selectText(null);
|
||||
this.selectMessage("");
|
||||
// clears the queryBuilder and adds a new blank clause
|
||||
this.queryBuilderViewModel().queryClauses.removeAll();
|
||||
this.queryBuilderViewModel().addNewClause();
|
||||
this._tableEntityListViewModel.tableQuery.filter = null;
|
||||
this._tableEntityListViewModel.tableQuery.top = null;
|
||||
this._tableEntityListViewModel.tableQuery.select = null;
|
||||
this._tableEntityListViewModel.oDataQuery("");
|
||||
this._tableEntityListViewModel.sqlQuery("SELECT * FROM c");
|
||||
this._tableEntityListViewModel.cqlQuery(
|
||||
`SELECT * FROM ${getQuotedCqlIdentifier(this.queryTablesTab.collection.databaseId)}.${getQuotedCqlIdentifier(
|
||||
this.queryTablesTab.collection.id()
|
||||
)}`
|
||||
);
|
||||
return this._tableEntityListViewModel.reloadTable(false);
|
||||
};
|
||||
|
||||
public selectQueryOptions(): Promise<any> {
|
||||
this.queryTablesTab.container.querySelectPane.queryViewModel = this;
|
||||
this.queryTablesTab.container.querySelectPane.open();
|
||||
return null;
|
||||
}
|
||||
|
||||
public onselectQueryOptionsKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||
this.selectQueryOptions();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
public getSelectMessage(): void {
|
||||
if (_.isEmpty(this.selectText()) || this.selectText() === null) {
|
||||
this.selectMessage("");
|
||||
} else {
|
||||
this.selectMessage(`${this.selectText().length} of ${this.columnOptions().length} columns selected.`);
|
||||
}
|
||||
}
|
||||
|
||||
public isSelected = ko.computed<boolean>(() => {
|
||||
return !(_.isEmpty(this.selectText()) || this.selectText() === null);
|
||||
});
|
||||
|
||||
private setCheckToSave(): void {
|
||||
this.unchangedSaveText(this.setFilter());
|
||||
this.unchangedSaveTop(this.topValue());
|
||||
this.unchangedSaveSelect(this.selectText());
|
||||
this.isSaveEnabled(false);
|
||||
}
|
||||
|
||||
public checkIfBuilderChanged(clause: QueryClauseViewModel): void {
|
||||
this.setFilter();
|
||||
}
|
||||
}
|
||||
import * as ko from "knockout";
|
||||
import * as _ from "underscore";
|
||||
|
||||
import QueryBuilderViewModel from "./QueryBuilderViewModel";
|
||||
import QueryClauseViewModel from "./QueryClauseViewModel";
|
||||
import TableEntityListViewModel from "../DataTable/TableEntityListViewModel";
|
||||
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
||||
import * as DataTableUtilities from "../DataTable/DataTableUtilities";
|
||||
import { KeyCodes } from "../../../Common/Constants";
|
||||
import { getQuotedCqlIdentifier } from "../CqlUtilities";
|
||||
|
||||
export default class QueryViewModel {
|
||||
public topValueLimitMessage: string = "Please input a number between 0 and 1000.";
|
||||
public queryBuilderViewModel = ko.observable<QueryBuilderViewModel>();
|
||||
public isHelperActive = ko.observable<boolean>(true);
|
||||
public isEditorActive = ko.observable<boolean>(false);
|
||||
public isExpanded = ko.observable<boolean>(false);
|
||||
public isWarningBox = ko.observable<boolean>();
|
||||
public hasQueryError: ko.Computed<boolean>;
|
||||
public queryErrorMessage: ko.Computed<string>;
|
||||
public isSaveEnabled: ko.PureComputed<boolean>;
|
||||
public isExceedingLimit: ko.Computed<boolean>;
|
||||
public canRunQuery: ko.Computed<boolean>;
|
||||
public queryTextIsReadOnly: ko.Computed<boolean>;
|
||||
public queryText = ko.observable<string>();
|
||||
public topValue = ko.observable<number>();
|
||||
public selectText = ko.observableArray<string>();
|
||||
public unchangedText = ko.observable<string>();
|
||||
public unchangedSaveText = ko.observable<string>();
|
||||
public unchangedSaveTop = ko.observable<number>();
|
||||
public unchangedSaveSelect = ko.observableArray<string>();
|
||||
public focusTopResult: ko.Observable<boolean>;
|
||||
public focusExpandIcon: ko.Observable<boolean>;
|
||||
|
||||
public savedQueryName = ko.observable<string>();
|
||||
public selectMessage = ko.observable<string>();
|
||||
|
||||
public columnOptions: ko.ObservableArray<string>;
|
||||
|
||||
public queryTablesTab: QueryTablesTab;
|
||||
public id: string;
|
||||
private _tableEntityListViewModel: TableEntityListViewModel;
|
||||
|
||||
constructor(queryTablesTab: QueryTablesTab) {
|
||||
this.queryTablesTab = queryTablesTab;
|
||||
this.id = `queryViewModel${this.queryTablesTab.tabId}`;
|
||||
this._tableEntityListViewModel = queryTablesTab.tableEntityListViewModel();
|
||||
|
||||
this.queryTextIsReadOnly = ko.computed<boolean>(() => {
|
||||
return !this.queryTablesTab.container.isPreferredApiCassandra();
|
||||
});
|
||||
let initialOptions = this._tableEntityListViewModel.headers;
|
||||
this.columnOptions = ko.observableArray<string>(initialOptions);
|
||||
this.focusTopResult = ko.observable<boolean>(false);
|
||||
this.focusExpandIcon = ko.observable<boolean>(false);
|
||||
|
||||
this.queryBuilderViewModel(new QueryBuilderViewModel(this, this._tableEntityListViewModel));
|
||||
|
||||
this.isSaveEnabled = ko.pureComputed<boolean>(
|
||||
() =>
|
||||
this.queryText() !== this.unchangedSaveText() ||
|
||||
this.selectText() !== this.unchangedSaveSelect() ||
|
||||
this.topValue() !== this.unchangedSaveTop()
|
||||
);
|
||||
|
||||
this.queryBuilderViewModel().clauseArray.subscribe((value) => {
|
||||
this.setFilter();
|
||||
});
|
||||
|
||||
this.isExceedingLimit = ko.computed<boolean>(() => {
|
||||
var currentTopValue: number = this.topValue();
|
||||
return currentTopValue < 0 || currentTopValue > 1000;
|
||||
});
|
||||
|
||||
this.canRunQuery = ko.computed<boolean>(() => {
|
||||
return !this.isExceedingLimit();
|
||||
});
|
||||
|
||||
this.hasQueryError = ko.computed<boolean>(() => {
|
||||
return !!this._tableEntityListViewModel.queryErrorMessage();
|
||||
});
|
||||
|
||||
this.queryErrorMessage = ko.computed<string>(() => {
|
||||
return this._tableEntityListViewModel.queryErrorMessage();
|
||||
});
|
||||
}
|
||||
|
||||
public selectHelper = (): void => {
|
||||
this.isHelperActive(true);
|
||||
this.isEditorActive(false);
|
||||
DataTableUtilities.forceRecalculateTableSize();
|
||||
};
|
||||
|
||||
public selectEditor = (): void => {
|
||||
this.setFilter();
|
||||
if (!this.isEditorActive()) {
|
||||
this.unchangedText(this.queryText());
|
||||
}
|
||||
this.isEditorActive(true);
|
||||
this.isHelperActive(false);
|
||||
DataTableUtilities.forceRecalculateTableSize();
|
||||
};
|
||||
|
||||
public toggleAdvancedOptions = () => {
|
||||
this.isExpanded(!this.isExpanded());
|
||||
if (this.isExpanded()) {
|
||||
this.focusTopResult(true);
|
||||
} else {
|
||||
this.focusExpandIcon(true);
|
||||
}
|
||||
DataTableUtilities.forceRecalculateTableSize(); // Fix for 261924, forces the resize event so DataTableBindingManager will redo the calculation on table size.
|
||||
};
|
||||
|
||||
public ontoggleAdvancedOptionsKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||
this.toggleAdvancedOptions();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
private _getSelectedResults = (): Array<string> => {
|
||||
return this.selectText();
|
||||
};
|
||||
|
||||
private setFilter = (): string => {
|
||||
var queryString = this.isEditorActive()
|
||||
? this.queryText()
|
||||
: this.queryTablesTab.container.isPreferredApiCassandra()
|
||||
? this.queryBuilderViewModel().getCqlFilterFromClauses()
|
||||
: this.queryBuilderViewModel().getODataFilterFromClauses();
|
||||
var filter = queryString;
|
||||
this.queryText(filter);
|
||||
return this.queryText();
|
||||
};
|
||||
|
||||
private setSqlFilter = (): string => {
|
||||
var filter = this.queryBuilderViewModel().getSqlFilterFromClauses();
|
||||
return filter;
|
||||
};
|
||||
|
||||
private setCqlFilter = (): string => {
|
||||
var filter = this.queryBuilderViewModel().getCqlFilterFromClauses();
|
||||
return filter;
|
||||
};
|
||||
|
||||
public isHelperEnabled = ko
|
||||
.computed<boolean>(() => {
|
||||
return (
|
||||
this.queryText() === this.unchangedText() ||
|
||||
this.queryText() === null ||
|
||||
this.queryText() === "" ||
|
||||
this.isHelperActive()
|
||||
);
|
||||
})
|
||||
.extend({
|
||||
notify: "always",
|
||||
});
|
||||
|
||||
public runQuery = (): DataTables.DataTable => {
|
||||
var filter = this.setFilter();
|
||||
if (filter && !this.queryTablesTab.container.isPreferredApiCassandra()) {
|
||||
filter = filter.replace(/"/g, "'");
|
||||
}
|
||||
var top = this.topValue();
|
||||
var selectOptions = this._getSelectedResults();
|
||||
var select = selectOptions;
|
||||
this._tableEntityListViewModel.tableQuery.filter = filter;
|
||||
this._tableEntityListViewModel.tableQuery.top = top;
|
||||
this._tableEntityListViewModel.tableQuery.select = select;
|
||||
this._tableEntityListViewModel.oDataQuery(filter);
|
||||
this._tableEntityListViewModel.sqlQuery(this.setSqlFilter());
|
||||
this._tableEntityListViewModel.cqlQuery(filter);
|
||||
|
||||
return this._tableEntityListViewModel.reloadTable(/*useSetting*/ false, /*resetHeaders*/ false);
|
||||
};
|
||||
|
||||
public clearQuery = (): DataTables.DataTable => {
|
||||
this.queryText(null);
|
||||
this.topValue(null);
|
||||
this.selectText(null);
|
||||
this.selectMessage("");
|
||||
// clears the queryBuilder and adds a new blank clause
|
||||
this.queryBuilderViewModel().queryClauses.removeAll();
|
||||
this.queryBuilderViewModel().addNewClause();
|
||||
this._tableEntityListViewModel.tableQuery.filter = null;
|
||||
this._tableEntityListViewModel.tableQuery.top = null;
|
||||
this._tableEntityListViewModel.tableQuery.select = null;
|
||||
this._tableEntityListViewModel.oDataQuery("");
|
||||
this._tableEntityListViewModel.sqlQuery("SELECT * FROM c");
|
||||
this._tableEntityListViewModel.cqlQuery(
|
||||
`SELECT * FROM ${getQuotedCqlIdentifier(this.queryTablesTab.collection.databaseId)}.${getQuotedCqlIdentifier(
|
||||
this.queryTablesTab.collection.id()
|
||||
)}`
|
||||
);
|
||||
return this._tableEntityListViewModel.reloadTable(false);
|
||||
};
|
||||
|
||||
public selectQueryOptions(): Promise<any> {
|
||||
this.queryTablesTab.container.querySelectPane.queryViewModel = this;
|
||||
this.queryTablesTab.container.querySelectPane.open();
|
||||
return null;
|
||||
}
|
||||
|
||||
public onselectQueryOptionsKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||
this.selectQueryOptions();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
public getSelectMessage(): void {
|
||||
if (_.isEmpty(this.selectText()) || this.selectText() === null) {
|
||||
this.selectMessage("");
|
||||
} else {
|
||||
this.selectMessage(`${this.selectText().length} of ${this.columnOptions().length} columns selected.`);
|
||||
}
|
||||
}
|
||||
|
||||
public isSelected = ko.computed<boolean>(() => {
|
||||
return !(_.isEmpty(this.selectText()) || this.selectText() === null);
|
||||
});
|
||||
|
||||
private setCheckToSave(): void {
|
||||
this.unchangedSaveText(this.setFilter());
|
||||
this.unchangedSaveTop(this.topValue());
|
||||
this.unchangedSaveSelect(this.selectText());
|
||||
this.isSaveEnabled(false);
|
||||
}
|
||||
|
||||
public checkIfBuilderChanged(clause: QueryClauseViewModel): void {
|
||||
this.setFilter();
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,194 +1,194 @@
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import * as Entities from "./Entities";
|
||||
import * as Constants from "./Constants";
|
||||
import * as DateTimeUtilities from "./QueryBuilder/DateTimeUtilities";
|
||||
|
||||
// For use exclusively with Tables API.
|
||||
|
||||
enum DataTypes {
|
||||
Guid = 0,
|
||||
Double = 1,
|
||||
String = 2,
|
||||
Binary = 5,
|
||||
Boolean = 8,
|
||||
DateTime = 9,
|
||||
Int32 = 16,
|
||||
Int64 = 18
|
||||
}
|
||||
|
||||
var tablesIndexers = {
|
||||
Value: "$v",
|
||||
Type: "$t"
|
||||
};
|
||||
|
||||
export var keyProperties = {
|
||||
PartitionKey: "$pk",
|
||||
Id: "id",
|
||||
Id2: "$id", // This should always be the same value as Id
|
||||
Timestamp: "_ts",
|
||||
resourceId: "_rid",
|
||||
self: "_self",
|
||||
etag: "_etag",
|
||||
attachments: "_attachments"
|
||||
};
|
||||
|
||||
export function convertDocumentsToEntities(documents: any[]): Entities.ITableEntityForTablesAPI[] {
|
||||
let results: Entities.ITableEntityForTablesAPI[] = [];
|
||||
documents &&
|
||||
documents.forEach(document => {
|
||||
if (!document.hasOwnProperty(keyProperties.PartitionKey) || !document.hasOwnProperty(keyProperties.Id2)) {
|
||||
//Document does not match the current required format for Tables, so we ignore it
|
||||
return; // The rest of the key properties should be guaranteed as DocumentDB properties
|
||||
}
|
||||
let entity: Entities.ITableEntityForTablesAPI = <Entities.ITableEntityForTablesAPI>{
|
||||
PartitionKey: {
|
||||
_: document[keyProperties.PartitionKey],
|
||||
$: Constants.TableType.String
|
||||
},
|
||||
RowKey: {
|
||||
_: document[keyProperties.Id],
|
||||
$: Constants.TableType.String
|
||||
},
|
||||
Timestamp: {
|
||||
// DocumentDB Timestamp is unix time so we convert to Javascript date here
|
||||
_: DateTimeUtilities.convertUnixToJSDate(document[keyProperties.Timestamp]).toUTCString(),
|
||||
$: Constants.TableType.DateTime
|
||||
},
|
||||
_rid: {
|
||||
_: document[keyProperties.resourceId],
|
||||
$: Constants.TableType.String
|
||||
},
|
||||
_self: {
|
||||
_: document[keyProperties.self],
|
||||
$: Constants.TableType.String
|
||||
},
|
||||
_etag: {
|
||||
_: document[keyProperties.etag],
|
||||
$: Constants.TableType.String
|
||||
},
|
||||
_attachments: {
|
||||
_: document[keyProperties.attachments],
|
||||
$: Constants.TableType.String
|
||||
}
|
||||
};
|
||||
for (var property in document) {
|
||||
if (document.hasOwnProperty(property)) {
|
||||
if (
|
||||
property !== keyProperties.PartitionKey &&
|
||||
property !== keyProperties.Id &&
|
||||
property !== keyProperties.Timestamp &&
|
||||
property !== keyProperties.resourceId &&
|
||||
property !== keyProperties.self &&
|
||||
property !== keyProperties.etag &&
|
||||
property !== keyProperties.attachments &&
|
||||
property !== keyProperties.Id2
|
||||
) {
|
||||
if (!document[property].hasOwnProperty("$v") || !document[property].hasOwnProperty("$t")) {
|
||||
return; //Document property does not match the current required format for Tables, so we ignore it
|
||||
}
|
||||
if (DataTypes[document[property][tablesIndexers.Type]] === DataTypes[DataTypes.DateTime]) {
|
||||
// Convert Ticks datetime to javascript date for better visualization in table
|
||||
entity[property] = {
|
||||
_: DateTimeUtilities.convertTicksToJSDate(document[property][tablesIndexers.Value]).toUTCString(),
|
||||
$: DataTypes[document[property][tablesIndexers.Type]]
|
||||
};
|
||||
} else {
|
||||
entity[property] = {
|
||||
_: document[property][tablesIndexers.Value],
|
||||
$: DataTypes[document[property][tablesIndexers.Type]]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
results.push(entity);
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
// Do not use this to create a document to send to the server, only for delete and for giving rid/self/collection to the utility methods.
|
||||
export function convertEntitiesToDocuments(
|
||||
entities: Entities.ITableEntityForTablesAPI[],
|
||||
collection: ViewModels.Collection
|
||||
): any[] {
|
||||
let results: any[] = [];
|
||||
entities &&
|
||||
entities.forEach(entity => {
|
||||
let document: any = {
|
||||
$id: entity.RowKey._,
|
||||
id: entity.RowKey._,
|
||||
ts: DateTimeUtilities.convertJSDateToUnix(entity.Timestamp._), // Convert back to unix time
|
||||
rid: entity._rid._,
|
||||
self: entity._self._,
|
||||
etag: entity._etag._,
|
||||
attachments: entity._attachments._,
|
||||
collection: collection
|
||||
};
|
||||
if (collection.partitionKey) {
|
||||
document["partitionKey"] = collection.partitionKey;
|
||||
document[collection.partitionKeyProperty] = entity.PartitionKey._;
|
||||
document["partitionKeyValue"] = entity.PartitionKey._;
|
||||
}
|
||||
for (var property in entity) {
|
||||
if (
|
||||
property !== Constants.EntityKeyNames.PartitionKey &&
|
||||
property !== Constants.EntityKeyNames.RowKey &&
|
||||
property !== Constants.EntityKeyNames.Timestamp &&
|
||||
property !== keyProperties.resourceId &&
|
||||
property !== keyProperties.self &&
|
||||
property !== keyProperties.etag &&
|
||||
property !== keyProperties.attachments &&
|
||||
property !== keyProperties.Id2
|
||||
) {
|
||||
if (entity[property].$ === Constants.TableType.DateTime) {
|
||||
// Convert javascript date back to ticks with 20 zeros padding
|
||||
document[property] = {
|
||||
$t: (<any>DataTypes)[entity[property].$],
|
||||
$v: DateTimeUtilities.convertJSDateToTicksWithPadding(entity[property]._)
|
||||
};
|
||||
} else {
|
||||
document[property] = {
|
||||
$t: (<any>DataTypes)[entity[property].$],
|
||||
$v: entity[property]._
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
results.push(document);
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
export function convertEntityToNewDocument(entity: Entities.ITableEntityForTablesAPI): any {
|
||||
let document: any = {
|
||||
$pk: entity.PartitionKey._,
|
||||
$id: entity.RowKey._,
|
||||
id: entity.RowKey._
|
||||
};
|
||||
for (var property in entity) {
|
||||
if (
|
||||
property !== Constants.EntityKeyNames.PartitionKey &&
|
||||
property !== Constants.EntityKeyNames.RowKey &&
|
||||
property !== Constants.EntityKeyNames.Timestamp &&
|
||||
property !== keyProperties.resourceId &&
|
||||
property !== keyProperties.self &&
|
||||
property !== keyProperties.etag &&
|
||||
property !== keyProperties.attachments &&
|
||||
property !== keyProperties.Id2
|
||||
) {
|
||||
if (entity[property].$ === Constants.TableType.DateTime) {
|
||||
// Convert javascript date back to ticks with 20 zeros padding
|
||||
document[property] = {
|
||||
$t: (<any>DataTypes)[entity[property].$],
|
||||
$v: DateTimeUtilities.convertJSDateToTicksWithPadding(entity[property]._)
|
||||
};
|
||||
} else {
|
||||
document[property] = {
|
||||
$t: (<any>DataTypes)[entity[property].$],
|
||||
$v: entity[property]._
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return document;
|
||||
}
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import * as Entities from "./Entities";
|
||||
import * as Constants from "./Constants";
|
||||
import * as DateTimeUtilities from "./QueryBuilder/DateTimeUtilities";
|
||||
|
||||
// For use exclusively with Tables API.
|
||||
|
||||
enum DataTypes {
|
||||
Guid = 0,
|
||||
Double = 1,
|
||||
String = 2,
|
||||
Binary = 5,
|
||||
Boolean = 8,
|
||||
DateTime = 9,
|
||||
Int32 = 16,
|
||||
Int64 = 18,
|
||||
}
|
||||
|
||||
var tablesIndexers = {
|
||||
Value: "$v",
|
||||
Type: "$t",
|
||||
};
|
||||
|
||||
export var keyProperties = {
|
||||
PartitionKey: "$pk",
|
||||
Id: "id",
|
||||
Id2: "$id", // This should always be the same value as Id
|
||||
Timestamp: "_ts",
|
||||
resourceId: "_rid",
|
||||
self: "_self",
|
||||
etag: "_etag",
|
||||
attachments: "_attachments",
|
||||
};
|
||||
|
||||
export function convertDocumentsToEntities(documents: any[]): Entities.ITableEntityForTablesAPI[] {
|
||||
let results: Entities.ITableEntityForTablesAPI[] = [];
|
||||
documents &&
|
||||
documents.forEach((document) => {
|
||||
if (!document.hasOwnProperty(keyProperties.PartitionKey) || !document.hasOwnProperty(keyProperties.Id2)) {
|
||||
//Document does not match the current required format for Tables, so we ignore it
|
||||
return; // The rest of the key properties should be guaranteed as DocumentDB properties
|
||||
}
|
||||
let entity: Entities.ITableEntityForTablesAPI = <Entities.ITableEntityForTablesAPI>{
|
||||
PartitionKey: {
|
||||
_: document[keyProperties.PartitionKey],
|
||||
$: Constants.TableType.String,
|
||||
},
|
||||
RowKey: {
|
||||
_: document[keyProperties.Id],
|
||||
$: Constants.TableType.String,
|
||||
},
|
||||
Timestamp: {
|
||||
// DocumentDB Timestamp is unix time so we convert to Javascript date here
|
||||
_: DateTimeUtilities.convertUnixToJSDate(document[keyProperties.Timestamp]).toUTCString(),
|
||||
$: Constants.TableType.DateTime,
|
||||
},
|
||||
_rid: {
|
||||
_: document[keyProperties.resourceId],
|
||||
$: Constants.TableType.String,
|
||||
},
|
||||
_self: {
|
||||
_: document[keyProperties.self],
|
||||
$: Constants.TableType.String,
|
||||
},
|
||||
_etag: {
|
||||
_: document[keyProperties.etag],
|
||||
$: Constants.TableType.String,
|
||||
},
|
||||
_attachments: {
|
||||
_: document[keyProperties.attachments],
|
||||
$: Constants.TableType.String,
|
||||
},
|
||||
};
|
||||
for (var property in document) {
|
||||
if (document.hasOwnProperty(property)) {
|
||||
if (
|
||||
property !== keyProperties.PartitionKey &&
|
||||
property !== keyProperties.Id &&
|
||||
property !== keyProperties.Timestamp &&
|
||||
property !== keyProperties.resourceId &&
|
||||
property !== keyProperties.self &&
|
||||
property !== keyProperties.etag &&
|
||||
property !== keyProperties.attachments &&
|
||||
property !== keyProperties.Id2
|
||||
) {
|
||||
if (!document[property].hasOwnProperty("$v") || !document[property].hasOwnProperty("$t")) {
|
||||
return; //Document property does not match the current required format for Tables, so we ignore it
|
||||
}
|
||||
if (DataTypes[document[property][tablesIndexers.Type]] === DataTypes[DataTypes.DateTime]) {
|
||||
// Convert Ticks datetime to javascript date for better visualization in table
|
||||
entity[property] = {
|
||||
_: DateTimeUtilities.convertTicksToJSDate(document[property][tablesIndexers.Value]).toUTCString(),
|
||||
$: DataTypes[document[property][tablesIndexers.Type]],
|
||||
};
|
||||
} else {
|
||||
entity[property] = {
|
||||
_: document[property][tablesIndexers.Value],
|
||||
$: DataTypes[document[property][tablesIndexers.Type]],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
results.push(entity);
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
// Do not use this to create a document to send to the server, only for delete and for giving rid/self/collection to the utility methods.
|
||||
export function convertEntitiesToDocuments(
|
||||
entities: Entities.ITableEntityForTablesAPI[],
|
||||
collection: ViewModels.Collection
|
||||
): any[] {
|
||||
let results: any[] = [];
|
||||
entities &&
|
||||
entities.forEach((entity) => {
|
||||
let document: any = {
|
||||
$id: entity.RowKey._,
|
||||
id: entity.RowKey._,
|
||||
ts: DateTimeUtilities.convertJSDateToUnix(entity.Timestamp._), // Convert back to unix time
|
||||
rid: entity._rid._,
|
||||
self: entity._self._,
|
||||
etag: entity._etag._,
|
||||
attachments: entity._attachments._,
|
||||
collection: collection,
|
||||
};
|
||||
if (collection.partitionKey) {
|
||||
document["partitionKey"] = collection.partitionKey;
|
||||
document[collection.partitionKeyProperty] = entity.PartitionKey._;
|
||||
document["partitionKeyValue"] = entity.PartitionKey._;
|
||||
}
|
||||
for (var property in entity) {
|
||||
if (
|
||||
property !== Constants.EntityKeyNames.PartitionKey &&
|
||||
property !== Constants.EntityKeyNames.RowKey &&
|
||||
property !== Constants.EntityKeyNames.Timestamp &&
|
||||
property !== keyProperties.resourceId &&
|
||||
property !== keyProperties.self &&
|
||||
property !== keyProperties.etag &&
|
||||
property !== keyProperties.attachments &&
|
||||
property !== keyProperties.Id2
|
||||
) {
|
||||
if (entity[property].$ === Constants.TableType.DateTime) {
|
||||
// Convert javascript date back to ticks with 20 zeros padding
|
||||
document[property] = {
|
||||
$t: (<any>DataTypes)[entity[property].$],
|
||||
$v: DateTimeUtilities.convertJSDateToTicksWithPadding(entity[property]._),
|
||||
};
|
||||
} else {
|
||||
document[property] = {
|
||||
$t: (<any>DataTypes)[entity[property].$],
|
||||
$v: entity[property]._,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
results.push(document);
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
export function convertEntityToNewDocument(entity: Entities.ITableEntityForTablesAPI): any {
|
||||
let document: any = {
|
||||
$pk: entity.PartitionKey._,
|
||||
$id: entity.RowKey._,
|
||||
id: entity.RowKey._,
|
||||
};
|
||||
for (var property in entity) {
|
||||
if (
|
||||
property !== Constants.EntityKeyNames.PartitionKey &&
|
||||
property !== Constants.EntityKeyNames.RowKey &&
|
||||
property !== Constants.EntityKeyNames.Timestamp &&
|
||||
property !== keyProperties.resourceId &&
|
||||
property !== keyProperties.self &&
|
||||
property !== keyProperties.etag &&
|
||||
property !== keyProperties.attachments &&
|
||||
property !== keyProperties.Id2
|
||||
) {
|
||||
if (entity[property].$ === Constants.TableType.DateTime) {
|
||||
// Convert javascript date back to ticks with 20 zeros padding
|
||||
document[property] = {
|
||||
$t: (<any>DataTypes)[entity[property].$],
|
||||
$v: DateTimeUtilities.convertJSDateToTicksWithPadding(entity[property]._),
|
||||
};
|
||||
} else {
|
||||
document[property] = {
|
||||
$t: (<any>DataTypes)[entity[property].$],
|
||||
$v: entity[property]._,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return document;
|
||||
}
|
||||
|
||||
@@ -1,279 +1,279 @@
|
||||
import * as _ from "underscore";
|
||||
import Q from "q";
|
||||
import * as Entities from "./Entities";
|
||||
import { CassandraTableKey } from "./TableDataClient";
|
||||
import * as Constants from "./Constants";
|
||||
|
||||
/**
|
||||
* Generates a pseudo-random GUID.
|
||||
*/
|
||||
export function guid() {
|
||||
function s4() {
|
||||
return Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
}
|
||||
return s4() + s4() + "-" + s4() + "-" + s4() + "-" + s4() + "-" + s4() + s4() + s4();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves in the specified number of milliseconds.
|
||||
*/
|
||||
export function delay(milliseconds: number): Q.Promise<any> {
|
||||
return Q.delay(milliseconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a value and minimum and maximum limits, returns the value if it is within the limits
|
||||
* (inclusive); or the maximum or minimum limit, if the value is greater or lesser than the
|
||||
* respective limit.
|
||||
*/
|
||||
export function ensureBetweenBounds(value: number, minimum: number, maximum: number): number {
|
||||
return Math.max(Math.min(value, maximum), minimum);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an appropriate error message for an error.
|
||||
* @param error The actual error
|
||||
* @param simpleMessage A simpler message to use instead of the actual error.
|
||||
* If supplied, the original error will be added as "details".
|
||||
*/
|
||||
export function getErrorMessage(error: any, simpleMessage?: string): string {
|
||||
var detailsMessage: string;
|
||||
if (typeof error === "string" || error instanceof String) {
|
||||
detailsMessage = error.toString();
|
||||
} else {
|
||||
detailsMessage = error.message || error.error || error.name;
|
||||
}
|
||||
|
||||
if (simpleMessage && detailsMessage) {
|
||||
return simpleMessage + getEnvironmentNewLine() + getEnvironmentNewLine() + "Details: " + detailsMessage;
|
||||
} else if (simpleMessage) {
|
||||
return simpleMessage;
|
||||
} else {
|
||||
return detailsMessage || "An unexpected error has occurred.";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the environment's new line characters
|
||||
*/
|
||||
export function getEnvironmentNewLine(): string {
|
||||
var platform = navigator.platform.toUpperCase();
|
||||
|
||||
if (platform.indexOf("WIN") >= 0) {
|
||||
return "\r\n";
|
||||
} else {
|
||||
// Mac OS X and *nix
|
||||
return "\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether two arrays have same elements in the same sequence.
|
||||
*/
|
||||
export function isEqual<T>(a: T[], b: T[]): boolean {
|
||||
var isEqual: boolean = false;
|
||||
if (!!a && !!b && a.length === b.length) {
|
||||
isEqual = _.every(a, (value: T, index: number) => value === b[index]);
|
||||
}
|
||||
return isEqual;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape meta-characters for jquery selector
|
||||
*/
|
||||
export function jQuerySelectorEscape(value: string): string {
|
||||
value = value || "";
|
||||
return value.replace(/[!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~]/g, "\\$&");
|
||||
}
|
||||
|
||||
export function copyTableQuery(query: Entities.ITableQuery): Entities.ITableQuery {
|
||||
if (!query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
filter: query.filter,
|
||||
select: query.select && query.select.slice(),
|
||||
top: query.top
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Html encode
|
||||
*/
|
||||
export function htmlEncode(value: string): string {
|
||||
var _divElem: JQuery = $("<div/>");
|
||||
return _divElem.text(value).html();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an action on a keyboard event.
|
||||
* Modifiers: ctrlKey - control/command key, shiftKey - shift key, altKey - alt/option key;
|
||||
* pass on 'null' to ignore the modifier (default).
|
||||
*/
|
||||
export function onKey(
|
||||
event: any,
|
||||
eventKeyCode: number,
|
||||
action: ($sourceElement: JQuery) => void,
|
||||
metaKey: boolean = null,
|
||||
shiftKey: boolean = null,
|
||||
altKey: boolean = null
|
||||
): boolean {
|
||||
var source: any = event.target || event.srcElement,
|
||||
keyCode: number = event.keyCode,
|
||||
$sourceElement = $(source),
|
||||
handled: boolean = false;
|
||||
|
||||
if (
|
||||
$sourceElement.length &&
|
||||
keyCode === eventKeyCode &&
|
||||
$.isFunction(action) &&
|
||||
(metaKey === null || metaKey === event.metaKey) &&
|
||||
(shiftKey === null || shiftKey === event.shiftKey) &&
|
||||
(altKey === null || altKey === event.altKey)
|
||||
) {
|
||||
action($sourceElement);
|
||||
handled = true;
|
||||
}
|
||||
|
||||
return handled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an action on an 'enter' keyboard event.
|
||||
*/
|
||||
export function onEnter(
|
||||
event: any,
|
||||
action: ($sourceElement: JQuery) => void,
|
||||
metaKey: boolean = null,
|
||||
shiftKey: boolean = null,
|
||||
altKey: boolean = null
|
||||
): boolean {
|
||||
return onKey(event, Constants.keyCodes.Enter, action, metaKey, shiftKey, altKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an action on a 'tab' keyboard event.
|
||||
*/
|
||||
export function onTab(
|
||||
event: any,
|
||||
action: ($sourceElement: JQuery) => void,
|
||||
metaKey: boolean = null,
|
||||
shiftKey: boolean = null,
|
||||
altKey: boolean = null
|
||||
): boolean {
|
||||
return onKey(event, Constants.keyCodes.Tab, action, metaKey, shiftKey, altKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an action on an 'Esc' keyboard event.
|
||||
*/
|
||||
export function onEsc(
|
||||
event: any,
|
||||
action: ($sourceElement: JQuery) => void,
|
||||
metaKey: boolean = null,
|
||||
shiftKey: boolean = null,
|
||||
altKey: boolean = null
|
||||
): boolean {
|
||||
return onKey(event, Constants.keyCodes.Esc, action, metaKey, shiftKey, altKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the environment 'ctrl' key press. This key is used for multi selection, like select one more item, select all.
|
||||
* For Windows and Linux, it's ctrl. For Mac, it's command.
|
||||
*/
|
||||
export function isEnvironmentCtrlPressed(event: JQueryEventObject): boolean {
|
||||
return isMac() ? event.metaKey : event.ctrlKey;
|
||||
}
|
||||
|
||||
export function isEnvironmentShiftPressed(event: JQueryEventObject): boolean {
|
||||
return event.shiftKey;
|
||||
}
|
||||
|
||||
export function isEnvironmentAltPressed(event: JQueryEventObject): boolean {
|
||||
return event.altKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current platform is MacOS.
|
||||
*/
|
||||
export function isMac(): boolean {
|
||||
var platform = navigator.platform.toUpperCase();
|
||||
return platform.indexOf("MAC") >= 0;
|
||||
}
|
||||
|
||||
// MAX_SAFE_INTEGER and MIN_SAFE_INTEGER will be provided by ECMAScript 6's Number
|
||||
export var MAX_SAFE_INTEGER = Math.pow(2, 53) - 1;
|
||||
export var MIN_SAFE_INTEGER = -MAX_SAFE_INTEGER;
|
||||
|
||||
/**
|
||||
* Tests whether a value a safe integer.
|
||||
* A safe integer is an integer that can be exactly represented as an IEEE-754 double precision number (all integers from (2^53 - 1) to -(2^53 - 1))
|
||||
* Note: Function and constants will be provided by ECMAScript 6's Number.
|
||||
*/
|
||||
export function isSafeInteger(value: any): boolean {
|
||||
var n: number = typeof value !== "number" ? Number(value) : value;
|
||||
|
||||
return Math.round(n) === n && MIN_SAFE_INTEGER <= n && n <= MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
export function getInputTypeFromDisplayedName(displayedName: string): string {
|
||||
switch (displayedName) {
|
||||
case Constants.TableType.DateTime:
|
||||
return Constants.InputType.DateTime;
|
||||
case Constants.TableType.Int32:
|
||||
case Constants.TableType.Int64:
|
||||
case Constants.TableType.Double:
|
||||
case Constants.CassandraType.Bigint:
|
||||
case Constants.CassandraType.Decimal:
|
||||
case Constants.CassandraType.Double:
|
||||
case Constants.CassandraType.Float:
|
||||
case Constants.CassandraType.Int:
|
||||
case Constants.CassandraType.Smallint:
|
||||
case Constants.CassandraType.Tinyint:
|
||||
return Constants.InputType.Number;
|
||||
default:
|
||||
return Constants.InputType.Text;
|
||||
}
|
||||
}
|
||||
|
||||
export function padLongWithZeros(value: string): string {
|
||||
var s = "0000000000000000000" + value;
|
||||
return s.substr(s.length - 20);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a data type for each header. The data type is inferred from entities.
|
||||
* Notice: Not every header will have a data type since some headers don't even exist in entities.
|
||||
*/
|
||||
export function getDataTypesFromEntities(headers: string[], entities: Entities.ITableEntity[]): any {
|
||||
var currentHeaders: string[] = _.clone(headers);
|
||||
var dataTypes: any = {};
|
||||
entities = entities || [];
|
||||
entities.forEach((entity: Entities.ITableEntity, index: number) => {
|
||||
if (currentHeaders.length) {
|
||||
var keys: string[] = _.keys(entity);
|
||||
var headersToProcess: string[] = _.intersection(currentHeaders, keys);
|
||||
headersToProcess &&
|
||||
headersToProcess.forEach((propertyName: string) => {
|
||||
dataTypes[propertyName] = entity[propertyName].$ || Constants.TableType.String;
|
||||
});
|
||||
currentHeaders = _.difference(currentHeaders, headersToProcess);
|
||||
}
|
||||
});
|
||||
return dataTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a data type for each header. The data type is inferred from Cassandra Schema.
|
||||
*/
|
||||
export function getDataTypesFromCassandraSchema(schema: CassandraTableKey[]): any {
|
||||
var dataTypes: any = {};
|
||||
schema &&
|
||||
schema.forEach((schemaItem: CassandraTableKey, index: number) => {
|
||||
dataTypes[schemaItem.property] = schemaItem.type;
|
||||
});
|
||||
return dataTypes;
|
||||
}
|
||||
import * as _ from "underscore";
|
||||
import Q from "q";
|
||||
import * as Entities from "./Entities";
|
||||
import { CassandraTableKey } from "./TableDataClient";
|
||||
import * as Constants from "./Constants";
|
||||
|
||||
/**
|
||||
* Generates a pseudo-random GUID.
|
||||
*/
|
||||
export function guid() {
|
||||
function s4() {
|
||||
return Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
}
|
||||
return s4() + s4() + "-" + s4() + "-" + s4() + "-" + s4() + "-" + s4() + s4() + s4();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves in the specified number of milliseconds.
|
||||
*/
|
||||
export function delay(milliseconds: number): Q.Promise<any> {
|
||||
return Q.delay(milliseconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a value and minimum and maximum limits, returns the value if it is within the limits
|
||||
* (inclusive); or the maximum or minimum limit, if the value is greater or lesser than the
|
||||
* respective limit.
|
||||
*/
|
||||
export function ensureBetweenBounds(value: number, minimum: number, maximum: number): number {
|
||||
return Math.max(Math.min(value, maximum), minimum);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an appropriate error message for an error.
|
||||
* @param error The actual error
|
||||
* @param simpleMessage A simpler message to use instead of the actual error.
|
||||
* If supplied, the original error will be added as "details".
|
||||
*/
|
||||
export function getErrorMessage(error: any, simpleMessage?: string): string {
|
||||
var detailsMessage: string;
|
||||
if (typeof error === "string" || error instanceof String) {
|
||||
detailsMessage = error.toString();
|
||||
} else {
|
||||
detailsMessage = error.message || error.error || error.name;
|
||||
}
|
||||
|
||||
if (simpleMessage && detailsMessage) {
|
||||
return simpleMessage + getEnvironmentNewLine() + getEnvironmentNewLine() + "Details: " + detailsMessage;
|
||||
} else if (simpleMessage) {
|
||||
return simpleMessage;
|
||||
} else {
|
||||
return detailsMessage || "An unexpected error has occurred.";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the environment's new line characters
|
||||
*/
|
||||
export function getEnvironmentNewLine(): string {
|
||||
var platform = navigator.platform.toUpperCase();
|
||||
|
||||
if (platform.indexOf("WIN") >= 0) {
|
||||
return "\r\n";
|
||||
} else {
|
||||
// Mac OS X and *nix
|
||||
return "\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether two arrays have same elements in the same sequence.
|
||||
*/
|
||||
export function isEqual<T>(a: T[], b: T[]): boolean {
|
||||
var isEqual: boolean = false;
|
||||
if (!!a && !!b && a.length === b.length) {
|
||||
isEqual = _.every(a, (value: T, index: number) => value === b[index]);
|
||||
}
|
||||
return isEqual;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape meta-characters for jquery selector
|
||||
*/
|
||||
export function jQuerySelectorEscape(value: string): string {
|
||||
value = value || "";
|
||||
return value.replace(/[!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~]/g, "\\$&");
|
||||
}
|
||||
|
||||
export function copyTableQuery(query: Entities.ITableQuery): Entities.ITableQuery {
|
||||
if (!query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
filter: query.filter,
|
||||
select: query.select && query.select.slice(),
|
||||
top: query.top,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Html encode
|
||||
*/
|
||||
export function htmlEncode(value: string): string {
|
||||
var _divElem: JQuery = $("<div/>");
|
||||
return _divElem.text(value).html();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an action on a keyboard event.
|
||||
* Modifiers: ctrlKey - control/command key, shiftKey - shift key, altKey - alt/option key;
|
||||
* pass on 'null' to ignore the modifier (default).
|
||||
*/
|
||||
export function onKey(
|
||||
event: any,
|
||||
eventKeyCode: number,
|
||||
action: ($sourceElement: JQuery) => void,
|
||||
metaKey: boolean = null,
|
||||
shiftKey: boolean = null,
|
||||
altKey: boolean = null
|
||||
): boolean {
|
||||
var source: any = event.target || event.srcElement,
|
||||
keyCode: number = event.keyCode,
|
||||
$sourceElement = $(source),
|
||||
handled: boolean = false;
|
||||
|
||||
if (
|
||||
$sourceElement.length &&
|
||||
keyCode === eventKeyCode &&
|
||||
$.isFunction(action) &&
|
||||
(metaKey === null || metaKey === event.metaKey) &&
|
||||
(shiftKey === null || shiftKey === event.shiftKey) &&
|
||||
(altKey === null || altKey === event.altKey)
|
||||
) {
|
||||
action($sourceElement);
|
||||
handled = true;
|
||||
}
|
||||
|
||||
return handled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an action on an 'enter' keyboard event.
|
||||
*/
|
||||
export function onEnter(
|
||||
event: any,
|
||||
action: ($sourceElement: JQuery) => void,
|
||||
metaKey: boolean = null,
|
||||
shiftKey: boolean = null,
|
||||
altKey: boolean = null
|
||||
): boolean {
|
||||
return onKey(event, Constants.keyCodes.Enter, action, metaKey, shiftKey, altKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an action on a 'tab' keyboard event.
|
||||
*/
|
||||
export function onTab(
|
||||
event: any,
|
||||
action: ($sourceElement: JQuery) => void,
|
||||
metaKey: boolean = null,
|
||||
shiftKey: boolean = null,
|
||||
altKey: boolean = null
|
||||
): boolean {
|
||||
return onKey(event, Constants.keyCodes.Tab, action, metaKey, shiftKey, altKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an action on an 'Esc' keyboard event.
|
||||
*/
|
||||
export function onEsc(
|
||||
event: any,
|
||||
action: ($sourceElement: JQuery) => void,
|
||||
metaKey: boolean = null,
|
||||
shiftKey: boolean = null,
|
||||
altKey: boolean = null
|
||||
): boolean {
|
||||
return onKey(event, Constants.keyCodes.Esc, action, metaKey, shiftKey, altKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the environment 'ctrl' key press. This key is used for multi selection, like select one more item, select all.
|
||||
* For Windows and Linux, it's ctrl. For Mac, it's command.
|
||||
*/
|
||||
export function isEnvironmentCtrlPressed(event: JQueryEventObject): boolean {
|
||||
return isMac() ? event.metaKey : event.ctrlKey;
|
||||
}
|
||||
|
||||
export function isEnvironmentShiftPressed(event: JQueryEventObject): boolean {
|
||||
return event.shiftKey;
|
||||
}
|
||||
|
||||
export function isEnvironmentAltPressed(event: JQueryEventObject): boolean {
|
||||
return event.altKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current platform is MacOS.
|
||||
*/
|
||||
export function isMac(): boolean {
|
||||
var platform = navigator.platform.toUpperCase();
|
||||
return platform.indexOf("MAC") >= 0;
|
||||
}
|
||||
|
||||
// MAX_SAFE_INTEGER and MIN_SAFE_INTEGER will be provided by ECMAScript 6's Number
|
||||
export var MAX_SAFE_INTEGER = Math.pow(2, 53) - 1;
|
||||
export var MIN_SAFE_INTEGER = -MAX_SAFE_INTEGER;
|
||||
|
||||
/**
|
||||
* Tests whether a value a safe integer.
|
||||
* A safe integer is an integer that can be exactly represented as an IEEE-754 double precision number (all integers from (2^53 - 1) to -(2^53 - 1))
|
||||
* Note: Function and constants will be provided by ECMAScript 6's Number.
|
||||
*/
|
||||
export function isSafeInteger(value: any): boolean {
|
||||
var n: number = typeof value !== "number" ? Number(value) : value;
|
||||
|
||||
return Math.round(n) === n && MIN_SAFE_INTEGER <= n && n <= MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
export function getInputTypeFromDisplayedName(displayedName: string): string {
|
||||
switch (displayedName) {
|
||||
case Constants.TableType.DateTime:
|
||||
return Constants.InputType.DateTime;
|
||||
case Constants.TableType.Int32:
|
||||
case Constants.TableType.Int64:
|
||||
case Constants.TableType.Double:
|
||||
case Constants.CassandraType.Bigint:
|
||||
case Constants.CassandraType.Decimal:
|
||||
case Constants.CassandraType.Double:
|
||||
case Constants.CassandraType.Float:
|
||||
case Constants.CassandraType.Int:
|
||||
case Constants.CassandraType.Smallint:
|
||||
case Constants.CassandraType.Tinyint:
|
||||
return Constants.InputType.Number;
|
||||
default:
|
||||
return Constants.InputType.Text;
|
||||
}
|
||||
}
|
||||
|
||||
export function padLongWithZeros(value: string): string {
|
||||
var s = "0000000000000000000" + value;
|
||||
return s.substr(s.length - 20);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a data type for each header. The data type is inferred from entities.
|
||||
* Notice: Not every header will have a data type since some headers don't even exist in entities.
|
||||
*/
|
||||
export function getDataTypesFromEntities(headers: string[], entities: Entities.ITableEntity[]): any {
|
||||
var currentHeaders: string[] = _.clone(headers);
|
||||
var dataTypes: any = {};
|
||||
entities = entities || [];
|
||||
entities.forEach((entity: Entities.ITableEntity, index: number) => {
|
||||
if (currentHeaders.length) {
|
||||
var keys: string[] = _.keys(entity);
|
||||
var headersToProcess: string[] = _.intersection(currentHeaders, keys);
|
||||
headersToProcess &&
|
||||
headersToProcess.forEach((propertyName: string) => {
|
||||
dataTypes[propertyName] = entity[propertyName].$ || Constants.TableType.String;
|
||||
});
|
||||
currentHeaders = _.difference(currentHeaders, headersToProcess);
|
||||
}
|
||||
});
|
||||
return dataTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a data type for each header. The data type is inferred from Cassandra Schema.
|
||||
*/
|
||||
export function getDataTypesFromCassandraSchema(schema: CassandraTableKey[]): any {
|
||||
var dataTypes: any = {};
|
||||
schema &&
|
||||
schema.forEach((schemaItem: CassandraTableKey, index: number) => {
|
||||
dataTypes[schemaItem.property] = schemaItem.type;
|
||||
});
|
||||
return dataTypes;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user