From f39647f2432dad86709f479c910dd15bffc8d7f1 Mon Sep 17 00:00:00 2001 From: Alan Ren Date: Wed, 26 Jun 2019 23:55:03 -0700 Subject: [PATCH] add save/load filter feature to profiler (#6170) * save/load profiler filter * add role for custom buttons --- .../browser/media/profilerFilterDialog.css | 13 ++- .../profiler/browser/profiler.contribution.ts | 14 ++- .../profiler/browser/profilerFilterDialog.ts | 88 +++++++++++++------ .../services/profiler/common/interfaces.ts | 12 +++ .../profiler/common/profilerService.ts | 17 ++-- 5 files changed, 106 insertions(+), 38 deletions(-) diff --git a/src/sql/workbench/parts/profiler/browser/media/profilerFilterDialog.css b/src/sql/workbench/parts/profiler/browser/media/profilerFilterDialog.css index 566d115bc6..05d6a8b8d2 100644 --- a/src/sql/workbench/parts/profiler/browser/media/profilerFilterDialog.css +++ b/src/sql/workbench/parts/profiler/browser/media/profilerFilterDialog.css @@ -6,7 +6,6 @@ .profiler-filter-dialog { height: 300px; padding: 10px; - overflow-y: scroll; } .profiler-filter-clause-table { @@ -14,13 +13,21 @@ margin-bottom: 10px; } +.clause-table-container{ + max-height: 270px; + overflow-y: scroll; +} + .profiler-filter-remove-condition { width:20px; height:20px; cursor: pointer; } -.profiler-filter-add-clause-prompt { +.profiler-filter-clause-table-action { cursor: pointer; - margin: 0px 2px 0px 2px + margin-right: 10px; + padding: 2px; + text-decoration: underline; + display: inline-block; } \ No newline at end of file diff --git a/src/sql/workbench/parts/profiler/browser/profiler.contribution.ts b/src/sql/workbench/parts/profiler/browser/profiler.contribution.ts index 4a7c720316..79eef45038 100644 --- a/src/sql/workbench/parts/profiler/browser/profiler.contribution.ts +++ b/src/sql/workbench/parts/profiler/browser/profiler.contribution.ts @@ -12,7 +12,7 @@ import * as nls from 'vs/nls'; import { ProfilerInput } from 'sql/workbench/parts/profiler/browser/profilerInput'; import { ProfilerEditor } from 'sql/workbench/parts/profiler/browser/profilerEditor'; -import { PROFILER_VIEW_TEMPLATE_SETTINGS, PROFILER_SESSION_TEMPLATE_SETTINGS, IProfilerViewTemplate, IProfilerSessionTemplate, EngineType } from 'sql/workbench/services/profiler/common/interfaces'; +import { PROFILER_VIEW_TEMPLATE_SETTINGS, PROFILER_SESSION_TEMPLATE_SETTINGS, IProfilerViewTemplate, IProfilerSessionTemplate, EngineType, PROFILER_FILTER_SETTINGS } from 'sql/workbench/services/profiler/common/interfaces'; const profilerDescriptor = new EditorDescriptor( ProfilerEditor, @@ -346,14 +346,20 @@ const profilerSessionTemplateSchema: IJSONSchema = { ] }; +const profilerFiltersSchema: IJSONSchema = { + description: nls.localize('profiler.settings.Filters', "Profiler Filters"), + type: 'array' +}; + const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); -const dashboardConfig: IConfigurationNode = { +const profilerConfig: IConfigurationNode = { id: 'Profiler', type: 'object', properties: { [PROFILER_VIEW_TEMPLATE_SETTINGS]: profilerViewTemplateSchema, - [PROFILER_SESSION_TEMPLATE_SETTINGS]: profilerSessionTemplateSchema + [PROFILER_SESSION_TEMPLATE_SETTINGS]: profilerSessionTemplateSchema, + [PROFILER_FILTER_SETTINGS]: profilerFiltersSchema } }; -configurationRegistry.registerConfiguration(dashboardConfig); +configurationRegistry.registerConfiguration(profilerConfig); diff --git a/src/sql/workbench/parts/profiler/browser/profilerFilterDialog.ts b/src/sql/workbench/parts/profiler/browser/profilerFilterDialog.ts index b31e0df4ca..d8a3b8f6ed 100644 --- a/src/sql/workbench/parts/profiler/browser/profilerFilterDialog.ts +++ b/src/sql/workbench/parts/profiler/browser/profilerFilterDialog.ts @@ -22,19 +22,20 @@ 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'; -import { ProfilerFilter, ProfilerFilterClause, ProfilerFilterClauseOperator } from 'sql/workbench/services/profiler/common/interfaces'; +import { ProfilerFilter, ProfilerFilterClause, ProfilerFilterClauseOperator, IProfilerService } from 'sql/workbench/services/profiler/common/interfaces'; import { ILogService } from 'vs/platform/log/common/log'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -const ClearText: string = localize('profilerFilterDialog.clear', "Clear All"); +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 RemoveText: string = localize('profilerFilterDialog.remove', "Remove this clause"); +const SaveFilterText: string = localize('profilerFilterDialog.saveFilter', "Save Filter"); +const LoadFilterText: string = localize('profilerFilterDialog.loadFilter', "Load Filter"); +const AddClauseText: string = localize('profilerFilterDialog.addClauseText', "Add a clause"); const TitleIconClass: string = 'icon filterLabel'; const FieldText: string = localize('profilerFilterDialog.fieldColumn', "Field"); @@ -61,9 +62,9 @@ export class ProfilerFilterDialog extends Modal { private _clauseBuilder: HTMLElement; private _okButton: Button; private _cancelButton: Button; - private _clearButton: Button; private _applyButton: Button; - private _addClauseButton: Button; + private _loadFilterButton: Button; + private _saveFilterButton: Button; private _input: ProfilerInput; private _clauseRows: ClauseRowUI[] = []; @@ -75,7 +76,8 @@ export class ProfilerFilterDialog extends Modal { @ITelemetryService telemetryService: ITelemetryService, @IContextKeyService contextKeyService: IContextKeyService, @ILogService logService: ILogService, - @IContextViewService private contextViewService: IContextViewService + @IContextViewService private contextViewService: IContextViewService, + @IProfilerService private profilerService: IProfilerService ) { super('', TelemetryKeys.ProfilerFilter, telemetryService, layoutService, clipboardService, themeService, logService, contextKeyService, { isFlyout: false, hasTitleIcon: true }); } @@ -96,21 +98,22 @@ export class ProfilerFilterDialog extends Modal { 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._saveFilterButton = this.addFooterButton(SaveFilterText, () => this.saveFilter(), 'left'); + this._loadFilterButton = this.addFooterButton(LoadFilterText, () => this.loadSavedFilter(), '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)); + this._register(attachButtonStyler(this._saveFilterButton, this._themeService)); + this._register(attachButtonStyler(this._loadFilterButton, this._themeService)); } protected renderBody(container: HTMLElement) { const body = DOM.append(container, DOM.$('.profiler-filter-dialog')); - this._clauseBuilder = DOM.append(body, DOM.$('table.profiler-filter-clause-table')); + const clauseTableContainer = DOM.append(body, DOM.$('.clause-table-container')); + this._clauseBuilder = DOM.append(clauseTableContainer, DOM.$('table.profiler-filter-clause-table')); const headerRow = DOM.append(this._clauseBuilder, DOM.$('tr')); DOM.append(headerRow, DOM.$('td')).innerText = FieldText; DOM.append(headerRow, DOM.$('td')).innerText = OperatorText; @@ -121,15 +124,8 @@ export class ProfilerFilterDialog extends Modal { this.addClauseRow(true, clause.field, this.convertToOperatorString(clause.operator), clause.value); }); - const prompt = DOM.append(body, DOM.$('.profiler-filter-add-clause-prompt', { tabIndex: '0' })); - prompt.innerText = AddClausePromptText; - DOM.addDisposableListener(prompt, DOM.EventType.CLICK, () => this.addClauseRow(false)); - DOM.addStandardDisposableListener(prompt, DOM.EventType.KEY_DOWN, (e: StandardKeyboardEvent) => { - if (e.equals(KeyCode.Space) || e.equals(KeyCode.Enter)) { - this.addClauseRow(false); - e.stopPropagation(); - } - }); + this.createClauseTableActionLink(AddClauseText, body, () => { this.addClauseRow(false); }); + this.createClauseTableActionLink(ClearText, body, () => { this.handleClearButtonClick(); }); } protected layout(height?: number): void { @@ -158,6 +154,21 @@ export class ProfilerFilterDialog extends Modal { this._clauseRows = []; } + private createClauseTableActionLink(text: string, parent: HTMLElement, handler: () => void): void { + const actionLink = DOM.append(parent, DOM.$('.profiler-filter-clause-table-action', { + 'tabIndex': '0', + 'role': 'button' + })); + actionLink.innerText = text; + DOM.addDisposableListener(actionLink, DOM.EventType.CLICK, handler); + DOM.addStandardDisposableListener(actionLink, DOM.EventType.KEY_DOWN, (e: StandardKeyboardEvent) => { + if (e.equals(KeyCode.Space) || e.equals(KeyCode.Enter)) { + handler(); + e.stopPropagation(); + } + }); + } + private createSelectBox(container: HTMLElement, options: string[], selectedOption: string, ariaLabel: string): SelectBox { const dropdown = new SelectBox(options, selectedOption, this.contextViewService, undefined, { ariaLabel: ariaLabel }); dropdown.render(container); @@ -165,10 +176,29 @@ export class ProfilerFilterDialog extends Modal { return dropdown; } - private filterSession() { + private filterSession(): void { this._input.filterSession(this.getFilter()); } + private saveFilter(): void { + this.profilerService.saveFilter(this.getFilter()); + } + + private loadSavedFilter(): void { + // for now we only have one saved filter, this is enough for what user asked for so far. + const savedFilters = this.profilerService.getFilters(); + if (savedFilters && savedFilters.length > 0) { + const savedFilter = savedFilters[0]; + this._clauseRows.forEach(clause => { + clause.row.remove(); + }); + this._clauseRows = []; + savedFilter.clauses.forEach(clause => { + this.addClauseRow(true, clause.field, this.convertToOperatorString(clause.operator), clause.value); + }); + } + } + private getFilter(): ProfilerFilter { const clauses: ProfilerFilterClause[] = []; @@ -181,15 +211,20 @@ export class ProfilerFilterDialog extends Modal { }); return { + name: 'default', clauses: clauses }; } - private addClauseRow(setInitialValue: boolean, field?: string, operator?: string, value?: string): any { + private addClauseRow(setInitialValue: boolean, field?: string, operator?: string, value?: string): void { + const columns = this._input.columns.map(column => column.name); + if (field && !columns.includes(field)) { + return; + } + const row = DOM.append(this._clauseBuilder, DOM.$('tr')); const clauseId = generateUuid(); - const columns = this._input.columns.map(column => column.name); const fieldDropDown = this.createSelectBox(DOM.append(row, DOM.$('td')), columns, columns[0], FieldText); const operatorDropDown = this.createSelectBox(DOM.append(row, DOM.$('td')), Operators, Operators[0], OperatorText); @@ -201,7 +236,8 @@ export class ProfilerFilterDialog extends Modal { const removeClauseButton = DOM.append(removeCell, DOM.$('.profiler-filter-remove-condition.icon.remove', { 'tabIndex': '0', 'aria-label': RemoveText, - 'title': RemoveText + 'title': RemoveText, + 'role': 'button' })); DOM.addStandardDisposableListener(removeClauseButton, DOM.EventType.KEY_DOWN, (e: StandardKeyboardEvent) => { diff --git a/src/sql/workbench/services/profiler/common/interfaces.ts b/src/sql/workbench/services/profiler/common/interfaces.ts index 8afd3eadde..b42d7dbb1a 100644 --- a/src/sql/workbench/services/profiler/common/interfaces.ts +++ b/src/sql/workbench/services/profiler/common/interfaces.ts @@ -17,6 +17,7 @@ export type ProfilerSessionID = string; export const PROFILER_VIEW_TEMPLATE_SETTINGS = 'profiler.viewTemplates'; export const PROFILER_SESSION_TEMPLATE_SETTINGS = 'profiler.sessionTemplates'; +export const PROFILER_FILTER_SETTINGS = 'profiler.filters'; export const PROFILER_SETTINGS = 'profiler'; /** @@ -130,11 +131,21 @@ export interface IProfilerService { * @param input input object */ launchFilterSessionDialog(input: ProfilerInput): void; + /** + * Gets the filters + */ + getFilters(): Array; + /** + * Saves the filter + * @param filter filter object + */ + saveFilter(filter: ProfilerFilter): void; } export interface IProfilerSettings { viewTemplates: Array; sessionTemplates: Array; + filters: Array; } export interface IColumnViewTemplate { @@ -160,6 +171,7 @@ export interface IProfilerSessionTemplate { } export interface ProfilerFilter { + name?: string; clauses: ProfilerFilterClause[]; } diff --git a/src/sql/workbench/services/profiler/common/profilerService.ts b/src/sql/workbench/services/profiler/common/profilerService.ts index 707863a048..03060566a5 100644 --- a/src/sql/workbench/services/profiler/common/profilerService.ts +++ b/src/sql/workbench/services/profiler/common/profilerService.ts @@ -4,17 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { IConnectionManagementService, IConnectionCompletionOptions, ConnectionType, RunQueryOnConnectionMode } from 'sql/platform/connection/common/connectionManagement'; -import { - ProfilerSessionID, IProfilerSession, IProfilerService, IProfilerViewTemplate, IProfilerSessionTemplate, - PROFILER_SETTINGS, IProfilerSettings, EngineType -} from './interfaces'; +import { ProfilerSessionID, IProfilerSession, IProfilerService, IProfilerViewTemplate, IProfilerSessionTemplate, PROFILER_SETTINGS, IProfilerSettings, EngineType, ProfilerFilter, PROFILER_FILTER_SETTINGS } from './interfaces'; import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { ProfilerInput } from 'sql/workbench/parts/profiler/browser/profilerInput'; import { ProfilerColumnEditorDialog } from 'sql/workbench/parts/profiler/browser/profilerColumnEditorDialog'; import * as azdata from 'azdata'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -242,4 +239,14 @@ export class ProfilerService implements IProfilerService { let dialog = this._instantiationService.createInstance(ProfilerFilterDialog); dialog.open(input); } + + public getFilters(): ProfilerFilter[] { + const config = this._configurationService.getValue(PROFILER_FILTER_SETTINGS); + return config; + } + + public saveFilter(filter: ProfilerFilter): void { + const config = [filter]; + this._configurationService.updateValue(PROFILER_FILTER_SETTINGS, config, ConfigurationTarget.USER); + } }