Adding filtering dialog and action to OE (#22937)

* Adding init change

* Adding filter cache in OE

* Adding more filtering changes

* Fixed stuff with dialog

* Fixing filter

* Adding support for connecitons

* Fixed stuff

* filtering

* Fixing  date

* Filters

* Removing is filtering supported

* Removing contracts

* Fixing filters

* Fixing cache

* Adding some accessibility changes

* Reverting some more changes to pull in changes from the main

* Adding comments

* Fixing boolean operators

* Fixing stuff

* Fixing stuff

* Fixing error handling and making dialog generic

* Fixing more stuff

* Making filter a generic dialog

* adding erase icon

* removing floating promises

* Fixing compile issue

* Adding support for choice filter with different and actual value.

* Adding null checks

* Adding durability type fix

* Fixing filtering for providers that do not play well with empty filter properties
This commit is contained in:
Aasim Khan
2023-05-08 11:00:59 -07:00
committed by GitHub
parent 4c10133ae9
commit 25bc14fb25
12 changed files with 964 additions and 13 deletions

View File

@@ -1,6 +1,6 @@
{
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
"version": "4.7.0.29",
"version": "4.7.0.31",
"downloadFileNames": {
"Windows_86": "win-x86-net7.0.zip",
"Windows_64": "win-x64-net7.0.zip",

View File

@@ -1818,14 +1818,27 @@ declare module 'azdata' {
/**
* The list of choices for the filter property if the type is choice
*/
choices: string[];
choices: NodeFilterChoicePropertyValue[];
}
export interface NodeFilterChoicePropertyValue {
/**
* The value of the choice
*/
value: string;
/**
* The display name of the choice
* If not specified, the value will be used as the display name
* If specified, the display name will be used in the dropdown
*/
displayName?: string;
}
export interface NodeFilter {
/**
* The name of the filter property
*/
name: string;
displayName: string;
/**
* The operator of the filter property
*/
@@ -1833,7 +1846,7 @@ declare module 'azdata' {
/**
* The applied values of the filter property
*/
value: string | string[] | number | boolean | undefined;
value: string | string[] | number | number[] | boolean | undefined;
}
export enum NodeFilterPropertyDataType {

View File

@@ -41,7 +41,7 @@ export class TableCellEditorFactory {
};
}
public getTextEditorClass(context: any, inputType: 'text' | 'number' = 'text'): any {
public getTextEditorClass(context: any, inputType: 'text' | 'number' | 'date' = 'text', presetValue?: string): any {
const self = this;
class TextEditor extends Disposable {
private _originalValue: string;
@@ -76,6 +76,8 @@ export class TableCellEditorFactory {
this._register(self._options.onStyleChange(() => {
self._options.editorStyler(this._input);
}));
this._input.value = presetValue ?? '';
}
private async commitEdit(): Promise<void> {
@@ -96,11 +98,21 @@ export class TableCellEditorFactory {
public loadValue(item: Slick.SlickData): void {
this._originalValue = self._options.valueGetter(item, this._args.column) ?? '';
this._input.value = this._originalValue;
if (inputType === 'date') {
this._input.inputElement.valueAsDate = new Date(this._originalValue);
} else {
this._input.value = this._originalValue;
}
}
public applyValue(item: Slick.SlickData, state: string): void {
const activeCell = this._args.grid.getActiveCell();
if (inputType === 'date') {
// Usually, the date picker will return the date in the local time zone and change the date to the previous day.
// We need to convert the date to UTC time zone to avoid this behavior so that the date will be the same as the
// date picked in the date picker.
state = new Date(state).toLocaleDateString(window.navigator.language, { timeZone: 'UTC' });
}
self._options.valueSetter(context, activeCell.row, item, this._args.column, state);
}

View File

@@ -47,6 +47,7 @@ import { ActionRunner } from 'vs/base/common/actions';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { USE_ASYNC_SERVER_TREE_CONFIG } from 'sql/workbench/contrib/objectExplorer/common/serverGroup.contribution';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { FilterDialog } from 'sql/workbench/services/objectExplorer/browser/filterDialog/filterDialog';
export const CONTEXT_SERVER_TREE_VIEW = new RawContextKey<ServerTreeViewView>('serverTreeView.view', ServerTreeViewView.all);
export const CONTEXT_SERVER_TREE_HAS_CONNECTIONS = new RawContextKey<boolean>('serverTreeView.hasConnections', false);
@@ -588,6 +589,43 @@ export class ServerTreeView extends Disposable implements IServerTreeView {
}
}
public async filterElementChildren(node: TreeNode): Promise<void> {
await FilterDialog.getFiltersForProperties(
node.filterProperties,
localize('objectExplorer.filterDialogTitle', "(Preview) Filter Settings: {0}", node.getConnectionProfile().title),
localize('objectExplorer.nodePath', "Node Path: {0}", node.nodePath),
node.filters,
async (filters) => {
let errorListener;
try {
let expansionError = undefined;
errorListener = this._objectExplorerService.onUpdateObjectExplorerNodes(e => {
if (e.errorMessage) {
expansionError = e.errorMessage;
}
errorListener.dispose();
});
node.forceRefresh = true;
node.filters = filters || [];
if (this._tree instanceof AsyncServerTree) {
await this._tree.rerender(node);
}
await this.refreshElement(node);
await this._tree.expand(node);
if (expansionError) {
throw new Error(expansionError);
}
} finally {
if (errorListener) {
errorListener.dispose();
}
}
return;
},
this._instantiationService
);
}
/**
* Filter connections based on view (recent/active)
*/

View File

@@ -310,3 +310,57 @@ export class DeleteConnectionAction extends Action {
}
}
}
export class FilterChildrenAction extends Action {
public static ID = 'objectExplorer.filterChildren';
public static LABEL = localize('objectExplorer.filterChildren', "Filter (Preview)");
constructor(
id: string,
label: string,
private _node: TreeNode,
@IObjectExplorerService private _objectExplorerService: IObjectExplorerService) {
super(id, label);
}
public override async run(): Promise<void> {
await this._objectExplorerService.getServerTreeView().filterElementChildren(this._node);
}
}
export class RemoveFilterAction extends Action {
public static ID = 'objectExplorer.removeFilter';
public static LABEL = localize('objectExplorer.removeFilter', "Remove Filter");
constructor(
id: string,
label: string,
private _node: TreeNode,
private _tree: AsyncServerTree | ITree,
private _profile: ConnectionProfile | undefined,
@IObjectExplorerService private _objectExplorerService: IObjectExplorerService
) {
super(id, label);
}
public override async run(): Promise<void> {
let node = this._node;
let nodeToRefresh: ServerTreeElement = this._node;
if (this._profile) {
node = this._objectExplorerService.getObjectExplorerNode(this._profile);
nodeToRefresh = this._profile;
}
node.filters = [];
if (nodeToRefresh instanceof TreeNode) {
nodeToRefresh.forceRefresh = true;
}
if (this._tree instanceof AsyncServerTree) {
await this._tree.rerender(nodeToRefresh);
await this._tree.updateChildren(nodeToRefresh);
await this._tree.expand(nodeToRefresh);
} else {
await this._tree.refresh(nodeToRefresh);
await this._tree.expand(nodeToRefresh);
}
}
}

View File

@@ -0,0 +1,726 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./../media/filterDialog';
import { Button } from 'sql/base/browser/ui/button/button';
import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import { Modal } from 'sql/workbench/browser/modal/modal'
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfiguration';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
import { ILogService } from 'vs/platform/log/common/log';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { localize } from 'vs/nls';
import { attachModalDialogStyler } from 'sql/workbench/common/styler';
import { attachButtonStyler, attachInputBoxStyler, attachSelectBoxStyler } from 'vs/platform/theme/common/styler';
import * as DOM from 'vs/base/browser/dom';
import * as azdata from 'azdata';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { NodeFilterPropertyDataType, NodeFilterOperator } from 'sql/workbench/api/common/sqlExtHostTypes';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { Table } from 'sql/base/browser/ui/table/table';
import { TableCellEditorFactory } from 'sql/base/browser/ui/table/tableCellEditorFactory';
import { Emitter } from 'vs/base/common/event';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
import { TableHeaderRowHeight, TableRowHeight } from 'sql/workbench/browser/designer/designerTableUtil';
import { textFormatter } from 'sql/base/browser/ui/table/formatters';
import { Dropdown } from 'sql/base/browser/ui/editableDropdown/browser/dropdown';
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
import { TabbedPanel } from 'sql/base/browser/ui/panel/panel';
import { attachTableStyler } from 'sql/platform/theme/common/styler';
import { ButtonColumn } from 'sql/base/browser/ui/table/plugins/buttonColumn.plugin';
import Severity from 'vs/base/common/severity';
import { status } from 'vs/base/browser/ui/aria/aria';
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
// strings for filter dialog
const OkButtonText = localize('objectExplorer.okButtonText', "OK");
const CancelButtonText = localize('objectExplorer.cancelButtonText', "Cancel");
const ClearAllButtonText = localize('objectExplorer.clearAllButtonText', "Clear All");
const TitleIconClass: string = 'icon filterLabel';
// strings for filter operator select box
const EQUALS_SELECT_BOX = localize('objectExplorer.equalsSelectBox', "Equals");
const NOT_EQUALS_SELECT_BOX = localize('objectExplorer.notEqualsSelectBox', "Not Equals");
const LESS_THAN_SELECT_BOX = localize('objectExplorer.lessThanSelectBox', "Less Than");
const LESS_THAN_OR_EQUALS_SELECT_BOX = localize('objectExplorer.lessThanOrEqualsSelectBox', "Less Than Or Equals");
const GREATER_THAN_SELECT_BOX = localize('objectExplorer.greaterThanSelectBox', "Greater Than");
const GREATER_THAN_OR_EQUALS_SELECT_BOX = localize('objectExplorer.greaterThanOrEqualsSelectBox', "Greater Than Or Equals");
const BETWEEN_SELECT_BOX = localize('objectExplorer.betweenSelectBox', "Between");
const NOT_BETWEEN_SELECT_BOX = localize('objectExplorer.notBetweenSelectBox', "Not Between");
const CONTAINS_SELECT_BOX = localize('objectExplorer.containsSelectBox', "Contains");
const NOT_CONTAINS_SELECT_BOX = localize('objectExplorer.notContainsSelectBox', "Not Contains");
const AND_SELECT_BOX = localize('objectExplorer.andSelectBox', "And");
const IS_NULL_SELECT_BOX = localize('objectExplorer.isNullSelectBox', "Is Null");
const IS_NOT_NULL_SELECT_BOX = localize('objectExplorer.isNotNullSelectBox', "Is Not Null");
// strings for filter table column headers
const PROPERTY_NAME_COLUMN_HEADER = localize('objectExplorer.propertyNameColumnHeader', "Property");
const OPERATOR_COLUMN_HEADER = localize('objectExplorer.operatorColumnHeader', "Operator");
const VALUE_COLUMN_HEADER = localize('objectExplorer.valueColumnHeader', "Value");
const CLEAR_COLUMN_HEADER = localize('objectExplorer.clearColumnHeader', "Clear");
// strings for value select box for boolean type filters
const TRUE_SELECT_BOX = localize('objectExplorer.trueSelectBox', "True");
const FALSE_SELECT_BOX = localize('objectExplorer.falseSelectBox', "False");
function nodePathDisplayString(nodepath: string): string { return localize('objectExplorer.nodePath', "Node Path: {0}", nodepath) }
const PROPERTY_COLUMN_ID = 'property';
const OPERATOR_COLUMN_ID = 'operator';
const VALUE_COLUMN_ID = 'value';
const CLEAR_COLUMN_ID = 'clear';
export class FilterDialog extends Modal {
private _okButton?: Button;
private _cancelButton?: Button;
private _clearAllButton?: Button;
private filterTable: Table<Slick.SlickData>;
private _tableCellEditorFactory: TableCellEditorFactory;
private _onStyleChangeEventEmitter = new Emitter<void>();
private _description: HTMLElement;
private _onFilterApplied = new Emitter<azdata.NodeFilter[]>();
public readonly onFilterApplied = this._onFilterApplied.event;
private _onCloseEvent = new Emitter<void>();
public readonly onDialogClose = this._onCloseEvent.event;
constructor(
private _properties: azdata.NodeFilterProperty[],
private _filterDialogTitle: string,
private _filterDialogSubtitle: string,
private _appliedFilters: azdata.NodeFilter[],
private applyFilterAction: (filters: azdata.NodeFilter[]) => Promise<void> | undefined,
@IThemeService themeService: IThemeService,
@IAdsTelemetryService telemetryService: IAdsTelemetryService,
@ILayoutService layoutService: ILayoutService,
@IClipboardService clipboardService: IClipboardService,
@ILogService logService: ILogService,
@ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService,
@IContextKeyService contextKeyService: IContextKeyService,
@IContextViewService private readonly _contextViewProvider: IContextViewService,
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
@IQuickInputService private readonly _quickInputService: IQuickInputService,
@IErrorMessageService private _errorMessageService: IErrorMessageService,
) {
super(
'ObjectExplorerServiceDialog',
'Object Explorer Service Dialog',
telemetryService,
layoutService,
clipboardService,
themeService,
logService,
textResourcePropertiesService,
contextKeyService,
{
dialogStyle: 'normal',
hasTitleIcon: true,
hasSpinner: true
}
);
}
public open(): void {
this.render();
this.show();
this._okButton.focus();
}
public override render() {
super.render();
this.title = this._filterDialogTitle;
this.titleIconClassName = TitleIconClass;
this._register(attachModalDialogStyler(this, this._themeService));
this._okButton = this.addFooterButton(OkButtonText, async () => { await this.onApply() });
this._cancelButton = this.addFooterButton(CancelButtonText, () => { this.onClose() });
this._clearAllButton = this.addFooterButton(ClearAllButtonText, () => { this.onClearAll() }, 'left', true);
this._register(attachButtonStyler(this._okButton, this._themeService));
this._register(attachButtonStyler(this._cancelButton, this._themeService));
this._register(attachButtonStyler(this._clearAllButton, this._themeService));
}
protected renderBody(container: HTMLElement): void {
const body = DOM.append(container, DOM.$('.filter-dialog-body'));
const subtitle = DOM.append(body, DOM.$('.filter-dialog-node-path'));
subtitle.innerText = nodePathDisplayString(this._filterDialogSubtitle);
const clauseTableContainer = DOM.append(body, DOM.$('.filter-table-container'));
const filter = DOM.append(clauseTableContainer, DOM.$('.filter-table'));
this._tableCellEditorFactory = new TableCellEditorFactory(
{
valueGetter: (item, column): string => {
// if the operator is And and the operator is date, we need to get the date from the previous
// row to make it more user friendly for the user to enter the next value.
if (column.field === VALUE_COLUMN_ID && item[OPERATOR_COLUMN_ID].value === AND_SELECT_BOX) {
const index = item.filterPropertyIndex;
const tableData = this.filterTable.getData().getItems();
if (this._properties[index].type === NodeFilterPropertyDataType.Date) {
let value1 = '';
for (let i = 0; i < tableData.length; i++) {
if (tableData[i].filterPropertyIndex === index) {
value1 = tableData[i].value.value;
break;
}
}
const value2 = item[column.field].value;
return value2 === '' ? value1 : value2;
}
}
return item[column.field].value;
},
valueSetter: (context: any, row: number, item: any, column: Slick.Column<Slick.SlickData>, value: string): void => {
item[column.field].value = value;
if (column.field === 'operator') {
const index = item.filterPropertyIndex;
const nodeOperator = this._properties[index].type;
if (nodeOperator === NodeFilterPropertyDataType.Date || nodeOperator === NodeFilterPropertyDataType.Number) {
if (value === BETWEEN_SELECT_BOX || value === NOT_BETWEEN_SELECT_BOX) {
const tableData = this.filterTable.getData().getItems();
if (tableData.length > row + 1) {
if (tableData[row + 1].operator.value === AND_SELECT_BOX) {
return;
}
}
const newRow: Slick.SlickData = {
property: {
value: ''
},
operator: {
value: AND_SELECT_BOX,
values: [AND_SELECT_BOX]
},
value: {
value: '',
values: []
},
filterPropertyIndex: tableData[row].filterPropertyIndex
};
const activeElement = this.filterTable.activeCell;
tableData.splice(row + 1, 0, newRow);
dataProvider.clear();
dataProvider.push(tableData);
this.filterTable.rerenderGrid();
this.filterTable.layout(new DOM.Dimension(600, (dataProvider.getItems().length + 2) * TableRowHeight));
this.filterTable.setActiveCell(activeElement.row, activeElement.cell);
} else {
const tableData = this.filterTable.getData().getItems();
if (tableData.length > row + 1) {
if (tableData[row + 1].operator.value === AND_SELECT_BOX) {
const activeElement = this.filterTable.activeCell;
tableData.splice(row + 1, 1);
dataProvider.clear();
dataProvider.push(tableData);
this.filterTable.rerenderGrid();
this.filterTable.layout(new DOM.Dimension(600, (dataProvider.getItems().length + 2) * TableRowHeight));
this.filterTable.setActiveCell(activeElement.row, activeElement.cell);
}
}
}
}
}
},
optionsGetter: (item, column): string[] => {
return item[column.field].values;
},
editorStyler: (component) => {
this.styleComponent(component);
},
onStyleChange: this._onStyleChangeEventEmitter.event
}, this._contextViewProvider
);
const columns: Slick.Column<Slick.SlickData>[] = [
{
id: PROPERTY_COLUMN_ID,
name: PROPERTY_NAME_COLUMN_HEADER,
field: PROPERTY_COLUMN_ID,
formatter: textFormatter,
width: 180,
},
{
id: OPERATOR_COLUMN_ID,
name: OPERATOR_COLUMN_HEADER,
editor: this._tableCellEditorFactory.getDropdownEditorClass(this, [], false),
field: OPERATOR_COLUMN_ID,
formatter: textFormatter,
width: 180
},
{
id: VALUE_COLUMN_ID,
name: VALUE_COLUMN_HEADER,
width: 180,
formatter: textFormatter,
field: VALUE_COLUMN_ID
}
];
const clearValueColumn = new ButtonColumn({
id: CLEAR_COLUMN_ID,
iconCssClass: 'icon erase',
name: CLEAR_COLUMN_HEADER,
title: CLEAR_COLUMN_HEADER,
width: 60,
resizable: true,
isFontIcon: true
});
this._register(clearValueColumn.onClick(e => {
const row = e.row;
const data = this.filterTable.getData().getItems();
data[row][VALUE_COLUMN_ID].value = '';
dataProvider.clear();
dataProvider.push(data);
this.filterTable.rerenderGrid();
}));
columns.push(clearValueColumn.definition);
const tableData: Slick.SlickData[] = [];
if (!this._appliedFilters) {
this._appliedFilters = [];
}
this._properties.forEach((f, i) => {
const appliedFilter = this._appliedFilters.find(filter => filter.displayName === f.displayName);
const filterOperators = this.getOperatorsForType(f.type);
const row: Slick.SlickData = {
property: {
value: f.displayName
},
operator: {
value: appliedFilter ? this.getFilterOperatorString(appliedFilter.operator) : filterOperators[0],
values: filterOperators
},
value: {
value: appliedFilter ? this.getStringValueForFilter(f, appliedFilter.value) : '',
values: this.getChoiceValuesForFilterProperties(f)
},
filterPropertyIndex: i
};
tableData.push(row);
if (appliedFilter?.operator === NodeFilterOperator.Between || appliedFilter?.operator === NodeFilterOperator.NotBetween) {
row.value.value = this.getStringValueForFilter(f, appliedFilter.value[0]);
const andRow: Slick.SlickData = {
property: {
value: ''
},
operator: {
value: AND_SELECT_BOX,
values: [AND_SELECT_BOX]
},
value: {
value: this.getStringValueForFilter(f, appliedFilter.value[1]),
values: []
},
datatype: f.type,
filterPropertyIndex: i
};
tableData.push(andRow);
}
});
const dataProvider = new TableDataView<Slick.SlickData>();
dataProvider.push(tableData);
// Sets up the editor for the value column
(<any>dataProvider).getItemMetadata = (row: number) => {
const rowData = dataProvider.getItem(row);
const filterProperty = this._properties[rowData.filterPropertyIndex];
let editor;
if (rowData.operator.value === AND_SELECT_BOX) {
if (filterProperty.type === NodeFilterPropertyDataType.Number) {
editor = this._tableCellEditorFactory.getTextEditorClass(this, 'number');
} else if (filterProperty.type === NodeFilterPropertyDataType.Date) {
editor = this._tableCellEditorFactory.getTextEditorClass(this, 'date');
}
} else {
if (filterProperty.type === NodeFilterPropertyDataType.String) {
editor = this._tableCellEditorFactory.getTextEditorClass(this, 'text');
} else if (filterProperty.type === NodeFilterPropertyDataType.Date) {
editor = this._tableCellEditorFactory.getTextEditorClass(this, 'date');
} else if (filterProperty.type === NodeFilterPropertyDataType.Boolean) {
editor = this._tableCellEditorFactory.getDropdownEditorClass(this, [TRUE_SELECT_BOX, FALSE_SELECT_BOX], false);
} else if (filterProperty.type === NodeFilterPropertyDataType.Number) {
editor = this._tableCellEditorFactory.getTextEditorClass(this, 'number');
} else if (filterProperty.type === NodeFilterPropertyDataType.Choice) {
editor = this._tableCellEditorFactory.getDropdownEditorClass(this, this.getDropdownOptionsForChoiceProperty(<azdata.NodeFilterChoiceProperty>filterProperty), false);
}
}
return {
columns: {
value: {
editor: editor
}
}
};
}
this.filterTable = new Table(filter, this._accessibilityService, this._quickInputService, {
dataProvider: dataProvider!,
columns: columns,
}, {
editable: true,
autoEdit: true,
dataItemColumnValueExtractor: (data: any, column: Slick.Column<Slick.SlickData>): string => {
if (column.field) {
return data[column.field]?.value;
} else {
return undefined;
}
},
rowHeight: TableRowHeight,
headerRowHeight: TableHeaderRowHeight,
editorLock: new Slick.EditorLock(),
autoHeight: true,
});
this.filterTable.grid.onActiveCellChanged.subscribe((e, any) => {
if (this.filterTable.grid.getActiveCell()) {
const row = this.filterTable.grid.getActiveCell().row;
const data = this.filterTable.getData().getItems()[row];
let index = data.filterPropertyIndex;
const filterPropertyDescription = this._properties[index].description;
this._description.innerText = filterPropertyDescription;
// Announcing the filter property description for screen reader users
status(filterPropertyDescription);
}
});
this.filterTable.registerPlugin(clearValueColumn);
this.filterTable.layout(new DOM.Dimension(600, (tableData.length + 2) * TableRowHeight));
this._register(attachTableStyler(this.filterTable, this._themeService));
this._description = DOM.append(body, DOM.$('.filter-dialog-description'));
this._description.innerHTML = this._properties[0].description;
}
protected layout(height?: number): void {
// noop
}
protected override onClose() {
this.hide('close');
this._onCloseEvent.fire();
}
protected onClearAll() {
const tableAllData = this.filterTable.getData().getItems();
tableAllData.forEach((row) => {
row.value.value = '';
});
this.filterTable.rerenderGrid();
}
// This method is called when the ok button is pressed
private async onApply(): Promise<void> {
const tableData = this.filterTable.getData().getItems();
this._appliedFilters = [];
for (let i = 0; i < tableData.length; i++) {
const row = tableData[i];
let filterProperty = this._properties[row.filterPropertyIndex]
let filter: azdata.NodeFilter = {
displayName: row.property.value,
operator: this.getFilterOperatorEnum(row.operator.value),
value: this.getFilterValue(filterProperty.type, row.value.value, filterProperty),
};
const isMultipleValueFilter = filter.operator === NodeFilterOperator.Between || filter.operator === NodeFilterOperator.NotBetween;
if (isMultipleValueFilter) {
i++;
const row2 = tableData[i];
var value1 = this.getFilterValue(filterProperty.type, row.value.value, filterProperty);
var value2 = this.getFilterValue(filterProperty.type, row2.value.value, filterProperty);
filter.value = <string[] | number[]>[value1, value2];
if (filterProperty.type === NodeFilterPropertyDataType.Date) {
if (filter.value[0] === '' && filter.value[1] !== '') {
// start date not specified.
this._errorMessageService.showDialog(Severity.Error, '', localize('filterDialog.errorStartDate', "Start date is not specified."));
return;
} else if (filter.value[0] !== '' && filter.value[1] === '') {
// end date not specified.
this._errorMessageService.showDialog(Severity.Error, '', localize('filterDialog.errorEndDate', "End date is not specified."));
return;
} else if (new Date(filter.value[0]) > new Date(filter.value[1])) {
// start date is greater than end date.
this._errorMessageService.showDialog(Severity.Error, '', localize('filterDialog.errorDateRange', "Start date cannot be greater than end date."));
return;
}
} else if (filterProperty.type === NodeFilterPropertyDataType.Number) {
if (filter.value[0] === '' && filter.value[1] !== '') {
// start number not specified.
this._errorMessageService.showDialog(Severity.Error, '', localize('filterDialog.errorStartNumber', "Start number is not specified."));
return;
} else if (filter.value[0] !== '' && filter.value[1] === '') {
// end number not specified.
this._errorMessageService.showDialog(Severity.Error, '', localize('filterDialog.errorEndNumber', "End number is not specified."));
return;
} else if (Number(filter.value[0]) > Number(filter.value[1])) {
// start number is greater than end number.
this._errorMessageService.showDialog(Severity.Error, '', localize('filterDialog.errorNumberRange', "Start number cannot be greater than end number."));
return;
}
}
if (value1 !== '' && value2 !== '') {
this._appliedFilters.push(filter);
}
} else {
if (filter.value !== '') {
this._appliedFilters.push(filter);
}
}
}
this.spinner = true;
try {
if (this.applyFilterAction) {
await this.applyFilterAction(this._appliedFilters);
}
this._onFilterApplied.fire(this._appliedFilters);
this.hide('ok');
}
catch (e) {
this.spinner = false;
throw e;
}
}
// This method is called by modal when the enter button is pressed
// We override it to do nothing so that the enter button doesn't close the dialog
protected override async onAccept() {
// noop
}
private getFilterValue(
filterType: NodeFilterPropertyDataType,
value: string,
filterProperty: azdata.NodeFilterProperty
): string | number | boolean {
if (value === '') {
return '';
}
switch (filterType) {
case NodeFilterPropertyDataType.Boolean:
if (value === TRUE_SELECT_BOX) {
return true;
} else if (value === FALSE_SELECT_BOX) {
return false;
}
case NodeFilterPropertyDataType.Number:
return Number(value);
case NodeFilterPropertyDataType.Choice:
const choice = ((<azdata.NodeFilterChoiceProperty>filterProperty).choices.find(c => c.displayName === value));
if (choice) {
return choice.value;
} else {
return value;
}
case NodeFilterPropertyDataType.Date:
case NodeFilterPropertyDataType.String:
return value;
}
}
private getStringValueForFilter(filter: azdata.NodeFilterProperty, value: string | number | boolean | number[] | string[]): string {
switch (filter.type) {
case NodeFilterPropertyDataType.Boolean:
if (value === true) {
return TRUE_SELECT_BOX;
} else if (value === false) {
return FALSE_SELECT_BOX;
}
break;
case NodeFilterPropertyDataType.Number:
return value.toString();
case NodeFilterPropertyDataType.Choice:
return (<azdata.NodeFilterChoiceProperty>filter).choices.find(c => c.value === value).displayName;
case NodeFilterPropertyDataType.Date:
case NodeFilterPropertyDataType.String:
return value as string;
}
return '';
}
private getOperatorsForType(type: NodeFilterPropertyDataType): string[] {
switch (type) {
case NodeFilterPropertyDataType.String:
return [
CONTAINS_SELECT_BOX,
NOT_CONTAINS_SELECT_BOX,
EQUALS_SELECT_BOX,
NOT_EQUALS_SELECT_BOX
];
case NodeFilterPropertyDataType.Number:
return [
EQUALS_SELECT_BOX,
NOT_EQUALS_SELECT_BOX,
GREATER_THAN_SELECT_BOX,
GREATER_THAN_OR_EQUALS_SELECT_BOX,
LESS_THAN_SELECT_BOX,
LESS_THAN_OR_EQUALS_SELECT_BOX,
BETWEEN_SELECT_BOX,
NOT_BETWEEN_SELECT_BOX
];
case NodeFilterPropertyDataType.Boolean:
return [
EQUALS_SELECT_BOX,
NOT_EQUALS_SELECT_BOX
];
case NodeFilterPropertyDataType.Choice:
return [
EQUALS_SELECT_BOX,
NOT_EQUALS_SELECT_BOX
];
case NodeFilterPropertyDataType.Date:
return [
EQUALS_SELECT_BOX,
NOT_EQUALS_SELECT_BOX,
GREATER_THAN_SELECT_BOX,
GREATER_THAN_OR_EQUALS_SELECT_BOX,
LESS_THAN_SELECT_BOX,
LESS_THAN_OR_EQUALS_SELECT_BOX,
BETWEEN_SELECT_BOX,
NOT_BETWEEN_SELECT_BOX
];
}
}
private getFilterOperatorString(operator: NodeFilterOperator): string {
switch (operator) {
case NodeFilterOperator.Contains:
return CONTAINS_SELECT_BOX;
case NodeFilterOperator.NotContains:
return NOT_CONTAINS_SELECT_BOX;
case NodeFilterOperator.Equals:
return EQUALS_SELECT_BOX;
case NodeFilterOperator.NotEquals:
return NOT_EQUALS_SELECT_BOX;
case NodeFilterOperator.GreaterThan:
return GREATER_THAN_SELECT_BOX;
case NodeFilterOperator.GreaterThanOrEquals:
return GREATER_THAN_OR_EQUALS_SELECT_BOX;
case NodeFilterOperator.LessThan:
return LESS_THAN_SELECT_BOX;
case NodeFilterOperator.LessThanOrEquals:
return LESS_THAN_OR_EQUALS_SELECT_BOX;
case NodeFilterOperator.Between:
return BETWEEN_SELECT_BOX;
case NodeFilterOperator.NotBetween:
return NOT_BETWEEN_SELECT_BOX;
case NodeFilterOperator.IsNull:
return IS_NULL_SELECT_BOX;
case NodeFilterOperator.IsNotNull:
return IS_NOT_NULL_SELECT_BOX;
default:
return '';
}
}
private getFilterOperatorEnum(operator: string): NodeFilterOperator {
switch (operator) {
case CONTAINS_SELECT_BOX:
return NodeFilterOperator.Contains;
case NOT_CONTAINS_SELECT_BOX:
return NodeFilterOperator.NotContains;
case EQUALS_SELECT_BOX:
return NodeFilterOperator.Equals;
case NOT_EQUALS_SELECT_BOX:
return NodeFilterOperator.NotEquals;
case GREATER_THAN_SELECT_BOX:
return NodeFilterOperator.GreaterThan;
case GREATER_THAN_OR_EQUALS_SELECT_BOX:
return NodeFilterOperator.GreaterThanOrEquals;
case LESS_THAN_SELECT_BOX:
return NodeFilterOperator.LessThan;
case LESS_THAN_OR_EQUALS_SELECT_BOX:
return NodeFilterOperator.LessThanOrEquals;
case BETWEEN_SELECT_BOX:
return NodeFilterOperator.Between;
case NOT_BETWEEN_SELECT_BOX:
return NodeFilterOperator.NotBetween;
case TRUE_SELECT_BOX:
return NodeFilterOperator.Equals;
case FALSE_SELECT_BOX:
return NodeFilterOperator.NotEquals;
default:
return undefined;
}
}
private getChoiceValuesForFilterProperties(f: azdata.NodeFilterProperty): string[] {
switch (f.type) {
case NodeFilterPropertyDataType.Boolean:
return ['', TRUE_SELECT_BOX, FALSE_SELECT_BOX];
case NodeFilterPropertyDataType.Choice:
return ['', ...this.getDropdownOptionsForChoiceProperty(<azdata.NodeFilterChoiceProperty>f)];
default:
return [];
}
}
private getDropdownOptionsForChoiceProperty(f: azdata.NodeFilterChoiceProperty): string[] {
return f.choices.map(choice => {
return choice.displayName ?? choice.value;
});
}
private styleComponent(component: TabbedPanel | InputBox | Checkbox | Table<Slick.SlickData> | SelectBox | Button | Dropdown): void {
if (component instanceof InputBox) {
this._register(attachInputBoxStyler(component, this._themeService));
} else if (component instanceof SelectBox) {
this._register(attachSelectBoxStyler(component, this._themeService));
} else if (component instanceof Table) {
this._register(attachTableStyler(component, this._themeService));
}
}
/**
* This method is used to let user apply filters on the given filters properties.
* @param properties Properties on which user can apply filters.
* @param filterDialogTitle Title of the filter dialog.
* @param filterDialogSubtile Subtitle of the filter dialog.
* @param appliedFilters Filters that are already applied so that we can prepopulate the filter dialog values.
* @param applyFilterAction Action to be performed when user clicks on apply button. We should pass this so that we can handle the spinner and error message within the dialog.
* @param instantiationService Instantiation service to create the filter dialog.
* @returns
*/
public static async getFiltersForProperties(
properties: azdata.NodeFilterProperty[],
filterDialogTitle: string,
filterDialogSubtile: string,
appliedFilters: azdata.NodeFilter[] | undefined,
applyFilterAction: (filters: azdata.NodeFilter[]) => Promise<void> | undefined,
instantiationService: IInstantiationService,
): Promise<azdata.NodeFilter[]> {
const dialog = instantiationService.createInstance(FilterDialog, properties, filterDialogTitle, filterDialogSubtile, appliedFilters, applyFilterAction);
dialog.open();
return new Promise<azdata.NodeFilter[]>((resolve, reject) => {
dialog.onFilterApplied(filters => {
resolve(filters);
});
dialog.onDialogClose(() => {
reject();
});
});
}
}

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048">
<path fill="#fff" d="M1115 1792h421v128H453L50 1516q-24-24-37-56t-13-68q0-35 13-67t38-58L1248 69l794 795-927 928zm133-1542L538 960l614 613 709-709-613-614zM933 1792l128-128-613-614-306 307q-14 14-14 35t14 35l364 365h427z" />
</svg>

After

Width:  |  Height:  |  Size: 299 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048">
<path d="M1115 1792h421v128H453L50 1516q-24-24-37-56t-13-68q0-35 13-67t38-58L1248 69l794 795-927 928zm133-1542L538 960l614 613 709-709-613-614zM933 1792l128-128-613-614-306 307q-14 14-14 35t14 35l364 365h427z" />
</svg>

After

Width:  |  Height:  |  Size: 287 B

View File

@@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.filter-dialog-body {
height: 400px;
padding: 10px;
display: flex;
flex-direction: column;
}
.filter-table-container .filter-table {
width: 100%;
margin-bottom: 10px;
}
.filter-dialog-body .filter-dialog-node-path {
padding: 10px 0px;
font-size: 13px;
font-weight: 600;
}
.filter-table-container {
max-height: 270px;
overflow-y: scroll;
flex: 1;
}
.filter-table-container table.filter-table-element {
width: 97.2%;
border-collapse: collapse;
table-layout: fixed;
}
.filter-table-container table.filter-table-element td {
width: calc((100% - 36px) / (4 - 1));
/* equal width for all but last column */
}
.filter-table-container table.filter-table-element td.delete-cell {
width: 36px;
}
.filter-action-button-container div {
padding: 5px;
}
.filter-table-element .monaco-inputbox {
height: 25px;
font-size: 11px;
}
.filter-dialog-body .filter-dialog-description {
height: 50px;
border: 1px solid;
padding: 5px;
}
.vs .icon.erase,
.hc-light .icon.erase {
background-image: url("eraseLight.svg");
width: 16px;
height: 100%;
background-repeat: no-repeat;
background-position: center;
}
.vs-dark .icon.erase,
.hc-black .icon.erase {
background-image: url("eraseDark.svg");
width: 16px;
height: 100%;
background-repeat: no-repeat;
background-position: center;
}

View File

@@ -55,6 +55,7 @@ export interface IServerTreeView {
renderBody(container: HTMLElement): Promise<void>;
layout(size: number): void;
showFilteredTree(view: ServerTreeViewView): void;
filterElementChildren(node: TreeNode): Promise<void>;
view: ServerTreeViewView;
}
@@ -484,7 +485,14 @@ export class ObjectExplorerService implements IObjectExplorerService {
this.logService.trace(`${session.sessionId}: got providers for node expansion: ${allProviders.map(p => p.providerId).join(', ')}`);
const resolveExpansion = () => {
resolve(self.mergeResults(allProviders, resultMap, node.nodePath));
const expansionResult = self.mergeResults(allProviders, resultMap, node.nodePath);
if (expansionResult.errorMessage || expansionResult.nodes.some(n => n.errorMessage)) {
this._onUpdateObjectExplorerNodes.fire({
connection: node.getConnectionProfile(),
errorMessage: expansionResult.errorMessage
});
}
resolve(expansionResult);
// Have to delete it after get all responses otherwise couldn't find session for not the first response
if (newRequest) {
delete self._sessions[session.sessionId!].nodes[node.nodePath];
@@ -531,12 +539,15 @@ export class ObjectExplorerService implements IObjectExplorerService {
});
if (newRequest) {
allProviders.forEach(provider => {
self.callExpandOrRefreshFromProvider(provider, {
let expandRequest: azdata.ExpandNodeInfo = {
sessionId: session.sessionId!,
nodePath: node.nodePath,
securityToken: session.securityToken,
filters: node.filters
}, refresh).then(isExpanding => {
};
if (node?.filters?.length > 0) {
expandRequest.filters = node.filters;
}
self.callExpandOrRefreshFromProvider(provider, expandRequest, refresh).then(isExpanding => {
if (!isExpanding) {
// The provider stated it's not going to expand the node, therefore do not need to track when merging results
let emptyResult: azdata.ObjectExplorerExpandInfo = {
@@ -613,6 +624,7 @@ export class ObjectExplorerService implements IObjectExplorerService {
});
}
finalResult.nodes = allNodes;
finalResult.errorMessage = errorMessages.join('\n');
}
return finalResult;
}

View File

@@ -10,7 +10,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import {
DisconnectConnectionAction, EditConnectionAction,
DeleteConnectionAction, RefreshAction, EditServerGroupAction, AddServerAction
DeleteConnectionAction, RefreshAction, EditServerGroupAction, AddServerAction, FilterChildrenAction, RemoveFilterAction
} from 'sql/workbench/services/objectExplorer/browser/connectionTreeAction';
import { TreeNode } from 'sql/workbench/services/objectExplorer/common/treeNode';
import { NodeType } from 'sql/workbench/services/objectExplorer/common/nodeType';
@@ -27,6 +27,8 @@ import { fillInActions } from 'vs/platform/actions/browser/menuEntryActionViewIt
import { AsyncServerTree, ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree';
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import { ILogService } from 'vs/platform/log/common/log';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { CONFIG_WORKBENCH_ENABLEPREVIEWFEATURES } from 'sql/workbench/common/constants';
/**
* Provides actions for the server tree elements
@@ -42,7 +44,8 @@ export class ServerTreeActionProvider {
@IMenuService private menuService: IMenuService,
@IContextKeyService private _contextKeyService: IContextKeyService,
@ICapabilitiesService private _capabilitiesService: ICapabilitiesService,
@ILogService private _logService: ILogService
@ILogService private _logService: ILogService,
@IConfigurationService private _configurationService: IConfigurationService
) {
}
@@ -224,8 +227,16 @@ export class ServerTreeActionProvider {
// Contribute refresh action for scriptable objects via contribution
if (!this.isScriptableObject(context)) {
actions.push(this._instantiationService.createInstance(RefreshAction, RefreshAction.ID, RefreshAction.LABEL, context.tree, context.treeNode || context.profile));
}
// Adding filter action if the node has filter properties
if (treeNode?.filterProperties?.length > 0 && this._configurationService.getValue<boolean>(CONFIG_WORKBENCH_ENABLEPREVIEWFEATURES)) {
actions.push(this._instantiationService.createInstance(FilterChildrenAction, FilterChildrenAction.ID, FilterChildrenAction.LABEL, context.treeNode));
}
// Adding remove filter action if the node has filters applied to it.
if (treeNode?.filters?.length > 0) {
actions.push(this._instantiationService.createInstance(RemoveFilterAction, RemoveFilterAction.ID, RemoveFilterAction.LABEL, context.treeNode, context.tree, undefined));
}
}
return actions;
}

View File

@@ -55,6 +55,8 @@ export class TestObjectExplorerService implements IObjectExplorerService {
public get onUpdateObjectExplorerNodes(): Event<ObjectExplorerNodeEventArgs> { throw new Error('Method not implemented'); }
public get onNodeExpandedError(): Event<NodeExpandInfoWithProviderId> { throw new Error('Method not implemented'); }
public get onSelectionOrFocusChange(): Event<void> { throw new Error('Method not implemented'); }
public async updateObjectExplorerNodes(connection: IConnectionProfile): Promise<void> { }