mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-16 09:35:36 -05:00
Alanren/profiler filter (#3760)
* profiler filter * add test cases * perf improvement with bulk insert * update dependency version and address comments
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -40,10 +40,14 @@ function defaultSort<T>(args: Slick.OnSortEventArgs<T>, data: Array<T>): Array<T
|
||||
}
|
||||
|
||||
export class TableDataView<T extends Slick.SlickData> implements IDisposableDataProvider<T> {
|
||||
//The data exposed publicly, when filter is enabled, _data holds the filtered data.
|
||||
private _data: Array<T>;
|
||||
//Used when filtering is enabled, _allData holds the complete set of data.
|
||||
private _allData: Array<T>;
|
||||
private _findArray: Array<IFindPosition>;
|
||||
private _findObs: Observable<IFindPosition>;
|
||||
private _findIndex: number;
|
||||
private _filterEnabled: boolean;
|
||||
|
||||
private _onRowCountChange = new Emitter<number>();
|
||||
get onRowCountChange(): Event<number> { return this._onRowCountChange.event; }
|
||||
@@ -51,10 +55,14 @@ export class TableDataView<T extends Slick.SlickData> implements IDisposableData
|
||||
private _onFindCountChange = new Emitter<number>();
|
||||
get onFindCountChange(): Event<number> { return this._onFindCountChange.event; }
|
||||
|
||||
private _onFilterStateChange = new Emitter<void>();
|
||||
get onFilterStateChange(): Event<void> { return this._onFilterStateChange.event; }
|
||||
|
||||
constructor(
|
||||
data?: Array<T>,
|
||||
private _findFn?: (val: T, exp: string) => Array<number>,
|
||||
private _sortFn?: (args: Slick.OnSortEventArgs<T>, data: Array<T>) => Array<T>
|
||||
private _sortFn?: (args: Slick.OnSortEventArgs<T>, data: Array<T>) => Array<T>,
|
||||
private _filterFn?: (data: Array<T>) => Array<T>
|
||||
) {
|
||||
if (data) {
|
||||
this._data = data;
|
||||
@@ -65,6 +73,35 @@ export class TableDataView<T extends Slick.SlickData> 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<T>) {
|
||||
@@ -79,20 +116,39 @@ export class TableDataView<T extends Slick.SlickData> implements IDisposableData
|
||||
return this._data[index];
|
||||
}
|
||||
|
||||
getLengthNonFiltered(): number {
|
||||
return this.filterEnabled ? this._allData.length : this._data.length;
|
||||
}
|
||||
|
||||
push(items: Array<T>);
|
||||
push(item: T);
|
||||
push(input: T | Array<T>) {
|
||||
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<T>();
|
||||
this._onRowCountChange.fire();
|
||||
if (this._filterEnabled) {
|
||||
this._allData = new Array<T>();
|
||||
}
|
||||
this._onRowCountChange.fire(this.getLength());
|
||||
}
|
||||
|
||||
find(exp: string, maxMatches: number = 0): Thenable<IFindPosition> {
|
||||
@@ -180,6 +236,7 @@ export class TableDataView<T extends Slick.SlickData> implements IDisposableData
|
||||
|
||||
dispose() {
|
||||
this._data = undefined;
|
||||
this._allData = undefined;
|
||||
this._findArray = undefined;
|
||||
this._findObs = undefined;
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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<boolean> {
|
||||
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<boolean> {
|
||||
input.clearFilter();
|
||||
return TPromise.wrap(true);
|
||||
}
|
||||
}
|
||||
|
||||
25
src/sql/parts/profiler/dialog/media/profilerFilterDialog.css
Normal file
25
src/sql/parts/profiler/dialog/media/profilerFilterDialog.css
Normal file
@@ -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
|
||||
}
|
||||
329
src/sql/parts/profiler/dialog/profilerFilterDialog.ts
Normal file
329
src/sql/parts/profiler/dialog/profilerFilterDialog.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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<IConfigurationChangedEvent>();
|
||||
public onDidChangeConfiguration: Event<IConfigurationChangedEvent> = 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<void> {
|
||||
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;
|
||||
|
||||
@@ -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<ProfilerInput, ProfilerTableViewState>();
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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<Slick.Column<Slick.SlickData>[]>();
|
||||
public onColumnsChanged: Event<Slick.Column<Slick.SlickData>[]> = 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<Slick.SlickData>(undefined, searchFn);
|
||||
|
||||
let filterFn = (data: Array<Slick.SlickData>): Array<Slick.SlickData> => {
|
||||
return FilterData(this._filter, data);
|
||||
};
|
||||
|
||||
this._data = new TableDataView<Slick.SlickData>(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<ConfirmResult> {
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void>;
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
102
src/sql/parts/profiler/service/profilerFilter.ts
Normal file
102
src/sql/parts/profiler/service/profilerFilter.ts
Normal file
@@ -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());
|
||||
}
|
||||
@@ -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<T, K> {
|
||||
private forwardMap: Map<T, K>;
|
||||
@@ -233,4 +234,9 @@ export class ProfilerService implements IProfilerService {
|
||||
public launchCreateSessionDialog(input?: ProfilerInput): Thenable<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,6 +548,13 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape {
|
||||
return this._resolveProvider<sqlops.ProfilerProvider>(handle).pauseSession(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect a profiler session
|
||||
*/
|
||||
public $disconnectSession(handle: number, sessionId: string): Thenable<boolean> {
|
||||
return this._resolveProvider<sqlops.ProfilerProvider>(handle).disconnectSession(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of running XEvent sessions on the session's target server
|
||||
*/
|
||||
|
||||
@@ -318,7 +318,7 @@ export class MainThreadDataProtocol implements MainThreadDataProtocolShape {
|
||||
return TPromise.as(true);
|
||||
},
|
||||
disconnectSession(sessionId: string): Thenable<boolean> {
|
||||
return TPromise.as(true);
|
||||
return self._proxy.$disconnectSession(handle, sessionId);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -356,6 +356,11 @@ export abstract class ExtHostDataProtocolShape {
|
||||
*/
|
||||
$getXEventSessions(handle: number, sessionId: string): Thenable<string[]> { throw ni(); }
|
||||
|
||||
/**
|
||||
* Disconnect a profiler session
|
||||
*/
|
||||
$disconnectSession(handle: number, sessionId: string): Thenable<boolean> { throw ni(); }
|
||||
|
||||
/**
|
||||
* Get Agent Job list
|
||||
*/
|
||||
|
||||
100
src/sqltest/base/browser/ui/table/tableDataView.test.ts
Normal file
100
src/sqltest/base/browser/ui/table/tableDataView.test.ts
Normal file
@@ -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}`;
|
||||
}
|
||||
122
src/sqltest/parts/profiler/service/profilerFilter.test.ts
Normal file
122
src/sqltest/parts/profiler/service/profilerFilter.test.ts
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user