Initial Move from Azure DevOps to GitHub

This commit is contained in:
Steve Faulkner
2020-05-25 21:30:55 -05:00
commit 36581fb6d9
986 changed files with 195242 additions and 0 deletions

View 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"
};

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

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

View File

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

View File

@@ -0,0 +1,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)
};
}
}
}

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

View File

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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