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