table based explorer widget (#10279)

* bump sts

* extend widget container

* remove title

* wip

* refactoring

* Revert "extend widget container"

* showTitle option

* fix properties widget error

* icon column

* icon and button columns

* use textwithicon column

* icon

* refactor and filter

* context menu

* refactor

* tests

* fix hygiene

* tests

* comments
This commit is contained in:
Alan Ren
2020-05-06 13:52:20 -07:00
committed by GitHub
parent df5df38a55
commit 0ace033a6f
26 changed files with 935 additions and 520 deletions

View File

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

View File

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

View File

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

View File

@@ -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<T extends Slick.SlickData> extends TextWithIconColumnDefinition<T> {
}
export interface ButtonColumnOptions {
iconCssClass?: string;
title?: string;
width?: number;
id?: string;
}
export interface ButtonClickEventArgs<T extends Slick.SlickData> {
item: T;
position: { x: number, y: number };
}
export class ButtonColumn<T extends Slick.SlickData> implements Slick.Plugin<T> {
private _handler = new Slick.EventHandler();
private _definition: ButtonColumnDefinition<T>;
private _grid: Slick.Grid<T>;
private _onClick = new Emitter<ButtonClickEventArgs<T>>();
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<T>, dataContext: T): string => {
return this.formatter(row, cell, value, columnDef, dataContext);
},
width: options.width,
selectable: false,
iconCssClassField: options.iconCssClass
};
}
public init(grid: Slick.Grid<T>): void {
this._grid = grid;
this._handler.subscribe(grid.onClick, (e: DOMEvent, args: Slick.OnClickEventArgs<T>) => this.handleClick(args));
this._handler.subscribe(grid.onKeyDown, (e: DOMEvent, args: Slick.OnKeyDownEventArgs<T>) => this.handleKeyboardEvent(e as KeyboardEvent, args));
}
public destroy(): void {
this._handler.unsubscribeAll();
}
private handleClick(args: Slick.OnClickEventArgs<T>): void {
if (this.shouldFireClickEvent(args.cell)) {
this._grid.setActiveCell(args.row, args.cell);
this.fireClickEvent();
}
}
private handleKeyboardEvent(e: KeyboardEvent, args: Slick.OnKeyDownEventArgs<T>): 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<T> {
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<T>, dataContext: T): string {
const buttonColumn = columnDef as ButtonColumnDefinition<T>;
return `<div class="codicon icon slick-button-cell-content ${buttonColumn.iconCssClassField}" aria-label="${this.options.title}"></div>`;
}
}

View File

@@ -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<T extends Slick.SlickData> extends Slick.Column<T> {
iconCssClassField?: string;
}
export interface TextWithIconColumnOptions {
iconCssClassField?: string;
field?: string;
width?: number;
id?: string;
resizable?: boolean;
name?: string;
}
export class TextWithIconColumn<T extends Slick.SlickData> {
private _definition: TextWithIconColumnDefinition<T>;
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<T>, dataContext: T): string {
const iconColumn = columnDef as TextWithIconColumnDefinition<T>;
return `<div class="icon codicon slick-icon-cell-content ${dataContext[iconColumn.iconCssClassField]}">${value}</div>`;
}
public get definition(): TextWithIconColumnDefinition<T> {
return this._definition;
}
}

View File

@@ -50,6 +50,9 @@ export class Table<T extends Slick.SlickData> extends Widget implements IDisposa
private _onClick = new Emitter<ITableMouseEvent>();
public readonly onClick: Event<ITableMouseEvent> = this._onClick.event;
private _onDoubleClick = new Emitter<ITableMouseEvent>();
public readonly onDoubleClick: Event<ITableMouseEvent> = this._onDoubleClick.event;
private _onHeaderClick = new Emitter<ITableMouseEvent>();
public readonly onHeaderClick: Event<ITableMouseEvent> = this._onHeaderClick.event;
@@ -116,6 +119,7 @@ export class Table<T extends Slick.SlickData> 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());
}

View File

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

View File

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

View File

