mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-20 01:11:25 +00:00
Initial Move from Azure DevOps to GitHub
This commit is contained in:
169
src/Explorer/Tables/Constants.ts
Normal file
169
src/Explorer/Tables/Constants.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
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",
|
||||
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"
|
||||
};
|
||||
26
src/Explorer/Tables/DataTable/CacheBase.ts
Normal file
26
src/Explorer/Tables/DataTable/CacheBase.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
abstract class CacheBase<T> {
|
||||
public data: T[];
|
||||
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;
|
||||
396
src/Explorer/Tables/DataTable/DataTableBindingManager.ts
Normal file
396
src/Explorer/Tables/DataTable/DataTableBindingManager.ts
Normal file
@@ -0,0 +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
|
||||
});
|
||||
}
|
||||
|
||||
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 as ViewModels.Explorer;
|
||||
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");
|
||||
}
|
||||
}
|
||||
};
|
||||
52
src/Explorer/Tables/DataTable/DataTableBuilder.ts
Normal file
52
src/Explorer/Tables/DataTable/DataTableBuilder.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import * as Utilities from "../Utilities";
|
||||
|
||||
/**
|
||||
* Wrapper function for creating data tables. Call this method, not the
|
||||
* data tables constructor when you want to create a data table. This
|
||||
* function makes sure that content without a render function is properly
|
||||
* encoded to prevent XSS.
|
||||
* @param{$dataTableElem} JQuery data table element
|
||||
* @param{$settings} Settings to use when creating the data table
|
||||
*/
|
||||
export function createDataTable($dataTableElem: JQuery, settings: any): DataTables.DataTable {
|
||||
return $dataTableElem.DataTable(applyDefaultRendering(settings));
|
||||
}
|
||||
|
||||
/**
|
||||
* Go through the settings for a data table and apply a simple HTML encode to any column
|
||||
* without a render function to prevent XSS.
|
||||
* @param{settings} The settings to check
|
||||
* @return The given settings with all columns having a rendering function
|
||||
*/
|
||||
function applyDefaultRendering(settings: any): DataTables.SettingsLegacy {
|
||||
var tableColumns: DataTables.ColumnLegacy[] = null;
|
||||
|
||||
if (settings.aoColumns) {
|
||||
tableColumns = settings.aoColumns;
|
||||
} else if (settings.aoColumnDefs) {
|
||||
// for tables we use aoColumnDefs instead of aoColumns
|
||||
tableColumns = settings.aoColumnDefs;
|
||||
}
|
||||
|
||||
// either the settings had no columns defined, or they were called
|
||||
// by a property name which we have not used before
|
||||
if (!tableColumns) {
|
||||
return settings;
|
||||
}
|
||||
|
||||
for (var i = 0; i < tableColumns.length; i++) {
|
||||
// the column does not have a render function
|
||||
if (!tableColumns[i].mRender) {
|
||||
tableColumns[i].mRender = defaultDataRender;
|
||||
}
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default data render function, whatever is done to data in here
|
||||
* will be done to any data which we do not specify a render for.
|
||||
*/
|
||||
function defaultDataRender(data: any, type: string, full: any) {
|
||||
return Utilities.htmlEncode(data);
|
||||
}
|
||||
150
src/Explorer/Tables/DataTable/DataTableContextMenu.ts
Normal file
150
src/Explorer/Tables/DataTable/DataTableContextMenu.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import Q from "q";
|
||||
|
||||
import * as Constants from "../Constants";
|
||||
import TableCommands from "./TableCommands";
|
||||
import TableEntityListViewModel from "./TableEntityListViewModel";
|
||||
|
||||
/*
|
||||
* ContextMenu view representation
|
||||
*/
|
||||
export default class DataTableContextMenu {
|
||||
public viewModel: TableEntityListViewModel;
|
||||
|
||||
// There is one context menu for each selector on each tab and they should all be registered here.
|
||||
// Once the context menus are registered, we should access them through this instance.
|
||||
public static Instance: { [key: string]: { contextMenu: DataTableContextMenu } } = {};
|
||||
|
||||
private _tableCommands: TableCommands;
|
||||
|
||||
constructor(viewModel: TableEntityListViewModel, tableCommands: TableCommands) {
|
||||
this.viewModel = viewModel;
|
||||
this._tableCommands = tableCommands;
|
||||
|
||||
this.registerTableBodyContextMenu();
|
||||
this.registerTableHeaderContextMenu();
|
||||
|
||||
DataTableContextMenu.Instance[viewModel.queryTablesTab.tabId] = { contextMenu: this };
|
||||
}
|
||||
|
||||
public unregisterContextMenu(selector: string): void {
|
||||
$.contextMenu("destroy", "div#" + this.viewModel.queryTablesTab.tabId + ".tab-pane " + selector);
|
||||
}
|
||||
|
||||
public registerTableBodyContextMenu(): void {
|
||||
// Localize
|
||||
$.contextMenu({
|
||||
selector:
|
||||
"div#" + this.viewModel.queryTablesTab.tabId + ".tab-pane " + Constants.htmlSelectors.dataTableBodyRowSelector,
|
||||
callback: this.bodyContextMenuSelect,
|
||||
items: {
|
||||
edit: {
|
||||
name: "Edit",
|
||||
cmd: TableCommands.editEntityCommand,
|
||||
icon: "edit-entity",
|
||||
disabled: () => !this.isEnabled(TableCommands.editEntityCommand)
|
||||
},
|
||||
delete: {
|
||||
name: "Delete",
|
||||
cmd: TableCommands.deleteEntitiesCommand,
|
||||
icon: "delete-entity",
|
||||
disabled: () => !this.isEnabled(TableCommands.deleteEntitiesCommand)
|
||||
},
|
||||
reorder: {
|
||||
name: "Reorder Columns Based on Schema",
|
||||
cmd: TableCommands.reorderColumnsCommand,
|
||||
icon: "shift-non-empty-columns-left",
|
||||
disabled: () => !this.isEnabled(TableCommands.reorderColumnsCommand)
|
||||
},
|
||||
reset: {
|
||||
name: "Reset Columns",
|
||||
cmd: TableCommands.resetColumnsCommand,
|
||||
icon: "reset-column-order"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public registerTableHeaderContextMenu(): void {
|
||||
// Localize
|
||||
$.contextMenu({
|
||||
selector:
|
||||
"div#" + this.viewModel.queryTablesTab.tabId + ".tab-pane " + Constants.htmlSelectors.dataTableHeadRowSelector,
|
||||
callback: this.headerContextMenuSelect,
|
||||
items: {
|
||||
customizeColumns: {
|
||||
name: "Column Options",
|
||||
cmd: TableCommands.customizeColumnsCommand,
|
||||
icon: "customize-columns"
|
||||
},
|
||||
reset: {
|
||||
name: "Reset Columns",
|
||||
cmd: TableCommands.resetColumnsCommand,
|
||||
icon: "reset-column-order"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private isEnabled(commandName: string): boolean {
|
||||
return this._tableCommands.isEnabled(commandName, this.viewModel.selected());
|
||||
}
|
||||
|
||||
private headerContextMenuSelect = (key: any, options: any): void => {
|
||||
var promise: Q.Promise<any> = null;
|
||||
|
||||
switch (key) {
|
||||
case TableCommands.customizeColumnsCommand:
|
||||
promise = this._tableCommands.customizeColumnsCommand(this.viewModel);
|
||||
break;
|
||||
case TableCommands.resetColumnsCommand:
|
||||
promise = Q.resolve(this._tableCommands.resetColumns(this.viewModel));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (promise) {
|
||||
promise.then(() => {
|
||||
this.viewModel.focusDataTable();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private bodyContextMenuSelect = (key: any, options: any): void => {
|
||||
var promise: Q.Promise<any> = null;
|
||||
|
||||
switch (key) {
|
||||
case TableCommands.editEntityCommand:
|
||||
promise = this._tableCommands.editEntityCommand(this.viewModel);
|
||||
break;
|
||||
case TableCommands.deleteEntitiesCommand:
|
||||
promise = this._tableCommands.deleteEntitiesCommand(this.viewModel);
|
||||
break;
|
||||
case TableCommands.reorderColumnsCommand:
|
||||
promise = this._tableCommands.reorderColumnsBasedOnSelectedEntities(this.viewModel);
|
||||
break;
|
||||
case TableCommands.resetColumnsCommand:
|
||||
promise = Q.resolve(this._tableCommands.resetColumns(this.viewModel));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (promise) {
|
||||
promise.then(() => {
|
||||
this.viewModel.focusDataTable();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A context menu factory to construct the one context menu for each tab/table view model.
|
||||
*/
|
||||
public static contextMenuFactory(viewModel: TableEntityListViewModel, tableCommands: TableCommands) {
|
||||
if (!DataTableContextMenu.Instance[viewModel.queryTablesTab.tabId]) {
|
||||
DataTableContextMenu.Instance[viewModel.queryTablesTab.tabId] = {
|
||||
contextMenu: new DataTableContextMenu(viewModel, tableCommands)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
313
src/Explorer/Tables/DataTable/DataTableOperationManager.ts
Normal file
313
src/Explorer/Tables/DataTable/DataTableOperationManager.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
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 contextMenu = (event: JQueryEventObject) => {
|
||||
var elem: JQuery = $(event.currentTarget);
|
||||
this.updateLastSelectedItem(elem, event.shiftKey);
|
||||
|
||||
this.applyContextMenuSelection(elem);
|
||||
setTimeout(function() {
|
||||
$(".context-menu-list")
|
||||
.attr("tabindex", -1)
|
||||
.focus();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
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("contextmenu", "tr", this.contextMenu);
|
||||
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();
|
||||
}
|
||||
}
|
||||
192
src/Explorer/Tables/DataTable/DataTableOperations.ts
Normal file
192
src/Explorer/Tables/DataTable/DataTableOperations.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import _ from "underscore";
|
||||
import Q from "q";
|
||||
|
||||
import * as Entities from "../Entities";
|
||||
import * as QueryBuilderConstants from "../Constants";
|
||||
import * as Utilities from "../Utilities";
|
||||
|
||||
export function getRowSelector(selectorSchema: Entities.IProperty[]): string {
|
||||
var selector: string = "";
|
||||
selectorSchema &&
|
||||
selectorSchema.forEach((p: Entities.IProperty) => {
|
||||
selector += "[" + p.key + '="' + Utilities.jQuerySelectorEscape(p.value) + '"]';
|
||||
});
|
||||
return QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector + selector;
|
||||
}
|
||||
|
||||
export function isRowVisible(dataTableScrollBodyQuery: JQuery, element: HTMLElement): boolean {
|
||||
var isVisible = false;
|
||||
|
||||
if (dataTableScrollBodyQuery.length && element) {
|
||||
var elementRect: ClientRect = element.getBoundingClientRect(),
|
||||
dataTableScrollBodyRect: ClientRect = dataTableScrollBodyQuery.get(0).getBoundingClientRect();
|
||||
|
||||
isVisible = elementRect.bottom <= dataTableScrollBodyRect.bottom && dataTableScrollBodyRect.top <= elementRect.top;
|
||||
}
|
||||
|
||||
return isVisible;
|
||||
}
|
||||
|
||||
export function scrollToRowIfNeeded(dataTableRows: JQuery, currentIndex: number, isScrollUp: boolean): void {
|
||||
if (dataTableRows.length) {
|
||||
var dataTableScrollBodyQuery: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector),
|
||||
selectedRowElement: HTMLElement = dataTableRows.get(currentIndex);
|
||||
|
||||
if (dataTableScrollBodyQuery.length && selectedRowElement) {
|
||||
var isVisible: boolean = isRowVisible(dataTableScrollBodyQuery, selectedRowElement);
|
||||
|
||||
if (!isVisible) {
|
||||
var selectedRowQuery: JQuery = $(selectedRowElement),
|
||||
scrollPosition: number = dataTableScrollBodyQuery.scrollTop(),
|
||||
selectedElementPosition: number = selectedRowQuery.position().top,
|
||||
newScrollPosition: number = 0;
|
||||
|
||||
if (isScrollUp) {
|
||||
newScrollPosition = scrollPosition + selectedElementPosition;
|
||||
} else {
|
||||
newScrollPosition =
|
||||
scrollPosition + (selectedElementPosition + selectedRowQuery.height() - dataTableScrollBodyQuery.height());
|
||||
}
|
||||
|
||||
dataTableScrollBodyQuery.scrollTop(newScrollPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function scrollToTopIfNeeded(): void {
|
||||
var $dataTableRows: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector),
|
||||
$dataTableScrollBody: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector);
|
||||
|
||||
if ($dataTableRows.length && $dataTableScrollBody.length) {
|
||||
$dataTableScrollBody.scrollTop(0);
|
||||
}
|
||||
}
|
||||
|
||||
export function setPaginationButtonEventHandlers(): void {
|
||||
$(QueryBuilderConstants.htmlSelectors.dataTablePaginationButtonSelector)
|
||||
.on("mousedown", (event: JQueryEventObject) => {
|
||||
// Prevents the table contents from briefly jumping when clicking on "Load more"
|
||||
event.preventDefault();
|
||||
})
|
||||
.attr("role", "button");
|
||||
}
|
||||
|
||||
export function filterColumns(table: DataTables.DataTable, settings: boolean[]): void {
|
||||
settings &&
|
||||
settings.forEach((value: boolean, index: number) => {
|
||||
table.column(index).visible(value, false);
|
||||
});
|
||||
table.columns.adjust().draw(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder columns based on current order.
|
||||
* If no current order is specified, reorder the columns based on intial order.
|
||||
*/
|
||||
export function reorderColumns(
|
||||
table: DataTables.DataTable,
|
||||
targetOrder: number[],
|
||||
currentOrder?: number[]
|
||||
): Q.Promise<any> {
|
||||
var columnsCount: number = targetOrder.length;
|
||||
var isCurrentOrderPassedIn: boolean = !!currentOrder;
|
||||
if (!isCurrentOrderPassedIn) {
|
||||
currentOrder = getInitialOrder(columnsCount);
|
||||
}
|
||||
var isSameOrder: boolean = Utilities.isEqual(currentOrder, targetOrder);
|
||||
|
||||
// if the targetOrder is the same as current order, do nothing.
|
||||
if (!isSameOrder) {
|
||||
// Otherwise, calculate the transformation order.
|
||||
// If current order not specified, then it'll be set to initial order,
|
||||
// i.e., either no reorder happened before or reordering to its initial order,
|
||||
// Then the transformation order will be the same as target order.
|
||||
// If current order is specified, then a transformation order is calculated.
|
||||
// Refer to calculateTransformationOrder for details about transformation order.
|
||||
var transformationOrder: number[] = isCurrentOrderPassedIn
|
||||
? calculateTransformationOrder(currentOrder, targetOrder)
|
||||
: targetOrder;
|
||||
try {
|
||||
$.fn.dataTable.ColReorder(table).fnOrder(transformationOrder);
|
||||
} catch (err) {
|
||||
return Q.reject(err);
|
||||
}
|
||||
}
|
||||
return Q.resolve(null);
|
||||
}
|
||||
|
||||
export function resetColumns(table: DataTables.DataTable): void {
|
||||
$.fn.dataTable.ColReorder(table).fnReset();
|
||||
}
|
||||
|
||||
/**
|
||||
* A table's initial order is described in the form of a natural ascending order.
|
||||
* E.g., for a table with 9 columns, the initial order will be: [0, 1, 2, 3, 4, 5, 6, 7, 8]
|
||||
*/
|
||||
export function getInitialOrder(columnsCount: number): number[] {
|
||||
return _.range(columnsCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current table's column order which is described based on initial table. E.g.,
|
||||
* Initial order: I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8}
|
||||
* Current order: C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8}
|
||||
*/
|
||||
export function getCurrentOrder(table: DataTables.DataTable): number[] {
|
||||
return $.fn.dataTable.ColReorder(table).fnOrder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the index and value for each element of an array. e.g.,
|
||||
* InputArray: [0, 1, 2, 6, 7, 3, 4, 5, 8]
|
||||
* Result: [0, 1, 2, 5, 6, 7, 3, 4, 8]
|
||||
*/
|
||||
export function invertIndexValues(inputArray: number[]): number[] {
|
||||
var invertedArray: number[] = [];
|
||||
if (inputArray) {
|
||||
inputArray.forEach((value: number, index: number) => {
|
||||
invertedArray[inputArray[index]] = index;
|
||||
});
|
||||
}
|
||||
|
||||
return invertedArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* DataTable fnOrder API is based on the current table. So we need to map the order targeting original table to targeting current table.
|
||||
* An detailed example for this. Assume the table has 9 columns.
|
||||
* Initial order (order of the initial table): I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8}
|
||||
* Current order (order of the current table): C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8}
|
||||
* Target order (order of the targeting table): T = [0, 1, 2, 5, 6, 7, 8, 3, 4] <----> {prop0, prop1, prop2, prop5, prop6, prop7, prop8, prop3, prop4}
|
||||
* Transformation order: an order passed to fnOrder API that transforms table from current order to target order.
|
||||
* When the table is constructed, it has the intial order. After an reordering with current order array, now the table is shown in current order, e.g.,
|
||||
* column 3 in the current table is actually column C[3]=6 in the intial table, both indicate the column with header prop6.
|
||||
* Now we want to continue to do another reorder to make the target table in the target order. Directly invoking API with the new order won't work as
|
||||
* the API only do reorder based on the current table like the first time we invoke the API. So an order based on the current table needs to be calulated.
|
||||
* Here is an example of how to calculate the transformation order:
|
||||
* In target table, column 3 should be column T[3]=5 in the intial table with header prop5, while in current table, column with header prop5 is column 7 as C[7]=5.
|
||||
* As a result, in transformation order, column 3 in the target table should be column 7 in the current table, Trans[3] = 7. In the same manner, we can get the
|
||||
* transformation order: Trans = [0, 1, 2, 7, 3, 4, 8, 5, 6]
|
||||
*/
|
||||
export function calculateTransformationOrder(currentOrder: number[], targetOrder: number[]): number[] {
|
||||
var transformationOrder: number[] = [];
|
||||
if (currentOrder && targetOrder && currentOrder.length === targetOrder.length) {
|
||||
var invertedCurrentOrder: number[] = invertIndexValues(currentOrder);
|
||||
transformationOrder = targetOrder.map((value: number) => invertedCurrentOrder[value]);
|
||||
}
|
||||
return transformationOrder;
|
||||
}
|
||||
|
||||
export function getDataTableHeaders(table: DataTables.DataTable): string[] {
|
||||
var columns: DataTables.ColumnsMethods = table.columns();
|
||||
var headers: string[] = [];
|
||||
if (columns) {
|
||||
// table.columns() return ColumnsMethods which is an array of arrays
|
||||
var columnIndexes: number[] = (<any>columns)[0];
|
||||
if (columnIndexes) {
|
||||
headers = columnIndexes.map((value: number) => $(table.columns(value).header()).html());
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
148
src/Explorer/Tables/DataTable/DataTableUtilities.ts
Normal file
148
src/Explorer/Tables/DataTable/DataTableUtilities.ts
Normal file
@@ -0,0 +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();
|
||||
}
|
||||
270
src/Explorer/Tables/DataTable/DataTableViewModel.ts
Normal file
270
src/Explorer/Tables/DataTable/DataTableViewModel.ts
Normal file
@@ -0,0 +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 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;
|
||||
155
src/Explorer/Tables/DataTable/TableCommands.ts
Normal file
155
src/Explorer/Tables/DataTable/TableCommands.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
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";
|
||||
|
||||
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: ViewModels.Explorer;
|
||||
|
||||
constructor(container: ViewModels.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();
|
||||
}
|
||||
}
|
||||
27
src/Explorer/Tables/DataTable/TableEntityCache.ts
Normal file
27
src/Explorer/Tables/DataTable/TableEntityCache.ts
Normal file
@@ -0,0 +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;
|
||||
}
|
||||
}
|
||||
541
src/Explorer/Tables/DataTable/TableEntityListViewModel.ts
Normal file
541
src/Explorer/Tables/DataTable/TableEntityListViewModel.ts
Normal file
@@ -0,0 +1,541 @@
|
||||
import * as ko from "knockout";
|
||||
import * as _ from "underscore";
|
||||
import Q from "q";
|
||||
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { CassandraTableKey, CassandraAPIDataClient } from "../TableDataClient";
|
||||
import DataTableViewModel from "./DataTableViewModel";
|
||||
import DataTableContextMenu from "./DataTableContextMenu";
|
||||
import * as DataTableUtilities from "./DataTableUtilities";
|
||||
import TableCommands from "./TableCommands";
|
||||
import TableEntityCache from "./TableEntityCache";
|
||||
import * as Constants from "../Constants";
|
||||
import { Areas } from "../../../Common/Constants";
|
||||
import * as Utilities from "../Utilities";
|
||||
import * as Entities from "../Entities";
|
||||
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
||||
import * as TableEntityProcessor from "../TableEntityProcessor";
|
||||
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import * as ErrorParserUtility from "../../../Common/ErrorParserUtility";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
|
||||
interface IListTableEntitiesSegmentedResult extends Entities.IListTableEntitiesResult {
|
||||
ExceedMaximumRetries?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage Table Entity List ViewModel
|
||||
*/
|
||||
export default class TableEntityListViewModel extends DataTableViewModel {
|
||||
// This is the number of retry attempts to fetch entities when the Azure Table service returns no results with a continuation token.
|
||||
// This number should ideally accommodate the service default timeout for queries of 30s, where each individual query execution can
|
||||
// take *up* to 5s (see https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx).
|
||||
// To be on the safe side, we are setting the total number of attempts to 120, assuming up to 4 queries per second (120q = 30s * 4q/s).
|
||||
// Experimentation also validates this "safe number": queries against a 10 million entity table took up to 13 fetch attempts.
|
||||
private static _maximumNumberOfPrefetchRetries = 120 - 1;
|
||||
|
||||
/* Observables */
|
||||
public headers: string[] = [Constants.defaultHeader];
|
||||
public useSetting: boolean = true;
|
||||
|
||||
//public tableExplorerContext: TableExplorerContext;
|
||||
public notifyColumnChanges: (enablePrompt: boolean, queryTablesTab: QueryTablesTab) => void;
|
||||
public tablePageStartIndex: number;
|
||||
public tableQuery: Entities.ITableQuery = {};
|
||||
public cqlQuery: ko.Observable<string>;
|
||||
public oDataQuery: ko.Observable<string>;
|
||||
public sqlQuery: ko.Observable<string>;
|
||||
public cache: TableEntityCache;
|
||||
public isCancelled: boolean = false;
|
||||
public queryErrorMessage: ko.Observable<string>;
|
||||
public id: string;
|
||||
|
||||
constructor(tableCommands: TableCommands, queryTablesTab: QueryTablesTab) {
|
||||
super();
|
||||
this.cache = new TableEntityCache();
|
||||
this.queryErrorMessage = ko.observable<string>();
|
||||
this.queryTablesTab = queryTablesTab;
|
||||
// Enable Context menu for the data table.
|
||||
DataTableContextMenu.contextMenuFactory(this, tableCommands);
|
||||
this.id = `tableEntityListViewModel${this.queryTablesTab.tabId}`;
|
||||
this.cqlQuery = ko.observable<string>(
|
||||
`SELECT * FROM ${this.queryTablesTab.collection.databaseId}.${this.queryTablesTab.collection.id()}`
|
||||
);
|
||||
this.oDataQuery = ko.observable<string>();
|
||||
this.sqlQuery = ko.observable<string>("SELECT * FROM c");
|
||||
}
|
||||
|
||||
public getTableEntityKeys(rowKey: string): Entities.IProperty[] {
|
||||
return [{ key: Constants.EntityKeyNames.RowKey, value: rowKey }];
|
||||
}
|
||||
|
||||
public reloadTable(useSetting: boolean = true, resetHeaders: boolean = true): DataTables.DataTable {
|
||||
this.clearCache();
|
||||
this.clearSelection();
|
||||
this.isCancelled = false;
|
||||
|
||||
this.useSetting = useSetting;
|
||||
if (resetHeaders) {
|
||||
this.updateHeaders([Constants.defaultHeader]);
|
||||
}
|
||||
return this.table.ajax.reload();
|
||||
}
|
||||
|
||||
public updateHeaders(newHeaders: string[], notifyColumnChanges: boolean = false, enablePrompt: boolean = true): void {
|
||||
this.headers = newHeaders;
|
||||
if (notifyColumnChanges) {
|
||||
this.clearSelection();
|
||||
this.notifyColumnChanges(enablePrompt, this.queryTablesTab);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This callback function called by datatable to fetch the next page of data and render.
|
||||
* sSource - ajax URL of data source, ignored in our case as we are not using ajax.
|
||||
* aoData - details about the next page of data datatable expected to render.
|
||||
* fnCallback - is the render callback with data to render.
|
||||
* oSetting: current settings used for table initialization.
|
||||
*/
|
||||
public renderNextPageAndupdateCache(sSource: any, aoData: any, fnCallback: any, oSettings: any) {
|
||||
var tablePageSize: number;
|
||||
var draw: number;
|
||||
var prefetchNeeded = true;
|
||||
var columnSortOrder: any;
|
||||
// Threshold(pages) for triggering cache prefetch.
|
||||
// If number remaining pages in cache falls below prefetchThreshold prefetch will be triggered.
|
||||
var prefetchThreshold = 10;
|
||||
var tableQuery = this.tableQuery;
|
||||
|
||||
for (var index in aoData) {
|
||||
var data = aoData[index];
|
||||
if (data.name === "length") {
|
||||
tablePageSize = data.value;
|
||||
}
|
||||
if (data.name === "start") {
|
||||
this.tablePageStartIndex = data.value;
|
||||
}
|
||||
if (data.name === "draw") {
|
||||
draw = data.value;
|
||||
}
|
||||
if (data.name === "order") {
|
||||
columnSortOrder = data.value;
|
||||
}
|
||||
}
|
||||
// Try cache if valid.
|
||||
if (this.isCacheValid(tableQuery)) {
|
||||
// Check if prefetch needed.
|
||||
if (this.tablePageStartIndex + tablePageSize <= this.cache.length || this.allDownloaded) {
|
||||
prefetchNeeded = false;
|
||||
if (columnSortOrder && (!this.cache.sortOrder || !_.isEqual(this.cache.sortOrder, columnSortOrder))) {
|
||||
this.sortColumns(columnSortOrder, oSettings);
|
||||
}
|
||||
this.renderPage(fnCallback, draw, this.tablePageStartIndex, tablePageSize, oSettings);
|
||||
if (
|
||||
!this.allDownloaded &&
|
||||
this.tablePageStartIndex > 0 && // This is a case now that we can hit this as we re-construct table when we update column
|
||||
this.cache.length - this.tablePageStartIndex + tablePageSize < prefetchThreshold * tablePageSize
|
||||
) {
|
||||
prefetchNeeded = true;
|
||||
}
|
||||
} else {
|
||||
prefetchNeeded = true;
|
||||
}
|
||||
} else {
|
||||
this.clearCache();
|
||||
}
|
||||
|
||||
if (prefetchNeeded) {
|
||||
var downloadSize = tableQuery.top || this.downloadSize;
|
||||
this.prefetchAndRender(
|
||||
tableQuery,
|
||||
this.tablePageStartIndex,
|
||||
tablePageSize,
|
||||
downloadSize,
|
||||
draw,
|
||||
fnCallback,
|
||||
oSettings,
|
||||
columnSortOrder
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public addEntityToCache(entity: Entities.ITableEntity): Q.Promise<any> {
|
||||
// Delay the add operation if we are fetching data from server, so as to avoid race condition.
|
||||
if (this.cache.serverCallInProgress) {
|
||||
return Utilities.delay(this.pollingInterval).then(() => {
|
||||
return this.updateCachedEntity(entity);
|
||||
});
|
||||
}
|
||||
|
||||
// Find the first item which is greater than the added entity.
|
||||
var oSettings: any = (<any>this.table).context[0];
|
||||
var index: number = _.findIndex(this.cache.data, (data: any) => {
|
||||
return this.dataComparer(data, entity, this.cache.sortOrder, oSettings) > 0;
|
||||
});
|
||||
|
||||
// If no such item, then insert at last.
|
||||
var insertIndex: number = Utilities.ensureBetweenBounds(
|
||||
index < 0 ? this.cache.length : index,
|
||||
0,
|
||||
this.cache.length
|
||||
);
|
||||
|
||||
this.cache.data.splice(insertIndex, 0, entity);
|
||||
|
||||
// Finally, select newly added entity
|
||||
this.clearSelection();
|
||||
this.selected.push(entity);
|
||||
|
||||
return Q.resolve(null);
|
||||
}
|
||||
|
||||
public updateCachedEntity(entity: Entities.ITableEntity): Q.Promise<any> {
|
||||
// Delay the add operation if we are fetching data from server, so as to avoid race condition.
|
||||
if (this.cache.serverCallInProgress) {
|
||||
return Utilities.delay(this.pollingInterval).then(() => {
|
||||
return this.updateCachedEntity(entity);
|
||||
});
|
||||
}
|
||||
var oldEntityIndex: number = _.findIndex(
|
||||
this.cache.data,
|
||||
(data: Entities.ITableEntity) => data.RowKey._ === entity.RowKey._
|
||||
);
|
||||
|
||||
this.cache.data.splice(oldEntityIndex, 1, entity);
|
||||
|
||||
return Q.resolve(null);
|
||||
}
|
||||
|
||||
public removeEntitiesFromCache(entities: Entities.ITableEntity[]): Q.Promise<any> {
|
||||
if (!entities) {
|
||||
return Q.resolve(null);
|
||||
}
|
||||
|
||||
// Delay the remove operation if we are fetching data from server, so as to avoid race condition.
|
||||
if (this.cache.serverCallInProgress) {
|
||||
return Utilities.delay(this.pollingInterval).then(() => {
|
||||
return this.removeEntitiesFromCache(entities);
|
||||
});
|
||||
}
|
||||
|
||||
entities &&
|
||||
entities.forEach((entity: Entities.ITableEntity) => {
|
||||
var cachedIndex: number = _.findIndex(
|
||||
this.cache.data,
|
||||
(e: Entities.ITableEntity) => e.RowKey._ === entity.RowKey._
|
||||
);
|
||||
if (cachedIndex >= 0) {
|
||||
this.cache.data.splice(cachedIndex, 1);
|
||||
}
|
||||
});
|
||||
this.clearSelection();
|
||||
|
||||
// Show last available page if there is not enough data
|
||||
var pageInfo = this.table.page.info();
|
||||
if (this.cache.length <= pageInfo.start) {
|
||||
var availablePages = Math.ceil(this.cache.length / pageInfo.length);
|
||||
var pageToShow = availablePages > 0 ? availablePages - 1 : 0;
|
||||
this.table.page(pageToShow);
|
||||
}
|
||||
|
||||
return Q.resolve(null);
|
||||
}
|
||||
|
||||
protected dataComparer(
|
||||
item1: Entities.ITableEntity,
|
||||
item2: Entities.ITableEntity,
|
||||
sortOrder: any[],
|
||||
oSettings: any
|
||||
): number {
|
||||
var sort: any;
|
||||
var itemA: any;
|
||||
var itemB: any;
|
||||
var length: number = $.isArray(sortOrder) ? sortOrder.length : 0; // sortOrder can be null
|
||||
var rowA: Entities.ITableEntity = item1;
|
||||
var rowB: Entities.ITableEntity = item2;
|
||||
|
||||
for (var k = 0; k < length; k++) {
|
||||
sort = sortOrder[k];
|
||||
var col = oSettings.aoColumns[sort.column].mData;
|
||||
|
||||
// If the value is null or undefined, show them at last
|
||||
var isItem1NullOrUndefined = _.isNull(rowA[col]) || _.isUndefined(rowA[col]);
|
||||
var isItem2NullOrUndefined = _.isNull(rowB[col]) || _.isUndefined(rowB[col]);
|
||||
|
||||
if (isItem1NullOrUndefined || isItem2NullOrUndefined) {
|
||||
if (isItem1NullOrUndefined && isItem2NullOrUndefined) {
|
||||
return 0;
|
||||
}
|
||||
return isItem1NullOrUndefined ? 1 : -1;
|
||||
}
|
||||
|
||||
switch ((<any>rowA[col]).$) {
|
||||
case Constants.TableType.Int32:
|
||||
case Constants.TableType.Int64:
|
||||
case Constants.CassandraType.Int:
|
||||
case Constants.CassandraType.Bigint:
|
||||
case Constants.CassandraType.Smallint:
|
||||
case Constants.CassandraType.Varint:
|
||||
case Constants.CassandraType.Tinyint:
|
||||
itemA = parseInt(<string>(<any>rowA[col])._, 0);
|
||||
itemB = parseInt(<string>(<any>rowB[col])._, 0);
|
||||
break;
|
||||
case Constants.TableType.Double:
|
||||
case Constants.CassandraType.Double:
|
||||
case Constants.CassandraType.Float:
|
||||
case Constants.CassandraType.Decimal:
|
||||
itemA = parseFloat(<string>(<any>rowA[col])._);
|
||||
itemB = parseFloat(<string>(<any>rowB[col])._);
|
||||
break;
|
||||
case Constants.TableType.DateTime:
|
||||
itemA = new Date(<string>(<any>rowA[col])._);
|
||||
itemB = new Date(<string>(<any>rowB[col])._);
|
||||
break;
|
||||
default:
|
||||
itemA = <string>(<any>rowA[col])._.toLowerCase();
|
||||
itemB = <string>(<any>rowB[col])._.toLowerCase();
|
||||
}
|
||||
var compareResult: number = itemA < itemB ? -1 : itemA > itemB ? 1 : 0;
|
||||
if (compareResult !== 0) {
|
||||
return sort.dir === "asc" ? compareResult : -compareResult;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected isCacheValid(tableQuery: Entities.ITableQuery): boolean {
|
||||
// Return false if either cache has no data or the search criteria don't match!
|
||||
if (!this.cache || !this.cache.data || this.cache.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!tableQuery && !this.cache.tableQuery) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Compare by value using JSON representation
|
||||
if (JSON.stringify(this.cache.tableQuery) !== JSON.stringify(tableQuery)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Override as table entity has special keys for a Data Table row.
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
protected matchesKeys(item: Entities.ITableEntity, itemKeys: Entities.IProperty[]): boolean {
|
||||
return itemKeys.every((property: Entities.IProperty) => {
|
||||
return this.stringCompare(item[property.key]._, property.value);
|
||||
});
|
||||
}
|
||||
|
||||
private prefetchAndRender(
|
||||
tableQuery: Entities.ITableQuery,
|
||||
tablePageStartIndex: number,
|
||||
tablePageSize: number,
|
||||
downloadSize: number,
|
||||
draw: number,
|
||||
renderCallBack: Function,
|
||||
oSettings: any,
|
||||
columnSortOrder: any
|
||||
): void {
|
||||
this.queryErrorMessage(null);
|
||||
if (this.cache.serverCallInProgress) {
|
||||
return;
|
||||
}
|
||||
this.prefetchData(tableQuery, downloadSize, /* currentRetry */ 0)
|
||||
.then((result: IListTableEntitiesSegmentedResult) => {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
var entities = this.cache.data;
|
||||
if (
|
||||
this.queryTablesTab.container.isPreferredApiCassandra() &&
|
||||
DataTableUtilities.checkForDefaultHeader(this.headers)
|
||||
) {
|
||||
(<CassandraAPIDataClient>this.queryTablesTab.container.tableDataClient)
|
||||
.getTableSchema(this.queryTablesTab.collection)
|
||||
.then((headers: CassandraTableKey[]) => {
|
||||
this.updateHeaders(
|
||||
headers.map(header => header.property),
|
||||
true
|
||||
);
|
||||
});
|
||||
} else {
|
||||
var selectedHeadersUnion: string[] = DataTableUtilities.getPropertyIntersectionFromTableEntities(
|
||||
entities,
|
||||
this.queryTablesTab.container.isPreferredApiCassandra()
|
||||
);
|
||||
var newHeaders: string[] = _.difference(selectedHeadersUnion, this.headers);
|
||||
if (newHeaders.length > 0) {
|
||||
// Any new columns found will be added into headers array, which will trigger a re-render of the DataTable.
|
||||
// So there is no need to call it here.
|
||||
this.updateHeaders(newHeaders, /* notifyColumnChanges */ true);
|
||||
} else {
|
||||
if (columnSortOrder) {
|
||||
this.sortColumns(columnSortOrder, oSettings);
|
||||
}
|
||||
this.renderPage(renderCallBack, draw, tablePageStartIndex, tablePageSize, oSettings);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.ExceedMaximumRetries) {
|
||||
var message: string = "We are having trouble getting your data. Please try again."; // localize
|
||||
}
|
||||
})
|
||||
.catch((error: any) => {
|
||||
const parsedErrors = ErrorParserUtility.parse(error);
|
||||
var errors = parsedErrors.map((error: DataModels.ErrorDataModel) => {
|
||||
return <ViewModels.QueryError>{
|
||||
message: error.message,
|
||||
start: error.location ? error.location.start : undefined,
|
||||
end: error.location ? error.location.end : undefined,
|
||||
code: error.code,
|
||||
severity: error.severity
|
||||
};
|
||||
});
|
||||
this.queryErrorMessage(errors[0].message);
|
||||
if (this.queryTablesTab.onLoadStartKey != null && this.queryTablesTab.onLoadStartKey != undefined) {
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.Tab,
|
||||
{
|
||||
databaseAccountName: this.queryTablesTab.collection.container.databaseAccount().name,
|
||||
databaseName: this.queryTablesTab.collection.databaseId,
|
||||
collectionName: this.queryTablesTab.collection.id(),
|
||||
defaultExperience: this.queryTablesTab.collection.container.defaultExperience(),
|
||||
dataExplorerArea: Areas.Tab,
|
||||
tabTitle: this.queryTablesTab.tabTitle(),
|
||||
error: error
|
||||
},
|
||||
this.queryTablesTab.onLoadStartKey
|
||||
);
|
||||
this.queryTablesTab.onLoadStartKey = null;
|
||||
}
|
||||
DataTableUtilities.turnOffProgressIndicator();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep recursively prefetching items if:
|
||||
* 1. Continuation token is not null
|
||||
* 2. And prefetched items hasn't reach predefined cache size.
|
||||
* 3. And retry times hasn't reach the predefined maximum retry number.
|
||||
*
|
||||
* It is possible for a query to return no results but still return a continuation header (e.g. if the query takes too long).
|
||||
* If this is the case, we try to fetch entities again.
|
||||
* Note that this also means that we can get less entities than the requested download size in a successful call.
|
||||
* See Microsoft Azure API Documentation at: https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx
|
||||
*/
|
||||
private prefetchData(
|
||||
tableQuery: Entities.ITableQuery,
|
||||
downloadSize: number,
|
||||
currentRetry: number = 0
|
||||
): Q.Promise<any> {
|
||||
if (!this.cache.serverCallInProgress) {
|
||||
this.cache.serverCallInProgress = true;
|
||||
this.allDownloaded = false;
|
||||
this.lastPrefetchTime = new Date().getTime();
|
||||
var time = this.lastPrefetchTime;
|
||||
|
||||
var promise: Q.Promise<IListTableEntitiesSegmentedResult>;
|
||||
if (this._documentIterator && this.continuationToken) {
|
||||
// TODO handle Cassandra case
|
||||
|
||||
promise = Q(this._documentIterator.fetchNext().then(response => response.resources)).then(
|
||||
(documents: any[]) => {
|
||||
let entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(documents);
|
||||
let finalEntities: IListTableEntitiesSegmentedResult = <IListTableEntitiesSegmentedResult>{
|
||||
Results: entities,
|
||||
ContinuationToken: this._documentIterator.hasMoreResults()
|
||||
};
|
||||
return Q.resolve(finalEntities);
|
||||
}
|
||||
);
|
||||
} else if (this.continuationToken && this.queryTablesTab.container.isPreferredApiCassandra()) {
|
||||
promise = this.queryTablesTab.container.tableDataClient.queryDocuments(
|
||||
this.queryTablesTab.collection,
|
||||
this.cqlQuery(),
|
||||
true,
|
||||
this.continuationToken
|
||||
);
|
||||
} else {
|
||||
let query = this.sqlQuery();
|
||||
if (this.queryTablesTab.container.isPreferredApiCassandra()) {
|
||||
query = this.cqlQuery();
|
||||
}
|
||||
promise = this.queryTablesTab.container.tableDataClient.queryDocuments(
|
||||
this.queryTablesTab.collection,
|
||||
query,
|
||||
true
|
||||
);
|
||||
}
|
||||
return promise
|
||||
.then((result: IListTableEntitiesSegmentedResult) => {
|
||||
if (!this._documentIterator) {
|
||||
this._documentIterator = result.iterator;
|
||||
}
|
||||
var actualDownloadSize: number = 0;
|
||||
|
||||
// If we hit this, it means another service call is triggered. We only handle the latest call.
|
||||
// And as another service call is during process, we don't set serverCallInProgress to false here.
|
||||
// Thus, end the prefetch.
|
||||
if (this.lastPrefetchTime !== time) {
|
||||
return Q.resolve(null);
|
||||
}
|
||||
|
||||
var entities = result.Results;
|
||||
actualDownloadSize = entities.length;
|
||||
|
||||
// Queries can fetch no results and still return a continuation header. See prefetchAndRender() method.
|
||||
this.continuationToken = this.isCancelled ? null : result.ContinuationToken;
|
||||
|
||||
if (!this.continuationToken) {
|
||||
this.allDownloaded = true;
|
||||
}
|
||||
|
||||
if (this.isCacheValid(tableQuery)) {
|
||||
// Append to cache.
|
||||
this.cache.data = this.cache.data.concat(entities.slice(0));
|
||||
} else {
|
||||
// Create cache.
|
||||
this.cache.data = entities;
|
||||
}
|
||||
|
||||
this.cache.tableQuery = tableQuery;
|
||||
this.cache.serverCallInProgress = false;
|
||||
|
||||
var nextDownloadSize: number = downloadSize - actualDownloadSize;
|
||||
if (nextDownloadSize === 0 && tableQuery.top) {
|
||||
this.allDownloaded = true;
|
||||
}
|
||||
|
||||
// There are three possible results for a prefetch:
|
||||
// 1. Continuation token is null or fetched items' size reaches predefined.
|
||||
// 2. Continuation token is not null and fetched items' size hasn't reach predefined.
|
||||
// 2.1 Retry times has reached predefined maximum.
|
||||
// 2.2 Retry times hasn't reached predefined maximum.
|
||||
// Correspondingly,
|
||||
// For #1, end prefetch.
|
||||
// For #2.1, set prefetch exceeds maximum retry number and end prefetch.
|
||||
// For #2.2, go to next round prefetch.
|
||||
if (this.allDownloaded || nextDownloadSize === 0) {
|
||||
return Q.resolve(result);
|
||||
}
|
||||
|
||||
if (currentRetry >= TableEntityListViewModel._maximumNumberOfPrefetchRetries) {
|
||||
result.ExceedMaximumRetries = true;
|
||||
return Q.resolve(result);
|
||||
}
|
||||
return this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1);
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
this.cache.serverCallInProgress = false;
|
||||
return Q.reject(error);
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
38
src/Explorer/Tables/Entities.ts
Normal file
38
src/Explorer/Tables/Entities.ts
Normal file
@@ -0,0 +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;
|
||||
}
|
||||
327
src/Explorer/Tables/QueryBuilder/ClauseGroup.ts
Normal file
327
src/Explorer/Tables/QueryBuilder/ClauseGroup.ts
Normal file
@@ -0,0 +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;
|
||||
}
|
||||
}
|
||||
49
src/Explorer/Tables/QueryBuilder/ClauseGroupViewModel.ts
Normal file
49
src/Explorer/Tables/QueryBuilder/ClauseGroupViewModel.ts
Normal file
@@ -0,0 +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];
|
||||
}
|
||||
}
|
||||
}
|
||||
377
src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts
Normal file
377
src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts
Normal file
@@ -0,0 +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 */
|
||||
}
|
||||
110
src/Explorer/Tables/QueryBuilder/DateTimeUtilities.test.ts
Normal file
110
src/Explorer/Tables/QueryBuilder/DateTimeUtilities.test.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import * as DateTimeUtilities from "./DateTimeUtilities";
|
||||
|
||||
describe("DateTimeUtilities", () => {
|
||||
const testDateTime1 = new Date("Fri Jul 26 2019 17:03:02 GMT-0700 (Pacific Daylight Time)");
|
||||
const testDateTime2 = new Date("Mon Dec 31 2018 16:00:00 GMT-0800 (Pacific Standard Time)");
|
||||
const testUnixTime1 = 1564185782;
|
||||
const testUnixTime2 = 1546300800;
|
||||
const testTicks1 = "00636997825820000000";
|
||||
const testTicks2 = "00636818976000000000";
|
||||
|
||||
describe("getLocalDateTime", () => {
|
||||
it("should return right local time for date time 1", () => {
|
||||
const time = DateTimeUtilities.getLocalDateTime(testDateTime1.toISOString());
|
||||
expect(new Date(time).toLocaleString()).toBe(testDateTime1.toLocaleString());
|
||||
});
|
||||
it("should return right local time for date time 2", () => {
|
||||
const time = DateTimeUtilities.getLocalDateTime(testDateTime2.toISOString());
|
||||
expect(new Date(time).toLocaleString()).toBe(testDateTime2.toLocaleString());
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUTCDateTime", () => {
|
||||
it("should return right utc time for date time 1", () => {
|
||||
const time = DateTimeUtilities.getUTCDateTime(testDateTime1.toISOString());
|
||||
expect(time).toBe("2019-07-27T00:03:02.000Z");
|
||||
});
|
||||
it("should return right utc time for date time 2", () => {
|
||||
const time = DateTimeUtilities.getUTCDateTime(testDateTime2.toISOString());
|
||||
expect(time).toBe("2019-01-01T00:00:00.000Z");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ensureDoubleDigits", () => {
|
||||
it("should return correct double digits with input of single digit", () => {
|
||||
const digits = DateTimeUtilities.ensureDoubleDigits(2);
|
||||
expect(digits).toBe("02");
|
||||
});
|
||||
it("should return correct double digits with input of double digit", () => {
|
||||
const digits = DateTimeUtilities.ensureDoubleDigits(53);
|
||||
expect(digits).toBe("53");
|
||||
});
|
||||
it("should return correct double digits with input of multi digit", () => {
|
||||
const digits = DateTimeUtilities.ensureDoubleDigits(321654);
|
||||
expect(digits).toBe("32");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ensureTripleDigits", () => {
|
||||
it("should return correct triple digits with input of single digit", () => {
|
||||
const digits = DateTimeUtilities.ensureTripleDigits(2);
|
||||
expect(digits).toBe("002");
|
||||
});
|
||||
it("should return correct triple digits with double digit", () => {
|
||||
const digits = DateTimeUtilities.ensureTripleDigits(53);
|
||||
expect(digits).toBe("053");
|
||||
});
|
||||
it("should return correct triple digits with triple digit", () => {
|
||||
const digits = DateTimeUtilities.ensureTripleDigits(344);
|
||||
expect(digits).toBe("344");
|
||||
});
|
||||
it("should return correct triple digits with multi digit", () => {
|
||||
const digits = DateTimeUtilities.ensureTripleDigits(321654);
|
||||
expect(digits).toBe("321");
|
||||
});
|
||||
});
|
||||
|
||||
describe("convertUnixToJSDate", () => {
|
||||
it("should convert unix number to JS Date for date time 1", () => {
|
||||
const time = DateTimeUtilities.convertUnixToJSDate(testUnixTime1);
|
||||
expect(time.toISOString()).toBe("2019-07-27T00:03:02.000Z");
|
||||
});
|
||||
it("should convert unix number to JS Date for date time 2", () => {
|
||||
const time = DateTimeUtilities.convertUnixToJSDate(testUnixTime2);
|
||||
expect(time.toISOString()).toBe(testDateTime2.toISOString());
|
||||
});
|
||||
});
|
||||
|
||||
describe("convertJSDateToUnix", () => {
|
||||
it("should convert JS Date to unix number for date time 1", () => {
|
||||
const time = DateTimeUtilities.convertJSDateToUnix(testDateTime1.toISOString());
|
||||
expect(time).toBe(testUnixTime1);
|
||||
});
|
||||
it("should convert JS Date to unix number for date time 2", () => {
|
||||
const time = DateTimeUtilities.convertJSDateToUnix(testDateTime2.toISOString());
|
||||
expect(time).toBe(testUnixTime2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("convertTicksToJSDate", () => {
|
||||
it("should convert ticks to JS Date for date time 1", () => {
|
||||
const time = DateTimeUtilities.convertTicksToJSDate(testTicks1);
|
||||
expect(time.toISOString()).toBe(testDateTime1.toISOString());
|
||||
});
|
||||
it("should convert ticks to JS Date for date time 2", () => {
|
||||
const time = DateTimeUtilities.convertTicksToJSDate(testTicks2);
|
||||
expect(time.toISOString()).toBe(testDateTime2.toISOString());
|
||||
});
|
||||
});
|
||||
|
||||
describe("convertJSDateToTicksWithPadding", () => {
|
||||
it("should convert JS Date to ticks for date time 1", () => {
|
||||
const time = DateTimeUtilities.convertJSDateToTicksWithPadding(testDateTime1.toISOString());
|
||||
expect(time).toBe(testTicks1);
|
||||
});
|
||||
it("should convert JS Date to ticks for date time 2", () => {
|
||||
const time = DateTimeUtilities.convertJSDateToTicksWithPadding(testDateTime2.toISOString());
|
||||
expect(time).toBe(testTicks2);
|
||||
});
|
||||
});
|
||||
});
|
||||
67
src/Explorer/Tables/QueryBuilder/DateTimeUtilities.ts
Normal file
67
src/Explorer/Tables/QueryBuilder/DateTimeUtilities.ts
Normal file
@@ -0,0 +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);
|
||||
}
|
||||
796
src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts
Normal file
796
src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts
Normal file
@@ -0,0 +1,796 @@
|
||||
import * as ko from "knockout";
|
||||
import * as CustomTimestampHelper from "./CustomTimestampHelper";
|
||||
import QueryClauseViewModel from "./QueryClauseViewModel";
|
||||
import ClauseGroup from "./ClauseGroup";
|
||||
import ClauseGroupViewModel from "./ClauseGroupViewModel";
|
||||
import QueryViewModel from "./QueryViewModel";
|
||||
import * as Constants from "../Constants";
|
||||
import TableEntityListViewModel from "../DataTable/TableEntityListViewModel";
|
||||
import * as DateTimeUtilities from "./DateTimeUtilities";
|
||||
import * as DataTableUtilities from "../DataTable/DataTableUtilities";
|
||||
import * as TableEntityProcessor from "../TableEntityProcessor";
|
||||
import * as Utilities from "../Utilities";
|
||||
import { KeyCodes } from "../../../Common/Constants";
|
||||
|
||||
export default class QueryBuilderViewModel {
|
||||
/* Labels */
|
||||
public andLabel = "And/Or"; // localize
|
||||
public actionLabel = "Action"; // localize
|
||||
public fieldLabel = "Field"; // localize
|
||||
public dataTypeLabel = "Type"; // localize
|
||||
public operatorLabel = "Operator"; // localize
|
||||
public valueLabel = "Value"; // localize
|
||||
|
||||
/* controls */
|
||||
public addNewClauseLine = "Add new clause"; // localize
|
||||
public insertNewFilterLine = "Insert new filter line"; // localize
|
||||
public removeThisFilterLine = "Remove this filter line"; // localize
|
||||
public groupSelectedClauses = "Group selected clauses"; // localize
|
||||
public clauseArray = ko.observableArray<QueryClauseViewModel>(); // This is for storing the clauses in flattened form queryClauses for easier UI data binding.
|
||||
public queryClauses = new ClauseGroup(true, null); // The actual data structure containing the clause information.
|
||||
public columnOptions: ko.ObservableArray<string>;
|
||||
public canGroupClauses = ko.observable<boolean>(false);
|
||||
|
||||
/* Observables */
|
||||
public edmTypes = ko.observableArray([
|
||||
Constants.TableType.String,
|
||||
Constants.TableType.Boolean,
|
||||
Constants.TableType.Binary,
|
||||
Constants.TableType.DateTime,
|
||||
Constants.TableType.Double,
|
||||
Constants.TableType.Guid,
|
||||
Constants.TableType.Int32,
|
||||
Constants.TableType.Int64,
|
||||
""
|
||||
]);
|
||||
public operators = ko.observableArray([
|
||||
Constants.Operator.Equal,
|
||||
Constants.Operator.GreaterThan,
|
||||
Constants.Operator.GreaterThanOrEqualTo,
|
||||
Constants.Operator.LessThan,
|
||||
Constants.Operator.LessThanOrEqualTo,
|
||||
Constants.Operator.NotEqualTo,
|
||||
""
|
||||
]);
|
||||
public clauseRules = ko.observableArray([Constants.ClauseRule.And, Constants.ClauseRule.Or]);
|
||||
public timeOptions = ko.observableArray([
|
||||
Constants.timeOptions.lastHour,
|
||||
Constants.timeOptions.last24Hours,
|
||||
Constants.timeOptions.last7Days,
|
||||
Constants.timeOptions.last31Days,
|
||||
Constants.timeOptions.last365Days,
|
||||
Constants.timeOptions.currentMonth,
|
||||
Constants.timeOptions.currentYear
|
||||
//Constants.timeOptions.custom
|
||||
]);
|
||||
public queryString = ko.observable<string>();
|
||||
private _queryViewModel: QueryViewModel;
|
||||
public tableEntityListViewModel: TableEntityListViewModel;
|
||||
private scrollEventListener: boolean;
|
||||
|
||||
constructor(queryViewModel: QueryViewModel, tableEntityListViewModel: TableEntityListViewModel) {
|
||||
if (tableEntityListViewModel.queryTablesTab.container.isPreferredApiCassandra()) {
|
||||
this.edmTypes([
|
||||
Constants.CassandraType.Text,
|
||||
Constants.CassandraType.Ascii,
|
||||
Constants.CassandraType.Bigint,
|
||||
Constants.CassandraType.Blob,
|
||||
Constants.CassandraType.Boolean,
|
||||
Constants.CassandraType.Decimal,
|
||||
Constants.CassandraType.Double,
|
||||
Constants.CassandraType.Float,
|
||||
Constants.CassandraType.Int,
|
||||
Constants.CassandraType.Uuid,
|
||||
Constants.CassandraType.Varchar,
|
||||
Constants.CassandraType.Varint,
|
||||
Constants.CassandraType.Inet,
|
||||
Constants.CassandraType.Smallint,
|
||||
Constants.CassandraType.Tinyint
|
||||
]);
|
||||
this.clauseRules([
|
||||
Constants.ClauseRule.And
|
||||
// OR is not supported in CQL
|
||||
]);
|
||||
this.andLabel = "And";
|
||||
}
|
||||
this.clauseArray();
|
||||
|
||||
this._queryViewModel = queryViewModel;
|
||||
this.tableEntityListViewModel = tableEntityListViewModel;
|
||||
this.columnOptions = ko.observableArray<string>(queryViewModel.columnOptions());
|
||||
|
||||
this.columnOptions.subscribe(newColumnOptions => {
|
||||
queryViewModel.columnOptions(newColumnOptions);
|
||||
});
|
||||
}
|
||||
|
||||
public setExample() {
|
||||
var example1 = new QueryClauseViewModel(
|
||||
this,
|
||||
"",
|
||||
"PartitionKey",
|
||||
this.edmTypes()[0],
|
||||
Constants.Operator.Equal,
|
||||
this.tableEntityListViewModel.items()[0].PartitionKey._,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
//null,
|
||||
true
|
||||
);
|
||||
var example2 = new QueryClauseViewModel(
|
||||
this,
|
||||
"And",
|
||||
"RowKey",
|
||||
this.edmTypes()[0],
|
||||
Constants.Operator.Equal,
|
||||
this.tableEntityListViewModel.items()[0].RowKey._,
|
||||
true,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
//null,
|
||||
true
|
||||
);
|
||||
this.addClauseImpl(example1, 0);
|
||||
this.addClauseImpl(example2, 1);
|
||||
}
|
||||
|
||||
public getODataFilterFromClauses = (): string => {
|
||||
var filterString: string = "";
|
||||
var treeTraversal = (group: ClauseGroup): void => {
|
||||
for (var i = 0; i < group.children.length; i++) {
|
||||
var currentItem = group.children[i];
|
||||
|
||||
if (currentItem instanceof QueryClauseViewModel) {
|
||||
var clause = <QueryClauseViewModel>currentItem;
|
||||
this.timestampToValue(clause);
|
||||
filterString = filterString.concat(
|
||||
this.constructODataClause(
|
||||
filterString === "" ? "" : clause.and_or(),
|
||||
this.generateLeftParentheses(clause),
|
||||
clause.field(),
|
||||
clause.type(),
|
||||
clause.operator(),
|
||||
clause.value(),
|
||||
this.generateRightParentheses(clause)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (currentItem instanceof ClauseGroup) {
|
||||
treeTraversal(<ClauseGroup>currentItem);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
treeTraversal(this.queryClauses);
|
||||
|
||||
return filterString.trim();
|
||||
};
|
||||
|
||||
public getSqlFilterFromClauses = (): string => {
|
||||
var filterString: string = "SELECT * FROM c";
|
||||
if (this._queryViewModel.selectText() && this._queryViewModel.selectText().length > 0) {
|
||||
filterString = "SELECT";
|
||||
const selectText = this._queryViewModel && this._queryViewModel.selectText && this._queryViewModel.selectText();
|
||||
selectText &&
|
||||
selectText.forEach((value: string) => {
|
||||
if (value === Constants.EntityKeyNames.PartitionKey) {
|
||||
value = `["${TableEntityProcessor.keyProperties.PartitionKey}"]`;
|
||||
filterString = filterString.concat(filterString === "SELECT" ? " c" : ", c");
|
||||
} else if (value === Constants.EntityKeyNames.RowKey) {
|
||||
value = `["${TableEntityProcessor.keyProperties.Id2}"]`;
|
||||
filterString = filterString.concat(filterString === "SELECT" ? " c" : ", c");
|
||||
} else {
|
||||
if (value === Constants.EntityKeyNames.Timestamp) {
|
||||
value = TableEntityProcessor.keyProperties.Timestamp;
|
||||
}
|
||||
filterString = filterString.concat(filterString === "SELECT" ? " c." : ", c.");
|
||||
}
|
||||
filterString = filterString.concat(value);
|
||||
});
|
||||
filterString = filterString.concat(" FROM c");
|
||||
}
|
||||
if (this.queryClauses.children.length === 0) {
|
||||
return filterString;
|
||||
}
|
||||
filterString = filterString.concat(" WHERE");
|
||||
var first = true;
|
||||
var treeTraversal = (group: ClauseGroup): void => {
|
||||
for (var i = 0; i < group.children.length; i++) {
|
||||
var currentItem = group.children[i];
|
||||
|
||||
if (currentItem instanceof QueryClauseViewModel) {
|
||||
var clause = <QueryClauseViewModel>currentItem;
|
||||
let timeStampValue: string = this.timestampToSqlValue(clause);
|
||||
var value = clause.value();
|
||||
if (!clause.isValue()) {
|
||||
value = timeStampValue;
|
||||
}
|
||||
filterString = filterString.concat(
|
||||
this.constructSqlClause(
|
||||
first ? "" : clause.and_or(),
|
||||
this.generateLeftParentheses(clause),
|
||||
clause.field(),
|
||||
clause.type(),
|
||||
clause.operator(),
|
||||
value,
|
||||
this.generateRightParentheses(clause)
|
||||
)
|
||||
);
|
||||
first = false;
|
||||
}
|
||||
|
||||
if (currentItem instanceof ClauseGroup) {
|
||||
treeTraversal(<ClauseGroup>currentItem);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
treeTraversal(this.queryClauses);
|
||||
|
||||
return filterString.trim();
|
||||
};
|
||||
|
||||
public getCqlFilterFromClauses = (): string => {
|
||||
const databaseId = this._queryViewModel.queryTablesTab.collection.databaseId;
|
||||
const collectionId = this._queryViewModel.queryTablesTab.collection.id();
|
||||
const tableToQuery = `${databaseId}.${collectionId}`;
|
||||
var filterString: string = `SELECT * FROM ${tableToQuery}`;
|
||||
if (this._queryViewModel.selectText() && this._queryViewModel.selectText().length > 0) {
|
||||
filterString = "SELECT";
|
||||
const selectText = this._queryViewModel && this._queryViewModel.selectText && this._queryViewModel.selectText();
|
||||
selectText &&
|
||||
selectText.forEach((value: string) => {
|
||||
filterString = filterString.concat(filterString === "SELECT" ? " " : ", ");
|
||||
filterString = filterString.concat(value);
|
||||
});
|
||||
filterString = filterString.concat(` FROM ${tableToQuery}`);
|
||||
}
|
||||
if (this.queryClauses.children.length === 0) {
|
||||
return filterString;
|
||||
}
|
||||
filterString = filterString.concat(" WHERE");
|
||||
var first = true;
|
||||
var treeTraversal = (group: ClauseGroup): void => {
|
||||
for (var i = 0; i < group.children.length; i++) {
|
||||
var currentItem = group.children[i];
|
||||
|
||||
if (currentItem instanceof QueryClauseViewModel) {
|
||||
var clause = <QueryClauseViewModel>currentItem;
|
||||
let timeStampValue: string = this.timestampToSqlValue(clause);
|
||||
var value = clause.value();
|
||||
if (!clause.isValue()) {
|
||||
value = timeStampValue;
|
||||
}
|
||||
filterString = filterString.concat(
|
||||
this.constructCqlClause(
|
||||
first ? "" : clause.and_or(),
|
||||
this.generateLeftParentheses(clause),
|
||||
clause.field(),
|
||||
clause.type(),
|
||||
clause.operator(),
|
||||
value,
|
||||
this.generateRightParentheses(clause)
|
||||
)
|
||||
);
|
||||
first = false;
|
||||
}
|
||||
|
||||
if (currentItem instanceof ClauseGroup) {
|
||||
treeTraversal(<ClauseGroup>currentItem);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
treeTraversal(this.queryClauses);
|
||||
|
||||
return filterString.trim();
|
||||
};
|
||||
|
||||
public updateColumnOptions = (): void => {
|
||||
let originalHeaders = this.columnOptions();
|
||||
let newHeaders = this.tableEntityListViewModel.headers;
|
||||
this.columnOptions(newHeaders.sort(DataTableUtilities.compareTableColumns));
|
||||
};
|
||||
|
||||
private generateLeftParentheses(clause: QueryClauseViewModel): string {
|
||||
var result = "";
|
||||
|
||||
if (clause.clauseGroup.isRootGroup || clause.clauseGroup.children.indexOf(clause) !== 0) {
|
||||
return result;
|
||||
} else {
|
||||
result = result.concat("(");
|
||||
}
|
||||
|
||||
var currentGroup: ClauseGroup = clause.clauseGroup;
|
||||
|
||||
while (
|
||||
!currentGroup.isRootGroup &&
|
||||
!currentGroup.parentGroup.isRootGroup &&
|
||||
currentGroup.parentGroup.children.indexOf(currentGroup) === 0
|
||||
) {
|
||||
result = result.concat("(");
|
||||
currentGroup = currentGroup.parentGroup;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private generateRightParentheses(clause: QueryClauseViewModel): string {
|
||||
var result = "";
|
||||
|
||||
if (
|
||||
clause.clauseGroup.isRootGroup ||
|
||||
clause.clauseGroup.children.indexOf(clause) !== clause.clauseGroup.children.length - 1
|
||||
) {
|
||||
return result;
|
||||
} else {
|
||||
result = result.concat(")");
|
||||
}
|
||||
|
||||
var currentGroup: ClauseGroup = clause.clauseGroup;
|
||||
|
||||
while (
|
||||
!currentGroup.isRootGroup &&
|
||||
!currentGroup.parentGroup.isRootGroup &&
|
||||
currentGroup.parentGroup.children.indexOf(currentGroup) === currentGroup.parentGroup.children.length - 1
|
||||
) {
|
||||
result = result.concat(")");
|
||||
currentGroup = currentGroup.parentGroup;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private constructODataClause = (
|
||||
clauseRule: string,
|
||||
leftParentheses: string,
|
||||
propertyName: string,
|
||||
type: string,
|
||||
operator: string,
|
||||
value: string,
|
||||
rightParentheses: string
|
||||
): string => {
|
||||
switch (type) {
|
||||
case Constants.TableType.DateTime:
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter(
|
||||
operator
|
||||
)} ${value}${rightParentheses}`;
|
||||
case Constants.TableType.String:
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter(
|
||||
operator
|
||||
)} \'${value}\'${rightParentheses}`;
|
||||
case Constants.TableType.Guid:
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter(
|
||||
operator
|
||||
)} guid\'${value}\'${rightParentheses}`;
|
||||
case Constants.TableType.Binary:
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter(
|
||||
operator
|
||||
)} binary\'${value}\'${rightParentheses}`;
|
||||
default:
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter(
|
||||
operator
|
||||
)} ${value}${rightParentheses}`;
|
||||
}
|
||||
};
|
||||
|
||||
private constructSqlClause = (
|
||||
clauseRule: string,
|
||||
leftParentheses: string,
|
||||
propertyName: string,
|
||||
type: string,
|
||||
operator: string,
|
||||
value: string,
|
||||
rightParentheses: string
|
||||
): string => {
|
||||
if (propertyName === Constants.EntityKeyNames.PartitionKey) {
|
||||
propertyName = TableEntityProcessor.keyProperties.PartitionKey;
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c["${propertyName}"] ${operator} \'${value}\'${rightParentheses}`;
|
||||
} else if (propertyName === Constants.EntityKeyNames.RowKey) {
|
||||
propertyName = TableEntityProcessor.keyProperties.Id;
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName} ${operator} \'${value}\'${rightParentheses}`;
|
||||
} else if (propertyName === Constants.EntityKeyNames.Timestamp) {
|
||||
propertyName = TableEntityProcessor.keyProperties.Timestamp;
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName} ${operator} ${DateTimeUtilities.convertJSDateToUnix(
|
||||
value
|
||||
)}${rightParentheses}`;
|
||||
}
|
||||
switch (type) {
|
||||
case Constants.TableType.DateTime:
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} \'${DateTimeUtilities.convertJSDateToTicksWithPadding(
|
||||
value
|
||||
)}\'${rightParentheses}`;
|
||||
case Constants.TableType.Int64:
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} \'${Utilities.padLongWithZeros(
|
||||
value
|
||||
)}\'${rightParentheses}`;
|
||||
case Constants.TableType.String:
|
||||
case Constants.TableType.Guid:
|
||||
case Constants.TableType.Binary:
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} \'${value}\'${rightParentheses}`;
|
||||
default:
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} ${value}${rightParentheses}`;
|
||||
}
|
||||
};
|
||||
|
||||
private constructCqlClause = (
|
||||
clauseRule: string,
|
||||
leftParentheses: string,
|
||||
propertyName: string,
|
||||
type: string,
|
||||
operator: string,
|
||||
value: string,
|
||||
rightParentheses: string
|
||||
): string => {
|
||||
if (
|
||||
type === Constants.CassandraType.Text ||
|
||||
type === Constants.CassandraType.Inet ||
|
||||
type === Constants.CassandraType.Ascii ||
|
||||
type === Constants.CassandraType.Varchar
|
||||
) {
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses} ${propertyName} ${operator} \'${value}\'${rightParentheses}`;
|
||||
}
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses} ${propertyName} ${operator} ${value}${rightParentheses}`;
|
||||
};
|
||||
|
||||
private operatorConverter = (operator: string): string => {
|
||||
switch (operator) {
|
||||
case Constants.Operator.Equal:
|
||||
return Constants.ODataOperator.EqualTo;
|
||||
case Constants.Operator.GreaterThan:
|
||||
return Constants.ODataOperator.GreaterThan;
|
||||
case Constants.Operator.GreaterThanOrEqualTo:
|
||||
return Constants.ODataOperator.GreaterThanOrEqualTo;
|
||||
case Constants.Operator.LessThan:
|
||||
return Constants.ODataOperator.LessThan;
|
||||
case Constants.Operator.LessThanOrEqualTo:
|
||||
return Constants.ODataOperator.LessThanOrEqualTo;
|
||||
case Constants.Operator.NotEqualTo:
|
||||
return Constants.ODataOperator.NotEqualTo;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
public groupClauses = (): void => {
|
||||
this.queryClauses.groupSelectedItems();
|
||||
this.updateClauseArray();
|
||||
this.updateCanGroupClauses();
|
||||
};
|
||||
|
||||
public addClauseIndex = (index: number, data: any): void => {
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
}
|
||||
var newClause = new QueryClauseViewModel(
|
||||
this,
|
||||
"And",
|
||||
"",
|
||||
this.edmTypes()[0],
|
||||
Constants.Operator.EqualTo,
|
||||
"",
|
||||
true,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
//null,
|
||||
true
|
||||
);
|
||||
this.addClauseImpl(newClause, index);
|
||||
if (index === this.clauseArray().length - 1) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
this.updateCanGroupClauses();
|
||||
newClause.isAndOrFocused(true);
|
||||
$(window).resize();
|
||||
};
|
||||
|
||||
// adds a new clause to the end of the array
|
||||
public addNewClause = (): void => {
|
||||
this.addClauseIndex(this.clauseArray().length, null);
|
||||
};
|
||||
|
||||
public onAddClauseKeyDown = (index: number, data: any, event: KeyboardEvent, source: any): boolean => {
|
||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||
this.addClauseIndex(index, data);
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
public onAddNewClauseKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||
this.addClauseIndex(this.clauseArray().length - 1, null);
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
public deleteClause = (index: number, data: any): void => {
|
||||
this.deleteClauseImpl(index);
|
||||
if (this.clauseArray().length !== 0) {
|
||||
this.clauseArray()[0].and_or("");
|
||||
this.clauseArray()[0].canAnd(false);
|
||||
}
|
||||
this.updateCanGroupClauses();
|
||||
$(window).resize();
|
||||
};
|
||||
|
||||
public onDeleteClauseKeyDown = (index: number, data: any, event: KeyboardEvent, source: any): boolean => {
|
||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||
this.deleteClause(index, data);
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates an array of ClauseGroupViewModel objects for UI to display group information for this clause.
|
||||
* All clauses have the same number of ClauseGroupViewModel objects, which is the depth of the clause tree.
|
||||
* If the current clause is not the deepest in the tree, then the array will be filled by either a placeholder
|
||||
* (transparent) or its parent group view models.
|
||||
*/
|
||||
public getClauseGroupViewModels = (clause: QueryClauseViewModel): ClauseGroupViewModel[] => {
|
||||
var placeHolderGroupViewModel = new ClauseGroupViewModel(this.queryClauses, false, this);
|
||||
var treeDepth = this.queryClauses.getTreeDepth();
|
||||
var groupViewModels = new Array<ClauseGroupViewModel>(treeDepth);
|
||||
|
||||
// Prefill the arry with placeholders.
|
||||
for (var i = 0; i < groupViewModels.length; i++) {
|
||||
groupViewModels[i] = placeHolderGroupViewModel;
|
||||
}
|
||||
|
||||
var currentGroup = clause.clauseGroup;
|
||||
|
||||
// This function determines whether the path from clause to the current group is on the left most.
|
||||
var isLeftMostPath = (): boolean => {
|
||||
var group = clause.clauseGroup;
|
||||
|
||||
if (group.children.indexOf(clause) !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (group.getId() === currentGroup.getId()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (group.parentGroup.children.indexOf(group) !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
group = group.parentGroup;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// This function determines whether the path from clause to the current group is on the right most.
|
||||
var isRightMostPath = (): boolean => {
|
||||
var group = clause.clauseGroup;
|
||||
|
||||
if (group.children.indexOf(clause) !== group.children.length - 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (group.getId() === currentGroup.getId()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (group.parentGroup.children.indexOf(group) !== group.parentGroup.children.length - 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
group = group.parentGroup;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
var vmIndex = groupViewModels.length - 1;
|
||||
var skipIndex = -1;
|
||||
var lastDepth = clause.groupDepth;
|
||||
|
||||
while (!currentGroup.isRootGroup) {
|
||||
// The current group will be rendered at least once, and if there are any sibling groups deeper
|
||||
// than the current group, we will repeat rendering the current group to fill up the gap between
|
||||
// current & deepest sibling.
|
||||
var deepestInSiblings = currentGroup.findDeepestGroupInChildren(skipIndex).getCurrentGroupDepth();
|
||||
// Find out the depth difference between the deepest group under the siblings of currentGroup and
|
||||
// the deepest group under currentGroup. If the result n is a positive number, it means there are
|
||||
// deeper groups in siblings and we need to draw n + 1 group blocks on UI to fill up the depth
|
||||
// differences. If the result n is a negative number, it means current group contains the deepest
|
||||
// sub-group, we only need to draw the group block once.
|
||||
var repeatCount = Math.max(deepestInSiblings - lastDepth, 0);
|
||||
|
||||
for (var i = 0; i <= repeatCount; i++) {
|
||||
var isLeftMost = isLeftMostPath();
|
||||
var isRightMost = isRightMostPath();
|
||||
var groupViewModel = new ClauseGroupViewModel(currentGroup, i === 0 && isLeftMost, this);
|
||||
|
||||
groupViewModel.showTopBorder(isLeftMost);
|
||||
groupViewModel.showBottomBorder(isRightMost);
|
||||
groupViewModel.showLeftBorder(i === repeatCount);
|
||||
groupViewModels[vmIndex] = groupViewModel;
|
||||
vmIndex--;
|
||||
}
|
||||
|
||||
skipIndex = currentGroup.parentGroup.children.indexOf(currentGroup);
|
||||
currentGroup = currentGroup.parentGroup;
|
||||
lastDepth = Math.max(deepestInSiblings, lastDepth);
|
||||
}
|
||||
|
||||
return groupViewModels;
|
||||
};
|
||||
|
||||
public runQuery = (): DataTables.DataTable => {
|
||||
return this._queryViewModel.runQuery();
|
||||
};
|
||||
|
||||
public addCustomRange(timestamp: CustomTimestampHelper.ITimestampQuery, clauseToAdd: QueryClauseViewModel): void {
|
||||
var index = this.clauseArray.peek().indexOf(clauseToAdd);
|
||||
|
||||
var newClause = new QueryClauseViewModel(
|
||||
this,
|
||||
//this._tableEntityListViewModel.tableExplorerContext.hostProxy,
|
||||
"And",
|
||||
clauseToAdd.field(),
|
||||
"DateTime",
|
||||
Constants.Operator.LessThan,
|
||||
"",
|
||||
true,
|
||||
Constants.timeOptions.custom,
|
||||
timestamp.endTime,
|
||||
"range",
|
||||
//null,
|
||||
true
|
||||
);
|
||||
|
||||
newClause.isLocal = ko.observable(timestamp.timeZone === "local");
|
||||
this.addClauseImpl(newClause, index + 1);
|
||||
|
||||
if (index + 1 === this.clauseArray().length - 1) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
private scrollToBottom(): void {
|
||||
var scrollBox = document.getElementById("scroll");
|
||||
if (!this.scrollEventListener) {
|
||||
scrollBox.addEventListener("scroll", function() {
|
||||
var translate = "translate(0," + this.scrollTop + "px)";
|
||||
const allTh = <NodeListOf<HTMLElement>>this.querySelectorAll("thead td");
|
||||
for (let i = 0; i < allTh.length; i++) {
|
||||
allTh[i].style.transform = translate;
|
||||
}
|
||||
});
|
||||
this.scrollEventListener = true;
|
||||
}
|
||||
var isScrolledToBottom = scrollBox.scrollHeight - scrollBox.clientHeight <= scrollBox.scrollHeight + 1;
|
||||
if (isScrolledToBottom) {
|
||||
scrollBox.scrollTop = scrollBox.scrollHeight - scrollBox.clientHeight;
|
||||
}
|
||||
}
|
||||
|
||||
private addClauseImpl(clause: QueryClauseViewModel, position: number): void {
|
||||
this.queryClauses.insertClauseBefore(clause, this.clauseArray()[position]);
|
||||
this.updateClauseArray();
|
||||
}
|
||||
|
||||
private deleteClauseImpl(index: number): void {
|
||||
var clause = this.clauseArray()[index];
|
||||
var previousClause = index === 0 ? 0 : index - 1;
|
||||
this.queryClauses.deleteClause(clause);
|
||||
this.updateClauseArray();
|
||||
if (this.clauseArray()[previousClause]) {
|
||||
this.clauseArray()[previousClause].isDeleteButtonFocused(true);
|
||||
}
|
||||
}
|
||||
|
||||
public updateCanGroupClauses(): void {
|
||||
this.canGroupClauses(this.queryClauses.canGroupSelectedItems());
|
||||
}
|
||||
|
||||
public updateClauseArray(): void {
|
||||
if (this.clauseArray().length > 0) {
|
||||
this.clauseArray()[0].canAnd(true);
|
||||
}
|
||||
|
||||
this.queryClauses.flattenClauses(this.clauseArray);
|
||||
|
||||
if (this.clauseArray().length > 0) {
|
||||
this.clauseArray()[0].canAnd(false);
|
||||
}
|
||||
|
||||
// Fix for 261924, forces the resize event so DataTableBindingManager will redo the calculation on table size.
|
||||
//DataTableUtilities.forceRecalculateTableSize();
|
||||
}
|
||||
|
||||
private timestampToValue(clause: QueryClauseViewModel): void {
|
||||
if (clause.isValue()) {
|
||||
return;
|
||||
} else if (clause.isTimestamp()) {
|
||||
this.getTimeStampToQuery(clause);
|
||||
// } else if (clause.isCustomLastTimestamp()) {
|
||||
// clause.value(`datetime'${CustomTimestampHelper._queryLastTime(clause.customLastTimestamp().lastNumber, clause.customLastTimestamp().lastTimeUnit)}'`);
|
||||
} else if (clause.isCustomRangeTimestamp()) {
|
||||
if (clause.isLocal()) {
|
||||
clause.value(`datetime'${DateTimeUtilities.getUTCDateTime(clause.customTimeValue())}'`);
|
||||
} else {
|
||||
clause.value(`datetime'${clause.customTimeValue()}Z'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private timestampToSqlValue(clause: QueryClauseViewModel): string {
|
||||
if (clause.isValue()) {
|
||||
return null;
|
||||
} else if (clause.isTimestamp()) {
|
||||
return this.getTimeStampToSqlQuery(clause);
|
||||
// } else if (clause.isCustomLastTimestamp()) {
|
||||
// clause.value(CustomTimestampHelper._queryLastTime(clause.customLastTimestamp().lastNumber, clause.customLastTimestamp().lastTimeUnit));
|
||||
} else if (clause.isCustomRangeTimestamp()) {
|
||||
if (clause.isLocal()) {
|
||||
return DateTimeUtilities.getUTCDateTime(clause.customTimeValue());
|
||||
} else {
|
||||
return clause.customTimeValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private getTimeStampToQuery(clause: QueryClauseViewModel): void {
|
||||
switch (clause.timeValue()) {
|
||||
case Constants.timeOptions.lastHour:
|
||||
clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(0, 1)}'`);
|
||||
break;
|
||||
case Constants.timeOptions.last24Hours:
|
||||
clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(0, 24)}'`);
|
||||
break;
|
||||
case Constants.timeOptions.last7Days:
|
||||
clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(7, 0)}'`);
|
||||
break;
|
||||
case Constants.timeOptions.last31Days:
|
||||
clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(31, 0)}'`);
|
||||
break;
|
||||
case Constants.timeOptions.last365Days:
|
||||
clause.value(`datetime'${CustomTimestampHelper._queryLastDaysHours(365, 0)}'`);
|
||||
break;
|
||||
case Constants.timeOptions.currentMonth:
|
||||
clause.value(`datetime'${CustomTimestampHelper._queryCurrentMonthLocal()}'`);
|
||||
break;
|
||||
case Constants.timeOptions.currentYear:
|
||||
clause.value(`datetime'${CustomTimestampHelper._queryCurrentYearLocal()}'`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private getTimeStampToSqlQuery(clause: QueryClauseViewModel): string {
|
||||
switch (clause.timeValue()) {
|
||||
case Constants.timeOptions.lastHour:
|
||||
return CustomTimestampHelper._queryLastDaysHours(0, 1);
|
||||
case Constants.timeOptions.last24Hours:
|
||||
return CustomTimestampHelper._queryLastDaysHours(0, 24);
|
||||
case Constants.timeOptions.last7Days:
|
||||
return CustomTimestampHelper._queryLastDaysHours(7, 0);
|
||||
case Constants.timeOptions.last31Days:
|
||||
return CustomTimestampHelper._queryLastDaysHours(31, 0);
|
||||
case Constants.timeOptions.last365Days:
|
||||
return CustomTimestampHelper._queryLastDaysHours(365, 0);
|
||||
case Constants.timeOptions.currentMonth:
|
||||
return CustomTimestampHelper._queryCurrentMonthLocal();
|
||||
case Constants.timeOptions.currentYear:
|
||||
return CustomTimestampHelper._queryCurrentYearLocal();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public checkIfClauseChanged(clause: QueryClauseViewModel): void {
|
||||
this._queryViewModel.checkIfBuilderChanged(clause);
|
||||
}
|
||||
}
|
||||
285
src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts
Normal file
285
src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts
Normal file
@@ -0,0 +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;
|
||||
}
|
||||
}
|
||||
234
src/Explorer/Tables/QueryBuilder/QueryViewModel.ts
Normal file
234
src/Explorer/Tables/QueryBuilder/QueryViewModel.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
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";
|
||||
|
||||
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 ${this.queryTablesTab.collection.databaseId}.${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();
|
||||
}
|
||||
}
|
||||
746
src/Explorer/Tables/TableDataClient.ts
Normal file
746
src/Explorer/Tables/TableDataClient.ts
Normal file
@@ -0,0 +1,746 @@
|
||||
import * as ko from "knockout";
|
||||
import Q from "q";
|
||||
|
||||
import { displayTokenRenewalPromptForStatus, getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import * as Entities from "./Entities";
|
||||
import EnvironmentUtility from "../../Common/EnvironmentUtility";
|
||||
import * as HeadersUtility from "../../Common/HeadersUtility";
|
||||
import { Logger } from "../../Common/Logger";
|
||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||
import * as TableConstants from "./Constants";
|
||||
import * as TableEntityProcessor from "./TableEntityProcessor";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { MessageTypes } from "../../Contracts/ExplorerContracts";
|
||||
import { MessageHandler } from "../../Common/MessageHandler";
|
||||
import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase";
|
||||
|
||||
export interface CassandraTableKeys {
|
||||
partitionKeys: CassandraTableKey[];
|
||||
clusteringKeys: CassandraTableKey[];
|
||||
}
|
||||
|
||||
export interface CassandraTableKey {
|
||||
property: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export abstract class TableDataClient {
|
||||
public documentClientUtility: DocumentClientUtilityBase;
|
||||
|
||||
constructor(documentClientUtility: DocumentClientUtilityBase) {
|
||||
this.documentClientUtility = documentClientUtility;
|
||||
}
|
||||
|
||||
public abstract createDocument(
|
||||
collection: ViewModels.Collection,
|
||||
entity: Entities.ITableEntity
|
||||
): Q.Promise<Entities.ITableEntity>;
|
||||
|
||||
public abstract updateDocument(
|
||||
collection: ViewModels.Collection,
|
||||
originalDocument: any,
|
||||
newEntity: Entities.ITableEntity
|
||||
): Q.Promise<Entities.ITableEntity>;
|
||||
|
||||
public abstract queryDocuments(
|
||||
collection: ViewModels.Collection,
|
||||
query: string,
|
||||
shouldNotify?: boolean,
|
||||
paginationToken?: string
|
||||
): Q.Promise<Entities.IListTableEntitiesResult>;
|
||||
|
||||
public abstract deleteDocuments(
|
||||
collection: ViewModels.Collection,
|
||||
entitiesToDelete: Entities.ITableEntity[]
|
||||
): Q.Promise<any>;
|
||||
}
|
||||
|
||||
export class TablesAPIDataClient extends TableDataClient {
|
||||
public createDocument(
|
||||
collection: ViewModels.Collection,
|
||||
entity: Entities.ITableEntity
|
||||
): Q.Promise<Entities.ITableEntity> {
|
||||
const deferred = Q.defer<Entities.ITableEntity>();
|
||||
this.documentClientUtility
|
||||
.createDocument(
|
||||
collection,
|
||||
TableEntityProcessor.convertEntityToNewDocument(<Entities.ITableEntityForTablesAPI>entity)
|
||||
)
|
||||
.then(
|
||||
(newDocument: any) => {
|
||||
const newEntity = TableEntityProcessor.convertDocumentsToEntities([newDocument])[0];
|
||||
deferred.resolve(newEntity);
|
||||
},
|
||||
reason => {
|
||||
deferred.reject(reason);
|
||||
}
|
||||
);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public updateDocument(
|
||||
collection: ViewModels.Collection,
|
||||
originalDocument: any,
|
||||
entity: Entities.ITableEntity
|
||||
): Q.Promise<Entities.ITableEntity> {
|
||||
const deferred = Q.defer<Entities.ITableEntity>();
|
||||
this.documentClientUtility
|
||||
.updateDocument(
|
||||
collection,
|
||||
originalDocument,
|
||||
TableEntityProcessor.convertEntityToNewDocument(<Entities.ITableEntityForTablesAPI>entity)
|
||||
)
|
||||
.then(
|
||||
(newDocument: any) => {
|
||||
const newEntity = TableEntityProcessor.convertDocumentsToEntities([newDocument])[0];
|
||||
deferred.resolve(newEntity);
|
||||
},
|
||||
reason => {
|
||||
deferred.reject(reason);
|
||||
}
|
||||
);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public queryDocuments(
|
||||
collection: ViewModels.Collection,
|
||||
query: string
|
||||
): Q.Promise<Entities.IListTableEntitiesResult> {
|
||||
const deferred = Q.defer<Entities.IListTableEntitiesResult>();
|
||||
|
||||
let options: any = {};
|
||||
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
|
||||
this.documentClientUtility.queryDocuments(collection.databaseId, collection.id(), query, options).then(
|
||||
iterator => {
|
||||
iterator
|
||||
.fetchNext()
|
||||
.then(response => response.resources)
|
||||
.then(
|
||||
(documents: any[] = []) => {
|
||||
let entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(documents);
|
||||
let finalEntities: Entities.IListTableEntitiesResult = <Entities.IListTableEntitiesResult>{
|
||||
Results: entities,
|
||||
ContinuationToken: iterator.hasMoreResults(),
|
||||
iterator: iterator
|
||||
};
|
||||
deferred.resolve(finalEntities);
|
||||
},
|
||||
reason => {
|
||||
deferred.reject(reason);
|
||||
}
|
||||
);
|
||||
},
|
||||
reason => {
|
||||
deferred.reject(reason);
|
||||
}
|
||||
);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public deleteDocuments(collection: ViewModels.Collection, entitiesToDelete: Entities.ITableEntity[]): Q.Promise<any> {
|
||||
let documentsToDelete: any[] = TableEntityProcessor.convertEntitiesToDocuments(
|
||||
<Entities.ITableEntityForTablesAPI[]>entitiesToDelete,
|
||||
collection
|
||||
);
|
||||
let promiseArray: Q.Promise<any>[] = [];
|
||||
documentsToDelete &&
|
||||
documentsToDelete.forEach(document => {
|
||||
document.id = ko.observable<string>(document.id);
|
||||
let promise: Q.Promise<any> = this.documentClientUtility.deleteDocument(collection, document);
|
||||
promiseArray.push(promise);
|
||||
});
|
||||
return Q.all(promiseArray);
|
||||
}
|
||||
}
|
||||
|
||||
export class CassandraAPIDataClient extends TableDataClient {
|
||||
public createDocument(
|
||||
collection: ViewModels.Collection,
|
||||
entity: Entities.ITableEntity
|
||||
): Q.Promise<Entities.ITableEntity> {
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Adding new row to table ${collection.id()}`
|
||||
);
|
||||
let properties = "(";
|
||||
let values = "(";
|
||||
for (let property in entity) {
|
||||
if (entity[property]._ === null) {
|
||||
continue;
|
||||
}
|
||||
properties = properties.concat(`${property}, `);
|
||||
const propertyType = entity[property].$;
|
||||
if (this.isStringType(propertyType)) {
|
||||
values = values.concat(`'${entity[property]._}', `);
|
||||
} else {
|
||||
values = values.concat(`${entity[property]._}, `);
|
||||
}
|
||||
}
|
||||
properties = properties.slice(0, properties.length - 2) + ")";
|
||||
values = values.slice(0, values.length - 2) + ")";
|
||||
const query = `INSERT INTO ${collection.databaseId}.${collection.id()} ${properties} VALUES ${values}`;
|
||||
const deferred = Q.defer<Entities.ITableEntity>();
|
||||
this.queryDocuments(collection, query)
|
||||
.then(
|
||||
(data: any) => {
|
||||
entity[TableConstants.EntityKeyNames.RowKey] = entity[this.getCassandraPartitionKeyProperty(collection)];
|
||||
entity[TableConstants.EntityKeyNames.RowKey]._ = entity[TableConstants.EntityKeyNames.RowKey]._.toString();
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully added new row to table ${collection.id()}`
|
||||
);
|
||||
deferred.resolve(entity);
|
||||
},
|
||||
reason => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while adding new row to table ${collection.id()}:\n ${JSON.stringify(reason)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(reason), "AddRowCassandra", reason.code);
|
||||
this._checkForbiddenError(reason);
|
||||
deferred.reject(reason);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public updateDocument(
|
||||
collection: ViewModels.Collection,
|
||||
originalDocument: any,
|
||||
newEntity: Entities.ITableEntity
|
||||
): Q.Promise<Entities.ITableEntity> {
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Updating row ${originalDocument.RowKey._}`
|
||||
);
|
||||
const deferred = Q.defer<Entities.ITableEntity>();
|
||||
let promiseArray: Q.Promise<any>[] = [];
|
||||
let query = `UPDATE ${collection.databaseId}.${collection.id()}`;
|
||||
let isChange: boolean = false;
|
||||
for (let property in newEntity) {
|
||||
if (!originalDocument[property] || newEntity[property]._.toString() !== originalDocument[property]._.toString()) {
|
||||
if (this.isStringType(newEntity[property].$)) {
|
||||
query = `${query} SET ${property} = '${newEntity[property]._}',`;
|
||||
} else {
|
||||
query = `${query} SET ${property} = ${newEntity[property]._},`;
|
||||
}
|
||||
isChange = true;
|
||||
}
|
||||
}
|
||||
query = query.slice(0, query.length - 1);
|
||||
let whereSegment = " WHERE";
|
||||
let keys: CassandraTableKey[] = collection.cassandraKeys.partitionKeys.concat(
|
||||
collection.cassandraKeys.clusteringKeys
|
||||
);
|
||||
for (let keyIndex in keys) {
|
||||
const key = keys[keyIndex].property;
|
||||
const keyType = keys[keyIndex].type;
|
||||
if (this.isStringType(keyType)) {
|
||||
whereSegment = `${whereSegment} ${key} = '${newEntity[key]._}' AND`;
|
||||
} else {
|
||||
whereSegment = `${whereSegment} ${key} = ${newEntity[key]._} AND`;
|
||||
}
|
||||
}
|
||||
whereSegment = whereSegment.slice(0, whereSegment.length - 4);
|
||||
query = query + whereSegment;
|
||||
if (isChange) {
|
||||
promiseArray.push(this.queryDocuments(collection, query));
|
||||
}
|
||||
query = `DELETE `;
|
||||
for (let property in originalDocument) {
|
||||
if (property !== TableConstants.EntityKeyNames.RowKey && !newEntity[property] && !!originalDocument[property]) {
|
||||
query = `${query} ${property},`;
|
||||
}
|
||||
}
|
||||
if (query.length > 7) {
|
||||
query = query.slice(0, query.length - 1);
|
||||
query = `${query} FROM ${collection.databaseId}.${collection.id()}${whereSegment}`;
|
||||
promiseArray.push(this.queryDocuments(collection, query));
|
||||
}
|
||||
Q.all(promiseArray)
|
||||
.then(
|
||||
(data: any) => {
|
||||
newEntity[TableConstants.EntityKeyNames.RowKey] = originalDocument[TableConstants.EntityKeyNames.RowKey];
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully updated row ${newEntity.RowKey._}`
|
||||
);
|
||||
deferred.resolve(newEntity);
|
||||
},
|
||||
reason => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to update row ${newEntity.RowKey._}: ${JSON.stringify(reason)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(reason), "UpdateRowCassandra", reason.code);
|
||||
this._checkForbiddenError(reason);
|
||||
deferred.reject(reason);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public queryDocuments(
|
||||
collection: ViewModels.Collection,
|
||||
query: string,
|
||||
shouldNotify?: boolean,
|
||||
paginationToken?: string
|
||||
): Q.Promise<Entities.IListTableEntitiesResult> {
|
||||
let notificationId: string;
|
||||
if (shouldNotify) {
|
||||
notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Querying rows for table ${collection.id()}`
|
||||
);
|
||||
}
|
||||
const deferred = Q.defer<Entities.IListTableEntitiesResult>();
|
||||
const authType = window.authType;
|
||||
const apiEndpoint: string =
|
||||
authType === AuthType.EncryptedToken
|
||||
? Constants.CassandraBackend.guestQueryApi
|
||||
: Constants.CassandraBackend.queryApi;
|
||||
$.ajax(`${EnvironmentUtility.getCassandraBackendEndpoint(collection.container)}${apiEndpoint}`, {
|
||||
type: "POST",
|
||||
data: {
|
||||
accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(
|
||||
collection.container.databaseAccount().properties.cassandraEndpoint
|
||||
),
|
||||
resourceId: collection.container.databaseAccount().id,
|
||||
keyspaceId: collection.databaseId,
|
||||
tableId: collection.id(),
|
||||
query: query,
|
||||
paginationToken: paginationToken
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader,
|
||||
error: this.handleAjaxError,
|
||||
cache: false
|
||||
})
|
||||
.then(
|
||||
(data: any) => {
|
||||
if (shouldNotify) {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully fetched ${data.result.length} rows for table ${collection.id()}`
|
||||
);
|
||||
}
|
||||
deferred.resolve({
|
||||
Results: data.result,
|
||||
ContinuationToken: data.paginationToken
|
||||
});
|
||||
},
|
||||
reason => {
|
||||
if (shouldNotify) {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to query rows for table ${collection.id()}: ${JSON.stringify(reason)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(reason), "QueryDocumentsCassandra", reason.status);
|
||||
this._checkForbiddenError(reason);
|
||||
}
|
||||
deferred.reject(reason);
|
||||
}
|
||||
)
|
||||
.done(() => {
|
||||
if (shouldNotify) {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
}
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public deleteDocuments(collection: ViewModels.Collection, entitiesToDelete: Entities.ITableEntity[]): Q.Promise<any> {
|
||||
const query = `DELETE FROM ${collection.databaseId}.${collection.id()} WHERE `;
|
||||
let promiseArray: Q.Promise<any>[] = [];
|
||||
let partitionKeyProperty = this.getCassandraPartitionKeyProperty(collection);
|
||||
for (let i = 0, len = entitiesToDelete.length; i < len; i++) {
|
||||
let currEntityToDelete: Entities.ITableEntity = entitiesToDelete[i];
|
||||
let currQuery = query;
|
||||
let partitionKeyValue = currEntityToDelete[partitionKeyProperty];
|
||||
if (partitionKeyValue._ != null && this.isStringType(partitionKeyValue.$)) {
|
||||
currQuery = `${currQuery}${partitionKeyProperty} = '${partitionKeyValue._}' AND `;
|
||||
} else {
|
||||
currQuery = `${currQuery}${partitionKeyProperty} = ${partitionKeyValue._} AND `;
|
||||
}
|
||||
currQuery = currQuery.slice(0, currQuery.length - 5);
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Deleting row ${currEntityToDelete.RowKey._}`
|
||||
);
|
||||
promiseArray.push(
|
||||
this.queryDocuments(collection, currQuery)
|
||||
.then(
|
||||
() => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully deleted row ${currEntityToDelete.RowKey._}`
|
||||
);
|
||||
},
|
||||
reason => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while deleting row ${currEntityToDelete.RowKey._}:\n ${JSON.stringify(reason)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(reason), "DeleteRowCassandra", reason.code);
|
||||
this._checkForbiddenError(reason);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
})
|
||||
);
|
||||
}
|
||||
return Q.all(promiseArray);
|
||||
}
|
||||
|
||||
public createKeyspace(
|
||||
cassandraEndpoint: string,
|
||||
resourceId: string,
|
||||
explorer: ViewModels.Explorer,
|
||||
createKeyspaceQuery: string
|
||||
): Q.Promise<any> {
|
||||
if (!createKeyspaceQuery) {
|
||||
return Q.reject("No query specified");
|
||||
}
|
||||
|
||||
const deferred: Q.Deferred<any> = Q.defer();
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Creating a new keyspace with query ${createKeyspaceQuery}`
|
||||
);
|
||||
this.createOrDeleteQuery(cassandraEndpoint, resourceId, createKeyspaceQuery, explorer)
|
||||
.then(
|
||||
(data: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully created a keyspace with query ${createKeyspaceQuery}`
|
||||
);
|
||||
explorer.documentClientUtility.refreshCachedResources().finally(() => deferred.resolve());
|
||||
},
|
||||
reason => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while creating a keyspace with query ${createKeyspaceQuery}:\n ${JSON.stringify(reason)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(reason), "CreateKeyspaceCassandra", reason.code);
|
||||
this._checkForbiddenError(reason);
|
||||
deferred.reject(reason);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
});
|
||||
|
||||
return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs);
|
||||
}
|
||||
|
||||
public createTableAndKeyspace(
|
||||
cassandraEndpoint: string,
|
||||
resourceId: string,
|
||||
explorer: ViewModels.Explorer,
|
||||
createTableQuery: string,
|
||||
createKeyspaceQuery?: string
|
||||
): Q.Promise<any> {
|
||||
let createKeyspacePromise: Q.Promise<any>;
|
||||
if (createKeyspaceQuery) {
|
||||
createKeyspacePromise = this.createKeyspace(cassandraEndpoint, resourceId, explorer, createKeyspaceQuery);
|
||||
} else {
|
||||
createKeyspacePromise = Q.resolve(null);
|
||||
}
|
||||
|
||||
const deferred = Q.defer();
|
||||
createKeyspacePromise.then(
|
||||
() => {
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Creating a new table with query ${createTableQuery}`
|
||||
);
|
||||
this.createOrDeleteQuery(cassandraEndpoint, resourceId, createTableQuery, explorer)
|
||||
.then(
|
||||
(data: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully created a table with query ${createTableQuery}`
|
||||
);
|
||||
this.documentClientUtility.refreshCachedResources(null).then(
|
||||
() => {
|
||||
deferred.resolve();
|
||||
},
|
||||
reason => {
|
||||
// Still resolve since the keyspace/table was successfully created at this point.
|
||||
deferred.resolve();
|
||||
}
|
||||
);
|
||||
},
|
||||
reason => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while creating a table with query ${createTableQuery}:\n ${JSON.stringify(reason)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(reason), "CreateTableCassandra", reason.code);
|
||||
this._checkForbiddenError(reason);
|
||||
deferred.reject(reason);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
});
|
||||
},
|
||||
reason => {
|
||||
deferred.reject(reason);
|
||||
}
|
||||
);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public deleteTableOrKeyspace(
|
||||
cassandraEndpoint: string,
|
||||
resourceId: string,
|
||||
deleteQuery: string,
|
||||
explorer: ViewModels.Explorer
|
||||
): Q.Promise<any> {
|
||||
const deferred = Q.defer<any>();
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Deleting resource with query ${deleteQuery}`
|
||||
);
|
||||
this.createOrDeleteQuery(cassandraEndpoint, resourceId, deleteQuery, explorer)
|
||||
.then(
|
||||
() => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully deleted resource with query ${deleteQuery}`
|
||||
);
|
||||
this.documentClientUtility.refreshCachedResources(null).then(
|
||||
() => {
|
||||
deferred.resolve();
|
||||
},
|
||||
reason => {
|
||||
// Still resolve since the keyspace/table was successfully deleted at this point.
|
||||
deferred.resolve();
|
||||
}
|
||||
);
|
||||
},
|
||||
reason => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while deleting resource with query ${deleteQuery}:\n ${JSON.stringify(reason)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(reason), "DeleteKeyspaceOrTableCassandra", reason.code);
|
||||
this._checkForbiddenError(reason);
|
||||
deferred.reject(reason);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
||||
if (!!collection.cassandraKeys) {
|
||||
return Q.resolve(collection.cassandraKeys);
|
||||
}
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Fetching keys for table ${collection.id()}`
|
||||
);
|
||||
const authType = window.authType;
|
||||
const apiEndpoint: string =
|
||||
authType === AuthType.EncryptedToken
|
||||
? Constants.CassandraBackend.guestKeysApi
|
||||
: Constants.CassandraBackend.keysApi;
|
||||
let endpoint = `${EnvironmentUtility.getCassandraBackendEndpoint(collection.container)}${apiEndpoint}`;
|
||||
const deferred = Q.defer<CassandraTableKeys>();
|
||||
$.ajax(endpoint, {
|
||||
type: "POST",
|
||||
data: {
|
||||
accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(
|
||||
collection.container.databaseAccount().properties.cassandraEndpoint
|
||||
),
|
||||
resourceId: collection.container.databaseAccount().id,
|
||||
keyspaceId: collection.databaseId,
|
||||
tableId: collection.id()
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader,
|
||||
error: this.handleAjaxError,
|
||||
cache: false
|
||||
})
|
||||
.then(
|
||||
(data: CassandraTableKeys) => {
|
||||
collection.cassandraKeys = data;
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully fetched keys for table ${collection.id()}`
|
||||
);
|
||||
deferred.resolve(data);
|
||||
},
|
||||
reason => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error fetching keys for table ${collection.id()}:\n ${JSON.stringify(reason)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(reason), "FetchKeysCassandra", reason.status);
|
||||
this._checkForbiddenError(reason);
|
||||
deferred.reject(reason);
|
||||
}
|
||||
)
|
||||
.done(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public getTableSchema(collection: ViewModels.Collection): Q.Promise<CassandraTableKey[]> {
|
||||
if (!!collection.cassandraSchema) {
|
||||
return Q.resolve(collection.cassandraSchema);
|
||||
}
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Fetching schema for table ${collection.id()}`
|
||||
);
|
||||
const authType = window.authType;
|
||||
const apiEndpoint: string =
|
||||
authType === AuthType.EncryptedToken
|
||||
? Constants.CassandraBackend.guestSchemaApi
|
||||
: Constants.CassandraBackend.schemaApi;
|
||||
let endpoint = `${EnvironmentUtility.getCassandraBackendEndpoint(collection.container)}${apiEndpoint}`;
|
||||
const deferred = Q.defer<CassandraTableKey[]>();
|
||||
$.ajax(endpoint, {
|
||||
type: "POST",
|
||||
data: {
|
||||
accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(
|
||||
collection.container.databaseAccount().properties.cassandraEndpoint
|
||||
),
|
||||
resourceId: collection.container.databaseAccount().id,
|
||||
keyspaceId: collection.databaseId,
|
||||
tableId: collection.id()
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader,
|
||||
error: this.handleAjaxError,
|
||||
cache: false
|
||||
})
|
||||
.then(
|
||||
(data: any) => {
|
||||
collection.cassandraSchema = data.columns;
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully fetched schema for table ${collection.id()}`
|
||||
);
|
||||
deferred.resolve(data.columns);
|
||||
},
|
||||
reason => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error fetching schema for table ${collection.id()}:\n ${JSON.stringify(reason)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(reason), "FetchSchemaCassandra", reason.status);
|
||||
this._checkForbiddenError(reason);
|
||||
deferred.reject(reason);
|
||||
}
|
||||
)
|
||||
.done(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
private createOrDeleteQuery(
|
||||
cassandraEndpoint: string,
|
||||
resourceId: string,
|
||||
query: string,
|
||||
explorer: ViewModels.Explorer
|
||||
): Q.Promise<any> {
|
||||
const deferred = Q.defer();
|
||||
const authType = window.authType;
|
||||
const apiEndpoint: string =
|
||||
authType === AuthType.EncryptedToken
|
||||
? Constants.CassandraBackend.guestCreateOrDeleteApi
|
||||
: Constants.CassandraBackend.createOrDeleteApi;
|
||||
$.ajax(`${EnvironmentUtility.getCassandraBackendEndpoint(explorer)}${apiEndpoint}`, {
|
||||
type: "POST",
|
||||
data: {
|
||||
accountName: explorer.databaseAccount() && explorer.databaseAccount().name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(cassandraEndpoint),
|
||||
resourceId: resourceId,
|
||||
query: query
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader,
|
||||
error: this.handleAjaxError,
|
||||
cache: false
|
||||
}).then(
|
||||
(data: any) => {
|
||||
deferred.resolve();
|
||||
},
|
||||
reason => {
|
||||
deferred.reject(reason);
|
||||
}
|
||||
);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
private trimCassandraEndpoint(cassandraEndpoint: string): string {
|
||||
if (!cassandraEndpoint) {
|
||||
return cassandraEndpoint;
|
||||
}
|
||||
|
||||
if (cassandraEndpoint.indexOf("https://") === 0) {
|
||||
cassandraEndpoint = cassandraEndpoint.slice(8, cassandraEndpoint.length);
|
||||
}
|
||||
|
||||
if (cassandraEndpoint.indexOf(":443/", cassandraEndpoint.length - 5) !== -1) {
|
||||
cassandraEndpoint = cassandraEndpoint.slice(0, cassandraEndpoint.length - 5);
|
||||
}
|
||||
|
||||
return cassandraEndpoint;
|
||||
}
|
||||
|
||||
private setAuthorizationHeader: (xhr: XMLHttpRequest) => boolean = (xhr: XMLHttpRequest): boolean => {
|
||||
const authorizationHeaderMetadata: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||
xhr.setRequestHeader(authorizationHeaderMetadata.header, authorizationHeaderMetadata.token);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
private isStringType(dataType: string): boolean {
|
||||
// TODO figure out rest of types that are considered strings by Cassandra (if any have been missed)
|
||||
return (
|
||||
dataType === TableConstants.CassandraType.Text ||
|
||||
dataType === TableConstants.CassandraType.Inet ||
|
||||
dataType === TableConstants.CassandraType.Ascii ||
|
||||
dataType === TableConstants.CassandraType.Varchar
|
||||
);
|
||||
}
|
||||
|
||||
private getCassandraPartitionKeyProperty(collection: ViewModels.Collection): string {
|
||||
return collection.cassandraKeys.partitionKeys[0].property;
|
||||
}
|
||||
|
||||
private handleAjaxError = (xhrObj: XMLHttpRequest, textStatus: string, errorThrown: string): void => {
|
||||
if (!xhrObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
displayTokenRenewalPromptForStatus(xhrObj.status);
|
||||
};
|
||||
|
||||
private _checkForbiddenError(reason: any) {
|
||||
if (reason && reason.code === Constants.HttpStatusCodes.Forbidden) {
|
||||
MessageHandler.sendMessage({
|
||||
type: MessageTypes.ForbiddenError,
|
||||
reason: typeof reason === "string" ? "reason" : JSON.stringify(reason)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
194
src/Explorer/Tables/TableEntityProcessor.ts
Normal file
194
src/Explorer/Tables/TableEntityProcessor.ts
Normal file
@@ -0,0 +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;
|
||||
}
|
||||
279
src/Explorer/Tables/Utilities.ts
Normal file
279
src/Explorer/Tables/Utilities.ts
Normal file
@@ -0,0 +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;
|
||||
}
|
||||
Reference in New Issue
Block a user