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:
Alan Ren
2019-01-18 16:25:18 -08:00
committed by GitHub
parent 637dc9b9b2
commit 3e7a09c1e3
21 changed files with 926 additions and 22 deletions

View File

@@ -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",

View File

@@ -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"

View File

@@ -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"

View File

@@ -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",

View File

@@ -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"

View File

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

View File

@@ -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:

View File

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

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

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

View File

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

View File

@@ -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 },

View File

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

View File

@@ -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
}

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

View File

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

View File

@@ -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
*/

View File

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

View File

@@ -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
*/

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

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