@@ -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<Property>;
}
export interface FlavorProperties {
flavor: string;
condition?: ConditionProperties;
conditions?: Array<ConditionProperties>;
databaseProperties: Array<Property>;
serverProperties: Array<Property>;
databasesListProperties?: Array<ObjectListViewProperty>;
objectsListProperties?: Array<ObjectListViewProperty>;
}
export interface ConditionProperties {
field: string;
operator: '==' | '<=' | '>=' | '!=';
value: string | boolean;
}
export interface ProviderProperties {
provider: string;
flavors: Array<FlavorProperties>;
}
export interface Property {
displayName: string;
value: string;
ignore?: Array<string>;
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<IDashboardTabGroup>;
}
export function getFlavor(serverInfo: ServerInfo, logService: ILogService, provider: string): FlavorProperties | undefined {
const dashboardRegistry = Registry.as<IDashboardRegistry>(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<string, ProviderProperties>();
private _tabs = new Array<IDashboardTab>();

View File

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

View File

@@ -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<Slick.SlickData>;
private _view: TableDataView<Slick.SlickData>;
private _actionsColumn: ButtonColumn<Slick.SlickData>;
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<Slick.SlickData>(parentElement, undefined, { forceFitColumns: true, rowHeight: 35 });
this._table.setSelectionModel(new RowSelectionModel());
this._actionsColumn = new ButtonColumn<Slick.SlickData>({
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<Slick.SlickData>(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<Slick.SlickData>[] {
const totalWidth = DOM.getContentWidth(this.parentElement);
let totalColumnWidthWeight: number = 0;
this._displayProperties.forEach(p => {
if (p.widthWeight) {
totalColumnWidthWeight += p.widthWeight;
}
});
const columns: Slick.Column<Slick.SlickData>[] = 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 <Slick.Column<Slick.SlickData>>{
id: property.value,
field: property.value,
name: property.displayName,
width: columnWidth
};
}
});
columns.push(this._actionsColumn.definition);
return columns;
}
}

View File

@@ -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<TreeResource[]> {
if (element instanceof ExplorerModel) {
return Promise.resolve(this._data);
} else {
return Promise.resolve(undefined);
}
}
public getParent(tree: tree.ITree, element: TreeResource | ExplorerModel): Promise<ExplorerModel> {
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;
}
}

View File

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

View File

@@ -4,12 +4,11 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div class="explorer-widget"
style="display: flex; flex-flow: column; position: absolute; height:100%; width:100%; padding: 10px; box-sizing: border-box">
<div class="explorer-widget" style="height:100%; width:100%; padding: 10px; box-sizing: border-box">
<div #input style="width: 100%"></div>
<div style="flex: 1 1 auto; position: relative">
<div [style.height]="getTableHeight()">
<loading-spinner [loading]="_loading" [loadingMessage]="_loadingMessage"
[loadingCompletedMessage]="_loadingCompletedMessage"></loading-spinner>
<div #table style="position: absolute; height: 100%; width: 100%"></div>
<div #table style="width: 100%;height: 100%;"></div>
</div>
</div>

View File

@@ -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<void>(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,
(<HTMLElement>this._el.nativeElement).innerText = message;
alert(message);
}
public getTableHeight(): string {
return `calc(100% - ${this._input.height}px)`;
}
}

View File

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

View File

@@ -13,3 +13,7 @@ explorer-widget .list-row {
align-items: center;
margin-left: -33px;
}
.explorer-widget .slick-cell {
border-right-style: none;
}

View File

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

View File

@@ -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<Property>;
}
export interface FlavorProperties {
flavor: string;
condition?: ConditionProperties;
conditions?: Array<ConditionProperties>;
databaseProperties: Array<Property>;
serverProperties: Array<Property>;
}
export interface ConditionProperties {
field: string;
operator: '==' | '<=' | '>=' | '!=';
value: string | boolean;
}
export interface ProviderProperties {
provider: string;
flavors: Array<FlavorProperties>;
}
export interface Property {
displayName: string;
value: string;
ignore?: Array<string>;
default?: string;
}
const dashboardRegistry = Registry.as<IDashboardRegistry>(DashboardExtensions.DashboardContributions);
@Component({
selector: 'properties-widget',
template: `
@@ -111,55 +78,10 @@ export class PropertiesWidgetComponent extends DashboardWidget implements IDashb
const config = <PropertiesConfig>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<T>(infoObject: ServerInfo | {}, propertyValue: string, defaultVal?: any): T {
let val: T = undefined;
if (infoObject) {

View File

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

View File

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

View File

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

View File

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

View File

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