diff --git a/extensions/notebook/resources/dark/delete_inverse.svg b/extensions/notebook/resources/dark/delete_inverse.svg new file mode 100644 index 0000000000..7274a63148 --- /dev/null +++ b/extensions/notebook/resources/dark/delete_inverse.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/extensions/notebook/resources/light/delete.svg b/extensions/notebook/resources/light/delete.svg new file mode 100644 index 0000000000..464a68434b --- /dev/null +++ b/extensions/notebook/resources/light/delete.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/notebook/src/common/iconHelper.ts b/extensions/notebook/src/common/iconHelper.ts new file mode 100644 index 0000000000..b307fc1cae --- /dev/null +++ b/extensions/notebook/src/common/iconHelper.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +export interface IconPath { + dark: string; + light: string; +} + +export class IconPathHelper { + private static extensionContext: vscode.ExtensionContext; + + public static delete: IconPath; + + public static setExtensionContext(extensionContext: vscode.ExtensionContext) { + IconPathHelper.extensionContext = extensionContext; + IconPathHelper.delete = { + dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/delete_inverse.svg'), + light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/delete.svg') + }; + } +} diff --git a/extensions/notebook/src/dialog/managePackages/installedPackagesTab.ts b/extensions/notebook/src/dialog/managePackages/installedPackagesTab.ts index 3e57af18e6..c2ac82857a 100644 --- a/extensions/notebook/src/dialog/managePackages/installedPackagesTab.ts +++ b/extensions/notebook/src/dialog/managePackages/installedPackagesTab.ts @@ -5,12 +5,14 @@ import * as nls from 'vscode-nls'; import * as azdata from 'azdata'; +import * as vscode from 'vscode'; import { JupyterServerInstallation, PythonPkgDetails } from '../../jupyter/jupyterServerInstallation'; import * as utils from '../../common/utils'; import { ManagePackagesDialog } from './managePackagesDialog'; import CodeAdapter from '../../prompts/adapter'; import { IQuestion, confirm } from '../../prompts/question'; +import { IconPathHelper } from '../../common/iconHelper'; const localize = nls.loadMessageBundle(); @@ -27,6 +29,7 @@ export class InstalledPackagesTab { private uninstallPackageButton: azdata.ButtonComponent; private view: azdata.ModelView | undefined; private formBuilder: azdata.FormBuilder; + private disposables: vscode.Disposable[] = []; constructor(private dialog: ManagePackagesDialog, private jupyterInstallation: JupyterServerInstallation) { this.prompter = new CodeAdapter(); @@ -35,6 +38,13 @@ export class InstalledPackagesTab { this.installedPkgTab.registerContent(async view => { this.view = view; + + // Dispose the resources + this.disposables.push(view.onClosed(() => { + this.disposables.forEach(d => { + try { d.dispose(); } catch { } + }); + })); let dropdownValues = this.dialog.model.getPackageTypes().map(x => { return { name: x.providerId, @@ -67,20 +77,39 @@ export class InstalledPackagesTab { this.installedPackagesTable = view.modelBuilder.table() .withProperties({ columns: [ - localize('managePackages.pkgNameColumn', "Name"), - localize('managePackages.newPkgVersionColumn', "Version") + { + value: localize('managePackages.pkgNameColumn', "Name"), + type: azdata.ColumnType.text + }, + { + value: localize('managePackages.newPkgVersionColumn', "Version"), + type: azdata.ColumnType.text + }, + { + value: localize('managePackages.deleteColumn', "Delete"), + type: azdata.ColumnType.button, + options: { + icon: IconPathHelper.delete + } + } ], data: [[]], height: '600px', width: '400px' }).component(); + this.disposables.push(this.installedPackagesTable.onCellAction(async (rowState) => { + let buttonState = rowState; + if (buttonState) { + await this.doUninstallPackage([rowState.row]); + } + })); this.uninstallPackageButton = view.modelBuilder.button() .withProperties({ label: localize('managePackages.uninstallButtonText', "Uninstall selected packages"), width: '200px' }).component(); - this.uninstallPackageButton.onDidClick(() => this.doUninstallPackage()); + this.uninstallPackageButton.onDidClick(() => this.doUninstallPackage(this.installedPackagesTable.selectedRows)); this.formBuilder = view.modelBuilder.formContainer() .withFormItems([{ @@ -214,8 +243,7 @@ export class InstalledPackagesTab { } } - private async doUninstallPackage(): Promise { - let rowNums = this.installedPackagesTable.selectedRows; + private async doUninstallPackage(rowNums: number[]): Promise { if (!rowNums || rowNums.length === 0) { return; } diff --git a/extensions/notebook/src/dialog/managePackages/managePackagesDialog.ts b/extensions/notebook/src/dialog/managePackages/managePackagesDialog.ts index e3bcdcc69a..51ac318396 100644 --- a/extensions/notebook/src/dialog/managePackages/managePackagesDialog.ts +++ b/extensions/notebook/src/dialog/managePackages/managePackagesDialog.ts @@ -5,6 +5,7 @@ import * as nls from 'vscode-nls'; import * as azdata from 'azdata'; +import * as vscode from 'vscode'; import { JupyterServerInstallation } from '../../jupyter/jupyterServerInstallation'; import { InstalledPackagesTab } from './installedPackagesTab'; @@ -19,7 +20,7 @@ export class ManagePackagesDialog { private addNewPkgTab: AddNewPackageTab; constructor( - private _managePackageDialogModel: ManagePackagesDialogModel) { + private _managePackageDialogModel: ManagePackagesDialogModel, private _extensionContext: vscode.ExtensionContext) { } /** @@ -58,6 +59,10 @@ export class ManagePackagesDialog { return this._managePackageDialogModel; } + public get extensionContext(): vscode.ExtensionContext { + return this._extensionContext; + } + /** * Changes the current provider id * @param providerId Provider Id diff --git a/extensions/notebook/src/jupyter/jupyterController.ts b/extensions/notebook/src/jupyter/jupyterController.ts index 52a4cf23cb..1dd735ef3c 100644 --- a/extensions/notebook/src/jupyter/jupyterController.ts +++ b/extensions/notebook/src/jupyter/jupyterController.ts @@ -30,6 +30,7 @@ import { LocalCondaPackageManageProvider } from './localCondaPackageManageProvid import { ManagePackagesDialogModel, ManagePackageDialogOptions } from '../dialog/managePackages/managePackagesDialogModel'; import { PyPiClient } from './pypiClient'; import { ConfigurePythonDialog } from '../dialog/configurePython/configurePythonDialog'; +import { IconPathHelper } from '../common/iconHelper'; let untitledCounter = 0; @@ -66,6 +67,7 @@ export class JupyterController implements vscode.Disposable { this.extensionContext.extensionPath, this.outputChannel); await this._jupyterInstallation.configurePackagePaths(); + IconPathHelper.setExtensionContext(this.extensionContext); // Add command/task handlers azdata.tasks.registerTask(constants.jupyterOpenNotebookTask, (profile: azdata.IConnectionProfile) => { @@ -212,7 +214,7 @@ export class JupyterController implements vscode.Disposable { let model = new ManagePackagesDialogModel(this._jupyterInstallation, this._packageManageProviders, options); await model.init(); - let packagesDialog = new ManagePackagesDialog(model); + let packagesDialog = new ManagePackagesDialog(model, this.extensionContext); packagesDialog.showDialog(); } catch (error) { let message = utils.getErrorMessage(error); diff --git a/extensions/notebook/src/test/managePackages/managePackagesDialog.test.ts b/extensions/notebook/src/test/managePackages/managePackagesDialog.test.ts index 9155e875b1..e522640ff2 100644 --- a/extensions/notebook/src/test/managePackages/managePackagesDialog.test.ts +++ b/extensions/notebook/src/test/managePackages/managePackagesDialog.test.ts @@ -110,7 +110,10 @@ describe('Manage Package Dialog', () => { let packageManageProviders = new Map(); packageManageProviders.set(LocalCondaPackageManageProvider.ProviderId, new LocalCondaPackageManageProvider(undefined)); let model = TypeMoq.Mock.ofInstance(new ManagePackagesDialogModel(undefined, packageManageProviders)); - let dialog = TypeMoq.Mock.ofInstance(new ManagePackagesDialog(model.object)); + const mockExtensionContext = TypeMoq.Mock.ofType(); + mockExtensionContext.setup(x => x.asAbsolutePath(TypeMoq.It.isAny())).returns(() => ''); + + let dialog = TypeMoq.Mock.ofInstance(new ManagePackagesDialog(model.object, mockExtensionContext.object)); dialog.setup(x => x.model).returns(() => model.object); let onClick: vscode.EventEmitter = new vscode.EventEmitter(); diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 48431f315c..bf50ad2d59 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -441,6 +441,14 @@ declare module 'azdata' { targetLocation?: string; } + export interface ButtonColumnOption { + icon?: string | vscode.Uri | { light: string | vscode.Uri; dark: string | vscode.Uri }; + } + + export interface ButtonCell extends TableCell { + columnName: string; + } + export namespace sqlAssessment { export enum SqlAssessmentTargetType { diff --git a/src/sql/base/browser/ui/table/plugins/buttonColumn.plugin.ts b/src/sql/base/browser/ui/table/plugins/buttonColumn.plugin.ts index bf7283bde2..99cbb8a0ed 100644 --- a/src/sql/base/browser/ui/table/plugins/buttonColumn.plugin.ts +++ b/src/sql/base/browser/ui/table/plugins/buttonColumn.plugin.ts @@ -20,6 +20,8 @@ export interface ButtonColumnOptions { export interface ButtonClickEventArgs { item: T; position: { x: number, y: number }; + row: number; + column: number; } export class ButtonColumn implements Slick.Plugin { @@ -57,8 +59,10 @@ export class ButtonColumn implements Slick.Plugin private handleActiveCellChanged(args: Slick.OnActiveCellChangedEventArgs): void { if (this.isCurrentColumn(args.cell)) { const cellElement = this._grid.getActiveCellNode(); - const button = cellElement.children[0] as HTMLButtonElement; - button.focus(); + if (cellElement && cellElement.children) { + const button = cellElement.children[0] as HTMLButtonElement; + button.focus(); + } } } @@ -92,6 +96,8 @@ export class ButtonColumn implements Slick.Plugin const activeCellPosition = this._grid.getActiveCellPosition(); if (activeCell && activeCellPosition) { this._onClick.fire({ + row: activeCell.row, + column: activeCell.cell, item: this._grid.getDataItem(activeCell.row), position: { x: (activeCellPosition.left + activeCellPosition.right) / 2, @@ -102,7 +108,7 @@ export class ButtonColumn implements Slick.Plugin } private isCurrentColumn(columnIndex: number): boolean { - return this._grid.getColumns()[columnIndex].id === this.definition.id; + return this._grid?.getColumns()[columnIndex]?.id === this.definition.id; } private formatter(row: number, cell: number, value: any, columnDef: Slick.Column, dataContext: T): string { diff --git a/src/sql/base/browser/ui/table/plugins/checkboxSelectColumn.plugin.ts b/src/sql/base/browser/ui/table/plugins/checkboxSelectColumn.plugin.ts index e3699e11bf..42489f0546 100644 --- a/src/sql/base/browser/ui/table/plugins/checkboxSelectColumn.plugin.ts +++ b/src/sql/base/browser/ui/table/plugins/checkboxSelectColumn.plugin.ts @@ -239,7 +239,7 @@ export class CheckboxSelectColumn implements Slick.Pl } } - public getColumnDefinition(): Slick.Column { + public get definition(): Slick.Column { return { id: this._options.columnId, name: this._options.title || strings.format(checkboxTemplate, '', ''), diff --git a/src/sql/workbench/browser/modelComponents/media/table.css b/src/sql/workbench/browser/modelComponents/media/table.css index 939e05ebc7..ea52ad73f5 100644 --- a/src/sql/workbench/browser/modelComponents/media/table.css +++ b/src/sql/workbench/browser/modelComponents/media/table.css @@ -20,4 +20,8 @@ .display-none { display: none; -} \ No newline at end of file +} + +.modelview-table-button-icon { + background-color: transparent; +} diff --git a/src/sql/workbench/browser/modelComponents/table.component.ts b/src/sql/workbench/browser/modelComponents/table.component.ts index e92310f90c..a663b09ee8 100644 --- a/src/sql/workbench/browser/modelComponents/table.component.ts +++ b/src/sql/workbench/browser/modelComponents/table.component.ts @@ -27,6 +27,8 @@ import { slickGridDataItemColumnValueWithNoData, textFormatter } from 'sql/base/ import { isUndefinedOrNull } from 'vs/base/common/types'; import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/platform/dashboard/browser/interfaces'; import { convertSizeToNumber } from 'sql/base/browser/dom'; +import { ButtonColumn, ButtonClickEventArgs } from 'sql/base/browser/ui/table/plugins/buttonColumn.plugin'; +import { createIconCssClass } from 'sql/workbench/browser/modelComponents/iconUtils'; export enum ColumnSizingMode { ForceFit = 0, // all columns will be sized to fit in viewable space, no horiz scroll bar @@ -47,8 +49,12 @@ export default class TableComponent extends ComponentBase implements IComponent, private _tableData: TableDataView; private _tableColumns; private _checkboxColumns: CheckboxSelectColumn<{}>[] = []; + private _buttonsColumns: ButtonColumn<{}>[] = []; + private _pluginsRegisterStatus: boolean[] = []; private _onCheckBoxChanged = new Emitter(); + private _onButtonClicked = new Emitter>(); public readonly onCheckBoxChanged: vsEvent = this._onCheckBoxChanged.event; + public readonly onButtonClicked: vsEvent> = this._onButtonClicked.event; @ViewChild('table', { read: ElementRef }) private _inputContainer: ElementRef; constructor( @@ -72,6 +78,9 @@ export default class TableComponent extends ComponentBase implements IComponent, if (col.type && col.type === 1) { this.createCheckBoxPlugin(col, index); } + else if (col.type && col.type === 2) { + this.createButtonPlugin(col); + } else if (col.value) { mycolumns.push(>{ name: col.value, @@ -238,7 +247,8 @@ export default class TableComponent extends ComponentBase implements IComponent, this._table.setSelectedRows(this.selectedRows); } - Object.keys(this._checkboxColumns).forEach(col => this.registerCheckboxPlugin(this._checkboxColumns[col])); + Object.keys(this._checkboxColumns).forEach(col => this.registerPlugins(col, this._checkboxColumns[col])); + Object.keys(this._buttonsColumns).forEach(col => this.registerPlugins(col, this._buttonsColumns[col])); if (this.ariaRowCount === -1) { this._table.removeAriaRowCount(); @@ -309,11 +319,41 @@ export default class TableComponent extends ComponentBase implements IComponent, } } - private registerCheckboxPlugin(checkboxSelectColumn: CheckboxSelectColumn<{}>): void { - this._tableColumns.splice(checkboxSelectColumn.index, 0, checkboxSelectColumn.getColumnDefinition()); - this._table.registerPlugin(checkboxSelectColumn); + private createButtonPlugin(col: any) { + let name = col.value; + if (!this._buttonsColumns[col.value]) { + this._buttonsColumns[col.value] = new ButtonColumn({ + title: col.title, + iconCssClass: 'modelview-table-button-icon ' + (col.options ? createIconCssClass(col.options.icon) : '') + }); + + this._register(this._buttonsColumns[col.value].onClick((state) => { + this.fireEvent({ + eventType: ComponentEventType.onCellAction, + args: { + row: state.row, + column: state.column, + name: name + } + }); + })); + } + } + + private registerPlugins(col: string, plugin: CheckboxSelectColumn<{}> | ButtonColumn<{}>): void { + + const index = 'index' in plugin ? plugin.index : this.columns?.findIndex(x => x === col || ('value' in x && x['value'] === col)); + if (index >= 0) { + this._tableColumns.splice(index, 0, plugin.definition); + if (!(col in this._pluginsRegisterStatus) || !this._pluginsRegisterStatus[col]) { + this._table.registerPlugin(plugin); + this._pluginsRegisterStatus[col] = true; + } + } + this._table.columns = this._tableColumns; this._table.autosizeColumns(); + } public focus(): void { @@ -335,7 +375,7 @@ export default class TableComponent extends ComponentBase implements IComponent, this.setPropertyFromUI((props, value) => props.data = value, newValue); } - public get columns(): string[] { + public get columns(): string[] | azdata.TableColumn[] { return this.getPropertyOrDefault((props) => props.columns, []); } @@ -343,8 +383,8 @@ export default class TableComponent extends ComponentBase implements IComponent, return this.getPropertyOrDefault((props) => props.fontSize, ''); } - public set columns(newValue: string[]) { - this.setPropertyFromUI((props, value) => props.columns = value, newValue); + public set columns(newValue: string[] | azdata.TableColumn[]) { + this.setPropertyFromUI((props, value) => props.columns = value, newValue); } public get selectedRows(): number[] { diff --git a/src/sql/workbench/services/restore/browser/restoreDialog.ts b/src/sql/workbench/services/restore/browser/restoreDialog.ts index adad186153..730581f2f9 100644 --- a/src/sql/workbench/services/restore/browser/restoreDialog.ts +++ b/src/sql/workbench/services/restore/browser/restoreDialog.ts @@ -830,7 +830,7 @@ export class RestoreDialog extends Modal { }); const checkboxSelectColumn = new CheckboxSelectColumn({ title: this._restoreLabel, toolTip: this._restoreLabel, width: 15 }); - this._restorePlanColumn.unshift(checkboxSelectColumn.getColumnDefinition()); + this._restorePlanColumn.unshift(checkboxSelectColumn.definition); this._restorePlanTable.columns = this._restorePlanColumn; this._restorePlanTable.registerPlugin(checkboxSelectColumn); this._restorePlanTable.autosizeColumns();