diff --git a/extensions/import/package.json b/extensions/import/package.json index 14e849fccc..f2ddf8878a 100644 --- a/extensions/import/package.json +++ b/extensions/import/package.json @@ -67,7 +67,7 @@ } }, "dependencies": { - "dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#0.2.11", + "dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#0.2.15", "opener": "^1.4.3", "service-downloader": "github:anthonydresser/service-downloader#0.1.5", "vscode-extension-telemetry": "0.0.18", diff --git a/extensions/import/yarn.lock b/extensions/import/yarn.lock index de5d331042..ca75146fa1 100644 --- a/extensions/import/yarn.lock +++ b/extensions/import/yarn.lock @@ -64,9 +64,9 @@ core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" -"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#0.2.11": - version "0.2.11" - resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/bc80d2226699d23f45a2ec26129cbcdee4781ca9" +"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#0.2.15": + version "0.2.15" + resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/a2cd2db109de882f0959f7b6421c86afa585f460" dependencies: vscode-languageclient "3.5.1" diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index fb0aab950a..c6634a00e7 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -18,7 +18,7 @@ "update-grammar": "node ../../build/npm/update-grammar.js Microsoft/vscode-mssql syntaxes/SQL.plist ./syntaxes/sql.tmLanguage.json" }, "dependencies": { - "dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#0.2.11", + "dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#0.2.15", "opener": "^1.4.3", "service-downloader": "github:anthonydresser/service-downloader#0.1.5", "vscode-extension-telemetry": "^0.0.15" diff --git a/extensions/mssql/src/config.json b/extensions/mssql/src/config.json index de5fde37b8..d1e7b7887b 100644 --- a/extensions/mssql/src/config.json +++ b/extensions/mssql/src/config.json @@ -1,6 +1,6 @@ { "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", - "version": "1.5.0-alpha.65", + "version": "1.5.0-alpha.66", "downloadFileNames": { "Windows_86": "win-x86-netcoreapp2.2.zip", "Windows_64": "win-x64-netcoreapp2.2.zip", diff --git a/extensions/mssql/yarn.lock b/extensions/mssql/yarn.lock index d51d436200..2b0e6587ef 100644 --- a/extensions/mssql/yarn.lock +++ b/extensions/mssql/yarn.lock @@ -64,9 +64,9 @@ core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" -"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#0.2.11": - version "0.2.11" - resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/bc80d2226699d23f45a2ec26129cbcdee4781ca9" +"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#0.2.15": + version "0.2.15" + resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/a2cd2db109de882f0959f7b6421c86afa585f460" dependencies: vscode-languageclient "3.5.1" diff --git a/src/sql/base/browser/ui/table/tableDataView.ts b/src/sql/base/browser/ui/table/tableDataView.ts index fd6d79008e..56ce33abd5 100644 --- a/src/sql/base/browser/ui/table/tableDataView.ts +++ b/src/sql/base/browser/ui/table/tableDataView.ts @@ -40,10 +40,14 @@ function defaultSort(args: Slick.OnSortEventArgs, data: Array): Array implements IDisposableDataProvider { + //The data exposed publicly, when filter is enabled, _data holds the filtered data. private _data: Array; + //Used when filtering is enabled, _allData holds the complete set of data. + private _allData: Array; private _findArray: Array; private _findObs: Observable; private _findIndex: number; + private _filterEnabled: boolean; private _onRowCountChange = new Emitter(); get onRowCountChange(): Event { return this._onRowCountChange.event; } @@ -51,10 +55,14 @@ export class TableDataView implements IDisposableData private _onFindCountChange = new Emitter(); get onFindCountChange(): Event { return this._onFindCountChange.event; } + private _onFilterStateChange = new Emitter(); + get onFilterStateChange(): Event { return this._onFilterStateChange.event; } + constructor( data?: Array, private _findFn?: (val: T, exp: string) => Array, - private _sortFn?: (args: Slick.OnSortEventArgs, data: Array) => Array + private _sortFn?: (args: Slick.OnSortEventArgs, data: Array) => Array, + private _filterFn?: (data: Array) => Array ) { if (data) { this._data = data; @@ -65,6 +73,35 @@ export class TableDataView implements IDisposableData if (!_sortFn) { this._sortFn = defaultSort; } + + if (!_filterFn) { + this._filterFn = (dataToFilter) => dataToFilter; + } + this._filterEnabled = false; + } + + public get filterEnabled(): boolean { + return this._filterEnabled; + } + + public filter() { + if (!this.filterEnabled) { + this._allData = new Array(...this._data); + this._data = this._filterFn(this._allData); + this._filterEnabled = true; + } + + this._data = this._filterFn(this._allData); + this._onFilterStateChange.fire(); + } + + public clearFilter() { + if (this._filterEnabled) { + this._data = this._allData; + this._allData = []; + this._filterEnabled = false; + this._onFilterStateChange.fire(); + } } sort(args: Slick.OnSortEventArgs) { @@ -79,20 +116,39 @@ export class TableDataView implements IDisposableData return this._data[index]; } + getLengthNonFiltered(): number { + return this.filterEnabled ? this._allData.length : this._data.length; + } + push(items: Array); push(item: T); push(input: T | Array) { + let inputArray = new Array(); if (Array.isArray(input)) { - this._data.push(...input); + inputArray.push(...input); } else { - this._data.push(input); + inputArray.push(input); } - this._onRowCountChange.fire(); + + if (this._filterEnabled) { + this._allData.push(...inputArray); + let filteredArray = this._filterFn(inputArray); + if (filteredArray.length !== 0) { + this._data.push(...filteredArray); + } + } else { + this._data.push(...inputArray); + } + + this._onRowCountChange.fire(this.getLength()); } clear() { this._data = new Array(); - this._onRowCountChange.fire(); + if (this._filterEnabled) { + this._allData = new Array(); + } + this._onRowCountChange.fire(this.getLength()); } find(exp: string, maxMatches: number = 0): Thenable { @@ -180,6 +236,7 @@ export class TableDataView implements IDisposableData dispose() { this._data = undefined; + this._allData = undefined; this._findArray = undefined; this._findObs = undefined; } diff --git a/src/sql/common/telemetryKeys.ts b/src/sql/common/telemetryKeys.ts index 53e954b5ed..b5aeb2b3db 100644 --- a/src/sql/common/telemetryKeys.ts +++ b/src/sql/common/telemetryKeys.ts @@ -41,6 +41,7 @@ export const Accounts = 'Accounts'; export const FireWallRule = 'FirewallRule'; export const AutoOAuth = 'AutoOAuth'; export const AddNewDashboardTab = 'AddNewDashboardTab'; +export const ProfilerFilter = 'ProfilerFilter'; // SQL Agent Events: diff --git a/src/sql/parts/profiler/contrib/profilerActions.ts b/src/sql/parts/profiler/contrib/profilerActions.ts index 955674396c..4977ff3ca6 100644 --- a/src/sql/parts/profiler/contrib/profilerActions.ts +++ b/src/sql/parts/profiler/contrib/profilerActions.ts @@ -5,7 +5,7 @@ 'use strict'; import 'vs/css!sql/parts/profiler/media/profiler'; -import { IProfilerService } from 'sql/parts/profiler/service/interfaces'; +import { IProfilerService, ProfilerFilter } from 'sql/parts/profiler/service/interfaces'; import { IProfilerController } from 'sql/parts/profiler/editor/controller/interfaces'; import { ProfilerInput } from 'sql/parts/profiler/editor/profilerInput'; import { BaseActionContext } from 'sql/workbench/common/actions'; @@ -298,3 +298,36 @@ export class NewProfilerAction extends Task { }); } } + +export class ProfilerFilterSession extends Action { + public static ID = 'profiler.filter'; + public static LABEL = nls.localize('profiler.filter', "Filter…"); + + constructor( + id: string, label: string, + @IProfilerService private _profilerService: IProfilerService + ) { + super(id, label, 'filterLabel'); + } + + public run(input: ProfilerInput): TPromise { + this._profilerService.launchFilterSessionDialog(input); + return TPromise.wrap(true); + } +} + +export class ProfilerClearSessionFilter extends Action { + public static ID = 'profiler.clearFilter'; + public static LABEL = nls.localize('profiler.clearFilter', "Clear Filter"); + + constructor( + id: string, label: string + ) { + super(id, label); + } + + public run(input: ProfilerInput): TPromise { + input.clearFilter(); + return TPromise.wrap(true); + } +} diff --git a/src/sql/parts/profiler/dialog/media/profilerFilterDialog.css b/src/sql/parts/profiler/dialog/media/profilerFilterDialog.css new file mode 100644 index 0000000000..fd75b3c2b4 --- /dev/null +++ b/src/sql/parts/profiler/dialog/media/profilerFilterDialog.css @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + .profiler-filter-dialog { + height: 300px; + padding: 10px; + overflow-y: scroll; +} + +.profiler-filter-clause-table { + width: 100%; + margin-bottom: 10px; +} + +.profiler-filter-remove-condition { + width:20px; + height:20px; + cursor: pointer; +} + +.profiler-filter-add-clause-prompt { + cursor: pointer; + margin: 0px 2px 0px 2px +} \ No newline at end of file diff --git a/src/sql/parts/profiler/dialog/profilerFilterDialog.ts b/src/sql/parts/profiler/dialog/profilerFilterDialog.ts new file mode 100644 index 0000000000..57e62a37bc --- /dev/null +++ b/src/sql/parts/profiler/dialog/profilerFilterDialog.ts @@ -0,0 +1,329 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; +import 'vs/css!sql/media/icons/common-icons'; +import 'vs/css!./media/profilerFilterDialog'; +import { Button } from 'sql/base/browser/ui/button/button'; +import { Modal } from 'sql/base/browser/ui/modal/modal'; +import * as TelemetryKeys from 'sql/common/telemetryKeys'; +import { attachButtonStyler, attachModalDialogStyler, attachInputBoxStyler } from 'sql/common/theme/styler'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { Builder } from 'vs/base/browser/builder'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IPartService } from 'vs/workbench/services/part/common/partService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { localize } from 'vs/nls'; +import { ProfilerFilter, ProfilerFilterClauseOperator, ProfilerFilterClause } from 'sql/parts/profiler/service/interfaces'; +import { ProfilerInput } from 'sql/parts/profiler/editor/profilerInput'; +import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox'; +import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox'; +import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { generateUuid } from 'vs/base/common/uuid'; +import * as DOM from 'vs/base/browser/dom'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; + + +const ClearText: string = localize('profilerFilterDialog.clear', 'Clear All'); +const ApplyText: string = localize('profilerFilterDialog.apply', 'Apply'); +const OkText: string = localize('profilerFilterDialog.ok', 'OK'); +const CancelText: string = localize('profilerFilterDialog.cancel', 'Cancel'); +const DialogTitle: string = localize('profilerFilterDialog.title', 'Filters'); +const RemoveText: string = localize('profilerFilterDialog.remove', 'Remove'); +const AddText: string = localize('profilerFilterDialog.add', 'Add'); +const AddClausePromptText: string = localize('profilerFilterDialog.addClauseText', 'Click here to add a clause'); +const TitleIconClass: string = 'icon filterLabel'; + +const FieldText: string = localize('profilerFilterDialog.fieldColumn', 'Field'); +const OperatorText: string = localize('profilerFilterDialog.operatorColumn', 'Operator'); +const ValueText: string = localize('profilerFilterDialog.valueColumn', 'Value'); + +const Equals: string = '='; +const NotEquals: string = '<>'; +const LessThan: string = '<'; +const LessThanOrEquals: string = '<='; +const GreaterThan: string = '>'; +const GreaterThanOrEquals: string = '>='; +const IsNull: string = localize('profilerFilterDialog.isNullOperator', 'Is Null'); +const IsNotNull: string = localize('profilerFilterDialog.isNotNullOperator', 'Is Not Null'); +const Contains: string = localize('profilerFilterDialog.containsOperator', 'Contains'); +const NotContains: string = localize('profilerFilterDialog.notContainsOperator', 'Not Contains'); +const StartsWith: string = localize('profilerFilterDialog.startsWithOperator', 'Starts With'); +const NotStartsWith: string = localize('profilerFilterDialog.notStartsWithOperator', 'Not Starts With'); + +const Operators = [Equals, NotEquals, LessThan, LessThanOrEquals, GreaterThan, GreaterThanOrEquals, GreaterThan, GreaterThanOrEquals, IsNull, IsNotNull, Contains, NotContains, StartsWith, NotStartsWith]; + +export class ProfilerFilterDialog extends Modal { + + private _clauseBuilder: Builder; + private _okButton: Button; + private _cancelButton: Button; + private _clearButton: Button; + private _applyButton: Button; + private _addClauseButton: Button; + private _input: ProfilerInput; + private _clauseRows: ClauseRowUI[] = []; + + + constructor( + @IThemeService themeService: IThemeService, + @IClipboardService clipboardService: IClipboardService, + @IPartService partService: IPartService, + @ITelemetryService telemetryService: ITelemetryService, + @IContextKeyService contextKeyService: IContextKeyService, + @IContextViewService private contextViewService: IContextViewService + ) { + super('', TelemetryKeys.ProfilerFilter, partService, telemetryService, clipboardService, themeService, contextKeyService, { isFlyout: false, hasTitleIcon: true }); + } + + public open(input: ProfilerInput) { + this._input = input; + this.render(); + this.show(); + this._okButton.focus(); + } + + public dispose(): void { + + } + + public render() { + super.render(); + this.title = DialogTitle; + this.titleIconClassName = TitleIconClass; + this._register(attachModalDialogStyler(this, this._themeService)); + this._addClauseButton = this.addFooterButton(AddText, () => this.addClauseRow(false), 'left'); + this._clearButton = this.addFooterButton(ClearText, () => this.handleClearButtonClick(), 'left'); + this._applyButton = this.addFooterButton(ApplyText, () => this.filterSession()); + this._okButton = this.addFooterButton(OkText, () => this.handleOkButtonClick()); + this._cancelButton = this.addFooterButton(CancelText, () => this.hide()); + this._register(attachButtonStyler(this._okButton, this._themeService)); + this._register(attachButtonStyler(this._cancelButton, this._themeService)); + this._register(attachButtonStyler(this._clearButton, this._themeService)); + this._register(attachButtonStyler(this._applyButton, this._themeService)); + this._register(attachButtonStyler(this._addClauseButton, this._themeService)); + } + + protected renderBody(container: HTMLElement) { + new Builder(container).div({ 'class': 'profiler-filter-dialog' }, (bodyBuilder) => { + bodyBuilder.element('table', { 'class': 'profiler-filter-clause-table' }, (builder) => { + this._clauseBuilder = builder; + }); + this._clauseBuilder.element('tr', {}, (headerBuilder) => { + headerBuilder.element('td').text(FieldText); + headerBuilder.element('td').text(OperatorText); + headerBuilder.element('td').text(ValueText); + headerBuilder.element('td').text(''); + }); + + this._input.filter.clauses.forEach(clause => { + this.addClauseRow(true, clause.field, this.convertToOperatorString(clause.operator), clause.value); + }); + + bodyBuilder.div({ + 'class': 'profiler-filter-add-clause-prompt', + 'tabIndex': '0' + }).text(AddClausePromptText).on(DOM.EventType.CLICK, () => { + this.addClauseRow(false); + }).on(DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => { + let event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) { + this.addClauseRow(false); + event.stopPropagation(); + } + }); + + }); + + } + + protected layout(height?: number): void { + // Nothing to re-layout + } + + /* espace key */ + protected onClose() { + this.hide(); + } + + /* enter key */ + protected onAccept() { + this.handleOkButtonClick(); + } + + private handleOkButtonClick(): void { + this.filterSession(); + this.hide(); + } + + private handleClearButtonClick() { + this._clauseRows.forEach(clause => { + clause.row.remove(); + }); + this._clauseRows = []; + } + + private createSelectBox(container: HTMLElement, options: string[], selectedOption: string, ariaLabel: string): SelectBox { + let dropdown = new SelectBox(options, selectedOption, this.contextViewService, undefined, { ariaLabel: ariaLabel }); + dropdown.render(container); + this._register(attachSelectBoxStyler(dropdown, this._themeService)); + return dropdown; + } + + private filterSession() { + this._input.filterSession(this.getFilter()); + } + + private getFilter(): ProfilerFilter { + let clauses: ProfilerFilterClause[] = []; + + this._clauseRows.forEach(row => { + clauses.push({ + field: row.field.value, + operator: this.convertToOperatorEnum(row.operator.value), + value: row.value.value + }); + }); + + return { + clauses: clauses + }; + } + + private addClauseRow(setInitialValue: boolean, field?: string, operator?: string, value?: string): any { + this._clauseBuilder.element('tr', {}, (rowBuilder) => { + let rowElement = rowBuilder.getHTMLElement(); + let clauseId = generateUuid(); + let fieldDropDown: SelectBox; + let operatorDropDown: SelectBox; + let valueText: InputBox; + + rowBuilder.element('td', {}, (fieldCell) => { + let columns = this._input.columns.map(column => column.name); + fieldDropDown = this.createSelectBox(fieldCell.getHTMLElement(), columns, columns[0], FieldText); + }); + rowBuilder.element('td', {}, (operatorCell) => { + operatorDropDown = this.createSelectBox(operatorCell.getHTMLElement(), Operators, Operators[0], OperatorText); + }); + rowBuilder.element('td', {}, (textCell) => { + valueText = new InputBox(textCell.getHTMLElement(), undefined, {}); + this._register(attachInputBoxStyler(valueText, this._themeService)); + }); + rowBuilder.element('td', {}, (removeImageCell) => { + let removeClauseButton = removeImageCell.div({ + 'class': 'profiler-filter-remove-condition icon remove', + 'tabIndex': '0', + 'aria-label': RemoveText, + 'title': RemoveText + }); + + removeClauseButton.on(DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => { + let event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) { + this.removeRow(clauseId); + event.stopPropagation(); + } + }); + + removeClauseButton.on(DOM.EventType.CLICK, () => { + this.removeRow(clauseId); + }); + }); + + if (setInitialValue) { + fieldDropDown.selectWithOptionName(field); + operatorDropDown.selectWithOptionName(operator); + valueText.value = value; + } + this._clauseRows.push({ + id: clauseId, + row: rowElement, + field: fieldDropDown, + operator: operatorDropDown, + value: valueText + }); + }); + } + + private removeRow(clauseId: string) { + let idx = this._clauseRows.findIndex((entry) => { return entry.id === clauseId; }); + if (idx !== -1) { + this._clauseRows[idx].row.remove(); + this._clauseRows.splice(idx, 1); + } + } + private convertToOperatorEnum(operator: string): ProfilerFilterClauseOperator { + switch (operator) { + case Equals: + return ProfilerFilterClauseOperator.Equals; + case NotEquals: + return ProfilerFilterClauseOperator.NotEquals; + case LessThan: + return ProfilerFilterClauseOperator.LessThan; + case LessThanOrEquals: + return ProfilerFilterClauseOperator.LessThanOrEquals; + case GreaterThan: + return ProfilerFilterClauseOperator.GreaterThan; + case GreaterThanOrEquals: + return ProfilerFilterClauseOperator.GreaterThanOrEquals; + case IsNull: + return ProfilerFilterClauseOperator.IsNull; + case IsNotNull: + return ProfilerFilterClauseOperator.IsNotNull; + case Contains: + return ProfilerFilterClauseOperator.Contains; + case NotContains: + return ProfilerFilterClauseOperator.NotContains; + case StartsWith: + return ProfilerFilterClauseOperator.StartsWith; + case NotStartsWith: + return ProfilerFilterClauseOperator.NotStartsWith; + default: + throw `Not a valid operator: ${operator}`; + } + } + + private convertToOperatorString(operator: ProfilerFilterClauseOperator): string { + switch (operator) { + case ProfilerFilterClauseOperator.Equals: + return Equals; + case ProfilerFilterClauseOperator.NotEquals: + return NotEquals; + case ProfilerFilterClauseOperator.LessThan: + return LessThan; + case ProfilerFilterClauseOperator.LessThanOrEquals: + return LessThanOrEquals; + case ProfilerFilterClauseOperator.GreaterThan: + return GreaterThan; + case ProfilerFilterClauseOperator.GreaterThanOrEquals: + return GreaterThanOrEquals; + case ProfilerFilterClauseOperator.IsNull: + return IsNull; + case ProfilerFilterClauseOperator.IsNotNull: + return IsNotNull; + case ProfilerFilterClauseOperator.Contains: + return Contains; + case ProfilerFilterClauseOperator.NotContains: + return NotContains; + case ProfilerFilterClauseOperator.StartsWith: + return StartsWith; + case ProfilerFilterClauseOperator.NotStartsWith: + return NotStartsWith; + default: + throw `Not a valid operator: ${operator}`; + } + } +} + +interface ClauseRowUI { + id: string; + row: HTMLElement; + field: SelectBox; + operator: SelectBox; + value: InputBox; +} \ No newline at end of file diff --git a/src/sql/parts/profiler/editor/controller/profilerTableEditor.ts b/src/sql/parts/profiler/editor/controller/profilerTableEditor.ts index 50a9ce14d2..dbe6106c7b 100644 --- a/src/sql/parts/profiler/editor/controller/profilerTableEditor.ts +++ b/src/sql/parts/profiler/editor/controller/profilerTableEditor.ts @@ -28,6 +28,8 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { Dimension } from 'vs/base/browser/dom'; import { textFormatter } from 'sql/parts/grid/services/sharedServices'; import { PROFILER_MAX_MATCHES } from 'sql/parts/profiler/editor/controller/profilerFindWidget'; +import { IStatusbarService, StatusbarAlignment, IStatusbarEntry } from 'vs/platform/statusbar/common/statusbar'; +import { localize } from 'vs/nls'; export interface ProfilerTableViewState { scrollTop: number; @@ -47,6 +49,8 @@ export class ProfilerTableEditor extends BaseEditor implements IProfilerControll private _overlay: HTMLElement; private _currentDimensions: Dimension; private _actionMap: { [x: string]: IEditorAction } = {}; + private _statusbarItem: IDisposable; + private _showStatusBarItem: boolean; private _onDidChangeConfiguration = new Emitter(); public onDidChangeConfiguration: Event = this._onDidChangeConfiguration.event; @@ -57,11 +61,13 @@ export class ProfilerTableEditor extends BaseEditor implements IProfilerControll @IContextViewService private _contextViewService: IContextViewService, @IKeybindingService private _keybindingService: IKeybindingService, @IContextKeyService private _contextKeyService: IContextKeyService, - @IInstantiationService private _instantiationService: IInstantiationService + @IInstantiationService private _instantiationService: IInstantiationService, + @IStatusbarService private _statusbarService: IStatusbarService ) { super(ProfilerTableEditor.ID, telemetryService, _themeService); this._actionMap[ACTION_IDS.FIND_NEXT] = this._instantiationService.createInstance(ProfilerFindNext, this); this._actionMap[ACTION_IDS.FIND_PREVIOUS] = this._instantiationService.createInstance(ProfilerFindPrevious, this); + this._showStatusBarItem = true; } public createEditor(parent: HTMLElement): void { @@ -99,7 +105,11 @@ export class ProfilerTableEditor extends BaseEditor implements IProfilerControll } public setInput(input: ProfilerInput): TPromise { + this._showStatusBarItem = true; this._input = input; + + this._updateRowCountStatus(); + if (this._columnListener) { this._columnListener.dispose(); } @@ -114,7 +124,16 @@ export class ProfilerTableEditor extends BaseEditor implements IProfilerControll this._stateListener.dispose(); } this._stateListener = input.state.addChangeListener(e => this._onStateChange(e)); - input.data.onRowCountChange(() => { this._profilerTable.updateRowCount(); }); + input.data.onRowCountChange(() => { + this._profilerTable.updateRowCount(); + this._updateRowCountStatus(); + }); + + input.data.onFilterStateChange(() => { + this._profilerTable.grid.invalidateAllRows(); + this._profilerTable.updateRowCount(); + this._updateRowCountStatus(); + }); if (this._findCountChangeListener) { this._findCountChangeListener.dispose(); @@ -128,6 +147,10 @@ export class ProfilerTableEditor extends BaseEditor implements IProfilerControll this._profilerTable.setActiveCell(val.row, val.col); this._updateFinderMatchState(); }, er => { }); + + this._input.onDispose(() => { + this._disposeStatusbarItem(); + }); return TPromise.as(null); } @@ -237,7 +260,26 @@ export class ProfilerTableEditor extends BaseEditor implements IProfilerControll } } + private _updateRowCountStatus(): void { + if (this._showStatusBarItem) { + let message = this._input.data.filterEnabled ? + localize('ProfilerTableEditor.eventCountFiltered', 'Events (Filtered): {0}/{1}', this._input.data.getLength(), this._input.data.getLengthNonFiltered()) + : localize('ProfilerTableEditor.eventCount', 'Events: {0}', this._input.data.getLength()); + + this._disposeStatusbarItem(); + this._statusbarItem = this._statusbarService.addEntry({ text: message }, StatusbarAlignment.RIGHT); + } + } + + private _disposeStatusbarItem() { + if (this._statusbarItem) { + this._statusbarItem.dispose(); + } + } + public saveViewState(): ProfilerTableViewState { + this._disposeStatusbarItem(); + this._showStatusBarItem = false; let viewElement = this._profilerTable.grid.getCanvasNode().parentElement; return { scrollTop: viewElement.scrollTop, @@ -246,6 +288,8 @@ export class ProfilerTableEditor extends BaseEditor implements IProfilerControll } public restoreViewState(state: ProfilerTableViewState): void { + this._showStatusBarItem = true; + this._updateRowCountStatus(); let viewElement = this._profilerTable.grid.getCanvasNode().parentElement; viewElement.scrollTop = state.scrollTop; viewElement.scrollLeft = state.scrollLeft; diff --git a/src/sql/parts/profiler/editor/profilerEditor.ts b/src/sql/parts/profiler/editor/profilerEditor.ts index 92de5a4bcb..5f99523f24 100644 --- a/src/sql/parts/profiler/editor/profilerEditor.ts +++ b/src/sql/parts/profiler/editor/profilerEditor.ts @@ -144,6 +144,8 @@ export class ProfilerEditor extends BaseEditor { private _autoscrollAction: Actions.ProfilerAutoScroll; private _createAction: Actions.ProfilerCreate; private _collapsedPanelAction: Actions.ProfilerCollapsablePanelAction; + private _filterAction: Actions.ProfilerFilterSession; + private _clearFilterAction: Actions.ProfilerClearSessionFilter; private _savedTableViewStates = new Map(); @@ -217,7 +219,10 @@ export class ProfilerEditor extends BaseEditor { this._pauseAction.enabled = false; this._connectAction = this._instantiationService.createInstance(Actions.ProfilerConnect, Actions.ProfilerConnect.ID, Actions.ProfilerConnect.LABEL); this._autoscrollAction = this._instantiationService.createInstance(Actions.ProfilerAutoScroll, Actions.ProfilerAutoScroll.ID, Actions.ProfilerAutoScroll.LABEL); - + this._filterAction = this._instantiationService.createInstance(Actions.ProfilerFilterSession, Actions.ProfilerFilterSession.ID, Actions.ProfilerFilterSession.LABEL); + this._filterAction.enabled = true; + this._clearFilterAction = this._instantiationService.createInstance(Actions.ProfilerClearSessionFilter, Actions.ProfilerClearSessionFilter.ID, Actions.ProfilerClearSessionFilter.LABEL); + this._clearFilterAction.enabled = true; this._viewTemplates = this._profilerService.getViewTemplates(); this._viewTemplateSelector = new SelectBox(this._viewTemplates.map(i => i.name), 'Standard View', this._contextViewService); this._viewTemplateSelector.setAriaLabel(nls.localize('profiler.viewSelectAccessibleName', 'Select View')); @@ -257,6 +262,9 @@ export class ProfilerEditor extends BaseEditor { { action: this._stopAction }, { action: this._pauseAction }, { element: Taskbar.createTaskbarSeparator() }, + { action: this._filterAction }, + { action: this._clearFilterAction }, + { element: Taskbar.createTaskbarSeparator() }, { element: this._createTextElement(nls.localize('profiler.viewSelectLabel', 'Select View:')) }, { element: viewTemplateContainer }, { action: this._autoscrollAction }, diff --git a/src/sql/parts/profiler/editor/profilerInput.ts b/src/sql/parts/profiler/editor/profilerInput.ts index c3b39da715..49143ee068 100644 --- a/src/sql/parts/profiler/editor/profilerInput.ts +++ b/src/sql/parts/profiler/editor/profilerInput.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { TableDataView } from 'sql/base/browser/ui/table/tableDataView'; -import { IProfilerSession, IProfilerService, ProfilerSessionID, IProfilerViewTemplate } from 'sql/parts/profiler/service/interfaces'; +import { IProfilerSession, IProfilerService, ProfilerSessionID, IProfilerViewTemplate, ProfilerFilter } from 'sql/parts/profiler/service/interfaces'; import { ProfilerState } from './profilerState'; import { IConnectionProfile } from 'sql/parts/connection/common/interfaces'; @@ -23,6 +23,7 @@ import { escape } from 'sql/base/common/strings'; import * as types from 'vs/base/common/types'; import URI from 'vs/base/common/uri'; import Severity from 'vs/base/common/severity'; +import { FilterData } from 'sql/parts/profiler/service/profilerFilter'; export class ProfilerInput extends EditorInput implements IProfilerSession { @@ -41,6 +42,8 @@ export class ProfilerInput extends EditorInput implements IProfilerSession { private _onColumnsChanged = new Emitter[]>(); public onColumnsChanged: Event[]> = this._onColumnsChanged.event; + private _filter: ProfilerFilter = { clauses: [] }; + constructor( public connection: IConnectionProfile, @IInstantiationService private _instantiationService: IInstantiationService, @@ -73,7 +76,12 @@ export class ProfilerInput extends EditorInput implements IProfilerSession { } return ret; }; - this._data = new TableDataView(undefined, searchFn); + + let filterFn = (data: Array): Array => { + return FilterData(this._filter, data); + }; + + this._data = new TableDataView(undefined, searchFn, undefined, filterFn); } public get providerType(): string { @@ -187,6 +195,10 @@ export class ProfilerInput extends EditorInput implements IProfilerSession { return this._state; } + public get filter(): ProfilerFilter { + return this._filter; + } + public onSessionStopped(notification: sqlops.ProfilerSessionStoppedParams) { this._notificationService.error(nls.localize("profiler.sessionStopped", "XEvent Profiler Session stopped unexpectedly on the server {0}.", this.connection.serverName)); @@ -232,6 +244,7 @@ export class ProfilerInput extends EditorInput implements IProfilerSession { this._notificationService.warn(nls.localize("profiler.eventsLost", "The XEvent Profiler session for {0} has lost events.", this.connection.serverName)); } + let newEvents = []; for (let i: number = 0; i < eventMessage.events.length && i < 500; ++i) { let e: sqlops.ProfilerEvent = eventMessage.events[i]; let data = {}; @@ -249,9 +262,26 @@ export class ProfilerInput extends EditorInput implements IProfilerSession { data[columnName] = escape(value); } } - this._data.push(data); + newEvents.push(data); } + if (newEvents.length > 0) { + this._data.push(newEvents); + } + } + + filterSession(filter: ProfilerFilter) { + this._filter = filter; + if (this._filter.clauses.length !== 0) { + this.data.filter(); + } else { + this.data.clearFilter(); + } + } + + clearFilter() { + this._filter = { clauses: [] }; + this.data.clearFilter(); } confirmSave(): TPromise { @@ -280,4 +310,9 @@ export class ProfilerInput extends EditorInput implements IProfilerSession { isDirty(): boolean { return this.state.isRunning || this.state.isPaused; } + + dispose() { + super.dispose(); + this._profilerService.disconnectSession(this.id); + } } diff --git a/src/sql/parts/profiler/service/interfaces.ts b/src/sql/parts/profiler/service/interfaces.ts index 225531004c..68c5d748e2 100644 --- a/src/sql/parts/profiler/service/interfaces.ts +++ b/src/sql/parts/profiler/service/interfaces.ts @@ -125,6 +125,11 @@ export interface IProfilerService { * @param input input object that contains the necessary information which will be modified based on used input */ launchCreateSessionDialog(input: ProfilerInput): Thenable; + /** + * Launches the dialog for collecting the filter object + * @param input input object + */ + launchFilterSessionDialog(input: ProfilerInput): void; } export interface IProfilerSettings { @@ -146,4 +151,29 @@ export interface IProfilerSessionTemplate { name: string; defaultView: string; createStatement: string; +} + +export interface ProfilerFilter { + clauses: ProfilerFilterClause[]; +} + +export interface ProfilerFilterClause { + field: string; + operator: ProfilerFilterClauseOperator; + value: string; +} + +export enum ProfilerFilterClauseOperator { + Equals, + NotEquals, + LessThan, + LessThanOrEquals, + GreaterThan, + GreaterThanOrEquals, + IsNull, + IsNotNull, + Contains, + NotContains, + StartsWith, + NotStartsWith } \ No newline at end of file diff --git a/src/sql/parts/profiler/service/profilerFilter.ts b/src/sql/parts/profiler/service/profilerFilter.ts new file mode 100644 index 0000000000..ae514323de --- /dev/null +++ b/src/sql/parts/profiler/service/profilerFilter.ts @@ -0,0 +1,102 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ProfilerFilter, ProfilerFilterClause, ProfilerFilterClauseOperator } from 'sql/parts/profiler/service/interfaces'; + +export function FilterData(filter: ProfilerFilter, data: any[]): any[] { + if (!data || !filter) { + return data; + } + return data.filter(item => matches(item, filter.clauses)); +} + +function matches(item: any, clauses: ProfilerFilterClause[]): boolean { + let match = true; + if (!item) { + match = false; + } else if (clauses) { + for (let i = 0; i < clauses.length; i++) { + let clause = clauses[i]; + if (!!clause && !!clause.field) { + let actualValue: any = item[clause.field]; + let expectedValue: any = clause.value; + let actualValueString: string = actualValue === undefined ? undefined : actualValue.toLocaleLowerCase(); + let expectedValueString: string = expectedValue === undefined ? undefined : expectedValue.toLocaleLowerCase(); + let actualValueDate = new Date(actualValue).valueOf(); + let expectedValueDate = new Date(expectedValue).valueOf(); + let actualValueNumber = new Number(actualValue).valueOf(); + let expectedValueNumber = new Number(expectedValue).valueOf(); + + if (isValidNumber(actualValue) && isValidNumber(expectedValue)) { + actualValue = actualValueNumber; + expectedValue = expectedValueNumber; + } else if (isValidDate(actualValue) && isValidDate(expectedValue)) { + actualValue = actualValueDate; + expectedValue = expectedValueDate; + } else { + actualValue = actualValueString; + expectedValue = expectedValueString; + } + + switch (clause.operator) { + case ProfilerFilterClauseOperator.Equals: + match = actualValue === expectedValue; + break; + case ProfilerFilterClauseOperator.NotEquals: + match = actualValue !== expectedValue; + break; + case ProfilerFilterClauseOperator.LessThan: + match = actualValue < expectedValue; + break; + case ProfilerFilterClauseOperator.LessThanOrEquals: + match = actualValue <= expectedValue; + break; + case ProfilerFilterClauseOperator.GreaterThan: + match = actualValue > expectedValue; + break; + case ProfilerFilterClauseOperator.GreaterThanOrEquals: + match = actualValue >= expectedValue; + break; + case ProfilerFilterClauseOperator.IsNull: + match = actualValue === undefined || actualValue === null || actualValue === ''; + break; + case ProfilerFilterClauseOperator.IsNotNull: + match = actualValue !== undefined && actualValue !== null && actualValue !== ''; + break; + case ProfilerFilterClauseOperator.Contains: + match = actualValueString && actualValueString.includes(expectedValueString); + break; + case ProfilerFilterClauseOperator.NotContains: + match = !actualValueString || !actualValueString.includes(expectedValueString); + break; + case ProfilerFilterClauseOperator.StartsWith: + match = actualValueString.startsWith(expectedValueString); + break; + case ProfilerFilterClauseOperator.NotStartsWith: + match = !actualValueString || !actualValueString.startsWith(expectedValueString); + break; + default: + throw `Not a valid operator: ${clause.operator}`; + } + } + + if (!match) { + break; + } + } + } + + return match; +} + +function isValidNumber(value: string): boolean { + let num = new Number(value); + return value !== undefined && !isNaN(num.valueOf()) && value.replace(' ', '') !== ''; +} + +function isValidDate(value: string): boolean { + let date = new Date(value); + return value !== undefined && !isNaN(date.valueOf()); +} diff --git a/src/sql/parts/profiler/service/profilerService.ts b/src/sql/parts/profiler/service/profilerService.ts index 021ea9a52d..debe8286ca 100644 --- a/src/sql/parts/profiler/service/profilerService.ts +++ b/src/sql/parts/profiler/service/profilerService.ts @@ -21,6 +21,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { ICommandService } from 'vs/platform/commands/common/commands'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { Scope as MementoScope, Memento } from 'vs/workbench/common/memento'; +import { ProfilerFilterDialog } from 'sql/parts/profiler/dialog/profilerFilterDialog'; class TwoWayMap { private forwardMap: Map; @@ -233,4 +234,9 @@ export class ProfilerService implements IProfilerService { public launchCreateSessionDialog(input?: ProfilerInput): Thenable { return this._commandService.executeCommand('profiler.openCreateSessionDialog', input.id, input.providerType, this.getSessionTemplates()); } + + public launchFilterSessionDialog(input: ProfilerInput): void { + let dialog = this._instantiationService.createInstance(ProfilerFilterDialog); + dialog.open(input); + } } diff --git a/src/sql/workbench/api/node/extHostDataProtocol.ts b/src/sql/workbench/api/node/extHostDataProtocol.ts index 528e2ee8a7..69698d053d 100644 --- a/src/sql/workbench/api/node/extHostDataProtocol.ts +++ b/src/sql/workbench/api/node/extHostDataProtocol.ts @@ -548,6 +548,13 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape { return this._resolveProvider(handle).pauseSession(sessionId); } + /** + * Disconnect a profiler session + */ + public $disconnectSession(handle: number, sessionId: string): Thenable { + return this._resolveProvider(handle).disconnectSession(sessionId); + } + /** * Get list of running XEvent sessions on the session's target server */ diff --git a/src/sql/workbench/api/node/mainThreadDataProtocol.ts b/src/sql/workbench/api/node/mainThreadDataProtocol.ts index 96c7edf287..8076986836 100644 --- a/src/sql/workbench/api/node/mainThreadDataProtocol.ts +++ b/src/sql/workbench/api/node/mainThreadDataProtocol.ts @@ -318,7 +318,7 @@ export class MainThreadDataProtocol implements MainThreadDataProtocolShape { return TPromise.as(true); }, disconnectSession(sessionId: string): Thenable { - return TPromise.as(true); + return self._proxy.$disconnectSession(handle, sessionId); } }); diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index 25276a9ffb..8698677573 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -356,6 +356,11 @@ export abstract class ExtHostDataProtocolShape { */ $getXEventSessions(handle: number, sessionId: string): Thenable { throw ni(); } + /** + * Disconnect a profiler session + */ + $disconnectSession(handle: number, sessionId: string): Thenable { throw ni(); } + /** * Get Agent Job list */ diff --git a/src/sqltest/base/browser/ui/table/tableDataView.test.ts b/src/sqltest/base/browser/ui/table/tableDataView.test.ts new file mode 100644 index 0000000000..24a6af5dd8 --- /dev/null +++ b/src/sqltest/base/browser/ui/table/tableDataView.test.ts @@ -0,0 +1,100 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as assert from 'assert'; +import { TableDataView } from 'sql/base/browser/ui/table/tableDataView'; + +suite('TableDataView Tests', () => { + test('Data can be filtered and filter can be cleared', () => { + const rowCount = 10; + const columnCount = 5; + const originalData = populateData(rowCount, columnCount); + + let filteredRowCount = 5; + const obj = new TableDataView(originalData, undefined, undefined, (data: any[]) => { + return populateData(filteredRowCount, columnCount); + }); + + let rowCountEventInvokeCount = 0; + let filterStateChangeEventInvokeCount = 0; + let rowCountEventParameter; + obj.onRowCountChange((count) => { + rowCountEventInvokeCount++; + rowCountEventParameter = count; + }); + + obj.onFilterStateChange(() => { + filterStateChangeEventInvokeCount++; + }); + + let verify = (expectedRowCountChangeInvokeCount: number, + expectedDataLength: number, + expectedNonFilteredDataLength: number, + expectedFilterStateChangeInvokeCount: number, + stepName: string, + verifyRowCountEventParameter: boolean = true) => { + assert.equal(rowCountEventInvokeCount, expectedRowCountChangeInvokeCount, 'RowCountChange event count - ' + stepName); + if (verifyRowCountEventParameter) { + assert.equal(rowCountEventParameter, expectedDataLength, 'Row count passed by RowCountChange event - ' + stepName); + } + assert.equal(obj.getLength(), expectedDataLength, 'Data length - ' + stepName); + assert.equal(obj.getLengthNonFiltered(), expectedNonFilteredDataLength, 'Length for all data - ' + stepName); + assert.equal(filterStateChangeEventInvokeCount, expectedFilterStateChangeInvokeCount, 'FilterStateChange event count - ' + stepName); + }; + + verify(0, rowCount, rowCount, 0, 'after initialization', false); + + obj.filter(); + + verify(0, filteredRowCount, rowCount, 1, 'after filtering', false); + + const additionalRowCount = 20; + const additionalData = populateData(additionalRowCount, columnCount); + obj.push(additionalData); + + verify(1, filteredRowCount * 2, rowCount + additionalRowCount, 1, 'after adding more data'); + + obj.clearFilter(); + + verify(1, rowCount + additionalRowCount, rowCount + additionalRowCount, 2, 'after clearing filter', false); + + //From this point on, nothing matches the filter criteria + filteredRowCount = 0; + + obj.filter(); + verify(1, 0, rowCount + additionalRowCount, 3, 'after 2nd filtering', false); + + obj.push(additionalData); + verify(2, 0, rowCount + additionalRowCount + additionalRowCount, 3, 'after 2nd adding more data'); + + obj.clearFilter(); + verify(2, rowCount + additionalRowCount + additionalRowCount, rowCount + additionalRowCount + additionalRowCount, 4, 'after 2nd clearing filter', false); + + obj.clearFilter(); + verify(2, rowCount + additionalRowCount + additionalRowCount, rowCount + additionalRowCount + additionalRowCount, 4, 'calling clearFilter() multiple times', false); + }); +}); + +function populateData(row: number, column: number): any[] { + let data = []; + for (let i: number = 0; i < row; i++) { + let row = {}; + for (let j: number = 0; j < column; j++) { + row[getColumnName(j)] = getCellValue(i, j); + } + data.push(row); + } + return data; +} + +function getColumnName(index: number): string { + return `column${index}`; +} + +function getCellValue(row: number, column: number): string { + return `row ${row} column ${column}`; +} \ No newline at end of file diff --git a/src/sqltest/parts/profiler/service/profilerFilter.test.ts b/src/sqltest/parts/profiler/service/profilerFilter.test.ts new file mode 100644 index 0000000000..3dc363f1b5 --- /dev/null +++ b/src/sqltest/parts/profiler/service/profilerFilter.test.ts @@ -0,0 +1,122 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as assert from 'assert'; +import { FilterData } from 'sql/parts/profiler/service/profilerFilter'; +import { ProfilerFilterClauseOperator, ProfilerFilter } from 'sql/parts/profiler/service/interfaces'; + +const property1 = 'property1'; +const property2 = 'property2'; + +suite('Profiler filter data tests', () => { + test('number type filter data test', () => { + let filter: ProfilerFilter = { clauses: [] }; + let entry1: TestData = { property1: '-1', property2: '0' }; + let entry2: TestData = { property1: '0', property2: '10' }; + let entry3: TestData = { property1: '10.0', property2: '-1' }; + + let data: TestData[] = [entry1, entry2, entry3]; + + filter.clauses = [{ field: property1, operator: ProfilerFilterClauseOperator.Equals, value: '10' }]; + filterAndVerify(filter, data, [entry3], 'Equals operator'); + + filter.clauses = [{ field: property1, operator: ProfilerFilterClauseOperator.NotEquals, value: '-1' }]; + filterAndVerify(filter, data, [entry2, entry3], 'NotEquals operator'); + + filter.clauses = [{ field: property1, operator: ProfilerFilterClauseOperator.GreaterThan, value: '2' }]; + filterAndVerify(filter, data, [entry3], 'GreaterThan operator'); + + filter.clauses = [{ field: property1, operator: ProfilerFilterClauseOperator.GreaterThanOrEquals, value: '0' }]; + filterAndVerify(filter, data, [entry2, entry3], 'GreaterThanOrEquals operator'); + + filter.clauses = [{ field: property1, operator: ProfilerFilterClauseOperator.LessThan, value: '0' }]; + filterAndVerify(filter, data, [entry1], 'LessThan operator'); + + filter.clauses = [{ field: property1, operator: ProfilerFilterClauseOperator.LessThanOrEquals, value: '0' }]; + filterAndVerify(filter, data, [entry1, entry2], 'LessThanOrEquals operator'); + + filter.clauses = [{ field: property1, operator: ProfilerFilterClauseOperator.LessThanOrEquals, value: '-2' }]; + filterAndVerify(filter, data, [], 'Empty result set'); + + filter.clauses = [{ field: property1, operator: ProfilerFilterClauseOperator.LessThanOrEquals, value: '10' }]; + filterAndVerify(filter, data, data, 'All matches'); + + filter.clauses = [{ field: property1, operator: ProfilerFilterClauseOperator.LessThanOrEquals, value: '0' }, + { field: property2, operator: ProfilerFilterClauseOperator.LessThan, value: '10' }]; + filterAndVerify(filter, data, [entry1], 'Multiple clauses'); + }); + + test('date type filter data test', () => { + let filter: ProfilerFilter = { clauses: [] }; + let entry1: TestData = { property1: '2019-01-02T19:00:00.000Z', property2: '' }; + let entry2: TestData = { property1: '2019-01-03T10:00:00.000Z', property2: '' }; + let entry3: TestData = { property1: '2019-01-04T10:00:00.000Z', property2: '' }; + + let data: TestData[] = [entry1, entry2, entry3]; + + filter.clauses = [{ field: property1, operator: ProfilerFilterClauseOperator.Equals, value: '2019-01-02T19:00:00Z' }]; + filterAndVerify(filter, data, [entry1], 'Equals operator'); + + filter.clauses = [{ field: property1, operator: ProfilerFilterClauseOperator.NotEquals, value: '2019-01-03T10:00:00Z' }]; + filterAndVerify(filter, data, [entry1, entry3], 'NotEquals operator'); + + filter.clauses = [{ field: property1, operator: ProfilerFilterClauseOperator.GreaterThan, value: '2019-01-01T00:00:00Z' }]; + filterAndVerify(filter, data, [entry1, entry2, entry3], 'GreaterThan operator'); + + filter.clauses = [{ field: property1, operator: ProfilerFilterClauseOperator.GreaterThanOrEquals, value: '2019-01-03T10:00:00.000Z' }]; + filterAndVerify(filter, data, [entry2, entry3], 'GreaterThanOrEquals operator'); + + filter.clauses = [{ field: property1, operator: ProfilerFilterClauseOperator.LessThan, value: '2019-01-03T10:00:00.000Z' }]; + filterAndVerify(filter, data, [entry1], 'LessThan operator'); + + filter.clauses = [{ field: property1, operator: ProfilerFilterClauseOperator.LessThanOrEquals, value: '2019-01-03T10:00:00Z' }]; + filterAndVerify(filter, data, [entry1, entry2], 'LessThanOrEquals operator'); + }); + + test('string type filter data test', () => { + let filter: ProfilerFilter = { clauses: [] }; + let entry1: TestData = { property1: '', property2: '' }; + let entry2: TestData = { property1: 'test string', property2: '' }; + let entry3: TestData = { property1: 'new string', property2: '' }; + + let data: TestData[] = [entry1, entry2, entry3]; + + filter.clauses = [{ field: property1, operator: ProfilerFilterClauseOperator.IsNull, value: '' }]; + filterAndVerify(filter, data, [entry1], 'IsNull operator'); + + filter.clauses = [{ field: property1, operator: ProfilerFilterClauseOperator.IsNotNull, value: '' }]; + filterAndVerify(filter, data, [entry2, entry3], 'IsNotNull operator'); + + filter.clauses = [{ field: property1, operator: ProfilerFilterClauseOperator.Contains, value: 'sTRing' }]; + filterAndVerify(filter, data, [entry2, entry3], 'Contains operator'); + + filter.clauses = [{ field: property1, operator: ProfilerFilterClauseOperator.NotContains, value: 'string' }]; + filterAndVerify(filter, data, [entry1], 'NotContains operator'); + + filter.clauses = [{ field: property1, operator: ProfilerFilterClauseOperator.StartsWith, value: 'tEst' }]; + filterAndVerify(filter, data, [entry2], 'StartsWith operator'); + + filter.clauses = [{ field: property1, operator: ProfilerFilterClauseOperator.NotStartsWith, value: 'Test' }]; + filterAndVerify(filter, data, [entry1, entry3], 'NotStartsWith operator'); + }); +}); + +function filterAndVerify(filter: ProfilerFilter, data: TestData[], expectedResult: TestData[], stepName: string) { + let actualResult = FilterData(filter, data); + assert.equal(actualResult.length, expectedResult.length, `length check for ${stepName}`); + for (let i = 0; i < actualResult.length; i++) { + let actual = actualResult[i]; + let expected = expectedResult[i]; + assert(actual.property1 === expected.property1 && actual.property2 === expected.property2, `array content check for ${stepName}`); + } +} + +interface TestData { + property1: string; + property2: string; +} +