Prettier 2.0 (#393)

This commit is contained in:
Steve Faulkner
2021-01-20 09:15:01 -06:00
committed by GitHub
parent c1937ca464
commit 4be53284b5
500 changed files with 41927 additions and 41838 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 */
}

View File

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

View File

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

View File

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

View File

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

View File

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