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