diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index 9b600c3d19..fe7cec0c0b 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -366,6 +366,28 @@ "displayName": "%onprem.serverProperties.osVersion%", "value": "osVersion" } + ], + "databasesListProperties": [ + { + "displayName": "%databasesListProperties.name%", + "value": "name", + "widthWeight": 60 + }, + { + "displayName": "%databasesListProperties.status%", + "value": "state", + "widthWeight": 10 + }, + { + "displayName": "%databasesListProperties.size%", + "value": "sizeInMB", + "widthWeight": 10 + }, + { + "displayName": "%databasesListProperties.lastBackup%", + "value": "lastBackup", + "widthWeight": 20 + } ] }, { @@ -404,6 +426,23 @@ "displayName": "%cloud.serverProperties.serverEdition%", "value": "serverEdition" } + ], + "databasesListProperties": [ + { + "displayName": "%databasesListProperties.name%", + "value": "name", + "widthWeight": 60 + }, + { + "displayName": "%databasesListProperties.status%", + "value": "state", + "widthWeight": 20 + }, + { + "displayName": "%databasesListProperties.size%", + "value": "sizeInMB", + "widthWeight": 20 + } ] }, { @@ -434,6 +473,23 @@ "displayName": "%cloud.serverProperties.serverEdition%", "value": "serverEdition" } + ], + "databasesListProperties": [ + { + "displayName": "%databasesListProperties.name%", + "value": "name", + "widthWeight": 60 + }, + { + "displayName": "%databasesListProperties.status%", + "value": "state", + "widthWeight": 20 + }, + { + "displayName": "%databasesListProperties.size%", + "value": "sizeInMB", + "widthWeight": 20 + } ] } ] diff --git a/extensions/mssql/package.nls.json b/extensions/mssql/package.nls.json index 983a7e3ca1..376e2e10cc 100644 --- a/extensions/mssql/package.nls.json +++ b/extensions/mssql/package.nls.json @@ -140,5 +140,10 @@ "mssql.connectionOptions.packetSize.displayName": "Packet size", "mssql.connectionOptions.packetSize.description": "Size in bytes of the network packets used to communicate with an instance of SQL Server", "mssql.connectionOptions.typeSystemVersion.displayName": "Type system version", - "mssql.connectionOptions.typeSystemVersion.description": "Indicates which server type system the provider will expose through the DataReader" + "mssql.connectionOptions.typeSystemVersion.description": "Indicates which server type system the provider will expose through the DataReader", + "databasesListProperties.name": "Name", + "databasesListProperties.status": "Status", + "databasesListProperties.size": "Size (MB)", + "databasesListProperties.lastBackup": "Last backup", + "objectsListProperties.name": "Name" } diff --git a/src/sql/base/browser/ui/table/media/table.css b/src/sql/base/browser/ui/table/media/table.css index 978bd4097a..ce3fcde201 100644 --- a/src/sql/base/browser/ui/table/media/table.css +++ b/src/sql/base/browser/ui/table/media/table.css @@ -162,3 +162,27 @@ .hc-black .slick-header-menu { color: #FFFFFF; } + +.slick-icon-cell-content, +.slick-button-cell-content { + background-position: 7px center !important; + background-repeat: no-repeat; + background-size: 16px !important; + width: 100%; + height: 100%; + padding-left: 30px; + color: inherit !important; + display: flex; + align-items: center; +} + +.slick-icon-cell, +.slick-button-cell { + padding: 0px !important; +} + +.slick-button-cell-content { + cursor: pointer; + border-width: 0px; + padding: 0px; +} diff --git a/src/sql/base/browser/ui/table/plugins/buttonColumn.plugin.ts b/src/sql/base/browser/ui/table/plugins/buttonColumn.plugin.ts new file mode 100644 index 0000000000..6cea0549a2 --- /dev/null +++ b/src/sql/base/browser/ui/table/plugins/buttonColumn.plugin.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TextWithIconColumnDefinition } from 'sql/base/browser/ui/table/plugins/textWithIconColumn'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { Emitter } from 'vs/base/common/event'; +import { KeyCode } from 'vs/base/common/keyCodes'; + +export interface ButtonColumnDefinition extends TextWithIconColumnDefinition { +} + +export interface ButtonColumnOptions { + iconCssClass?: string; + title?: string; + width?: number; + id?: string; +} + +export interface ButtonClickEventArgs { + item: T; + position: { x: number, y: number }; +} + +export class ButtonColumn implements Slick.Plugin { + private _handler = new Slick.EventHandler(); + private _definition: ButtonColumnDefinition; + private _grid: Slick.Grid; + private _onClick = new Emitter>(); + public onClick = this._onClick.event; + + constructor(private options: ButtonColumnOptions) { + this._definition = { + id: options.id, + resizable: false, + name: '', + formatter: (row: number, cell: number, value: any, columnDef: Slick.Column, dataContext: T): string => { + return this.formatter(row, cell, value, columnDef, dataContext); + }, + width: options.width, + selectable: false, + iconCssClassField: options.iconCssClass + }; + } + + public init(grid: Slick.Grid): void { + this._grid = grid; + this._handler.subscribe(grid.onClick, (e: DOMEvent, args: Slick.OnClickEventArgs) => this.handleClick(args)); + this._handler.subscribe(grid.onKeyDown, (e: DOMEvent, args: Slick.OnKeyDownEventArgs) => this.handleKeyboardEvent(e as KeyboardEvent, args)); + } + + public destroy(): void { + this._handler.unsubscribeAll(); + } + + private handleClick(args: Slick.OnClickEventArgs): void { + if (this.shouldFireClickEvent(args.cell)) { + this._grid.setActiveCell(args.row, args.cell); + this.fireClickEvent(); + } + } + + private handleKeyboardEvent(e: KeyboardEvent, args: Slick.OnKeyDownEventArgs): void { + let event = new StandardKeyboardEvent(e); + if ((event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) && this.shouldFireClickEvent(args.cell)) { + event.stopPropagation(); + event.preventDefault(); + this.fireClickEvent(); + } + } + + public get definition(): ButtonColumnDefinition { + return this._definition; + } + + private fireClickEvent(): void { + const activeCell = this._grid.getActiveCell(); + const activeCellPosition = this._grid.getActiveCellPosition(); + if (activeCell && activeCellPosition) { + this._onClick.fire({ + item: this._grid.getDataItem(activeCell.row), + position: { + x: (activeCellPosition.left + activeCellPosition.right) / 2, + y: (activeCellPosition.bottom + activeCellPosition.top) / 2 + } + }); + } + } + + private shouldFireClickEvent(columnIndex: number): boolean { + return this._grid.getColumns()[columnIndex].id === this.definition.id; + } + + private formatter(row: number, cell: number, value: any, columnDef: Slick.Column, dataContext: T): string { + const buttonColumn = columnDef as ButtonColumnDefinition; + return `
`; + } +} diff --git a/src/sql/base/browser/ui/table/plugins/textWithIconColumn.ts b/src/sql/base/browser/ui/table/plugins/textWithIconColumn.ts new file mode 100644 index 0000000000..023c0670cf --- /dev/null +++ b/src/sql/base/browser/ui/table/plugins/textWithIconColumn.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Definition for column with icon on the left of text. + */ +export interface TextWithIconColumnDefinition extends Slick.Column { + iconCssClassField?: string; +} + +export interface TextWithIconColumnOptions { + iconCssClassField?: string; + field?: string; + width?: number; + id?: string; + resizable?: boolean; + name?: string; +} + +export class TextWithIconColumn { + + private _definition: TextWithIconColumnDefinition; + + constructor(options: TextWithIconColumnOptions) { + this._definition = { + id: options.id, + field: options.field, + resizable: options.resizable, + formatter: this.formatter, + width: options.width, + name: options.name, + iconCssClassField: options.iconCssClassField, + cssClass: 'slick-icon-cell' + }; + } + private formatter(row: number, cell: number, value: any, columnDef: Slick.Column, dataContext: T): string { + const iconColumn = columnDef as TextWithIconColumnDefinition; + return `
${value}
`; + } + + public get definition(): TextWithIconColumnDefinition { + return this._definition; + } +} diff --git a/src/sql/base/browser/ui/table/table.ts b/src/sql/base/browser/ui/table/table.ts index eee4fb5d24..14b2e9bd59 100644 --- a/src/sql/base/browser/ui/table/table.ts +++ b/src/sql/base/browser/ui/table/table.ts @@ -50,6 +50,9 @@ export class Table extends Widget implements IDisposa private _onClick = new Emitter(); public readonly onClick: Event = this._onClick.event; + private _onDoubleClick = new Emitter(); + public readonly onDoubleClick: Event = this._onDoubleClick.event; + private _onHeaderClick = new Emitter(); public readonly onHeaderClick: Event = this._onHeaderClick.event; @@ -116,6 +119,7 @@ export class Table extends Widget implements IDisposa this.mapMouseEvent(this._grid.onContextMenu, this._onContextMenu); this.mapMouseEvent(this._grid.onClick, this._onClick); this.mapMouseEvent(this._grid.onHeaderClick, this._onHeaderClick); + this.mapMouseEvent(this._grid.onDblClick, this._onDoubleClick); this._grid.onColumnsResized.subscribe(() => this._onColumnResize.fire()); } diff --git a/src/sql/workbench/contrib/backup/browser/backup.contribution.ts b/src/sql/workbench/contrib/backup/browser/backup.contribution.ts index 7726877f65..ac2fb8c3c8 100644 --- a/src/sql/workbench/contrib/backup/browser/backup.contribution.ts +++ b/src/sql/workbench/contrib/backup/browser/backup.contribution.ts @@ -9,7 +9,7 @@ import { BackupAction } from 'sql/workbench/contrib/backup/browser/backupActions import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { ManageActionContext } from 'sql/workbench/browser/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { ItemContextKey } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTreeContext'; +import { ItemContextKey } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerContext'; import { MssqlNodeContext } from 'sql/workbench/services/objectExplorer/browser/mssqlNodeContext'; import { NodeType } from 'sql/workbench/services/objectExplorer/common/nodeType'; import { mssqlProviderName } from 'sql/platform/connection/common/constants'; diff --git a/src/sql/workbench/contrib/dashboard/browser/contents/dashboardWidgetWrapper.css b/src/sql/workbench/contrib/dashboard/browser/contents/dashboardWidgetWrapper.css index 7533ddf821..ac6ac2aebe 100644 --- a/src/sql/workbench/contrib/dashboard/browser/contents/dashboardWidgetWrapper.css +++ b/src/sql/workbench/contrib/dashboard/browser/contents/dashboardWidgetWrapper.css @@ -24,7 +24,7 @@ dashboard-widget-wrapper .widgetHeader { line-height: 20px; } -dashboard-widget-wrapper .icon { +dashboard-widget-wrapper .widgetHeader .icon { display: inline-block; padding: 10px; margin-left: 5px; diff --git a/src/sql/workbench/contrib/dashboard/browser/dashboardRegistry.ts b/src/sql/workbench/contrib/dashboard/browser/dashboardRegistry.ts index 40575482b4..3a173c38a0 100644 --- a/src/sql/workbench/contrib/dashboard/browser/dashboardRegistry.ts +++ b/src/sql/workbench/contrib/dashboard/browser/dashboardRegistry.ts @@ -9,17 +9,57 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as nls from 'vs/nls'; import { IExtensionPointUser, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { ProviderProperties } from 'sql/workbench/contrib/dashboard/browser/widgets/properties/propertiesWidget.component'; import { DATABASE_DASHBOARD_TABS } from 'sql/workbench/contrib/dashboard/browser/pages/databaseDashboardPage.contribution'; import { SERVER_DASHBOARD_TABS } from 'sql/workbench/contrib/dashboard/browser/pages/serverDashboardPage.contribution'; import { DASHBOARD_CONFIG_ID, DASHBOARD_TABS_KEY_PROPERTY } from 'sql/workbench/contrib/dashboard/browser/pages/dashboardPageContribution'; import { find } from 'vs/base/common/arrays'; import { IDashboardTab, IDashboardTabGroup } from 'sql/workbench/services/dashboard/browser/common/interfaces'; +import { ILogService } from 'vs/platform/log/common/log'; export const Extensions = { DashboardContributions: 'dashboard.contributions' }; +export interface ServerInfo { + [key: string]: any; +} + +export interface PropertiesConfig { + properties: Array; +} + +export interface FlavorProperties { + flavor: string; + condition?: ConditionProperties; + conditions?: Array; + databaseProperties: Array; + serverProperties: Array; + databasesListProperties?: Array; + objectsListProperties?: Array; +} + +export interface ConditionProperties { + field: string; + operator: '==' | '<=' | '>=' | '!='; + value: string | boolean; +} + +export interface ProviderProperties { + provider: string; + flavors: Array; +} + +export interface Property { + displayName: string; + value: string; + ignore?: Array; + default?: string; +} + +export interface ObjectListViewProperty extends Property { + widthWeight?: number; +} + export interface IDashboardRegistry { registerDashboardProvider(id: string, properties: ProviderProperties): void; getProperties(id: string): ProviderProperties; @@ -29,6 +69,84 @@ export interface IDashboardRegistry { tabGroups: Array; } +export function getFlavor(serverInfo: ServerInfo, logService: ILogService, provider: string): FlavorProperties | undefined { + const dashboardRegistry = Registry.as(Extensions.DashboardContributions); + const providerProperties = dashboardRegistry.getProperties(provider); + + if (!providerProperties) { + logService.error('No property definitions found for provider', provider); + return undefined; + } + + let flavor: FlavorProperties; + + // find correct flavor + if (providerProperties.flavors.length === 1) { + flavor = providerProperties.flavors[0]; + } else if (providerProperties.flavors.length === 0) { + logService.error('No flavor definitions found for "', provider, + '. If there are not multiple flavors of this provider, add one flavor without a condition'); + return undefined; + } else { + const flavorArray = providerProperties.flavors.filter((item) => { + + // For backward compatibility we are supporting array of conditions and single condition. + // If nothing is specified, we return false. + if (item.conditions) { + let conditionResult = true; + for (let i = 0; i < item.conditions.length; i++) { + conditionResult = conditionResult && getConditionResult(logService, serverInfo, item, item.conditions[i]); + } + + return conditionResult; + } + else if (item.condition) { + return getConditionResult(logService, serverInfo, item, item.condition); + } + else { + logService.error('No condition was specified.'); + return false; + } + }); + + if (flavorArray.length === 0) { + logService.error('Could not determine flavor'); + return undefined; + } else if (flavorArray.length > 1) { + logService.error('Multiple flavors matched correctly for this provider', provider); + return undefined; + } + + flavor = flavorArray[0]; + } + return flavor; +} + +function getConditionResult(logService: ILogService, serverInfo: ServerInfo, item: FlavorProperties, conditionItem: ConditionProperties): boolean { + let condition = serverInfo[conditionItem.field]; + + // If we need to compare strings, then we should ensure that condition is string + // Otherwise tripple equals/unequals would return false values + if (typeof conditionItem.value === 'string') { + condition = condition.toString(); + } + + switch (conditionItem.operator) { + case '==': + return condition === conditionItem.value; + case '!=': + return condition !== conditionItem.value; + case '>=': + return condition >= conditionItem.value; + case '<=': + return condition <= conditionItem.value; + default: + logService.error('Could not parse operator: "', conditionItem.operator, + '" on item "', item, '"'); + return false; + } +} + class DashboardRegistry implements IDashboardRegistry { private _properties = new Map(); private _tabs = new Array(); diff --git a/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTreeActions.ts b/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerActions.ts similarity index 100% rename from src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTreeActions.ts rename to src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerActions.ts diff --git a/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTreeContext.ts b/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerContext.ts similarity index 100% rename from src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTreeContext.ts rename to src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerContext.ts diff --git a/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerFilter.ts b/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerFilter.ts new file mode 100644 index 0000000000..366ce81bdb --- /dev/null +++ b/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerFilter.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MetadataType } from 'sql/platform/connection/common/connectionManagement'; + +export class ExplorerFilter { + constructor(private context: string, private targetProperties: string[]) { + } + + public filter(filterString: string, data: Slick.SlickData[]): Slick.SlickData[] { + if (filterString) { + let metadataType: MetadataType; + if (this.context === 'database' && filterString.indexOf(':') > -1) { + const filterArray = filterString.split(':'); + + if (filterArray.length > 2) { + filterString = filterArray.slice(1, filterArray.length - 1).join(':'); + } else { + filterString = filterArray[1]; + } + + switch (filterArray[0].toLowerCase()) { + case 'v': + metadataType = MetadataType.View; + break; + case 't': + metadataType = MetadataType.Table; + break; + case 'sp': + metadataType = MetadataType.SProc; + break; + case 'f': + metadataType = MetadataType.Function; + break; + default: + break; + } + } + + return data.filter((item: Slick.SlickData) => { + if (metadataType !== undefined && item.metadataType !== metadataType) { + return false; + } + + let match = false; + for (let i = 0; i < this.targetProperties.length; i++) { + const property = this.targetProperties[i]; + const val = item[property]; + if (item[property] && typeof val === 'string' && + val.toLowerCase().indexOf(filterString.toLowerCase()) !== -1) { + match = true; + break; + } + } + return match; + }); + } else { + return data; + } + } +} diff --git a/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTable.ts b/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTable.ts new file mode 100644 index 0000000000..93816f4ff3 --- /dev/null +++ b/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTable.ts @@ -0,0 +1,214 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Router } from '@angular/router'; +import { ButtonColumn } from 'sql/base/browser/ui/table/plugins/buttonColumn.plugin'; +import { RowSelectionModel } from 'sql/base/browser/ui/table/plugins/rowSelectionModel.plugin'; +import { TextWithIconColumn } from 'sql/base/browser/ui/table/plugins/textWithIconColumn'; +import { Table } from 'sql/base/browser/ui/table/table'; +import { TableDataView } from 'sql/base/browser/ui/table/tableDataView'; +import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; +import { attachTableStyler } from 'sql/platform/theme/common/styler'; +import { BaseActionContext, ManageActionContext } from 'sql/workbench/browser/actions'; +import { getFlavor, ObjectListViewProperty } from 'sql/workbench/contrib/dashboard/browser/dashboardRegistry'; +import { ItemContextKey } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerContext'; +import { ExplorerFilter } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerFilter'; +import { ExplorerView, NameProperty } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerView'; +import { ObjectMetadataWrapper } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/objectMetadataWrapper'; +import { CommonServiceInterface } from 'sql/workbench/services/bootstrap/browser/commonServiceInterface.service'; +import * as DOM from 'vs/base/browser/dom'; +import { status } from 'vs/base/browser/ui/aria/aria'; +import { IAction } from 'vs/base/common/actions'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { Disposable } from 'vs/base/common/lifecycle'; +import * as nls from 'vs/nls'; +import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IEditorProgressService } from 'vs/platform/progress/common/progress'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; + +const ShowActionsText: string = nls.localize('dashboard.explorer.actions', "Show Actions"); +const IconClassProperty: string = 'iconClass'; +export const ConnectionProfilePropertyName: string = 'connection_profile'; + +/** + * Table for explorer widget + */ +export class ExplorerTable extends Disposable { + private readonly contextKey = new ItemContextKey(this.contextKeyService); + private _table: Table; + private _view: TableDataView; + private _actionsColumn: ButtonColumn; + private _filterStr: string; + private _explorerView: ExplorerView; + private _displayProperties: ObjectListViewProperty[]; + + constructor(private parentElement: HTMLElement, + private readonly router: Router, + private readonly context: string, + private readonly bootStrapService: CommonServiceInterface, + readonly themeService: IThemeService, + private readonly contextMenuService: IContextMenuService, + private readonly menuService: IMenuService, + private readonly contextKeyService: IContextKeyService, + private readonly progressService: IEditorProgressService, + private readonly logService: ILogService) { + super(); + this._explorerView = new ExplorerView(this.context); + this._table = new Table(parentElement, undefined, { forceFitColumns: true, rowHeight: 35 }); + this._table.setSelectionModel(new RowSelectionModel()); + this._actionsColumn = new ButtonColumn({ + id: 'actions', + iconCssClass: 'toggle-more', + title: ShowActionsText, + width: 40 + }); + const connectionInfo = this.bootStrapService.connectionManagementService.connectionInfo; + this._displayProperties = this._explorerView.getPropertyList(getFlavor(connectionInfo.serverInfo, this.logService, connectionInfo.providerId)); + const explorerFilter = new ExplorerFilter(this.context, this._displayProperties.map(p => p.value)); + this._table.registerPlugin(this._actionsColumn); + this._register(this._actionsColumn.onClick((args) => { + this.showContextMenu(args.item, args.position); + })); + this._register(this._table.onContextMenu((e) => { + if (e.cell) { + this.showContextMenu(this._view.getItem(e.cell.row), e.anchor); + } + })); + this._register(this._table.onDoubleClick((e) => { + if (e.cell) { + this.handleDoubleClick(this._view.getItem(e.cell.row)); + } + })); + this._register(attachTableStyler(this._table, themeService)); + this._view = new TableDataView(undefined, undefined, undefined, (data: Slick.SlickData[]): Slick.SlickData[] => { + return explorerFilter.filter(this._filterStr, data); + }); + this._register(this._view); + this._register(this._view.onRowCountChange(() => { + this._table.updateRowCount(); + })); + this._register(this._view.onFilterStateChange(() => { + this._table.grid.invalidateAllRows(); + this._table.updateRowCount(); + })); + } + + private showContextMenu(item: Slick.SlickData, anchor: HTMLElement | { x: number, y: number }): void { + const dataContext = (item instanceof ObjectMetadataWrapper) ? item : item[ConnectionProfilePropertyName] as ConnectionProfile; + + this.contextKey.set({ + resource: dataContext, + providerName: this.bootStrapService.connectionManagementService.connectionInfo.providerId, + isCloud: this.bootStrapService.connectionManagementService.connectionInfo.serverInfo.isCloud, + engineEdition: this.bootStrapService.connectionManagementService.connectionInfo.serverInfo.engineEditionId + }); + + let context: ManageActionContext | BaseActionContext; + + if (dataContext instanceof ObjectMetadataWrapper) { + context = { + object: dataContext, + profile: this.bootStrapService.connectionManagementService.connectionInfo.connectionProfile + }; + } else { + context = { + profile: dataContext, + uri: this.bootStrapService.getUnderlyingUri() + }; + } + + const menu = this.menuService.createMenu(MenuId.ExplorerWidgetContext, this.contextKeyService); + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => g === 'inline'); + + this.contextMenuService.showContextMenu({ + getAnchor: () => anchor, + getActions: () => result.secondary, + getActionsContext: () => context + }); + } + + private handleDoubleClick(item: Slick.SlickData): void { + if (this.context === 'server') { + this.progressService.showWhile(this.bootStrapService.connectionManagementService.changeDatabase(item[NameProperty]).then(result => { + this.router.navigate(['database-dashboard']).catch(onUnexpectedError); + })); + } + } + + public filter(filterStr: string): void { + this._filterStr = filterStr; + this._view.clearFilter(); + this._view.filter(); + const count = this._view.getItems().length; + let message: string; + if (count === 0) { + message = nls.localize('explorerSearchNoMatchResultMessage', "No matching item found"); + } else if (count === 1) { + message = nls.localize('explorerSearchSingleMatchResultMessage', "Filtered search list to 1 item"); + } else { + message = nls.localize('explorerSearchMatchResultMessage', "Filtered search list to {0} items", count); + } + status(message); + } + + public layout(): void { + this._table.layout(new DOM.Dimension( + DOM.getContentWidth(this.parentElement), + DOM.getContentHeight(this.parentElement))); + this._table.columns = this.columnDefinitions; + } + + public setData(items: Slick.SlickData[]): void { + this._table.columns = this.columnDefinitions; + this._view.clear(); + this._view.clearFilter(); + items.forEach(item => { + item[IconClassProperty] = this._explorerView.getIconClass(item); + }); + this._table.setData(this._view); + this._view.push(items); + } + + private get columnDefinitions(): Slick.Column[] { + const totalWidth = DOM.getContentWidth(this.parentElement); + let totalColumnWidthWeight: number = 0; + this._displayProperties.forEach(p => { + if (p.widthWeight) { + totalColumnWidthWeight += p.widthWeight; + } + }); + + const columns: Slick.Column[] = this._displayProperties.map(property => { + const columnWidth = property.widthWeight ? totalWidth * (property.widthWeight / totalColumnWidthWeight) : undefined; + if (property.value === NameProperty) { + const nameColumn = new TextWithIconColumn({ + id: property.value, + iconCssClassField: IconClassProperty, + width: columnWidth, + field: property.value, + name: property.displayName + }); + return nameColumn.definition; + } else { + return >{ + id: property.value, + field: property.value, + name: property.displayName, + width: columnWidth + }; + } + }); + columns.push(this._actionsColumn.definition); + return columns; + } +} + diff --git a/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTree.ts b/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTree.ts deleted file mode 100644 index babc9a48a8..0000000000 --- a/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTree.ts +++ /dev/null @@ -1,318 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Router } from '@angular/router'; - -import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; -import { MetadataType } from 'sql/platform/connection/common/connectionManagement'; -import { SingleConnectionManagementService, CommonServiceInterface } from 'sql/workbench/services/bootstrap/browser/commonServiceInterface.service'; -import { ManageActionContext, BaseActionContext } from 'sql/workbench/browser/actions'; - -import * as tree from 'vs/base/parts/tree/browser/tree'; -import * as TreeDefaults from 'vs/base/parts/tree/browser/treeDefaults'; -import { IMouseEvent } from 'vs/base/browser/mouseEvent'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IAction } from 'vs/base/common/actions'; -import { generateUuid } from 'vs/base/common/uuid'; -import { $ } from 'vs/base/browser/dom'; -import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; -import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { IEditorProgressService } from 'vs/platform/progress/common/progress'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { ItemContextKey } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTreeContext'; -import { ObjectMetadataWrapper } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/objectMetadataWrapper'; -import { onUnexpectedError } from 'vs/base/common/errors'; - -export declare type TreeResource = IConnectionProfile | ObjectMetadataWrapper; - -// Empty class just for tree input -export class ExplorerModel { - public static readonly id = generateUuid(); -} - -export class ExplorerController extends TreeDefaults.DefaultController { - private readonly contextKey = new ItemContextKey(this.contextKeyService); - - constructor( - // URI for the dashboard for managing, should look into some other way of doing this - private _uri: string, - private _connectionService: SingleConnectionManagementService, - private _router: Router, - private readonly bootStrapService: CommonServiceInterface, - @IContextMenuService private readonly contextMenuService: IContextMenuService, - @IEditorProgressService private readonly progressService: IEditorProgressService, - @IMenuService private readonly menuService: IMenuService, - @IContextKeyService private readonly contextKeyService: IContextKeyService - ) { - super(); - } - - protected onLeftClick(tree: tree.ITree, element: TreeResource, event: IMouseEvent, origin: string = 'mouse'): boolean { - const payload = { origin: origin }; - const isDoubleClick = (origin === 'mouse' && event.detail === 2); - // Cancel Event - const isMouseDown = event && event.browserEvent && event.browserEvent.type === 'mousedown'; - - if (!isMouseDown) { - event.preventDefault(); // we cannot preventDefault onMouseDown because this would break DND otherwise - } - - event.stopPropagation(); - - tree.setFocus(element, payload); - - if (!(element instanceof ObjectMetadataWrapper) && isDoubleClick) { - event.preventDefault(); // focus moves to editor, we need to prevent default - this.handleItemDoubleClick(element); - } else { - tree.setFocus(element, payload); - tree.setSelection([element], payload); - } - - return true; - } - - public onContextMenu(tree: tree.ITree, element: TreeResource, event: tree.ContextMenuEvent): boolean { - this.contextKey.set({ - resource: element, - providerName: this.bootStrapService.connectionManagementService.connectionInfo.providerId, - isCloud: this.bootStrapService.connectionManagementService.connectionInfo.serverInfo.isCloud, - engineEdition: this.bootStrapService.connectionManagementService.connectionInfo.serverInfo.engineEditionId - }); - - let context: ManageActionContext | BaseActionContext; - - if (element instanceof ObjectMetadataWrapper) { - context = { - object: element, - profile: this._connectionService.connectionInfo.connectionProfile - }; - } else { - context = { - profile: element, - uri: this._uri - }; - } - - const menu = this.menuService.createMenu(MenuId.ExplorerWidgetContext, this.contextKeyService); - const primary: IAction[] = []; - const secondary: IAction[] = []; - const result = { primary, secondary }; - createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => g === 'inline'); - - this.contextMenuService.showContextMenu({ - getAnchor: () => { return { x: event.posx, y: event.posy }; }, - getActions: () => result.secondary, - getActionsContext: () => context - }); - - return true; - } - - private handleItemDoubleClick(element: IConnectionProfile): void { - this.progressService.showWhile(this._connectionService.changeDatabase(element.databaseName).then(result => { - this._router.navigate(['database-dashboard']).catch(onUnexpectedError); - })); - } - - protected onEnter(tree: tree.ITree, event: IKeyboardEvent): boolean { - const result = super.onEnter(tree, event); - if (result) { - const focus = tree.getFocus(); - if (focus && !(focus instanceof ObjectMetadataWrapper)) { - this._connectionService.changeDatabase(focus.databaseName).then(result => { - this._router.navigate(['database-dashboard']).catch(onUnexpectedError); - }); - } - } - return result; - } -} - -export class ExplorerDataSource implements tree.IDataSource { - private _data: TreeResource[]; - - public getId(tree: tree.ITree, element: TreeResource | ExplorerModel): string { - if (element instanceof ObjectMetadataWrapper) { - return element.urn || element.schema + element.name; - } else if (element instanceof ExplorerModel) { - return ExplorerModel.id; - } else { - return (element as IConnectionProfile).getOptionsKey(); - } - } - - public hasChildren(tree: tree.ITree, element: TreeResource | ExplorerModel): boolean { - if (element instanceof ExplorerModel) { - return true; - } else { - return false; - } - } - - public getChildren(tree: tree.ITree, element: TreeResource | ExplorerModel): Promise { - if (element instanceof ExplorerModel) { - return Promise.resolve(this._data); - } else { - return Promise.resolve(undefined); - } - } - - public getParent(tree: tree.ITree, element: TreeResource | ExplorerModel): Promise { - if (element instanceof ExplorerModel) { - return Promise.resolve(undefined); - } else { - return Promise.resolve(new ExplorerModel()); - } - } - - public set data(data: TreeResource[]) { - this._data = data; - } -} - -enum TEMPLATEIDS { - profile = 'profile', - object = 'object' -} - -export interface IListTemplate { - icon?: HTMLElement; - label: HTMLElement; -} - -export class ExplorerRenderer implements tree.IRenderer { - public getHeight(tree: tree.ITree, element: TreeResource): number { - return 22; - } - - public getTemplateId(tree: tree.ITree, element: TreeResource): string { - if (element instanceof ObjectMetadataWrapper) { - return TEMPLATEIDS.object; - } else { - return TEMPLATEIDS.profile; - } - } - - public renderTemplate(tree: tree.ITree, templateId: string, container: HTMLElement): IListTemplate { - const row = $('.list-row'); - const label = $('.label'); - - let icon: HTMLElement; - if (templateId === TEMPLATEIDS.object) { - icon = $('div'); - } else { - icon = $('.icon.database'); - } - - row.appendChild(icon); - row.appendChild(label); - container.appendChild(row); - - return { icon, label }; - } - - public renderElement(tree: tree.ITree, element: TreeResource, templateId: string, templateData: IListTemplate): void { - if (element instanceof ObjectMetadataWrapper) { - switch (element.metadataType) { - case MetadataType.Function: - templateData.icon.className = 'icon scalarvaluedfunction'; - break; - case MetadataType.SProc: - templateData.icon.className = 'icon storedprocedure'; - break; - case MetadataType.Table: - templateData.icon.className = 'icon table'; - break; - case MetadataType.View: - templateData.icon.className = 'icon view'; - break; - } - templateData.label.innerText = element.schema + '.' + element.name; - } else { - templateData.label.innerText = element.databaseName; - } - templateData.label.title = templateData.label.innerText; - } - - public disposeTemplate(tree: tree.ITree, templateId: string, templateData: IListTemplate): void { - // no op - } - -} - -export class ExplorerFilter implements tree.IFilter { - private _filterString: string; - - public isVisible(tree: tree.ITree, element: TreeResource): boolean { - if (element instanceof ObjectMetadataWrapper) { - return this._doIsVisibleObjectMetadata(element); - } else { - return this._doIsVisibleConnectionProfile(element); - } - } - - // apply filter to databasename of the profile - private _doIsVisibleConnectionProfile(element: IConnectionProfile): boolean { - if (!this._filterString) { - return true; - } - const filterString = this._filterString.trim().toLowerCase(); - return element.databaseName.toLowerCase().indexOf(filterString) > -1; - } - - // apply filter for objectmetadatawrapper - // could be improved by pre-processing the filter string - private _doIsVisibleObjectMetadata(element: ObjectMetadataWrapper): boolean { - if (!this._filterString) { - return true; - } - // freeze filter string for edge cases - let filterString = this._filterString.trim().toLowerCase(); - - // determine if a filter is applied - let metadataType: MetadataType; - - if (filterString.indexOf(':') > -1) { - const filterArray = filterString.split(':'); - - if (filterArray.length > 2) { - filterString = filterArray.slice(1, filterArray.length - 1).join(':'); - } else { - filterString = filterArray[1]; - } - - switch (filterArray[0].toLowerCase()) { - case 'v': - metadataType = MetadataType.View; - break; - case 't': - metadataType = MetadataType.Table; - break; - case 'sp': - metadataType = MetadataType.SProc; - break; - case 'f': - metadataType = MetadataType.Function; - break; - case 'a': - return true; - default: - break; - } - } - - if (metadataType !== undefined) { - return element.metadataType === metadataType && (element.schema + '.' + element.name).toLowerCase().indexOf(filterString) > -1; - } else { - return (element.schema + '.' + element.name).toLowerCase().indexOf(filterString) > -1; - } - } - - public set filterString(val: string) { - this._filterString = val; - } -} diff --git a/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerView.ts b/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerView.ts new file mode 100644 index 0000000000..35688c75b1 --- /dev/null +++ b/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerView.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MetadataType } from 'sql/platform/connection/common/connectionManagement'; +import { FlavorProperties, ObjectListViewProperty } from 'sql/workbench/contrib/dashboard/browser/dashboardRegistry'; +import * as nls from 'vs/nls'; + +export const NameProperty: string = 'name'; +const NamePropertyDisplayText: string = nls.localize('dashboard.explorer.namePropertyDisplayValue', "Name"); + +export class ExplorerView { + constructor(private context: string) { + } + + public getPropertyList(flavorProperties: FlavorProperties): ObjectListViewProperty[] { + let propertyList; + if (this.context === 'database') { + if (flavorProperties && flavorProperties.objectsListProperties && flavorProperties.objectsListProperties.length > 0) { + propertyList = flavorProperties.objectsListProperties; + } else { + propertyList = [{ + displayName: NamePropertyDisplayText, + value: NameProperty, + widthWeight: 60 + }, { + displayName: nls.localize('dashboard.explorer.schemaDisplayValue', "Schema"), + value: 'schema', + widthWeight: 20 + }, { + displayName: nls.localize('dashboard.explorer.objectTypeDisplayValue', "Type"), + value: 'metadataTypeName', + widthWeight: 20 + }]; + } + } else { + if (flavorProperties && flavorProperties.databasesListProperties && flavorProperties.databasesListProperties.length > 0) { + propertyList = flavorProperties.databasesListProperties; + } else { + propertyList = [{ + displayName: NamePropertyDisplayText, + value: NameProperty, + widthWeight: 80 + }]; + } + } + return propertyList; + } + + public getIconClass(item: Slick.SlickData): string { + if (this.context === 'database') { + let iconClass: string = undefined; + switch (item.metadataType) { + case MetadataType.Function: + iconClass = 'scalarvaluedfunction'; + break; + case MetadataType.SProc: + iconClass = 'storedprocedure'; + break; + case MetadataType.Table: + iconClass = 'table'; + break; + case MetadataType.View: + iconClass = 'view'; + break; + } + return iconClass; + } else { + return 'database-colored'; + } + } +} diff --git a/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerWidget.component.html b/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerWidget.component.html index 505909e405..e6d92628ad 100644 --- a/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerWidget.component.html +++ b/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerWidget.component.html @@ -4,12 +4,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ --> -
+
-
+
-
+
diff --git a/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerWidget.component.ts b/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerWidget.component.ts index 98795c871f..c6acf9ae9d 100644 --- a/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerWidget.component.ts +++ b/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerWidget.component.ts @@ -3,31 +3,29 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/explorerWidget'; - -import { Component, Inject, forwardRef, OnInit, ViewChild, ElementRef, ChangeDetectorRef } from '@angular/core'; +import { ChangeDetectorRef, Component, ElementRef, forwardRef, Inject, OnInit, ViewChild } from '@angular/core'; import { Router } from '@angular/router'; - -import { DashboardWidget, IDashboardWidget, WidgetConfig, WIDGET_CONFIG } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget'; -import { CommonServiceInterface } from 'sql/workbench/services/bootstrap/browser/commonServiceInterface.service'; -import { ExplorerFilter, ExplorerRenderer, ExplorerDataSource, ExplorerController, ExplorerModel } from './explorerTree'; -import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; -import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; - -import { InputBox, IInputOptions } from 'vs/base/browser/ui/inputbox/inputBox'; -import { attachInputBoxStyler, attachListStyler } from 'vs/platform/theme/common/styler'; -import * as nls from 'vs/nls'; -import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; -import { getContentHeight } from 'vs/base/browser/dom'; -import { Delayer } from 'vs/base/common/async'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ScrollbarVisibility } from 'vs/base/common/scrollable'; +import { DatabaseInfo } from 'azdata'; import { subscriptionToDisposable } from 'sql/base/browser/lifecycle'; +import { DashboardWidget, IDashboardWidget, WidgetConfig, WIDGET_CONFIG } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget'; +import { ConnectionProfilePropertyName, ExplorerTable } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTable'; +import { NameProperty } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerView'; import { ObjectMetadataWrapper } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/objectMetadataWrapper'; -import { status, alert } from 'vs/base/browser/ui/aria/aria'; +import { CommonServiceInterface } from 'sql/workbench/services/bootstrap/browser/commonServiceInterface.service'; +import { alert } from 'vs/base/browser/ui/aria/aria'; +import { IInputOptions, InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; +import { Delayer } from 'vs/base/common/async'; +import { assign } from 'vs/base/common/objects'; import { isStringArray } from 'vs/base/common/types'; +import 'vs/css!./media/explorerWidget'; +import * as nls from 'vs/nls'; +import { IMenuService } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IEditorProgressService } from 'vs/platform/progress/common/progress'; +import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; +import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; @Component({ selector: 'explorer-widget', @@ -35,17 +33,8 @@ import { isStringArray } from 'vs/base/common/types'; }) export class ExplorerWidget extends DashboardWidget implements IDashboardWidget, OnInit { private _input: InputBox; - private _tree: Tree; + private _table: ExplorerTable; private _filterDelayer = new Delayer(200); - private _treeController = this.instantiationService.createInstance(ExplorerController, - this._bootstrap.getUnderlyingUri(), - this._bootstrap.connectionManagementService, - this._router, - this._bootstrap - ); - private _treeRenderer = new ExplorerRenderer(); - private _treeDataSource = new ExplorerDataSource(); - private _treeFilter = new ExplorerFilter(); @ViewChild('input') private _inputContainer: ElementRef; @ViewChild('table') private _tableContainer: ElementRef; @@ -57,8 +46,11 @@ export class ExplorerWidget extends DashboardWidget implements IDashboardWidget, @Inject(forwardRef(() => ElementRef)) private readonly _el: ElementRef, @Inject(IWorkbenchThemeService) private readonly themeService: IWorkbenchThemeService, @Inject(IContextViewService) private readonly contextViewService: IContextViewService, - @Inject(IInstantiationService) private readonly instantiationService: IInstantiationService, - @Inject(ICapabilitiesService) private readonly capabilitiesService: ICapabilitiesService, + @Inject(ILogService) private readonly logService: ILogService, + @Inject(IContextMenuService) private readonly contextMenuService: IContextMenuService, + @Inject(IMenuService) private readonly menuService: IMenuService, + @Inject(IContextKeyService) private readonly contextKeyService: IContextKeyService, + @Inject(IEditorProgressService) private readonly progressService: IEditorProgressService, @Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef ) { super(changeRef); @@ -70,60 +62,43 @@ export class ExplorerWidget extends DashboardWidget implements IDashboardWidget, ngOnInit() { this._inited = true; - const placeholderLabel = this._config.context === 'database' ? nls.localize('seachObjects', "Search by name of type (a:, t:, v:, f:, or sp:)") : nls.localize('searchDatabases', "Search databases"); + const placeholderLabel = this._config.context === 'database' ? nls.localize('seachObjects', "Search by name of type (t:, v:, f:, or sp:)") : nls.localize('searchDatabases', "Search databases"); const inputOptions: IInputOptions = { placeholder: placeholderLabel, ariaLabel: placeholderLabel }; this._input = new InputBox(this._inputContainer.nativeElement, this.contextViewService, inputOptions); - this._register(this._input.onDidChange(e => { - this._filterDelayer.trigger(async () => { - this._treeFilter.filterString = e; - await this._tree.refresh(); - const navigator = this._tree.getNavigator(); - let item = navigator.next(); - let count = 0; - while (item) { - count++; - item = navigator.next(); - } - let message: string; - if (count === 0) { - message = nls.localize('explorerSearchNoMatchResultMessage', "No matching item found"); - } else if (count === 1) { - message = nls.localize('explorerSearchSingleMatchResultMessage', "Filtered search list to 1 item"); - } else { - message = nls.localize('explorerSearchMatchResultMessage', "Filtered search list to {0} items", count); - } - status(message); - }); - })); - this._tree = new Tree(this._tableContainer.nativeElement, { - controller: this._treeController, - dataSource: this._treeDataSource, - filter: this._treeFilter, - renderer: this._treeRenderer - }, { horizontalScrollMode: ScrollbarVisibility.Auto }); - this._tree.layout(getContentHeight(this._tableContainer.nativeElement)); + this._table = new ExplorerTable(this._tableContainer.nativeElement, + this._router, + this._config.context, + this._bootstrap, + this.themeService, + this.contextMenuService, + this.menuService, + this.contextKeyService, + this.progressService, + this.logService); this._register(this._input); this._register(attachInputBoxStyler(this._input, this.themeService)); - this._register(this._tree); - this._register(attachListStyler(this._tree, this.themeService)); + this._register(this._table); + this._register(this._input.onDidChange(e => { + this._filterDelayer.trigger(async () => { + this._table.filter(e); + }); + })); } private init(): void { this.setLoadingStatus(true); - if (this._config.context === 'database') { this._register(subscriptionToDisposable(this._bootstrap.metadataService.metadata.subscribe( data => { if (data) { + const objectData = ObjectMetadataWrapper.createFromObjectMetadata(data.objectMetadata); objectData.sort(ObjectMetadataWrapper.sort); - this._treeDataSource.data = objectData; - this._tree.setInput(new ExplorerModel()); - this.setLoadingStatus(false); + this.updateTable(objectData); } }, error => { @@ -131,22 +106,26 @@ export class ExplorerWidget extends DashboardWidget implements IDashboardWidget, } ))); } else { - const currentProfile = this._bootstrap.connectionManagementService.connectionInfo.connectionProfile; this._register(subscriptionToDisposable(this._bootstrap.metadataService.databases.subscribe( data => { // Handle the case where there is no metadata service data = data || []; - if (!isStringArray(data)) { - data = data.map(item => item.options['name'] as string); + if (isStringArray(data)) { + data = data.map(item => { + const dbInfo: DatabaseInfo = { options: {} }; + dbInfo.options[NameProperty] = item; + return dbInfo; + }); } - const profileData = data.map(d => { - const profile = new ConnectionProfile(this.capabilitiesService, currentProfile); - profile.databaseName = d; - return profile; - }); - this._treeDataSource.data = profileData; - this._tree.setInput(new ExplorerModel()); - this.setLoadingStatus(false); + + const currentProfile = this._bootstrap.connectionManagementService.connectionInfo.connectionProfile; + this.updateTable(data.map(d => { + const item = assign({}, d.options); + const profile = currentProfile.toIConnectionProfile(); + profile.databaseName = d.options[NameProperty]; + item[ConnectionProfilePropertyName] = profile; + return item; + })); }, error => { this.showErrorMessage(nls.localize('dashboard.explorer.databaseError', "Unable to load databases")); @@ -155,13 +134,19 @@ export class ExplorerWidget extends DashboardWidget implements IDashboardWidget, } } + private updateTable(data: Slick.SlickData[]) { + this._table.setData(data); + this.setLoadingStatus(false); + } + public refresh(): void { + this._input.inputElement.value = ''; this.init(); } public layout(): void { if (this._inited) { - this._tree.layout(getContentHeight(this._tableContainer.nativeElement)); + this._table.layout(); } } @@ -169,4 +154,8 @@ export class ExplorerWidget extends DashboardWidget implements IDashboardWidget, (this._el.nativeElement).innerText = message; alert(message); } + + public getTableHeight(): string { + return `calc(100% - ${this._input.height}px)`; + } } diff --git a/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerWidget.contribution.ts b/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerWidget.contribution.ts index b568efae09..03d2a922a7 100644 --- a/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerWidget.contribution.ts +++ b/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerWidget.contribution.ts @@ -6,10 +6,10 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { registerDashboardWidget } from 'sql/platform/dashboard/browser/widgetRegistry'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { ExplorerManageAction } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTreeActions'; +import { ExplorerManageAction } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerActions'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ItemContextKey } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTreeContext'; +import { ItemContextKey } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerContext'; const explorerSchema: IJSONSchema = { type: 'object', diff --git a/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/media/explorerWidget.css b/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/media/explorerWidget.css index b440e98cfd..e919d65838 100644 --- a/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/media/explorerWidget.css +++ b/src/sql/workbench/contrib/dashboard/browser/widgets/explorer/media/explorerWidget.css @@ -13,3 +13,7 @@ explorer-widget .list-row { align-items: center; margin-left: -33px; } + +.explorer-widget .slick-cell { + border-right-style: none; +} diff --git a/src/sql/workbench/contrib/dashboard/browser/widgets/properties/propertiesJson.ts b/src/sql/workbench/contrib/dashboard/browser/widgets/properties/propertiesJson.ts index 408c78e79b..4b99bdde0f 100644 --- a/src/sql/workbench/contrib/dashboard/browser/widgets/properties/propertiesJson.ts +++ b/src/sql/workbench/contrib/dashboard/browser/widgets/properties/propertiesJson.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ProviderProperties } from './propertiesWidget.component'; +import { ProviderProperties } from 'sql/workbench/contrib/dashboard/browser/dashboardRegistry'; import * as nls from 'vs/nls'; import { mssqlProviderName } from 'sql/platform/connection/common/constants'; diff --git a/src/sql/workbench/contrib/dashboard/browser/widgets/properties/propertiesWidget.component.ts b/src/sql/workbench/contrib/dashboard/browser/widgets/properties/propertiesWidget.component.ts index 49d985fce7..56dbab187e 100644 --- a/src/sql/workbench/contrib/dashboard/browser/widgets/properties/propertiesWidget.component.ts +++ b/src/sql/workbench/contrib/dashboard/browser/widgets/properties/propertiesWidget.component.ts @@ -7,50 +7,17 @@ import { Component, Inject, forwardRef, ChangeDetectorRef, OnInit, ElementRef, V import { DashboardWidget, IDashboardWidget, WidgetConfig, WIDGET_CONFIG } from 'sql/workbench/contrib/dashboard/browser/core/dashboardWidget'; import { CommonServiceInterface } from 'sql/workbench/services/bootstrap/browser/commonServiceInterface.service'; import { ConnectionManagementInfo } from 'sql/platform/connection/common/connectionManagementInfo'; -import { IDashboardRegistry, Extensions as DashboardExtensions } from 'sql/workbench/contrib/dashboard/browser/dashboardRegistry'; +import { Property, PropertiesConfig, getFlavor } from 'sql/workbench/contrib/dashboard/browser/dashboardRegistry'; import { DatabaseInfo, ServerInfo } from 'azdata'; import * as types from 'vs/base/common/types'; import * as nls from 'vs/nls'; -import { Registry } from 'vs/platform/registry/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; import { subscriptionToDisposable } from 'sql/base/browser/lifecycle'; import { PropertiesContainer, PropertyItem } from 'sql/base/browser/ui/propertiesContainer/propertiesContainer.component'; import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { PROPERTIES_CONTAINER_PROPERTY_NAME, PROPERTIES_CONTAINER_PROPERTY_VALUE } from 'vs/workbench/common/theme'; -export interface PropertiesConfig { - properties: Array; -} - -export interface FlavorProperties { - flavor: string; - condition?: ConditionProperties; - conditions?: Array; - databaseProperties: Array; - serverProperties: Array; -} - -export interface ConditionProperties { - field: string; - operator: '==' | '<=' | '>=' | '!='; - value: string | boolean; -} - -export interface ProviderProperties { - provider: string; - flavors: Array; -} - -export interface Property { - displayName: string; - value: string; - ignore?: Array; - default?: string; -} - -const dashboardRegistry = Registry.as(DashboardExtensions.DashboardContributions); - @Component({ selector: 'properties-widget', template: ` @@ -111,55 +78,10 @@ export class PropertiesWidgetComponent extends DashboardWidget implements IDashb const config = this._config.widget['properties-widget']; propertyArray = config.properties; } else { - const providerProperties = dashboardRegistry.getProperties(provider as string); - - if (!providerProperties) { - this.logService.error('No property definitions found for provider', provider); + const flavor = getFlavor(this._connection.serverInfo, this.logService, provider as string); + if (!flavor) { return []; } - - let flavor: FlavorProperties; - - // find correct flavor - if (providerProperties.flavors.length === 1) { - flavor = providerProperties.flavors[0]; - } else if (providerProperties.flavors.length === 0) { - this.logService.error('No flavor definitions found for "', provider, - '. If there are not multiple flavors of this provider, add one flavor without a condition'); - return []; - } else { - const flavorArray = providerProperties.flavors.filter((item) => { - - // For backward compatibility we are supporting array of conditions and single condition. - // If nothing is specified, we return false. - if (item.conditions) { - let conditionResult = true; - for (let i = 0; i < item.conditions.length; i++) { - conditionResult = conditionResult && this.getConditionResult(item, item.conditions[i]); - } - - return conditionResult; - } - else if (item.condition) { - return this.getConditionResult(item, item.condition); - } - else { - this.logService.error('No condition was specified.'); - return false; - } - }); - - if (flavorArray.length === 0) { - this.logService.error('Could not determine flavor'); - return []; - } else if (flavorArray.length > 1) { - this.logService.error('Multiple flavors matched correctly for this provider', provider); - return []; - } - - flavor = flavorArray[0]; - } - // determine what context we should be pulling from if (this._config.context === 'database') { if (!Array.isArray(flavor.databaseProperties)) { @@ -210,31 +132,6 @@ export class PropertiesWidgetComponent extends DashboardWidget implements IDashb }); } - private getConditionResult(item: FlavorProperties, conditionItem: ConditionProperties): boolean { - let condition = this._connection.serverInfo[conditionItem.field]; - - // If we need to compare strings, then we should ensure that condition is string - // Otherwise tripple equals/unequals would return false values - if (typeof conditionItem.value === 'string') { - condition = condition.toString(); - } - - switch (conditionItem.operator) { - case '==': - return condition === conditionItem.value; - case '!=': - return condition !== conditionItem.value; - case '>=': - return condition >= conditionItem.value; - case '<=': - return condition <= conditionItem.value; - default: - this.logService.error('Could not parse operator: "', conditionItem.operator, - '" on item "', item, '"'); - return false; - } - } - private getValueOrDefault(infoObject: ServerInfo | {}, propertyValue: string, defaultVal?: any): T { let val: T = undefined; if (infoObject) { diff --git a/src/sql/workbench/contrib/dashboard/test/electron-browser/explorerWidget.component.test.ts b/src/sql/workbench/contrib/dashboard/test/electron-browser/explorerWidget.component.test.ts index a5778c7133..fa9fad4b97 100644 --- a/src/sql/workbench/contrib/dashboard/test/electron-browser/explorerWidget.component.test.ts +++ b/src/sql/workbench/contrib/dashboard/test/electron-browser/explorerWidget.component.test.ts @@ -7,6 +7,9 @@ import { MetadataType } from 'sql/platform/connection/common/connectionManagemen import * as assert from 'assert'; import { ObjectMetadataWrapper } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/objectMetadataWrapper'; +import { ExplorerFilter } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerFilter'; +import { ExplorerView } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerView'; +import { FlavorProperties } from 'sql/workbench/contrib/dashboard/browser/dashboardRegistry'; suite('Explorer Widget Tests', () => { test('Sorting dashboard search objects works correctly', () => { @@ -56,4 +59,143 @@ suite('Explorer Widget Tests', () => { let expectedList = [testMetadata[1], testMetadata[4], testMetadata[0], testMetadata[2], testMetadata[3]]; expectedList.forEach((expectedWrapper, index) => assert.equal(sortedMetadata[index], expectedWrapper)); }); + + test('Filter is only performed on the specified properties', () => { + const prop1 = 'prop1'; + const prop2 = 'prop2'; + const prop3 = 'prop3'; + const filter = new ExplorerFilter('server', [prop1, prop2]); + const obj1 = {}; + obj1[prop1] = 'abc'; + obj1[prop2] = 'def'; + obj1[prop3] = 'MatCh'; + const obj2 = {}; + obj2[prop1] = 'abc'; + obj2[prop2] = 'Match'; + obj2[prop3] = 'cd'; + const result = filter.filter('ATc', [obj1, obj2]); + assert.equal(result.length, 1, 'filtered result set should container 1 item'); + assert.equal(result[0], obj2, 'filtered result set does not match expectation'); + }); + + test('object type filter', () => { + const testMetadata = ObjectMetadataWrapper.createFromObjectMetadata( + [ + { + metadataType: MetadataType.View, + metadataTypeName: undefined, + urn: undefined, + name: 'testView', + schema: undefined + }, + { + metadataType: MetadataType.Table, + metadataTypeName: undefined, + urn: undefined, + name: 'testTable', + schema: undefined + }, + { + metadataType: MetadataType.SProc, + metadataTypeName: undefined, + urn: undefined, + name: 'testSProc', + schema: undefined + }, + { + metadataType: MetadataType.Function, + metadataTypeName: undefined, + urn: undefined, + name: 'testFunction', + schema: undefined + }, + { + metadataType: MetadataType.View, + metadataTypeName: undefined, + urn: undefined, + name: 'firstView', + schema: undefined + } + ]); + const filter = new ExplorerFilter('database', ['name']); + let result = filter.filter('t:', testMetadata); + assert.equal(result.length, 1, 'table type filter should return only 1 item'); + assert.equal(result[0]['name'], 'testTable', 'table type filter does not return correct data'); + result = filter.filter('v:', testMetadata); + assert.equal(result.length, 2, 'view type filter should return only 1 item'); + assert.equal(result[0]['name'], 'testView', 'view type filter does not return correct data'); + assert.equal(result[1]['name'], 'firstView', 'view type filter does not return correct data'); + result = filter.filter('sp:', testMetadata); + assert.equal(result.length, 1, 'stored proc type filter should return only 1 item'); + assert.equal(result[0]['name'], 'testSProc', 'stored proc type filter does not return correct data'); + result = filter.filter('f:', testMetadata); + assert.equal(result.length, 1, 'function type filter should return only 1 item'); + assert.equal(result[0]['name'], 'testFunction', 'function type filter does not return correct data'); + result = filter.filter('v:first', testMetadata); + assert.equal(result.length, 1, 'view type and name filter should return only 1 item'); + assert.equal(result[0]['name'], 'firstView', 'view type and name filter does not return correct data'); + }); + + test('Icon css class test', () => { + const serverView = new ExplorerView('server'); + let icon = serverView.getIconClass({}); + assert.equal(icon, 'database-colored'); + const databaseView = new ExplorerView('database'); + const obj = {}; + obj['metadataType'] = MetadataType.Function; + icon = databaseView.getIconClass(obj); + assert.equal(icon, 'scalarvaluedfunction'); + obj['metadataType'] = MetadataType.SProc; + icon = databaseView.getIconClass(obj); + assert.equal(icon, 'storedprocedure'); + obj['metadataType'] = MetadataType.Table; + icon = databaseView.getIconClass(obj); + assert.equal(icon, 'table'); + obj['metadataType'] = MetadataType.View; + icon = databaseView.getIconClass(obj); + assert.equal(icon, 'view'); + }); + + test('explorer property list', () => { + const serverView = new ExplorerView('server'); + const emptyFlavor: FlavorProperties = { + flavor: '', + databaseProperties: [], + serverProperties: [], + databasesListProperties: [ + ], + objectsListProperties: [] + }; + + const flavor: FlavorProperties = { + flavor: '', + databaseProperties: [], + serverProperties: [], + databasesListProperties: [ + { + displayName: '', + value: 'dbprop1' + } + ], + objectsListProperties: [{ + displayName: '', + value: 'objprop1' + }] + }; + let propertyList = serverView.getPropertyList(emptyFlavor); + assert.equal(propertyList.length, 1, 'default database property list should contain 1 property'); + assert.equal(propertyList[0].value, 'name', 'default database property list should contain name property'); + propertyList = serverView.getPropertyList(flavor); + assert.equal(propertyList.length, 1, 'database property list should contain 1 property'); + assert.equal(propertyList[0].value, 'dbprop1', 'database property list should contain dbprop1 property'); + const databaseView = new ExplorerView('database'); + propertyList = databaseView.getPropertyList(emptyFlavor); + assert.equal(propertyList.length, 3, 'default object property list should contain 3 property'); + assert.equal(propertyList[0].value, 'name', 'default object property list should contain name property'); + assert.equal(propertyList[1].value, 'schema', 'default object property list should contain schema property'); + assert.equal(propertyList[2].value, 'metadataTypeName', 'default object property list should contain metadataTypeName property'); + propertyList = databaseView.getPropertyList(flavor); + assert.equal(propertyList.length, 1, 'object property list should contain 1 property'); + assert.equal(propertyList[0].value, 'objprop1', 'object property list should contain objprop1 property'); + }); }); diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts index 71d90e3b37..41300ed99f 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -36,7 +36,7 @@ import { TreeViewItemHandleArg } from 'sql/workbench/common/views'; import { ConnectedContext } from 'azdata'; import { TreeNodeContextKey } from 'sql/workbench/services/objectExplorer/common/treeNodeContextKey'; import { ObjectExplorerActionsContext } from 'sql/workbench/services/objectExplorer/browser/objectExplorerActions'; -import { ItemContextKey } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTreeContext'; +import { ItemContextKey } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerContext'; import { ManageActionContext } from 'sql/workbench/browser/actions'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { MarkdownOutputComponent } from 'sql/workbench/contrib/notebook/browser/outputs/markdownOutput.component'; diff --git a/src/sql/workbench/contrib/query/browser/query.contribution.ts b/src/sql/workbench/contrib/query/browser/query.contribution.ts index 665e84b87c..b6d688f369 100644 --- a/src/sql/workbench/contrib/query/browser/query.contribution.ts +++ b/src/sql/workbench/contrib/query/browser/query.contribution.ts @@ -41,7 +41,7 @@ import { TreeNodeContextKey } from 'sql/workbench/services/objectExplorer/common import { MssqlNodeContext } from 'sql/workbench/services/objectExplorer/browser/mssqlNodeContext'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { ManageActionContext } from 'sql/workbench/browser/actions'; -import { ItemContextKey } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTreeContext'; +import { ItemContextKey } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerContext'; export const QueryEditorVisibleCondition = ContextKeyExpr.has(queryContext.queryEditorVisibleId); export const ResultsGridFocusCondition = ContextKeyExpr.and(ContextKeyExpr.has(queryContext.resultsVisibleId), ContextKeyExpr.has(queryContext.resultsGridFocussedId)); diff --git a/src/sql/workbench/contrib/restore/browser/restore.contribution.ts b/src/sql/workbench/contrib/restore/browser/restore.contribution.ts index 60723638df..ebbb953362 100644 --- a/src/sql/workbench/contrib/restore/browser/restore.contribution.ts +++ b/src/sql/workbench/contrib/restore/browser/restore.contribution.ts @@ -16,7 +16,7 @@ import { TreeNodeContextKey } from 'sql/workbench/services/objectExplorer/common import { ObjectExplorerActionsContext } from 'sql/workbench/services/objectExplorer/browser/objectExplorerActions'; import { ConnectionContextKey } from 'sql/workbench/services/connection/common/connectionContextKey'; import { ManageActionContext } from 'sql/workbench/browser/actions'; -import { ItemContextKey } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTreeContext'; +import { ItemContextKey } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerContext'; import { ServerInfoContextKey } from 'sql/workbench/services/connection/common/serverInfoContextKey'; import { DatabaseEngineEdition } from 'sql/workbench/api/common/sqlExtHostTypes'; diff --git a/src/sql/workbench/contrib/scripting/browser/scripting.contribution.ts b/src/sql/workbench/contrib/scripting/browser/scripting.contribution.ts index d2cb7ac5b4..cb96be4bb0 100644 --- a/src/sql/workbench/contrib/scripting/browser/scripting.contribution.ts +++ b/src/sql/workbench/contrib/scripting/browser/scripting.contribution.ts @@ -13,7 +13,7 @@ import { ConnectionContextKey } from 'sql/workbench/services/connection/common/c import { NodeType } from 'sql/workbench/services/objectExplorer/common/nodeType'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ItemContextKey } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTreeContext'; +import { ItemContextKey } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerContext'; import { EditDataAction } from 'sql/workbench/browser/scriptingActions'; import { DatabaseEngineEdition } from 'sql/workbench/api/common/sqlExtHostTypes';