table column with iconcss (#13056)

This commit is contained in:
Vladimir Chernov
2020-10-31 02:23:38 +03:00
committed by GitHub
parent 2f571d868b
commit da6f800f11
11 changed files with 168 additions and 41 deletions

View File

@@ -28,20 +28,47 @@ export class AssessmentResultGrid implements vscode.Disposable {
private descriptionCaption!: azdata.TextComponent; private descriptionCaption!: azdata.TextComponent;
private asmtType!: AssessmentType; private asmtType!: AssessmentType;
private targetTypeIcon: { [targetType: number]: azdata.IconColumnCellValue };
private readonly checkIdColOrder = 4;
private readonly targetColOrder = 0; private readonly checkIdColOrder = 5;
private readonly targetColOrder = 1;
public get component(): azdata.Component { public get component(): azdata.Component {
return this.rootContainer; return this.rootContainer;
} }
public constructor(view: azdata.ModelView) { public constructor(view: azdata.ModelView, extensionContext: vscode.ExtensionContext) {
const headerCssClass = 'no-borders align-with-header'; const headerCssClass = 'no-borders align-with-header';
this.targetTypeIcon = {
[azdata.sqlAssessment.SqlAssessmentTargetType.Database]: {
icon: {
dark: extensionContext.asAbsolutePath('resources/dark/database.svg'),
light: extensionContext.asAbsolutePath('resources/light/database.svg')
},
ariaLabel: localize('databaseIconLabel', "Database Icon")
},
[azdata.sqlAssessment.SqlAssessmentTargetType.Server]: {
icon: {
dark: extensionContext.asAbsolutePath('resources/dark/server.svg'),
light: extensionContext.asAbsolutePath('resources/light/server.svg')
},
ariaLabel: localize('serverIconLabel', "Server Icon")
}
};
this.table = view.modelBuilder.table() this.table = view.modelBuilder.table()
.withProperties<azdata.TableComponentProperties>({ .withProperties<azdata.TableComponentProperties>({
data: [], data: [],
columns: [ columns: [
{
value: 'targetType',
name: '',
type: azdata.ColumnType.icon,
width: 10,
headerCssClass: headerCssClass,
toolTip: localize('asmt.column.targetType', "Target Type"),
},
{ value: LocalizedStrings.TARGET_COLUMN_NAME, headerCssClass: headerCssClass, width: 125 }, { value: LocalizedStrings.TARGET_COLUMN_NAME, headerCssClass: headerCssClass, width: 125 },
{ value: LocalizedStrings.SEVERITY_COLUMN_NAME, headerCssClass: headerCssClass, width: 100 }, { value: LocalizedStrings.SEVERITY_COLUMN_NAME, headerCssClass: headerCssClass, width: 100 },
{ value: LocalizedStrings.MESSAGE_COLUMN_NAME, headerCssClass: headerCssClass, width: 900 }, { value: LocalizedStrings.MESSAGE_COLUMN_NAME, headerCssClass: headerCssClass, width: 900 },
@@ -250,6 +277,7 @@ export class AssessmentResultGrid implements vscode.Disposable {
private convertToDataView(asmtResult: azdata.SqlAssessmentResultItem): any[] { private convertToDataView(asmtResult: azdata.SqlAssessmentResultItem): any[] {
return [ return [
this.targetTypeIcon[asmtResult.targetType],
asmtResult.targetName, asmtResult.targetName,
asmtResult.level, asmtResult.level,
this.asmtType === AssessmentType.InvokeAssessment ? asmtResult.message : asmtResult.displayName, this.asmtType === AssessmentType.InvokeAssessment ? asmtResult.message : asmtResult.displayName,

View File

@@ -131,7 +131,7 @@ export class AssessmentEngine {
let databaseListRequest = azdata.connection.listDatabases(this.connectionProfile.connectionId); let databaseListRequest = azdata.connection.listDatabases(this.connectionProfile.connectionId);
let assessmentResult: azdata.SqlAssessmentResult; let assessmentResult: azdata.SqlAssessmentResult;
if (AssessmentType.InvokeAssessment) { if (asmtType === AssessmentType.InvokeAssessment) {
TelemetryReporter.sendActionEvent(SqlAssessmentTelemetryView, SqlTelemetryActions.InvokeServerAssessment); TelemetryReporter.sendActionEvent(SqlAssessmentTelemetryView, SqlTelemetryActions.InvokeServerAssessment);
assessmentResult = await this.sqlAssessment.assessmentInvoke(this.connectionUri, azdata.sqlAssessment.SqlAssessmentTargetType.Server); assessmentResult = await this.sqlAssessment.assessmentInvoke(this.connectionUri, azdata.sqlAssessment.SqlAssessmentTargetType.Server);

View File

@@ -74,7 +74,7 @@ export class SqlAssessmentMainTab extends SqlAssessmentTab {
} }
}); });
this.resultGrid = new AssessmentResultGrid(view); this.resultGrid = new AssessmentResultGrid(view, this.extensionContext);
rootContainer.addItem(this.resultGrid.component, { rootContainer.addItem(this.resultGrid.component, {
flex: '1 1 auto', flex: '1 1 auto',
CSSStyles: { CSSStyles: {

View File

@@ -80,7 +80,7 @@ export class SqlAssessmentHistoryTab extends SqlAssessmentTab {
root.clearItems(); root.clearItems();
this.resultGrid = new AssessmentResultGrid(view); this.resultGrid = new AssessmentResultGrid(view, this.extensionContext);
this.toDispose.push(this.resultGrid); this.toDispose.push(this.resultGrid);
await view.initializeModel(title); await view.initializeModel(title);

View File

@@ -819,10 +819,25 @@ declare module 'azdata' {
} }
export interface TableComponent { export interface TableComponent {
/** /**
* Append data to an exsiting table data. * Append data to an exsiting table data.
*/ */
appendData(data: any[][]); appendData(data: any[][]);
} }
export interface IconColumnCellValue {
icon: string | vscode.Uri | { light: string | vscode.Uri; dark: string | vscode.Uri };
ariaLabel: string;
}
export enum ColumnType {
icon = 3
}
export interface TableColumn {
/**
* The text to display on the column heading. 'value' property will be used, if not specified
**/
name?: string;
}
} }

View File

@@ -36,6 +36,12 @@ export interface HyperlinkCellValue {
linkOrCommand: string | ExecuteCommandInfo; linkOrCommand: string | ExecuteCommandInfo;
} }
export interface CssIconCellValue {
iconCssClass: string,
ariaLabel: string
}
export namespace DBCellValue { export namespace DBCellValue {
export function isDBCellValue(object: any): boolean { export function isDBCellValue(object: any): boolean {
return (object !== undefined && object.displayValue !== undefined && object.isNull !== undefined); return (object !== undefined && object.displayValue !== undefined && object.isNull !== undefined);
@@ -50,6 +56,10 @@ export function isHyperlinkCellValue(obj: any | undefined): obj is HyperlinkCell
return !!(<HyperlinkCellValue>obj)?.linkOrCommand; return !!(<HyperlinkCellValue>obj)?.linkOrCommand;
} }
export function isCssIconCellValue(obj: any | undefined): obj is CssIconCellValue {
return !!(<CssIconCellValue>obj)?.iconCssClass;
}
/** /**
* Format xml field into a hyperlink and performs HTML entity encoding * Format xml field into a hyperlink and performs HTML entity encoding
*/ */
@@ -102,6 +112,14 @@ export function textFormatter(row: number | undefined, cell: any | undefined, va
return `<span title="${titleValue}" class="${cellClasses}">${valueToDisplay}</span>`; return `<span title="${titleValue}" class="${cellClasses}">${valueToDisplay}</span>`;
} }
export function iconCssFormatter(row: number | undefined, cell: any | undefined, value: any, columnDef: any | undefined, dataContext: any | undefined): string {
if (isCssIconCellValue(value)) {
return `<div role='image' aria-label="${escape(value.ariaLabel)}" class="grid-cell-value-container icon codicon slick-icon-cell-content ${value.iconCssClass}"></div>`;
}
return textFormatter(row, cell, value, columnDef, dataContext);
}
export function imageFormatter(row: number | undefined, cell: any | undefined, value: any, columnDef: any | undefined, dataContext: any | undefined): string { export function imageFormatter(row: number | undefined, cell: any | undefined, value: any, columnDef: any | undefined, dataContext: any | undefined): string {
return `<img src="${value.text}" />`; return `<img src="${value.text}" />`;
} }
@@ -129,7 +147,7 @@ export function slickGridDataItemColumnValueExtractor(value: any, columnDef: any
* In this case, for no display value ariaLabel will be set to specific string "no data available" for accessibily support for screen readers * In this case, for no display value ariaLabel will be set to specific string "no data available" for accessibily support for screen readers
* Set 'no data' label only if cell is present and has no value (so that checkbox and other custom plugins do not get 'no data' label) * Set 'no data' label only if cell is present and has no value (so that checkbox and other custom plugins do not get 'no data' label)
*/ */
export function slickGridDataItemColumnValueWithNoData(value: any, columnDef: any): { text: string; ariaLabel: string; } { export function slickGridDataItemColumnValueWithNoData(value: any, columnDef: any): { text: string; ariaLabel: string; } | CssIconCellValue {
let displayValue = value[columnDef.field]; let displayValue = value[columnDef.field];
if (typeof displayValue === 'number') { if (typeof displayValue === 'number') {
displayValue = displayValue.toString(); displayValue = displayValue.toString();
@@ -137,6 +155,11 @@ export function slickGridDataItemColumnValueWithNoData(value: any, columnDef: an
if (displayValue instanceof Array) { if (displayValue instanceof Array) {
displayValue = displayValue.toString(); displayValue = displayValue.toString();
} }
if (isCssIconCellValue(displayValue)) {
return displayValue;
}
return { return {
text: displayValue, text: displayValue,
ariaLabel: displayValue ? escape(displayValue) : ((displayValue !== undefined) ? localize("tableCell.NoDataAvailable", "no data available") : displayValue) ariaLabel: displayValue ? escape(displayValue) : ((displayValue !== undefined) ? localize("tableCell.NoDataAvailable", "no data available") : displayValue)

View File

@@ -12,29 +12,32 @@ export interface TextWithIconColumnDefinition<T extends Slick.SlickData> extends
iconCssClassField?: string; iconCssClassField?: string;
} }
export interface TextWithIconColumnOptions { export interface TextWithIconColumnOptions<T extends Slick.SlickData> {
iconCssClassField?: string; iconCssClassField?: string;
field?: string; field?: string;
width?: number; width?: number;
id?: string; id?: string;
resizable?: boolean; resizable?: boolean;
name?: string; name?: string;
headerCssClass?: string;
formatter?: Slick.Formatter<T>
} }
export class TextWithIconColumn<T extends Slick.SlickData> { export class TextWithIconColumn<T extends Slick.SlickData> {
private _definition: TextWithIconColumnDefinition<T>; private _definition: TextWithIconColumnDefinition<T>;
constructor(options: TextWithIconColumnOptions) { constructor(options: TextWithIconColumnOptions<T>) {
this._definition = { this._definition = {
id: options.id, id: options.id,
field: options.field, field: options.field,
resizable: options.resizable, resizable: options.resizable,
formatter: this.formatter, formatter: options.formatter ?? this.formatter,
width: options.width, width: options.width,
name: options.name, name: options.name,
iconCssClassField: options.iconCssClassField, iconCssClassField: options.iconCssClassField,
cssClass: 'slick-icon-cell' cssClass: 'slick-icon-cell',
headerCssClass: options.headerCssClass
}; };
} }
private formatter(row: number, cell: number, value: any, columnDef: Slick.Column<T>, dataContext: T): string { private formatter(row: number, cell: number, value: any, columnDef: Slick.Column<T>, dataContext: T): string {

View File

@@ -811,7 +811,8 @@ export enum SchemaObjectType {
export enum ColumnType { export enum ColumnType {
text = 0, text = 0,
checkBox = 1, checkBox = 1,
button = 2 button = 2,
icon = 3
} }
export enum ActionOnCellCheckboxCheck { export enum ActionOnCellCheckboxCheck {

View File

@@ -52,3 +52,7 @@ function getIconUri(iconPath: string | URI): URI {
return URI.revive(iconPath); return URI.revive(iconPath);
} }
} }
export function getIconKey(iconPath: IUserFriendlyIcon): string {
return getLightIconUri(iconPath).toString(true) + getDarkIconUri(iconPath)?.toString(true);
}

View File

@@ -23,12 +23,12 @@ import { CheckboxSelectColumn, ICheckboxCellActionEventArgs } from 'sql/base/bro
import { Emitter, Event as vsEvent } from 'vs/base/common/event'; import { Emitter, Event as vsEvent } from 'vs/base/common/event';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { slickGridDataItemColumnValueWithNoData, textFormatter } from 'sql/base/browser/ui/table/formatters'; import { slickGridDataItemColumnValueWithNoData, textFormatter, iconCssFormatter, CssIconCellValue } from 'sql/base/browser/ui/table/formatters';
import { isUndefinedOrNull } from 'vs/base/common/types'; import { isUndefinedOrNull } from 'vs/base/common/types';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType, ModelViewAction } from 'sql/platform/dashboard/browser/interfaces'; import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType, ModelViewAction } from 'sql/platform/dashboard/browser/interfaces';
import { convertSizeToNumber } from 'sql/base/browser/dom'; import { convertSizeToNumber } from 'sql/base/browser/dom';
import { ButtonColumn, ButtonClickEventArgs } from 'sql/base/browser/ui/table/plugins/buttonColumn.plugin'; import { ButtonColumn, ButtonClickEventArgs } from 'sql/base/browser/ui/table/plugins/buttonColumn.plugin';
import { createIconCssClass } from 'sql/workbench/browser/modelComponents/iconUtils'; import { IUserFriendlyIcon, createIconCssClass, getIconKey } from 'sql/workbench/browser/modelComponents/iconUtils';
import { HeaderFilter } from 'sql/base/browser/ui/table/plugins/headerFilter.plugin'; import { HeaderFilter } from 'sql/base/browser/ui/table/plugins/headerFilter.plugin';
export enum ColumnSizingMode { export enum ColumnSizingMode {
@@ -37,6 +37,13 @@ export enum ColumnSizingMode {
DataFit = 2 // columns use sizing based on cell data, horiz scroll bar present if more cells than visible in view area DataFit = 2 // columns use sizing based on cell data, horiz scroll bar present if more cells than visible in view area
} }
enum ColumnType {
text = 0,
checkBox = 1,
button = 2,
icon = 3
}
@Component({ @Component({
selector: 'modelview-table', selector: 'modelview-table',
template: ` template: `
@@ -57,6 +64,7 @@ export default class TableComponent extends ComponentBase<azdata.TableComponentP
private _onButtonClicked = new Emitter<ButtonClickEventArgs<{}>>(); private _onButtonClicked = new Emitter<ButtonClickEventArgs<{}>>();
public readonly onCheckBoxChanged: vsEvent<ICheckboxCellActionEventArgs> = this._onCheckBoxChanged.event; public readonly onCheckBoxChanged: vsEvent<ICheckboxCellActionEventArgs> = this._onCheckBoxChanged.event;
public readonly onButtonClicked: vsEvent<ButtonClickEventArgs<{}>> = this._onButtonClicked.event; public readonly onButtonClicked: vsEvent<ButtonClickEventArgs<{}>> = this._onButtonClicked.event;
private _iconCssMap: { [iconKey: string]: string } = {};
@ViewChild('table', { read: ElementRef }) private _inputContainer: ElementRef; @ViewChild('table', { read: ElementRef }) private _inputContainer: ElementRef;
constructor( constructor(
@@ -76,24 +84,17 @@ export default class TableComponent extends ComponentBase<azdata.TableComponentP
if (tableColumns) { if (tableColumns) {
let mycolumns: Slick.Column<any>[] = []; let mycolumns: Slick.Column<any>[] = [];
let index: number = 0; let index: number = 0;
(<any[]>columns).map(col => { (<any[]>columns).map(col => {
if (col.type && col.type === 1) { if (col.type === ColumnType.checkBox) {
this.createCheckBoxPlugin(col, index); this.createCheckBoxPlugin(col, index);
} } else if (col.type === ColumnType.button) {
else if (col.type && col.type === 2) {
this.createButtonPlugin(col); this.createButtonPlugin(col);
} else if (col.type === ColumnType.icon) {
mycolumns.push(TableComponent.createIconColumn(col));
} }
else if (col.value) { else if (col.value) {
mycolumns.push(<Slick.Column<any>>{ mycolumns.push(TableComponent.createTextColumn(col as azdata.TableColumn));
name: col.value,
id: col.value,
field: col.value,
width: col.width,
cssClass: col.cssClass,
headerCssClass: col.headerCssClass,
toolTip: col.toolTip,
formatter: textFormatter,
});
} else { } else {
mycolumns.push(<Slick.Column<any>>{ mycolumns.push(<Slick.Column<any>>{
name: <string>col, name: <string>col,
@@ -116,16 +117,57 @@ export default class TableComponent extends ComponentBase<azdata.TableComponentP
} }
} }
public static transformData(rows: string[][], columns: any[]): { [key: string]: string }[] { private static createIconColumn<T extends Slick.SlickData>(col: azdata.TableColumn): Slick.Column<T> {
return <Slick.Column<T>>{
name: col.name ?? col.value,
id: col.value,
field: col.value,
width: col.width,
cssClass: col.cssClass,
headerCssClass: col.headerCssClass,
toolTip: col.toolTip,
formatter: iconCssFormatter,
filterable: false
};
}
private static createTextColumn<T extends Slick.SlickData>(col: azdata.TableColumn): Slick.Column<T> {
return {
name: col.name ?? col.value,
id: col.value,
field: col.value,
width: col.width,
cssClass: col.cssClass,
headerCssClass: col.headerCssClass,
toolTip: col.toolTip,
formatter: textFormatter
};
}
public transformData(rows: (string | azdata.IconColumnCellValue)[][], columns: any[]): { [key: string]: string | CssIconCellValue }[] {
if (rows && columns) { if (rows && columns) {
return rows.map(row => { return rows.map(row => {
let object: { [key: string]: string } = {}; let object: { [key: string]: string | CssIconCellValue } = {};
if (row.forEach) { if (!Array.isArray(row)) {
return object;
}
row.forEach((val, index) => { row.forEach((val, index) => {
let columnName: string = (columns[index].value) ? columns[index].value : <string>columns[index]; let columnName: string = (columns[index].value) ? columns[index].value : <string>columns[index];
object[columnName] = val; if (isIconColumnCellValue(val)) {
}); const icon: IUserFriendlyIcon = val.icon;
const iconKey: string = getIconKey(icon);
const iconCssClass = this._iconCssMap[iconKey] ?? createIconCssClass(icon);
if (!this._iconCssMap[iconKey]) {
this._iconCssMap[iconKey] = iconCssClass;
} }
object[columnName] = { iconCssClass: iconCssClass, ariaLabel: val.ariaLabel };
} else {
object[columnName] = <string>val;
}
});
return object; return object;
}); });
} else { } else {
@@ -263,7 +305,7 @@ export default class TableComponent extends ComponentBase<azdata.TableComponentP
public setProperties(properties: { [key: string]: any; }): void { public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties); super.setProperties(properties);
this._tableData.clear(); this._tableData.clear();
this._tableData.push(TableComponent.transformData(this.data, this.columns)); this._tableData.push(this.transformData(this.data, this.columns));
this._tableColumns = this.transformColumns(this.columns); this._tableColumns = this.transformColumns(this.columns);
this._table.columns = this._tableColumns; this._table.columns = this._tableColumns;
this._table.setData(this._tableData); this._table.setData(this._tableData);
@@ -497,8 +539,12 @@ export default class TableComponent extends ComponentBase<azdata.TableComponentP
} }
} }
private appendData(data: any[][]) { private appendData(data: any[][]) {
this._tableData.push(TableComponent.transformData(data, this.columns)); this._tableData.push(this.transformData(data, this.columns));
this.data = this._tableData.getItems().map(dataObject => Object.values(dataObject)); this.data = this._tableData.getItems().map(dataObject => Object.values(dataObject));
this.layoutTable(); this.layoutTable();
} }
} }
function isIconColumnCellValue(obj: any | undefined): obj is azdata.IconColumnCellValue {
return !!(<azdata.IconColumnCellValue>obj)?.icon;
}

View File

@@ -5,10 +5,12 @@
import * as assert from 'assert'; import * as assert from 'assert';
import TableComponent from 'sql/workbench/browser/modelComponents/table.component'; import TableComponent from 'sql/workbench/browser/modelComponents/table.component';
import { CssIconCellValue } from 'sql/base/browser/ui/table/formatters';
suite('TableComponent Tests', () => { suite('TableComponent Tests', () => {
setup(() => { setup(() => {
}); });
test('Table transformData should convert data and columns successfully given valid inputs', () => { test('Table transformData should convert data and columns successfully given valid inputs', () => {
@@ -17,7 +19,9 @@ suite('TableComponent Tests', () => {
['4', '5', '6'] ['4', '5', '6']
]; ];
let columns = ['c1', 'c2', 'c3']; let columns = ['c1', 'c2', 'c3'];
let actual: { [key: string]: string }[] = TableComponent.transformData(data, columns); const tableComponent = new TableComponent(undefined, undefined, undefined);
let actual: { [key: string]: string | CssIconCellValue }[] = tableComponent.transformData(data, columns);
let expected: { [key: string]: string }[] = [ let expected: { [key: string]: string }[] = [
{ {
'c1': '1', 'c1': '1',
@@ -35,8 +39,9 @@ suite('TableComponent Tests', () => {
test('Table transformData should return empty array given undefined rows', () => { test('Table transformData should return empty array given undefined rows', () => {
let data = undefined; let data = undefined;
const tableComponent = new TableComponent(undefined, undefined, undefined);
let columns = ['c1', 'c2', 'c3']; let columns = ['c1', 'c2', 'c3'];
let actual: { [key: string]: string }[] = TableComponent.transformData(data, columns); let actual: { [key: string]: string | CssIconCellValue }[] = tableComponent.transformData(data, columns);
let expected: { [key: string]: string }[] = []; let expected: { [key: string]: string }[] = [];
assert.deepEqual(actual, expected); assert.deepEqual(actual, expected);
}); });
@@ -47,7 +52,8 @@ suite('TableComponent Tests', () => {
['4', '5', '6'] ['4', '5', '6']
]; ];
let columns; let columns;
let actual: { [key: string]: string }[] = TableComponent.transformData(data, columns); const tableComponent = new TableComponent(undefined, undefined, undefined);
let actual: { [key: string]: string | CssIconCellValue }[] = tableComponent.transformData(data, columns);
let expected: { [key: string]: string }[] = []; let expected: { [key: string]: string }[] = [];
assert.deepEqual(actual, expected); assert.deepEqual(actual, expected);
}); });
@@ -57,8 +63,9 @@ suite('TableComponent Tests', () => {
['1', '2'], ['1', '2'],
['4', '5'] ['4', '5']
]; ];
const tableComponent = new TableComponent(undefined, undefined, undefined);
let columns = ['c1', 'c2', 'c3']; let columns = ['c1', 'c2', 'c3'];
let actual: { [key: string]: string }[] = TableComponent.transformData(data, columns); let actual: { [key: string]: string | CssIconCellValue }[] = tableComponent.transformData(data, columns);
let expected: { [key: string]: string }[] = [ let expected: { [key: string]: string }[] = [
{ {
'c1': '1', 'c1': '1',