From a9b48e78eec82f5b4ab8a28fafd1bde376e5ce3a Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 4 Jun 2024 16:32:16 -0400 Subject: [PATCH 001/107] wip(poc): TableBuilder Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/package.json | 1 + .../src/vscode/ui/TableView.ts | 40 +++++++++++++++++++ pnpm-lock.yaml | 3 ++ 3 files changed, 44 insertions(+) create mode 100644 packages/zowe-explorer-api/src/vscode/ui/TableView.ts diff --git a/packages/zowe-explorer-api/package.json b/packages/zowe-explorer-api/package.json index a4c22e066a..7c392ea867 100644 --- a/packages/zowe-explorer-api/package.json +++ b/packages/zowe-explorer-api/package.json @@ -31,6 +31,7 @@ "@zowe/zos-uss-for-zowe-sdk": "8.0.0-next.202404032038", "@zowe/zosmf-for-zowe-sdk": "8.0.0-next.202404032038", "handlebars": "^4.7.7", + "preact": "^10.16.0", "semver": "^7.6.0" }, "scripts": { diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts new file mode 100644 index 0000000000..ed9bec3a1d --- /dev/null +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -0,0 +1,40 @@ +import { WebView } from "./WebView"; +import { EventEmitter } from "vscode"; +import { AnyComponent, JSX } from "preact"; + +type TableData = { + rows: object[]; +}; + +type TableAction = AnyComponent | JSX.Element; + +type TableIndex = number | "start" | "end"; +type TableCoord = [TableIndex, TableIndex]; + +class TableView extends WebView {} + +class TableBuilder { + private data: { + actions: TableAction[]; + dividers: TableCoord[]; + headers: string[]; + }; + + public headers(newHeaders: string[]): void { + this.data.headers = newHeaders; + } + + public columnAction(action: TableAction): void {} + + public rowAction(action: TableAction): void { + this.data.actions.push(action); + } + + public divider(coordinates: TableCoord): void { + this.data.dividers.push(coordinates); + } + + public build(): TableView { + //return new TableView(); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92089f5f00..6133d36e41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -253,6 +253,9 @@ importers: handlebars: specifier: ^4.7.7 version: 4.7.8 + preact: + specifier: ^10.16.0 + version: 10.22.0 semver: specifier: ^7.6.0 version: 7.6.2 From 644245f57102adb0b56a2a85be678aaf34042b81 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 6 Jun 2024 10:00:13 -0400 Subject: [PATCH 002/107] wip: Table namespace, Builder, Mediator Signed-off-by: Trae Yelovich --- .../src/vscode/ui/TableView.ts | 54 +++++++++---------- .../src/vscode/ui/WebView.ts | 14 +++-- .../src/vscode/ui/utils/TableBuilder.ts | 46 ++++++++++++++++ .../src/vscode/ui/utils/TableMediator.ts | 33 ++++++++++++ 4 files changed, 115 insertions(+), 32 deletions(-) create mode 100644 packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts create mode 100644 packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index ed9bec3a1d..56bb49bdbf 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -1,40 +1,38 @@ import { WebView } from "./WebView"; -import { EventEmitter } from "vscode"; +import { EventEmitter, ExtensionContext } from "vscode"; import { AnyComponent, JSX } from "preact"; +import { randomUUID } from "crypto"; -type TableData = { - rows: object[]; -}; +export namespace Table { + export type Action = AnyComponent | JSX.Element; + export type Index = number | "start" | "end"; + export type Coord = [Index, Index]; -type TableAction = AnyComponent | JSX.Element; - -type TableIndex = number | "start" | "end"; -type TableCoord = [TableIndex, TableIndex]; - -class TableView extends WebView {} - -class TableBuilder { - private data: { - actions: TableAction[]; - dividers: TableCoord[]; + export type RowData = Record; + export type Data = { + actions: Action[]; + dividers: Coord[]; headers: string[]; + rows: RowData[]; + title?: string; }; - public headers(newHeaders: string[]): void { - this.data.headers = newHeaders; - } + export class View extends WebView { + private data: Data; + private onTableDataReceived: EventEmitter; + private onTableDisplayChange: EventEmitter; - public columnAction(action: TableAction): void {} + public constructor(context: ExtensionContext, data: Data) { + super(data.title, "table-view", context); + this.data = data; + } - public rowAction(action: TableAction): void { - this.data.actions.push(action); - } - - public divider(coordinates: TableCoord): void { - this.data.dividers.push(coordinates); - } + public getId(): string { + return `${this.data.title ?? randomUUID()}##${this.context.extension.id}`; + } - public build(): TableView { - //return new TableView(); + public dispose(): void { + super.dispose(); + } } } diff --git a/packages/zowe-explorer-api/src/vscode/ui/WebView.ts b/packages/zowe-explorer-api/src/vscode/ui/WebView.ts index fe85bc86d9..92852a3023 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/WebView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/WebView.ts @@ -16,11 +16,15 @@ import { Disposable, ExtensionContext, Uri, ViewColumn, WebviewPanel, window } f import { join as joinPath } from "path"; import { randomUUID } from "crypto"; +export type WebViewOpts = { + retainContext: boolean; +}; + export class WebView { - private disposables: Disposable[]; + protected disposables: Disposable[]; // The webview HTML content to render after filling the HTML template. - private webviewContent: string; + protected webviewContent: string; public panel: WebviewPanel; // Resource identifiers for the on-disk content and vscode-webview resource. @@ -31,8 +35,9 @@ export class WebView { // Unique identifier private nonce: string; + protected title: string; - private title: string; + protected context: ExtensionContext; /** * Constructs a webview for use with bundled assets. @@ -50,6 +55,7 @@ export class WebView { onDidReceiveMessage?: (message: object) => void | Promise, retainContext?: boolean ) { + this.context = context; this.disposables = []; // Generate random nonce for loading the bundled script @@ -90,7 +96,7 @@ export class WebView { /** * Disposes of the webview instance */ - private dispose(): void { + protected dispose(): void { this.panel.dispose(); for (const disp of this.disposables) { diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts new file mode 100644 index 0000000000..3ef5fe85c9 --- /dev/null +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -0,0 +1,46 @@ +import { ExtensionContext } from "vscode"; +import { Table } from "../TableView"; +import { TableMediator } from "./TableMediator"; + +export class TableBuilder { + private context: ExtensionContext; + private data: Table.Data; + + public constructor(context: ExtensionContext) { + this.context = context; + } + + public rows(...rows: Table.RowData[]): TableBuilder { + this.data.rows = rows; + return this; + } + + public headers(newHeaders: string[]): TableBuilder { + this.data.headers = newHeaders; + return this; + } + + public columnAction(action: Table.Action): TableBuilder { + return this; + } + + public rowAction(action: Table.Action): TableBuilder { + this.data.actions.push(action); + return this; + } + + public divider(coordinates: Table.Coord): TableBuilder { + this.data.dividers.push(coordinates); + return this; + } + + public build(): Table.View { + return new Table.View(this.context, this.data); + } + + public buildAndShare(): Table.View { + const table = new Table.View(this.context, this.data); + TableMediator.getInstance().addTable(table.getId(), table); + return table; + } +} diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts new file mode 100644 index 0000000000..f6509612ce --- /dev/null +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts @@ -0,0 +1,33 @@ +import { Table } from "../TableView"; + +export class TableMediator { + private static instance: TableMediator; + private tables: Map; + private constructor() {} + + public static getInstance(): TableMediator { + if (!this.instance) { + this.instance = new TableMediator(); + } + + return this.instance; + } + + public addTable(id: string, table: Table.View): void { + this.tables.set(id, table); + } + + public getTable(id: string): Table.View | undefined { + return this.tables.get(id); + } + + public deleteTable(id: string): boolean { + if (this.tables.has(id)) { + return false; + } + + const table = this.tables.get(id); + table.dispose(); + return this.tables.delete(id); + } +} From ba8d65d815ff00333a31ec0271e5a9f0d3296433 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 6 Jun 2024 16:51:45 -0400 Subject: [PATCH 003/107] feat: TableBuilder, TableMediator, TableView Signed-off-by: Trae Yelovich --- .../src/vscode/ui/TableView.ts | 170 ++++++++++++++++-- .../src/vscode/ui/utils/TableBuilder.ts | 149 ++++++++++++++- .../src/vscode/ui/utils/TableMediator.ts | 73 +++++++- 3 files changed, 368 insertions(+), 24 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 56bb49bdbf..46938f4d79 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -1,36 +1,186 @@ import { WebView } from "./WebView"; -import { EventEmitter, ExtensionContext } from "vscode"; +import { Event, EventEmitter, ExtensionContext } from "vscode"; import { AnyComponent, JSX } from "preact"; import { randomUUID } from "crypto"; export namespace Table { export type Action = AnyComponent | JSX.Element; - export type Index = number | "start" | "end"; - export type Coord = [Index, Index]; + export type Axes = "row" | "column"; - export type RowData = Record; + export type RowContent = Record; export type Data = { - actions: Action[]; - dividers: Coord[]; + // Actions to apply to the given row or column index + actions: { + column: Map; + row: Map; + }; + // Dividers to place within the table at a specific row or column index + dividers: { + column: number[]; + row: number[]; + }; + // Headers for the top of the table headers: string[]; - rows: RowData[]; + // The row data for the table. Each row contains a set of variables corresponding to the data for each column in that row + rows: RowContent[]; + // The display title for the table title?: string; }; + /** + * A class that acts as a controller between the extension and the table view. Based off of the {@link WebView} class. + * + * @remarks + * ## Usage + * + * To easily configure a table before creating a table view, + * use the `TableBuilder` class to prepare table data and build an instance. + */ export class View extends WebView { private data: Data; - private onTableDataReceived: EventEmitter; - private onTableDisplayChange: EventEmitter; + private onTableDataReceived: Event; + private onTableDisplayChanged: EventEmitter; public constructor(context: ExtensionContext, data: Data) { super(data.title, "table-view", context); this.data = data; + this.panel.webview.onDidReceiveMessage((message) => this.onMessageReceived(message)); } + /** + * (Receiver) message handler for the table view. + * Used to dispatch client-side updates of the table to subscribers when the table's display has changed. + * + * @param message The message received from the webview + */ + private onMessageReceived(message: any): void { + if (message.command === "ondisplaychanged") { + this.onTableDisplayChanged.fire(message.data); + } + } + + /** + * (Sender) message handler for the table view. + * Used to send data and table layout changes to the table view to be re-rendered. + * + * @returns Whether the webview received the update that was sent + */ + private async updateWebview(): Promise { + return this.panel.webview.postMessage({ + command: "ondatachanged", + data: this.data, + }); + } + + /** + * Access the unique ID for the table view instance. + * + * @returns The unique ID for this table view + */ public getId(): string { - return `${this.data.title ?? randomUUID()}##${this.context.extension.id}`; + const uuid = randomUUID(); + return `${this.data.title}-${uuid.substring(0, uuid.indexOf("-"))}##${this.context.extension.id}`; + } + + /** + * Add one or more actions to the given row or column index. + * + * @param axis The axis to add an action to (either "row" or "column") + * @param index The index where the action should be displayed + * @param actions The actions to add to the given row/column index + * + * @returns Whether the webview successfully received the new action(s) + */ + public addAction(axis: Axes, index: number, ...actions: Action[]): Promise { + const actionMap = axis === "row" ? this.data.actions.row : this.data.actions.column; + if (actionMap.has(index)) { + const existingActions = actionMap.get(index); + actionMap.set(index, [...existingActions, ...actions]); + } else { + actionMap.set(index, actions); + } + return this.updateWebview(); + } + + /** + * Add rows of content to the table view. + * @param rows The rows of data to add to the table + * @returns Whether the webview successfully received the new content + */ + public async addContent(...rows: RowContent[]): Promise { + this.data.rows.push(...rows); + return this.updateWebview(); + } + + /** + * Adds a divider to the given row and column + * + * @param coord The coordinate + * @returns + */ + public async addDivider(axis: Axes, index: number): Promise { + (axis === "row" ? this.data.dividers.row : this.data.dividers.column).push(index); + return this.updateWebview(); + } + + /** + * Adds headers to the end of the existing header list in the table view. + * + * @param headers The headers to add to the existing header list + * @returns Whether the webview successfully received the list of headers + */ + public async addHeaders(...headers: string[]): Promise { + this.data.headers.push(...headers); + return this.updateWebview(); + } + + /** + * Sets the content for the table; replaces any pre-existing content. + * + * @param rows The rows of data to apply to the table + * @returns Whether the webview successfully received the new content + */ + public async setContent(rows: RowContent[]): Promise { + this.data.rows = rows; + return this.updateWebview(); + } + + /** + * Sets the dividers for the table; replaces any pre-existing dividers. + * + * @param rows The dividers to use for the table + * @returns Whether the webview successfully received the new dividers + */ + public async setDividers(): Promise { + // TODO: implement + return this.updateWebview(); + } + + /** + * Sets the headers for the table. + * + * @param headers The new headers to use for the table + * @returns Whether the webview successfully received the new headers + */ + public async setHeaders(headers: string[]): Promise { + this.data.headers = headers; + return this.updateWebview(); + } + + /** + * Sets the display title for the table view. + * + * @param title The new title for the table + * @returns Whether the webview successfully received the new title + */ + public async setTitle(title: string): Promise { + this.data.title = title; + return this.updateWebview(); } + /** + * Closes the table view and marks it as disposed. + */ public dispose(): void { super.dispose(); } diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index 3ef5fe85c9..a054045944 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -2,6 +2,25 @@ import { ExtensionContext } from "vscode"; import { Table } from "../TableView"; import { TableMediator } from "./TableMediator"; +/** + * A builder class for quickly instantiating {@link Table.View} instances. + * Especially useful for building multiple tables with similar layouts or data. + * + * @remarks + * + * ## Building a table + * + * Developers can build a table using the helper methods provided by the builder. + * Once a table is ready to be built, use the {@link TableBuilder.build} function to create + * a new {@link Table.View} instance with the given configuration. + * + * ## Sharing tables + * + * Share a table during the build process by using the {@link TableBuilder.buildAndShare} function. + * This function will create the {@link Table.View} instance while also adding it to the mediator + * in Zowe Explorer's extender API. Extenders who would like to contribute to shared tables can do + * so by accessing the table by its unique ID. + */ export class TableBuilder { private context: ExtensionContext; private data: Table.Data; @@ -10,37 +29,153 @@ export class TableBuilder { this.context = context; } - public rows(...rows: Table.RowData[]): TableBuilder { + /** + * Set the content for the next table. + * + * @param rows The rows of content to use for the table + * @returns The same {@link TableBuilder} instance with the content added + */ + public content(...rows: Table.RowContent[]): TableBuilder { this.data.rows = rows; return this; } + /** + * Set the headers for the next table. + * + * @param rows The headers to use for the table + * @returns The same {@link TableBuilder} instance with the headers added + */ public headers(newHeaders: string[]): TableBuilder { this.data.headers = newHeaders; return this; } - public columnAction(action: Table.Action): TableBuilder { + /** + * Add an action for the next table. + * + * @param actionMap the map of indices to {@link Table.Action} arrays to add to for the table + * @param index the index of the row or column to add an action to + */ + private action(actionMap: Map, index: number, action: Table.Action): void { + if (actionMap.has(index)) { + const actions = actionMap.get(index); + actionMap.set(index, [...actions, action]); + } else { + actionMap.set(index, [action]); + } + } + + /** + * Add column actions for the next table. + * + * @param actionMap the map of indices to {@link Table.Action} arrays to use for the table + * @returns The same {@link TableBuilder} instance with the column actions added + */ + public columnActions(actionMap: Map): TableBuilder { + this.data.actions.column = actionMap; + return this; + } + + /** + * Add row actions for the next table. + * + * @param actionMap the map of indices to {@link Table.Action} arrays to use for the table + * @returns The same {@link TableBuilder} instance with the row actions added + */ + public rowActions(actionMap: Map): TableBuilder { + this.data.actions.row = actionMap; + return this; + } + + /** + * Add a column action to the next table. + * + * @param index The column index to add an action to + * @returns The same {@link TableBuilder} instance with the column action added + */ + public columnAction(index: number, action: Table.Action): TableBuilder { + this.action(this.data.actions.column, index, action); + return this; + } + + /** + * Add a row action to the next table. + * + * @param index The column index to add an action to + * @returns The same {@link TableBuilder} instance with the row action added + */ + public rowAction(index: number, action: Table.Action): TableBuilder { + this.action(this.data.actions.row, index, action); + return this; + } + + /** + * Adds a divider to the table at the specified axis and the given index. + * + * @param axis The axis to add a divider to (either "row" or "column") + * @param index The index where the divider should be added + * @returns The same {@link TableBuilder} instance with the divider added + */ + private divider(axis: Table.Axes, index: number): TableBuilder { + (axis === "row" ? this.data.dividers.row : this.data.dividers.column).push(index); return this; } - public rowAction(action: Table.Action): TableBuilder { - this.data.actions.push(action); + /** + * Adds a divider to the table at the given row index. + * + * @param index The index where the divider should be added + * @returns The same {@link TableBuilder} instance with the row divider added + */ + public rowDivider(index: number): TableBuilder { + this.divider("row", index); return this; } - public divider(coordinates: Table.Coord): TableBuilder { - this.data.dividers.push(coordinates); + /** + * Adds a divider to the table at the given column index. + * + * @param index The index where the divider should be added + * @returns The same {@link TableBuilder} instance with the column divider added + */ + public columnDivider(index: number): TableBuilder { + this.divider("column", index); return this; } + /** + * Builds the table with the given data. + * + * @returns A new {@link Table.View} instance with the given data/options + */ public build(): Table.View { return new Table.View(this.context, this.data); } + /** + * Builds the table with the given data and shares it with the TableMediator singleton. + * + * @returns A new, **shared** {@link Table.View} instance with the given data/options + */ public buildAndShare(): Table.View { const table = new Table.View(this.context, this.data); - TableMediator.getInstance().addTable(table.getId(), table); + TableMediator.getInstance().addTable(table); return table; } + + /** + * Resets all data configured in the builder from previously-created table views. + */ + public reset(): void { + this.data.actions.row.clear(); + this.data.actions.column.clear(); + + this.data.dividers.row = []; + this.data.dividers.column = []; + + this.data.headers = []; + this.data.rows = []; + this.data.title = ""; + } } diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts index f6509612ce..f4ebd4ff77 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts @@ -1,10 +1,49 @@ import { Table } from "../TableView"; +/** + * Mediator class for managing and accessing shared tables in Zowe Explorer. + * Developers can expose their tables to allow external extenders to contribute to them. + * This class serves to satisfy three main use cases: + * + * @remarks + * + * ## Adding a table + * + * Tables are added to the mediator based on their unique ID using {@link TableMediator.addTable}. The ID is in the following format: + * + * `-<8digit-Unique-Id>##` + * + * This avoids indirect naming conflicts by making the table identifier specific to the contributing extension. + * The table ID can be accessed directly from a {@link Table.View} instance using the {@link Table.View.getId} function. + * + * ## Accessing a table + * + * Tables are only accessible by their unique IDs using {@link TableMediator.getTable}. Extenders must communicate a table ID + * to another extender to facilitate table changes between extensions. This facilitates explicit access requests + * by ensuring that two or more extenders are coordinating changes to the same table. + * + * The map containing the tables is not publicly exposed and is defined as a + * [private class property](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_properties) + * to avoid exposing any tables to extenders who do not have explicit access. + * + * ## Removing a table + * + * Tables can only be removed by the extender that has contributed them using {@link TableMediator.deleteTable}. + * This establishes a read-only relationship between the mediator and extenders that have not contributed the table they are trying to access. + * + * **Note** that this does not prevent an extender with access to the ID from disposing the table once they've received access to the instance. + */ export class TableMediator { private static instance: TableMediator; - private tables: Map; + #tables: Map; + private constructor() {} + /** + * Access the singleton instance. + * + * @returns the global {@link TableMediator} instance + */ public static getInstance(): TableMediator { if (!this.instance) { this.instance = new TableMediator(); @@ -13,21 +52,41 @@ export class TableMediator { return this.instance; } - public addTable(id: string, table: Table.View): void { - this.tables.set(id, table); + /** + * Adds a table to the mediator to enable sharing between extensions. + * + * @param table The {@link Table.View} instance to add to the mediator + */ + public addTable(table: Table.View): void { + this.#tables.set(table.getId(), table); } + /** + * Accesses a table in the mediator based on its unique ID. + * + * @param id The unique identifier for the desired table + * + * @returns + * * {@link Table.View} instance if the table exists + * * `undefined` if the instance was deleted or does not exist + */ public getTable(id: string): Table.View | undefined { - return this.tables.get(id); + return this.#tables.get(id); } + /** + * Removes a table and disposes of it from within the mediator. + * + * @param id The unique ID of the table to delete + * @returns `true` if the table was deleted; `false` otherwise + */ public deleteTable(id: string): boolean { - if (this.tables.has(id)) { + if (this.#tables.has(id)) { return false; } - const table = this.tables.get(id); + const table = this.#tables.get(id); table.dispose(); - return this.tables.delete(id); + return this.#tables.delete(id); } } From 5f80837537722f395ec8f464ac76f65d50e191ce Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Fri, 7 Jun 2024 15:33:26 -0400 Subject: [PATCH 004/107] feat: Load CSS for WebView instances (if present) Signed-off-by: Trae Yelovich --- .../src/vscode/ui/WebView.ts | 6 +++++ .../src/vscode/ui/utils/HTMLTemplate.ts | 5 +++- .../zowe-explorer/src/webviews/package.json | 5 +++- .../src/webviews/src/edit-history/App.tsx | 2 +- .../PersistentTable/PersistentDataPanel.tsx | 3 ++- .../components/PersistentUtils.ts | 24 ++++--------------- .../src/webviews/src/table-view/index.html | 10 ++++++++ .../zowe-explorer/src/webviews/src/utils.ts | 24 +++++++++++++++++++ .../zowe-explorer/src/webviews/tsconfig.json | 7 ++++++ .../zowe-explorer/src/webviews/vite.config.ts | 10 +++++++- 10 files changed, 72 insertions(+), 24 deletions(-) create mode 100644 packages/zowe-explorer/src/webviews/src/table-view/index.html create mode 100644 packages/zowe-explorer/src/webviews/src/utils.ts diff --git a/packages/zowe-explorer-api/src/vscode/ui/WebView.ts b/packages/zowe-explorer-api/src/vscode/ui/WebView.ts index 92852a3023..0738e99367 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/WebView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/WebView.ts @@ -10,6 +10,7 @@ */ import * as Handlebars from "handlebars"; +import * as fs from "fs"; import HTMLTemplate from "./utils/HTMLTemplate"; import { Types } from "../../Types"; import { Disposable, ExtensionContext, Uri, ViewColumn, WebviewPanel, window } from "vscode"; @@ -62,10 +63,14 @@ export class WebView { this.nonce = randomUUID(); this.title = title; + const cssPath = joinPath(context.extensionPath, "src", "webviews", "dist", "style", "style.css"); + const cssExists = fs.existsSync(cssPath); + // Build URIs for the webview directory and get the paths as VScode resources this.uris.disk = { build: Uri.file(joinPath(context.extensionPath, "src", "webviews")), script: Uri.file(joinPath(context.extensionPath, "src", "webviews", "dist", webviewName, `${webviewName}.js`)), + css: cssExists ? Uri.file(cssPath) : undefined }; this.panel = window.createWebviewPanel("ZEAPIWebview", this.title, ViewColumn.Beside, { @@ -78,6 +83,7 @@ export class WebView { this.uris.resource = { build: this.panel.webview.asWebviewUri(this.uris.disk.build), script: this.panel.webview.asWebviewUri(this.uris.disk.script), + css: this.uris.disk.css ? this.panel.webview.asWebviewUri(this.uris.disk.css) : undefined }; const template = Handlebars.compile(HTMLTemplate); diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/HTMLTemplate.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/HTMLTemplate.ts index 81ec181818..7dc152c2ca 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/HTMLTemplate.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/HTMLTemplate.ts @@ -21,10 +21,13 @@ const HTMLTemplate: string = ` + {{#if uris.resource.css}} + + {{/if}} diff --git a/packages/zowe-explorer/src/webviews/package.json b/packages/zowe-explorer/src/webviews/package.json index 13ba4f96bf..1b382d861d 100644 --- a/packages/zowe-explorer/src/webviews/package.json +++ b/packages/zowe-explorer/src/webviews/package.json @@ -21,8 +21,11 @@ "dependencies": { "@types/vscode-webview": "^1.57.1", "@vscode/webview-ui-toolkit": "^1.2.2", + "ag-grid-community": "^31.3.2", + "ag-grid-react": "^31.3.2", "lodash": "^4.17.21", - "preact": "^10.16.0" + "preact": "^10.16.0", + "preact-render-to-string": "^6.5.4" }, "devDependencies": { "@preact/preset-vite": "^2.5.0", diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/App.tsx b/packages/zowe-explorer/src/webviews/src/edit-history/App.tsx index 3c9ed2e157..5929236632 100644 --- a/packages/zowe-explorer/src/webviews/src/edit-history/App.tsx +++ b/packages/zowe-explorer/src/webviews/src/edit-history/App.tsx @@ -12,7 +12,7 @@ import { useEffect, useState } from "preact/hooks"; import { VSCodeDivider, VSCodePanels, VSCodePanelTab } from "@vscode/webview-ui-toolkit/react"; import { JSXInternal } from "preact/src/jsx"; -import { isSecureOrigin } from "./components/PersistentUtils"; +import { isSecureOrigin } from "../utils"; import PersistentDataPanel from "./components/PersistentTable/PersistentDataPanel"; import PersistentVSCodeAPI from "./components/PersistentVSCodeAPI"; import PersistentManagerHeader from "./components/PersistentManagerHeader/PersistentManagerHeader"; diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentTable/PersistentDataPanel.tsx b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentTable/PersistentDataPanel.tsx index 5abf8d6e74..6757938843 100644 --- a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentTable/PersistentDataPanel.tsx +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentTable/PersistentDataPanel.tsx @@ -12,7 +12,8 @@ import { useEffect, useMemo, useState } from "preact/hooks"; import { VSCodePanelView, VSCodeDataGrid } from "@vscode/webview-ui-toolkit/react"; import { JSXInternal } from "preact/src/jsx"; -import { DataPanelContext, isSecureOrigin } from "../PersistentUtils"; +import { DataPanelContext } from "../PersistentUtils"; +import { isSecureOrigin } from "../../../utils"; import { panelId } from "../../types"; import PersistentToolBar from "../PersistentToolBar/PersistentToolBar"; import PersistentTableData from "./PersistentTableData"; diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentUtils.ts b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentUtils.ts index 86eca22d96..2d9578eb46 100644 --- a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentUtils.ts +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentUtils.ts @@ -15,24 +15,10 @@ import { useContext } from "preact/hooks"; export const DataPanelContext = createContext(null); -export function isSecureOrigin(origin: string): boolean { - const eventUrl = new URL(origin); - const isWebUser = - (eventUrl.protocol === document.location.protocol && eventUrl.hostname === document.location.hostname) || - eventUrl.hostname.endsWith(".github.dev"); - const isLocalVSCodeUser = eventUrl.protocol === "vscode-webview:"; - - if (!isWebUser && !isLocalVSCodeUser) { - return false; - } - - return true; -} - export function useDataPanelContext(): DataPanelContextType { - const dataPanelContext = useContext(DataPanelContext); - if (!dataPanelContext) { - throw new Error("DataPanelContext has to be used within "); - } - return dataPanelContext; + const dataPanelContext = useContext(DataPanelContext); + if (!dataPanelContext) { + throw new Error("DataPanelContext has to be used within "); + } + return dataPanelContext; } diff --git a/packages/zowe-explorer/src/webviews/src/table-view/index.html b/packages/zowe-explorer/src/webviews/src/table-view/index.html new file mode 100644 index 0000000000..6b6499c3e0 --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/table-view/index.html @@ -0,0 +1,10 @@ + + + + + $Title$ + + +$END$ + + diff --git a/packages/zowe-explorer/src/webviews/src/utils.ts b/packages/zowe-explorer/src/webviews/src/utils.ts new file mode 100644 index 0000000000..63e1cdab76 --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/utils.ts @@ -0,0 +1,24 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +export function isSecureOrigin(origin: string): boolean { + const eventUrl = new URL(origin); + const isWebUser = + (eventUrl.protocol === document.location.protocol && eventUrl.hostname === document.location.hostname) || + eventUrl.hostname.endsWith(".github.dev"); + const isLocalVSCodeUser = eventUrl.protocol === "vscode-webview:"; + + if (!isWebUser && !isLocalVSCodeUser) { + return false; + } + + return true; +} diff --git a/packages/zowe-explorer/src/webviews/tsconfig.json b/packages/zowe-explorer/src/webviews/tsconfig.json index 464518df51..819fdfdb60 100644 --- a/packages/zowe-explorer/src/webviews/tsconfig.json +++ b/packages/zowe-explorer/src/webviews/tsconfig.json @@ -5,6 +5,13 @@ "module": "ESNext", "lib": ["ES2020", "DOM", "DOM.Iterable"], "skipLibCheck": true, + "baseUrl": "./", + "paths": { + "react": ["./node_modules/preact/compat/"], + "react/jsx-runtime": ["./node_modules/preact/jsx-runtime"], + "react-dom": ["./node_modules/preact/compat/"], + "react-dom/*": ["./node_modules/preact/compat/*"] + }, /* Bundler mode */ "moduleResolution": "node", diff --git a/packages/zowe-explorer/src/webviews/vite.config.ts b/packages/zowe-explorer/src/webviews/vite.config.ts index 63643cfaa5..bc213a9308 100644 --- a/packages/zowe-explorer/src/webviews/vite.config.ts +++ b/packages/zowe-explorer/src/webviews/vite.config.ts @@ -41,8 +41,10 @@ export default defineConfig({ typescript: true, }), ], + root: path.resolve(__dirname, "src"), build: { + cssCodeSplit: false, emptyOutDir: true, outDir: path.resolve(__dirname, "dist"), rollupOptions: { @@ -50,8 +52,14 @@ export default defineConfig({ output: { entryFileNames: `[name]/[name].js`, chunkFileNames: `[name]/[name].js`, - assetFileNames: `assets/[name].[ext]`, + assetFileNames: `[name]/[name].[ext]`, }, }, }, + resolve: { + alias: { + "react": "preact/compat", + "react-dom": "preact/compat" + } + } }); From d37ddde4e876054458f1071b72dfba807d611430 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Fri, 7 Jun 2024 15:34:24 -0400 Subject: [PATCH 005/107] wip(feat): Display AG grid in webview Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/src/Types.ts | 1 + .../src/vscode/ui/TableView.ts | 33 ++++++++-- .../zowe-explorer-api/src/vscode/ui/index.ts | 1 + .../src/vscode/ui/utils/TableBuilder.ts | 11 ++++ .../src/vscode/ui/utils/TableMediator.ts | 11 ++++ packages/zowe-explorer/package.json | 5 ++ .../src/trees/shared/SharedInit.ts | 6 ++ .../src/webviews/src/table-view/App.tsx | 50 +++++++++++++++ .../src/webviews/src/table-view/index.html | 12 +++- .../src/webviews/src/table-view/index.tsx | 15 +++++ pnpm-lock.yaml | 63 ++++++++++++++++++- 11 files changed, 199 insertions(+), 9 deletions(-) create mode 100644 packages/zowe-explorer/src/webviews/src/table-view/App.tsx create mode 100644 packages/zowe-explorer/src/webviews/src/table-view/index.tsx diff --git a/packages/zowe-explorer-api/src/Types.ts b/packages/zowe-explorer-api/src/Types.ts index ae3fa3d581..0af2c48255 100644 --- a/packages/zowe-explorer-api/src/Types.ts +++ b/packages/zowe-explorer-api/src/Types.ts @@ -36,6 +36,7 @@ export namespace Types { export type WebviewUris = { build: Uri; script: Uri; + css?: Uri; }; export type FileAttributes = { diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 46938f4d79..cc7ee9207c 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -1,3 +1,14 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + import { WebView } from "./WebView"; import { Event, EventEmitter, ExtensionContext } from "vscode"; import { AnyComponent, JSX } from "preact"; @@ -8,6 +19,10 @@ export namespace Table { export type Axes = "row" | "column"; export type RowContent = Record; + export type Dividers = { + rows: number[]; + columns: number[]; + }; export type Data = { // Actions to apply to the given row or column index actions: { @@ -115,8 +130,9 @@ export namespace Table { /** * Adds a divider to the given row and column * - * @param coord The coordinate - * @returns + * @param axis The axis to add the divider to + * @param index The index on the axis where the divider should be added + * @returns Whether the webview successfully received the new divider */ public async addDivider(axis: Axes, index: number): Promise { (axis === "row" ? this.data.dividers.row : this.data.dividers.column).push(index); @@ -148,11 +164,18 @@ export namespace Table { /** * Sets the dividers for the table; replaces any pre-existing dividers. * - * @param rows The dividers to use for the table + * @param rows The row dividers to use for the table + * @param columns The column dividers to use for the table * @returns Whether the webview successfully received the new dividers */ - public async setDividers(): Promise { - // TODO: implement + public async setDividers(dividers: Pick | Pick): Promise { + if ("rows" in dividers) { + this.data.dividers.row = dividers.rows; + } + if ("columns" in dividers) { + this.data.dividers.column = dividers.columns; + } + return this.updateWebview(); } diff --git a/packages/zowe-explorer-api/src/vscode/ui/index.ts b/packages/zowe-explorer-api/src/vscode/ui/index.ts index 739e319f3b..baab4f66dc 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/index.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/index.ts @@ -9,4 +9,5 @@ * */ +export * from "./TableView"; export * from "./WebView"; diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index a054045944..acbf8c515f 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -1,3 +1,14 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + import { ExtensionContext } from "vscode"; import { Table } from "../TableView"; import { TableMediator } from "./TableMediator"; diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts index f4ebd4ff77..742b32264c 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts @@ -1,3 +1,14 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + import { Table } from "../TableView"; /** diff --git a/packages/zowe-explorer/package.json b/packages/zowe-explorer/package.json index 07f2aceedb..4a87e6ca79 100644 --- a/packages/zowe-explorer/package.json +++ b/packages/zowe-explorer/package.json @@ -854,6 +854,11 @@ "command": "zowe.placeholderCommand", "title": "%zowe.placeholderCommand%", "enablement": "false" + }, + { + "command": "zowe.tableView", + "title": "Open table view", + "category": "Zowe Explorer" } ], "menus": { diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index 05db5f63fe..9f32ebf7b6 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -32,6 +32,7 @@ import { ProfilesUtils } from "../../utils/ProfilesUtils"; import { DatasetFSProvider } from "../dataset/DatasetFSProvider"; import { ExtensionUtils } from "../../utils/ExtensionUtils"; import type { Definitions } from "../../configuration/Definitions"; +import { Table } from "@zowe/zowe-explorer-api"; export class SharedInit { public static registerRefreshCommand( @@ -223,6 +224,11 @@ export class SharedInit { // This command does nothing, its here to let us disable individual items in the tree view }) ); + context.subscriptions.push( + vscode.commands.registerCommand("zowe.tableView", () => { + new Table.View(context, {} as any); + }) + ); // initialize the Constants.filesToCompare array during initialization LocalFileManagement.resetCompareSelection(); } diff --git a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx new file mode 100644 index 0000000000..27f5932225 --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx @@ -0,0 +1,50 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid +import "ag-grid-community/styles/ag-theme-quartz.css"; // Optional Theme +import { AgGridReact, AgGridReactProps } from "ag-grid-react"; +import { useEffect, useState } from "preact/hooks"; +import { isSecureOrigin } from "../utils"; + +export function App() { + const [rowData, _setRowData] = useState([ + { make: "Tesla", model: "Model Y", price: 64950, electric: true }, + { make: "Ford", model: "F-Series", price: 33850, electric: false }, + { make: "Toyota", model: "Corolla", price: 29600, electric: false }, + ]); + + // Column Definitions: Defines the columns to be displayed. + const [colDefs, _setColDefs] = useState["columnDefs"]>([ + { field: "make" }, + { field: "model" }, + { field: "price" }, + { field: "electric" }, + ]); + + useEffect(() => { + window.addEventListener("message", (event): void => { + if (!isSecureOrigin(event.origin)) { + return; + } + }); + }, []); + + return ( + // wrapping container with theme & size +
+ +
+ ); +} diff --git a/packages/zowe-explorer/src/webviews/src/table-view/index.html b/packages/zowe-explorer/src/webviews/src/table-view/index.html index 6b6499c3e0..ad45961f7b 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/index.html +++ b/packages/zowe-explorer/src/webviews/src/table-view/index.html @@ -1,10 +1,16 @@ + + - - $Title$ + + + Table View -$END$ +
+ diff --git a/packages/zowe-explorer/src/webviews/src/table-view/index.tsx b/packages/zowe-explorer/src/webviews/src/table-view/index.tsx new file mode 100644 index 0000000000..748009dcd9 --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/table-view/index.tsx @@ -0,0 +1,15 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { render } from "preact"; +import { App } from "./App"; + +render(, document.getElementById("webviewRoot")!); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6133d36e41..2aafa1c350 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -297,12 +297,21 @@ importers: '@vscode/webview-ui-toolkit': specifier: ^1.2.2 version: 1.4.0(react@18.3.1) + ag-grid-community: + specifier: ^31.3.2 + version: 31.3.2 + ag-grid-react: + specifier: ^31.3.2 + version: 31.3.2(react-dom@18.3.1)(react@18.3.1) lodash: specifier: ^4.17.21 version: 4.17.21 preact: specifier: ^10.16.0 version: 10.22.0 + preact-render-to-string: + specifier: ^6.5.4 + version: 6.5.4(preact@10.22.0) devDependencies: '@preact/preset-vite': specifier: ^2.5.0 @@ -2814,6 +2823,22 @@ packages: hasBin: true dev: true + /ag-grid-community@31.3.2: + resolution: {integrity: sha512-GxqFRD0OcjaVRE1gwLgoP0oERNPH8Lk8wKJ1txulsxysEQ5dZWHhiIoXXSiHjvOCVMkK/F5qzY6HNrn6VeDMTQ==} + dev: false + + /ag-grid-react@31.3.2(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-SFHN05bsXp901rIT00Fa6iQLCtyavoJiKaXEDUtAU5LMu+GTkjs/FPQBQ8754omgdDFr4NsS3Ri6QbqBne3rug==} + peerDependencies: + react: ^16.3.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.3.0 || ^17.0.0 || ^18.0.0 + dependencies: + ag-grid-community: 31.3.2 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -7649,6 +7674,11 @@ packages: boolbase: 1.0.0 dev: true + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + dev: false + /object-copy@0.1.0: resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==} engines: {node: '>=0.10.0'} @@ -7982,6 +8012,14 @@ packages: source-map-js: 1.2.0 dev: true + /preact-render-to-string@6.5.4(preact@10.22.0): + resolution: {integrity: sha512-06s0E3cEMLoXQznmtJ/K/xbFs3uwo52Qpgf8lzbe+VbF/XzwJ0LxZGtVLZekhaEeC39+W1MEf05F4lUikzPnxA==} + peerDependencies: + preact: '>=10' + dependencies: + preact: 10.22.0 + dev: false + /preact@10.22.0: resolution: {integrity: sha512-RRurnSjJPj4rp5K6XoP45Ui33ncb7e4H7WiOHVpjbkvqvA3U+N8Z6Qbo0AE6leGYBV66n8EhEaFixvIu3SkxFw==} @@ -8126,6 +8164,14 @@ packages: sisteransi: 1.0.5 dev: true + /prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + dev: false + /pseudo-localization@2.4.0: resolution: {integrity: sha512-ISYMOKY8+f+PmiXMFw2y6KLY74LBrv/8ml/VjjoVEV2k+MS+OJZz7ydciK5ntJwxPrKQPTU1+oXq9Mx2b0zEzg==} hasBin: true @@ -8193,9 +8239,18 @@ packages: strip-json-comments: 2.0.1 dev: true + /react-dom@18.3.1(react@18.3.1): + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - dev: true /react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -8527,6 +8582,12 @@ packages: resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} dev: true + /scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + dependencies: + loose-envify: 1.4.0 + dev: false + /schema-utils@3.3.0: resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} engines: {node: '>= 10.13.0'} From b3dfa8a8279a5b92a93c316f21e16b57c9049331 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 11 Jun 2024 14:40:03 -0400 Subject: [PATCH 006/107] wip(feat): Populate table with data from builder & view class Signed-off-by: Trae Yelovich --- .../src/vscode/ui/TableView.ts | 36 ++++++++++------ .../zowe-explorer-api/src/vscode/ui/index.ts | 2 + .../src/vscode/ui/utils/TableBuilder.ts | 37 +++++++++++++--- .../src/trees/shared/SharedInit.ts | 22 ++++++++-- .../src/webviews/src/table-view/App.tsx | 43 +++++++++++-------- 5 files changed, 99 insertions(+), 41 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index cc7ee9207c..563b93e0fd 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -34,8 +34,8 @@ export namespace Table { column: number[]; row: number[]; }; - // Headers for the top of the table - headers: string[]; + // Column headers for the top of the table + columns: { field: string }[]; // The row data for the table. Each row contains a set of variables corresponding to the data for each column in that row rows: RowContent[]; // The display title for the table @@ -56,10 +56,12 @@ export namespace Table { private onTableDataReceived: Event; private onTableDisplayChanged: EventEmitter; - public constructor(context: ExtensionContext, data: Data) { - super(data.title, "table-view", context); - this.data = data; + public constructor(context: ExtensionContext, data?: Data) { + super(data.title ?? "Table view", "table-view", context); this.panel.webview.onDidReceiveMessage((message) => this.onMessageReceived(message)); + if (data) { + this.data = data; + } } /** @@ -68,9 +70,17 @@ export namespace Table { * * @param message The message received from the webview */ - private onMessageReceived(message: any): void { - if (message.command === "ondisplaychanged") { - this.onTableDisplayChanged.fire(message.data); + private async onMessageReceived(message: any): Promise { + if (!("command" in message)) { + return; + } + switch (message.command) { + case "ondisplaychanged": + this.onTableDisplayChanged.fire(message.data); + break; + case "ready": + await this.updateWebview(); + break; } } @@ -109,7 +119,7 @@ export namespace Table { public addAction(axis: Axes, index: number, ...actions: Action[]): Promise { const actionMap = axis === "row" ? this.data.actions.row : this.data.actions.column; if (actionMap.has(index)) { - const existingActions = actionMap.get(index); + const existingActions = actionMap.get(index)!; actionMap.set(index, [...existingActions, ...actions]); } else { actionMap.set(index, actions); @@ -145,8 +155,8 @@ export namespace Table { * @param headers The headers to add to the existing header list * @returns Whether the webview successfully received the list of headers */ - public async addHeaders(...headers: string[]): Promise { - this.data.headers.push(...headers); + public async addColumns(...columns: string[]): Promise { + this.data.columns.push(...columns.map((column) => ({ field: column }))); return this.updateWebview(); } @@ -185,8 +195,8 @@ export namespace Table { * @param headers The new headers to use for the table * @returns Whether the webview successfully received the new headers */ - public async setHeaders(headers: string[]): Promise { - this.data.headers = headers; + public async setColumns(columns: string[]): Promise { + this.data.columns = columns.map((column) => ({ field: column })); return this.updateWebview(); } diff --git a/packages/zowe-explorer-api/src/vscode/ui/index.ts b/packages/zowe-explorer-api/src/vscode/ui/index.ts index baab4f66dc..11f482ea44 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/index.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/index.ts @@ -9,5 +9,7 @@ * */ +export * from "./utils/TableBuilder"; +export * from "./utils/TableMediator"; export * from "./TableView"; export * from "./WebView"; diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index acbf8c515f..24bd557574 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -34,19 +34,42 @@ import { TableMediator } from "./TableMediator"; */ export class TableBuilder { private context: ExtensionContext; - private data: Table.Data; + private data: Table.Data = { + actions: { + column: new Map(), + row: new Map(), + }, + dividers: { + column: [], + row: [], + }, + columns: [], + rows: [], + title: "", + }; public constructor(context: ExtensionContext) { this.context = context; } /** - * Set the content for the next table. + * Set the title for the next table. + * + * @param name The name of the table + * @returns The same {@link TableBuilder} instance with the title added + */ + public title(name: string): TableBuilder { + this.data.title = name; + return this; + } + + /** + * Set the rows for the next table. * * @param rows The rows of content to use for the table - * @returns The same {@link TableBuilder} instance with the content added + * @returns The same {@link TableBuilder} instance with the rows added */ - public content(...rows: Table.RowContent[]): TableBuilder { + public rows(...rows: Table.RowContent[]): TableBuilder { this.data.rows = rows; return this; } @@ -57,8 +80,8 @@ export class TableBuilder { * @param rows The headers to use for the table * @returns The same {@link TableBuilder} instance with the headers added */ - public headers(newHeaders: string[]): TableBuilder { - this.data.headers = newHeaders; + public columns(newColumns: string[]): TableBuilder { + this.data.columns = newColumns.map((column) => ({ field: column })); return this; } @@ -185,7 +208,7 @@ export class TableBuilder { this.data.dividers.row = []; this.data.dividers.column = []; - this.data.headers = []; + this.data.columns = []; this.data.rows = []; this.data.title = ""; } diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index 9f32ebf7b6..576a2fbd5c 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -10,7 +10,7 @@ */ import * as vscode from "vscode"; -import { FileManagement, IZoweTree, IZoweTreeNode, Validation, ZoweScheme } from "@zowe/zowe-explorer-api"; +import { FileManagement, IZoweTree, IZoweTreeNode, TableBuilder, Validation, ZoweScheme } from "@zowe/zowe-explorer-api"; import { SharedActions } from "./SharedActions"; import { SharedHistoryView } from "./SharedHistoryView"; import { SharedTreeProviders } from "./SharedTreeProviders"; @@ -32,7 +32,6 @@ import { ProfilesUtils } from "../../utils/ProfilesUtils"; import { DatasetFSProvider } from "../dataset/DatasetFSProvider"; import { ExtensionUtils } from "../../utils/ExtensionUtils"; import type { Definitions } from "../../configuration/Definitions"; -import { Table } from "@zowe/zowe-explorer-api"; export class SharedInit { public static registerRefreshCommand( @@ -225,8 +224,23 @@ export class SharedInit { }) ); context.subscriptions.push( - vscode.commands.registerCommand("zowe.tableView", () => { - new Table.View(context, {} as any); + vscode.commands.registerCommand("zowe.tableView", async () => { + new TableBuilder(context) + .title("Cars for sale") + .rows( + { + make: "Ford", + model: "Model T", + year: 1908, + }, + { + make: "Tesla", + model: "Model Y", + year: 2022, + } + ) + .columns(["make", "model", "year"]) + .build(); }) ); // initialize the Constants.filesToCompare array during initialization diff --git a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx index 27f5932225..1d04eb1f7a 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx @@ -11,40 +11,49 @@ import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid import "ag-grid-community/styles/ag-theme-quartz.css"; // Optional Theme -import { AgGridReact, AgGridReactProps } from "ag-grid-react"; +import { AgGridReact } from "ag-grid-react"; import { useEffect, useState } from "preact/hooks"; import { isSecureOrigin } from "../utils"; +const vscodeApi = acquireVsCodeApi(); + export function App() { - const [rowData, _setRowData] = useState([ - { make: "Tesla", model: "Model Y", price: 64950, electric: true }, - { make: "Ford", model: "F-Series", price: 33850, electric: false }, - { make: "Toyota", model: "Corolla", price: 29600, electric: false }, - ]); - - // Column Definitions: Defines the columns to be displayed. - const [colDefs, _setColDefs] = useState["columnDefs"]>([ - { field: "make" }, - { field: "model" }, - { field: "price" }, - { field: "electric" }, - ]); + const [tableData, setTableData] = useState({ + rows: null, + columns: null, + title: "", + }); useEffect(() => { - window.addEventListener("message", (event): void => { + window.addEventListener("message", (event: any): void => { if (!isSecureOrigin(event.origin)) { return; } + + if (!("data" in event)) { + return; + } + + const eventInfo = event.data; + + switch (eventInfo.command) { + case "ondatachanged": + setTableData(eventInfo.data); + break; + default: + break; + } }); + vscodeApi.postMessage({ command: "ready" }); }, []); return ( // wrapping container with theme & size
- +
); } From 596000f0735a18a7241f28fc061560e61bcc22a6 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 11 Jun 2024 15:48:24 -0400 Subject: [PATCH 007/107] feat: enable pagination in table view Signed-off-by: Trae Yelovich --- .../src/vscode/ui/TableView.ts | 4 +-- .../src/trees/shared/SharedInit.ts | 3 +- .../src/webviews/src/table-view/App.tsx | 28 +++++++++++++------ 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 563b93e0fd..d00535b353 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -35,9 +35,9 @@ export namespace Table { row: number[]; }; // Column headers for the top of the table - columns: { field: string }[]; + columns: { field: string }[] | null; // The row data for the table. Each row contains a set of variables corresponding to the data for each column in that row - rows: RowContent[]; + rows: RowContent[] | null; // The display title for the table title?: string; }; diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index 576a2fbd5c..7d8e6b1081 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -225,7 +225,7 @@ export class SharedInit { ); context.subscriptions.push( vscode.commands.registerCommand("zowe.tableView", async () => { - new TableBuilder(context) + const table = new TableBuilder(context) .title("Cars for sale") .rows( { @@ -241,6 +241,7 @@ export class SharedInit { ) .columns(["make", "model", "year"]) .build(); + await table.addContent({ make: "Toyota", model: "Corolla", year: 2007 }); }) ); // initialize the Constants.filesToCompare array during initialization diff --git a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx index 1d04eb1f7a..f93a0c49d4 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx @@ -14,13 +14,22 @@ import "ag-grid-community/styles/ag-theme-quartz.css"; // Optional Theme import { AgGridReact } from "ag-grid-react"; import { useEffect, useState } from "preact/hooks"; import { isSecureOrigin } from "../utils"; +import type { Table } from "@zowe/zowe-explorer-api"; const vscodeApi = acquireVsCodeApi(); export function App() { - const [tableData, setTableData] = useState({ - rows: null, + const [tableData, setTableData] = useState({ + actions: { + column: new Map(), + row: new Map(), + }, + dividers: { + column: [], + row: [], + }, columns: null, + rows: null, title: "", }); @@ -49,11 +58,14 @@ export function App() { return ( // wrapping container with theme & size -
- -
+ <> + {tableData.title ?

{tableData.title}

: null} +
+ +
+ ); } From 728ddfc7ce17de355c5916ba3dfe2c2c9fc0b52e Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 12 Jun 2024 15:02:26 -0400 Subject: [PATCH 008/107] wip: VS Code colors for table, add Table.Instance Signed-off-by: Trae Yelovich --- .../src/vscode/ui/TableView.ts | 6 ++++++ .../src/vscode/ui/WebView.ts | 2 +- .../src/vscode/ui/utils/TableBuilder.ts | 12 +++++------ .../src/vscode/ui/utils/TableMediator.ts | 8 +++---- .../src/webviews/src/table-view/App.tsx | 21 +++++++++++++++---- 5 files changed, 34 insertions(+), 15 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index d00535b353..f478ee372a 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -210,6 +210,12 @@ export namespace Table { this.data.title = title; return this.updateWebview(); } + } + + export class Instance extends View { + public constructor(context: ExtensionContext, data: Table.Data) { + super(context, data); + } /** * Closes the table view and marks it as disposed. diff --git a/packages/zowe-explorer-api/src/vscode/ui/WebView.ts b/packages/zowe-explorer-api/src/vscode/ui/WebView.ts index 0738e99367..9c0d64601f 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/WebView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/WebView.ts @@ -26,7 +26,7 @@ export class WebView { // The webview HTML content to render after filling the HTML template. protected webviewContent: string; - public panel: WebviewPanel; + protected panel: WebviewPanel; // Resource identifiers for the on-disk content and vscode-webview resource. private uris: { diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index 24bd557574..be422af9f3 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -181,19 +181,19 @@ export class TableBuilder { /** * Builds the table with the given data. * - * @returns A new {@link Table.View} instance with the given data/options + * @returns A new {@link Table.Instance} with the given data/options */ - public build(): Table.View { - return new Table.View(this.context, this.data); + public build(): Table.Instance { + return new Table.Instance(this.context, this.data); } /** * Builds the table with the given data and shares it with the TableMediator singleton. * - * @returns A new, **shared** {@link Table.View} instance with the given data/options + * @returns A new, **shared** {@link Table.Instance} with the given data/options */ - public buildAndShare(): Table.View { - const table = new Table.View(this.context, this.data); + public buildAndShare(): Table.Instance { + const table = new Table.Instance(this.context, this.data); TableMediator.getInstance().addTable(table); return table; } diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts index 742b32264c..6527c98cb8 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts @@ -44,6 +44,7 @@ import { Table } from "../TableView"; * * **Note** that this does not prevent an extender with access to the ID from disposing the table once they've received access to the instance. */ + export class TableMediator { private static instance: TableMediator; #tables: Map; @@ -86,18 +87,17 @@ export class TableMediator { } /** - * Removes a table and disposes of it from within the mediator. + * Removes a table from the mediator. + * Note that the * * @param id The unique ID of the table to delete * @returns `true` if the table was deleted; `false` otherwise */ - public deleteTable(id: string): boolean { + public removeTable(id: string): boolean { if (this.#tables.has(id)) { return false; } - const table = this.#tables.get(id); - table.dispose(); return this.#tables.delete(id); } } diff --git a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx index f93a0c49d4..4bc9d9bcae 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx @@ -57,14 +57,27 @@ export function App() { }, []); return ( - // wrapping container with theme & size <> {tableData.title ?

{tableData.title}

: null}
- +
); From 566b8b31ca6db931f983a96bf77be6c525b11a37 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 13 Jun 2024 11:17:05 -0400 Subject: [PATCH 009/107] refactor: move table style/colors into object Signed-off-by: Trae Yelovich --- .../src/webviews/src/table-view/App.tsx | 20 ++----------------- .../src/webviews/src/table-view/types.ts | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 18 deletions(-) create mode 100644 packages/zowe-explorer/src/webviews/src/table-view/types.ts diff --git a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx index 4bc9d9bcae..75ef28696b 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx @@ -15,6 +15,7 @@ import { AgGridReact } from "ag-grid-react"; import { useEffect, useState } from "preact/hooks"; import { isSecureOrigin } from "../utils"; import type { Table } from "@zowe/zowe-explorer-api"; +import { tableStyle } from "./types"; const vscodeApi = acquireVsCodeApi(); @@ -59,24 +60,7 @@ export function App() { return ( <> {tableData.title ?

{tableData.title}

: null} -
+
diff --git a/packages/zowe-explorer/src/webviews/src/table-view/types.ts b/packages/zowe-explorer/src/webviews/src/table-view/types.ts new file mode 100644 index 0000000000..80d197c19f --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/table-view/types.ts @@ -0,0 +1,20 @@ + +const tableColors = { + "--ag-icon-font-family": "agGridQuartz", + "--ag-row-hover-color": "var(--vscode-list-hoverBackground)", + "--ag-range-selection-background-color": "var(--vscode-list-activeSelectionBackground)", + "--ag-range-selection-highlight-color": "var(--vscode-list-activeSelectionForeground)", + "--ag-background-color": "var(--vscode-editor-background)", + "--ag-control-panel-background-color": "var(--vscode-editorWidget-background)", + "--ag-border-color": "var(--vscode-editorWidget-border)", + "--ag-header-background-color": "var(--vscode-keybindingTable-headerBackground)", + "--ag-range-selection-border-color": "var(--vscode-tab-activeForeground)", + "--ag-foreground-color": "var(--vscode-foreground)", + "--ag-selected-row-background-color": "var(--vscode-notebook-selectedCellBackground)", +}; + +export const tableStyle = { + height: 500, + marginTop: "1em", + ...tableColors +}; \ No newline at end of file From ccbdbefb42ed707f20a8248fb01a3e1957f886ff Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 13 Jun 2024 11:41:36 -0400 Subject: [PATCH 010/107] chore: add license to table types Signed-off-by: Trae Yelovich --- .../src/webviews/src/table-view/types.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/zowe-explorer/src/webviews/src/table-view/types.ts b/packages/zowe-explorer/src/webviews/src/table-view/types.ts index 80d197c19f..40d7b2c578 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/types.ts +++ b/packages/zowe-explorer/src/webviews/src/table-view/types.ts @@ -1,3 +1,14 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + const tableColors = { "--ag-icon-font-family": "agGridQuartz", From 78adc5cea1fe4c9767a83e603533eb9829af7bd9 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Fri, 14 Jun 2024 15:15:57 -0400 Subject: [PATCH 011/107] feat: TableViewProvider impl. Signed-off-by: Trae Yelovich --- .../src/vscode/ui/TableView.ts | 12 +++++-- .../src/vscode/ui/TableViewProvider.ts | 31 +++++++++++++++++++ .../src/vscode/ui/WebView.ts | 14 +++++---- 3 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index f478ee372a..97024b6550 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -9,7 +9,7 @@ * */ -import { WebView } from "./WebView"; +import { UriPair, WebView } from "./WebView"; import { Event, EventEmitter, ExtensionContext } from "vscode"; import { AnyComponent, JSX } from "preact"; import { randomUUID } from "crypto"; @@ -56,6 +56,14 @@ export namespace Table { private onTableDataReceived: Event; private onTableDisplayChanged: EventEmitter; + public getUris(): UriPair { + return this.uris; + } + + public getHtml(): string { + return this.htmlContent; + } + public constructor(context: ExtensionContext, data?: Data) { super(data.title ?? "Table view", "table-view", context); this.panel.webview.onDidReceiveMessage((message) => this.onMessageReceived(message)); @@ -70,7 +78,7 @@ export namespace Table { * * @param message The message received from the webview */ - private async onMessageReceived(message: any): Promise { + public async onMessageReceived(message: any): Promise { if (!("command" in message)) { return; } diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts b/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts new file mode 100644 index 0000000000..d422e4cd45 --- /dev/null +++ b/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts @@ -0,0 +1,31 @@ +import { CancellationToken, WebviewView, WebviewViewProvider, WebviewViewResolveContext } from "vscode"; +import { Table } from "./TableView"; + +export class TableViewProvider implements WebviewViewProvider { + private view: WebviewView; + private tableView: Table.View; + + public setTableView(tableView: Table.View): void { + this.tableView = tableView; + } + + public getTableView(): Table.View { + return this.tableView; + } + + public resolveWebviewView( + webviewView: WebviewView, + context: WebviewViewResolveContext, + token: CancellationToken + ): void | Thenable { + this.view = webviewView; + + this.view.webview.options = { + enableScripts: true, + localResourceRoots: [this.tableView.getUris().disk.build], + }; + + this.view.webview.html = this.tableView.getHtml(); + this.view.webview.onDidReceiveMessage((data) => this.tableView.onMessageReceived(data)); + } +} diff --git a/packages/zowe-explorer-api/src/vscode/ui/WebView.ts b/packages/zowe-explorer-api/src/vscode/ui/WebView.ts index 9c0d64601f..11d700ceca 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/WebView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/WebView.ts @@ -21,6 +21,11 @@ export type WebViewOpts = { retainContext: boolean; }; +export type UriPair = { + disk?: Types.WebviewUris; + resource?: Types.WebviewUris; +}; + export class WebView { protected disposables: Disposable[]; @@ -29,10 +34,7 @@ export class WebView { protected panel: WebviewPanel; // Resource identifiers for the on-disk content and vscode-webview resource. - private uris: { - disk?: Types.WebviewUris; - resource?: Types.WebviewUris; - } = {}; + protected uris: UriPair = {}; // Unique identifier private nonce: string; @@ -70,7 +72,7 @@ export class WebView { this.uris.disk = { build: Uri.file(joinPath(context.extensionPath, "src", "webviews")), script: Uri.file(joinPath(context.extensionPath, "src", "webviews", "dist", webviewName, `${webviewName}.js`)), - css: cssExists ? Uri.file(cssPath) : undefined + css: cssExists ? Uri.file(cssPath) : undefined, }; this.panel = window.createWebviewPanel("ZEAPIWebview", this.title, ViewColumn.Beside, { @@ -83,7 +85,7 @@ export class WebView { this.uris.resource = { build: this.panel.webview.asWebviewUri(this.uris.disk.build), script: this.panel.webview.asWebviewUri(this.uris.disk.script), - css: this.uris.disk.css ? this.panel.webview.asWebviewUri(this.uris.disk.css) : undefined + css: this.uris.disk.css ? this.panel.webview.asWebviewUri(this.uris.disk.css) : undefined, }; const template = Handlebars.compile(HTMLTemplate); From 8448bd92b2d98afdb1a8c89c131fba55a2efa327 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 18 Jun 2024 09:04:14 -0400 Subject: [PATCH 012/107] TableViewProvider: update html in setTableView Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts b/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts index d422e4cd45..035b139eb8 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts @@ -7,6 +7,9 @@ export class TableViewProvider implements WebviewViewProvider { public setTableView(tableView: Table.View): void { this.tableView = tableView; + if (this.view && this.view.webview.html !== this.tableView.getHtml()) { + this.view.webview.html = this.tableView.getHtml(); + } } public getTableView(): Table.View { From 2cec54595aae902d6a68b3ec0d018b773525d435 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 18 Jun 2024 13:00:56 -0400 Subject: [PATCH 013/107] wip: table view examples; feat: add column type Signed-off-by: Trae Yelovich --- .../src/vscode/ui/TableView.ts | 11 ++++++----- .../src/vscode/ui/TableViewProvider.ts | 11 +++++++++++ .../src/vscode/ui/utils/TableBuilder.ts | 4 ++-- packages/zowe-explorer/package.json | 12 +++++++++++- .../src/trees/shared/SharedInit.ts | 17 ++++++++++++++++- 5 files changed, 46 insertions(+), 9 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 97024b6550..9f1010b2a4 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -19,6 +19,7 @@ export namespace Table { export type Axes = "row" | "column"; export type RowContent = Record; + export type Column = { field: string; filter?: boolean }; export type Dividers = { rows: number[]; columns: number[]; @@ -35,7 +36,7 @@ export namespace Table { row: number[]; }; // Column headers for the top of the table - columns: { field: string }[] | null; + columns: Column[] | null; // The row data for the table. Each row contains a set of variables corresponding to the data for each column in that row rows: RowContent[] | null; // The display title for the table @@ -163,8 +164,8 @@ export namespace Table { * @param headers The headers to add to the existing header list * @returns Whether the webview successfully received the list of headers */ - public async addColumns(...columns: string[]): Promise { - this.data.columns.push(...columns.map((column) => ({ field: column }))); + public async addColumns(...columns: Column[]): Promise { + this.data.columns.push(...columns); return this.updateWebview(); } @@ -203,8 +204,8 @@ export namespace Table { * @param headers The new headers to use for the table * @returns Whether the webview successfully received the new headers */ - public async setColumns(columns: string[]): Promise { - this.data.columns = columns.map((column) => ({ field: column })); + public async setColumns(columns: Column[]): Promise { + this.data.columns = columns; return this.updateWebview(); } diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts b/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts index 035b139eb8..a448365224 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts @@ -1,3 +1,14 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + import { CancellationToken, WebviewView, WebviewViewProvider, WebviewViewResolveContext } from "vscode"; import { Table } from "./TableView"; diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index be422af9f3..0fbe0167a7 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -80,8 +80,8 @@ export class TableBuilder { * @param rows The headers to use for the table * @returns The same {@link TableBuilder} instance with the headers added */ - public columns(newColumns: string[]): TableBuilder { - this.data.columns = newColumns.map((column) => ({ field: column })); + public columns(newColumns: Table.Column[]): TableBuilder { + this.data.columns = newColumns; return this; } diff --git a/packages/zowe-explorer/package.json b/packages/zowe-explorer/package.json index 4a87e6ca79..7fb3cdecb8 100644 --- a/packages/zowe-explorer/package.json +++ b/packages/zowe-explorer/package.json @@ -857,7 +857,17 @@ }, { "command": "zowe.tableView", - "title": "Open table view", + "title": "Show table view (basic)", + "category": "Zowe Explorer" + }, + { + "command": "zowe.tableView2", + "title": "Show table view (several entries)", + "category": "Zowe Explorer" + }, + { + "command": "zowe.tableView3", + "title": "Show table view (all features)", "category": "Zowe Explorer" } ], diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index 7d8e6b1081..ef13782c74 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -32,6 +32,7 @@ import { ProfilesUtils } from "../../utils/ProfilesUtils"; import { DatasetFSProvider } from "../dataset/DatasetFSProvider"; import { ExtensionUtils } from "../../utils/ExtensionUtils"; import type { Definitions } from "../../configuration/Definitions"; +import { randomInt, randomUUID } from "crypto"; export class SharedInit { public static registerRefreshCommand( @@ -239,11 +240,25 @@ export class SharedInit { year: 2022, } ) - .columns(["make", "model", "year"]) + .columns([{ field: "make" }, { field: "model" }, { field: "year" }]) .build(); await table.addContent({ make: "Toyota", model: "Corolla", year: 2007 }); }) ); + context.subscriptions.push( + vscode.commands.registerCommand("zowe.tableView2", () => { + new TableBuilder(context) + .title("Random data") + .rows( + ...Array.from({ length: 1024 }, (val) => ({ + name: randomUUID(), + value: randomInt(2147483647), + })) + ) + .columns([{ field: "name", filter: true }, { field: "value" }]) + .build(); + }) + ); // initialize the Constants.filesToCompare array during initialization LocalFileManagement.resetCompareSelection(); } From 372c6b3993796d50d8d60ba601c9179622d9019d Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 18 Jun 2024 15:21:48 -0400 Subject: [PATCH 014/107] refactor(table): remove dividers, wip: consistent style Signed-off-by: Trae Yelovich --- .../src/vscode/ui/TableView.ts | 35 ---------------- .../src/vscode/ui/utils/TableBuilder.ts | 41 ------------------- .../src/trees/shared/SharedInit.ts | 22 +++++++++- .../src/webviews/src/table-view/App.tsx | 8 +--- .../src/webviews/src/table-view/types.ts | 14 ++++++- pnpm-lock.yaml | 34 +++++++++------ 6 files changed, 57 insertions(+), 97 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 9f1010b2a4..1e71ea19ce 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -30,11 +30,6 @@ export namespace Table { column: Map; row: Map; }; - // Dividers to place within the table at a specific row or column index - dividers: { - column: number[]; - row: number[]; - }; // Column headers for the top of the table columns: Column[] | null; // The row data for the table. Each row contains a set of variables corresponding to the data for each column in that row @@ -146,18 +141,6 @@ export namespace Table { return this.updateWebview(); } - /** - * Adds a divider to the given row and column - * - * @param axis The axis to add the divider to - * @param index The index on the axis where the divider should be added - * @returns Whether the webview successfully received the new divider - */ - public async addDivider(axis: Axes, index: number): Promise { - (axis === "row" ? this.data.dividers.row : this.data.dividers.column).push(index); - return this.updateWebview(); - } - /** * Adds headers to the end of the existing header list in the table view. * @@ -180,24 +163,6 @@ export namespace Table { return this.updateWebview(); } - /** - * Sets the dividers for the table; replaces any pre-existing dividers. - * - * @param rows The row dividers to use for the table - * @param columns The column dividers to use for the table - * @returns Whether the webview successfully received the new dividers - */ - public async setDividers(dividers: Pick | Pick): Promise { - if ("rows" in dividers) { - this.data.dividers.row = dividers.rows; - } - if ("columns" in dividers) { - this.data.dividers.column = dividers.columns; - } - - return this.updateWebview(); - } - /** * Sets the headers for the table. * diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index 0fbe0167a7..59d8e4b440 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -39,10 +39,6 @@ export class TableBuilder { column: new Map(), row: new Map(), }, - dividers: { - column: [], - row: [], - }, columns: [], rows: [], title: "", @@ -144,40 +140,6 @@ export class TableBuilder { return this; } - /** - * Adds a divider to the table at the specified axis and the given index. - * - * @param axis The axis to add a divider to (either "row" or "column") - * @param index The index where the divider should be added - * @returns The same {@link TableBuilder} instance with the divider added - */ - private divider(axis: Table.Axes, index: number): TableBuilder { - (axis === "row" ? this.data.dividers.row : this.data.dividers.column).push(index); - return this; - } - - /** - * Adds a divider to the table at the given row index. - * - * @param index The index where the divider should be added - * @returns The same {@link TableBuilder} instance with the row divider added - */ - public rowDivider(index: number): TableBuilder { - this.divider("row", index); - return this; - } - - /** - * Adds a divider to the table at the given column index. - * - * @param index The index where the divider should be added - * @returns The same {@link TableBuilder} instance with the column divider added - */ - public columnDivider(index: number): TableBuilder { - this.divider("column", index); - return this; - } - /** * Builds the table with the given data. * @@ -205,9 +167,6 @@ export class TableBuilder { this.data.actions.row.clear(); this.data.actions.column.clear(); - this.data.dividers.row = []; - this.data.dividers.column = []; - this.data.columns = []; this.data.rows = []; this.data.title = ""; diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index ef13782c74..0eaa1af24b 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -255,7 +255,27 @@ export class SharedInit { value: randomInt(2147483647), })) ) - .columns([{ field: "name", filter: true }, { field: "value" }]) + .columns([ + { field: "name", filter: true }, + { field: "value", filter: true }, + ]) + .build(); + }) + ); + context.subscriptions.push( + vscode.commands.registerCommand("zowe.tableView3", () => { + new TableBuilder(context) + .title("Comprehensive table with actions") + .rows( + ...Array.from({ length: 1024 }, (val) => ({ + name: randomUUID(), + value: randomInt(2147483647), + })) + ) + .columns([ + { field: "name", filter: true }, + { field: "value", filter: true }, + ]) .build(); }) ); diff --git a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx index 75ef28696b..f113b7b6f4 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx @@ -15,7 +15,7 @@ import { AgGridReact } from "ag-grid-react"; import { useEffect, useState } from "preact/hooks"; import { isSecureOrigin } from "../utils"; import type { Table } from "@zowe/zowe-explorer-api"; -import { tableStyle } from "./types"; +import { tableProps, tableStyle } from "./types"; const vscodeApi = acquireVsCodeApi(); @@ -25,10 +25,6 @@ export function App() { column: new Map(), row: new Map(), }, - dividers: { - column: [], - row: [], - }, columns: null, rows: null, title: "", @@ -61,7 +57,7 @@ export function App() { <> {tableData.title ?

{tableData.title}

: null}
- +
); diff --git a/packages/zowe-explorer/src/webviews/src/table-view/types.ts b/packages/zowe-explorer/src/webviews/src/table-view/types.ts index 40d7b2c578..87681536c4 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/types.ts +++ b/packages/zowe-explorer/src/webviews/src/table-view/types.ts @@ -9,6 +9,8 @@ * */ +import type { Table } from "@zowe/zowe-explorer-api"; +import { AgGridReactProps } from "ag-grid-react"; const tableColors = { "--ag-icon-font-family": "agGridQuartz", @@ -27,5 +29,13 @@ const tableColors = { export const tableStyle = { height: 500, marginTop: "1em", - ...tableColors -}; \ No newline at end of file + ...tableColors, +}; + +export const tableProps = (tableData: Table.Data): Partial => ({ + enableCellTextSelection: true, + ensureDomOrder: true, + rowData: tableData.rows, + columnDefs: tableData.columns, + pagination: true, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2aafa1c350..6fb4ffd104 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -327,10 +327,10 @@ importers: version: 5.4.5 vite: specifier: ^4.5.3 - version: 4.5.3(@types/node@18.19.33) + version: 4.5.3 vite-plugin-checker: specifier: ^0.6.4 - version: 0.6.4(eslint@8.57.0)(typescript@5.4.5)(vite@4.5.3) + version: 0.6.4(typescript@5.4.5)(vite@4.5.3) packages: @@ -522,7 +522,7 @@ packages: '@babel/traverse': 7.24.5 '@babel/types': 7.24.5 convert-source-map: 2.0.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -843,7 +843,7 @@ packages: '@babel/helper-split-export-declaration': 7.24.5 '@babel/parser': 7.24.5 '@babel/types': 7.24.5 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -1870,14 +1870,14 @@ packages: '@prefresh/vite': 2.4.5(preact@10.22.0)(vite@4.5.3) '@rollup/pluginutils': 4.2.1 babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.24.5) - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4 kolorist: 1.8.0 magic-string: 0.30.5 node-html-parser: 6.1.13 resolve: 1.22.8 source-map: 0.7.4 stack-trace: 1.0.0-pre2 - vite: 4.5.3(@types/node@18.19.33) + vite: 4.5.3 transitivePeerDependencies: - preact - supports-color @@ -1911,7 +1911,7 @@ packages: '@prefresh/utils': 1.2.0 '@rollup/pluginutils': 4.2.1 preact: 10.22.0 - vite: 4.5.3(@types/node@18.19.33) + vite: 4.5.3 transitivePeerDependencies: - supports-color dev: true @@ -3917,6 +3917,18 @@ packages: ms: 2.0.0 dev: true + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + /debug@4.3.4(supports-color@8.1.1): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -9690,7 +9702,7 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dev: false - /vite-plugin-checker@0.6.4(eslint@8.57.0)(typescript@5.4.5)(vite@4.5.3): + /vite-plugin-checker@0.6.4(typescript@5.4.5)(vite@4.5.3): resolution: {integrity: sha512-2zKHH5oxr+ye43nReRbC2fny1nyARwhxdm0uNYp/ERy4YvU9iZpNOsueoi/luXw5gnpqRSvjcEPxXbS153O2wA==} engines: {node: '>=14.16'} peerDependencies: @@ -9726,7 +9738,6 @@ packages: chalk: 4.1.2 chokidar: 3.6.0 commander: 8.3.0 - eslint: 8.57.0 fast-glob: 3.3.2 fs-extra: 11.2.0 npm-run-path: 4.0.1 @@ -9734,14 +9745,14 @@ packages: strip-ansi: 6.0.1 tiny-invariant: 1.3.3 typescript: 5.4.5 - vite: 4.5.3(@types/node@18.19.33) + vite: 4.5.3 vscode-languageclient: 7.0.0 vscode-languageserver: 7.0.0 vscode-languageserver-textdocument: 1.0.11 vscode-uri: 3.0.8 dev: true - /vite@4.5.3(@types/node@18.19.33): + /vite@4.5.3: resolution: {integrity: sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -9769,7 +9780,6 @@ packages: terser: optional: true dependencies: - '@types/node': 18.19.33 esbuild: 0.18.20 postcss: 8.4.38 rollup: 3.29.4 From 110f192b78fcc022847c152b21f2417ba2254f10 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 18 Jun 2024 15:42:35 -0400 Subject: [PATCH 015/107] fix(table): Use CSS variables & support AG Grid icons in webview Signed-off-by: Trae Yelovich --- .../src/vscode/ui/utils/HTMLTemplate.ts | 2 +- .../src/webviews/src/table-view/App.tsx | 5 +++-- .../src/webviews/src/table-view/style.css | 19 ++++++++++++++++++ .../src/webviews/src/table-view/types.ts | 20 ------------------- 4 files changed, 23 insertions(+), 23 deletions(-) create mode 100644 packages/zowe-explorer/src/webviews/src/table-view/style.css diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/HTMLTemplate.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/HTMLTemplate.ts index 7dc152c2ca..aeedf2a920 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/HTMLTemplate.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/HTMLTemplate.ts @@ -21,7 +21,7 @@ const HTMLTemplate: string = ` diff --git a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx index f113b7b6f4..eb3a667875 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx @@ -15,7 +15,8 @@ import { AgGridReact } from "ag-grid-react"; import { useEffect, useState } from "preact/hooks"; import { isSecureOrigin } from "../utils"; import type { Table } from "@zowe/zowe-explorer-api"; -import { tableProps, tableStyle } from "./types"; +import { tableProps } from "./types"; +import "./style.css"; const vscodeApi = acquireVsCodeApi(); @@ -56,7 +57,7 @@ export function App() { return ( <> {tableData.title ?

{tableData.title}

: null} -
+
diff --git a/packages/zowe-explorer/src/webviews/src/table-view/style.css b/packages/zowe-explorer/src/webviews/src/table-view/style.css new file mode 100644 index 0000000000..6e43eaa373 --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/table-view/style.css @@ -0,0 +1,19 @@ +.ag-theme-vsc { + height: 50vh; + margin-top: 1em; + --ag-icon-font-family: "agGridQuartz"; + --ag-row-hover-color: var(--vscode-list-hoverBackground); + --ag-range-selection-background-color: var(--vscode-list-activeSelectionBackground); + --ag-range-selection-highlight-color: var(--vscode-list-activeSelectionForeground); + --ag-background-color: var(--vscode-editor-background); + --ag-control-panel-background-color: var(--vscode-editorWidget-background); + --ag-border-color: var(--vscode-editorWidget-border); + --ag-header-background-color: var(--vscode-keybindingTable-headerBackground); + --ag-range-selection-border-color: var(--vscode-tab-activeForeground); + --ag-foreground-color: var(--vscode-foreground); + --ag-selected-row-background-color: var(--vscode-notebook-selectedCellBackground); +} + +.ag-theme-vsc.ag-popup { + position: absolute; +} diff --git a/packages/zowe-explorer/src/webviews/src/table-view/types.ts b/packages/zowe-explorer/src/webviews/src/table-view/types.ts index 87681536c4..bdbcb70ec6 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/types.ts +++ b/packages/zowe-explorer/src/webviews/src/table-view/types.ts @@ -12,26 +12,6 @@ import type { Table } from "@zowe/zowe-explorer-api"; import { AgGridReactProps } from "ag-grid-react"; -const tableColors = { - "--ag-icon-font-family": "agGridQuartz", - "--ag-row-hover-color": "var(--vscode-list-hoverBackground)", - "--ag-range-selection-background-color": "var(--vscode-list-activeSelectionBackground)", - "--ag-range-selection-highlight-color": "var(--vscode-list-activeSelectionForeground)", - "--ag-background-color": "var(--vscode-editor-background)", - "--ag-control-panel-background-color": "var(--vscode-editorWidget-background)", - "--ag-border-color": "var(--vscode-editorWidget-border)", - "--ag-header-background-color": "var(--vscode-keybindingTable-headerBackground)", - "--ag-range-selection-border-color": "var(--vscode-tab-activeForeground)", - "--ag-foreground-color": "var(--vscode-foreground)", - "--ag-selected-row-background-color": "var(--vscode-notebook-selectedCellBackground)", -}; - -export const tableStyle = { - height: 500, - marginTop: "1em", - ...tableColors, -}; - export const tableProps = (tableData: Table.Data): Partial => ({ enableCellTextSelection: true, ensureDomOrder: true, From d13c0315276254ee1ff47968fc9b2a4852c08d26 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 18 Jun 2024 15:51:20 -0400 Subject: [PATCH 016/107] chore: comments for table view Signed-off-by: Trae Yelovich --- packages/zowe-explorer/src/webviews/src/table-view/App.tsx | 7 +++++-- .../zowe-explorer/src/webviews/src/table-view/types.ts | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx index eb3a667875..3d3daeb4d4 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx @@ -9,13 +9,16 @@ * */ -import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid -import "ag-grid-community/styles/ag-theme-quartz.css"; // Optional Theme +// Required CSS for AG Grid +import "ag-grid-community/styles/ag-grid.css"; +// AG Grid Quartz Theme (used as base theme) +import "ag-grid-community/styles/ag-theme-quartz.css"; import { AgGridReact } from "ag-grid-react"; import { useEffect, useState } from "preact/hooks"; import { isSecureOrigin } from "../utils"; import type { Table } from "@zowe/zowe-explorer-api"; import { tableProps } from "./types"; +// Custom styling (font family, VS Code color scheme, etc.) import "./style.css"; const vscodeApi = acquireVsCodeApi(); diff --git a/packages/zowe-explorer/src/webviews/src/table-view/types.ts b/packages/zowe-explorer/src/webviews/src/table-view/types.ts index bdbcb70ec6..04a1dc13fd 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/types.ts +++ b/packages/zowe-explorer/src/webviews/src/table-view/types.ts @@ -12,6 +12,7 @@ import type { Table } from "@zowe/zowe-explorer-api"; import { AgGridReactProps } from "ag-grid-react"; +// Define props for the AG Grid table here export const tableProps = (tableData: Table.Data): Partial => ({ enableCellTextSelection: true, ensureDomOrder: true, From 9fcf48f756693ac8f4bfc7919ba3d7b35fbaea0b Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 18 Jun 2024 16:19:28 -0400 Subject: [PATCH 017/107] fix(table): add tooltip css style, update on theme change Signed-off-by: Trae Yelovich --- .../src/webviews/src/table-view/App.tsx | 13 ++++++++++++- .../src/webviews/src/table-view/style.css | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx index 3d3daeb4d4..a9109e1317 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx @@ -33,8 +33,13 @@ export function App() { rows: null, title: "", }); + const [baseTheme, setBaseTheme] = useState("ag-theme-quartz"); useEffect(() => { + const userTheme = document.body.getAttribute("data-vscode-theme-kind"); + if (userTheme === "vscode-dark") { + setBaseTheme("ag-theme-quartz-dark"); + } window.addEventListener("message", (event: any): void => { if (!isSecureOrigin(event.origin)) { return; @@ -55,12 +60,18 @@ export function App() { } }); vscodeApi.postMessage({ command: "ready" }); + + const mutationObserver = new MutationObserver((_mutations, _observer) => { + const themeAttr = document.body.getAttribute("data-vscode-theme-kind"); + setBaseTheme(themeAttr === "vscode-dark" ? "ag-theme-quartz-dark" : "ag-theme-quartz"); + }); + mutationObserver.observe(document.body, { attributes: true }); }, []); return ( <> {tableData.title ?

{tableData.title}

: null} -
+
diff --git a/packages/zowe-explorer/src/webviews/src/table-view/style.css b/packages/zowe-explorer/src/webviews/src/table-view/style.css index 6e43eaa373..58ffa738ee 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/style.css +++ b/packages/zowe-explorer/src/webviews/src/table-view/style.css @@ -7,6 +7,7 @@ --ag-range-selection-highlight-color: var(--vscode-list-activeSelectionForeground); --ag-background-color: var(--vscode-editor-background); --ag-control-panel-background-color: var(--vscode-editorWidget-background); + --ag-tooltip-background-color: var(--vscode-editorWidget-background); --ag-border-color: var(--vscode-editorWidget-border); --ag-header-background-color: var(--vscode-keybindingTable-headerBackground); --ag-range-selection-border-color: var(--vscode-tab-activeForeground); From a9bb2b2eab30d1375caf250fc49994c53b75044f Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 19 Jun 2024 13:50:00 -0400 Subject: [PATCH 018/107] fix(table): Update theme auto-detection to handle high contrast Signed-off-by: Trae Yelovich --- packages/zowe-explorer/src/webviews/src/table-view/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx index a9109e1317..5c6064f107 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx @@ -37,7 +37,7 @@ export function App() { useEffect(() => { const userTheme = document.body.getAttribute("data-vscode-theme-kind"); - if (userTheme === "vscode-dark") { + if (userTheme !== "vscode-light") { setBaseTheme("ag-theme-quartz-dark"); } window.addEventListener("message", (event: any): void => { @@ -63,7 +63,7 @@ export function App() { const mutationObserver = new MutationObserver((_mutations, _observer) => { const themeAttr = document.body.getAttribute("data-vscode-theme-kind"); - setBaseTheme(themeAttr === "vscode-dark" ? "ag-theme-quartz-dark" : "ag-theme-quartz"); + setBaseTheme(themeAttr === "vscode-light" ? "ag-theme-quartz" : "ag-theme-quartz-dark"); }); mutationObserver.observe(document.body, { attributes: true }); }, []); From f7e556a4537c3b81c87f98bbb7203b3771b09ef3 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 19 Jun 2024 16:03:36 -0400 Subject: [PATCH 019/107] feat: useMutationObserver hook; wip: table actions Signed-off-by: Trae Yelovich --- .../src/vscode/ui/TableView.ts | 7 ++- .../src/trees/shared/SharedInit.ts | 1 + .../src/webviews/src/table-view/App.tsx | 45 +++++++++++++++---- .../zowe-explorer/src/webviews/src/utils.ts | 32 +++++++++---- 4 files changed, 63 insertions(+), 22 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 1e71ea19ce..4c84126fdc 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -11,11 +11,10 @@ import { UriPair, WebView } from "./WebView"; import { Event, EventEmitter, ExtensionContext } from "vscode"; -import { AnyComponent, JSX } from "preact"; import { randomUUID } from "crypto"; export namespace Table { - export type Action = AnyComponent | JSX.Element; + export type Action = string; export type Axes = "row" | "column"; export type RowContent = Record; @@ -31,9 +30,9 @@ export namespace Table { row: Map; }; // Column headers for the top of the table - columns: Column[] | null; + columns: Column[] | null | undefined; // The row data for the table. Each row contains a set of variables corresponding to the data for each column in that row - rows: RowContent[] | null; + rows: RowContent[] | null | undefined; // The display title for the table title?: string; }; diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index 0eaa1af24b..374ca5eb01 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -259,6 +259,7 @@ export class SharedInit { { field: "name", filter: true }, { field: "value", filter: true }, ]) + .rowAction(0, "test") .build(); }) ); diff --git a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx index 5c6064f107..9a5e6c2f80 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx @@ -15,7 +15,7 @@ import "ag-grid-community/styles/ag-grid.css"; import "ag-grid-community/styles/ag-theme-quartz.css"; import { AgGridReact } from "ag-grid-react"; import { useEffect, useState } from "preact/hooks"; -import { isSecureOrigin } from "../utils"; +import { getVsCodeTheme, isSecureOrigin, useMutableObserver } from "../utils"; import type { Table } from "@zowe/zowe-explorer-api"; import { tableProps } from "./types"; // Custom styling (font family, VS Code color scheme, etc.) @@ -36,10 +36,13 @@ export function App() { const [baseTheme, setBaseTheme] = useState("ag-theme-quartz"); useEffect(() => { - const userTheme = document.body.getAttribute("data-vscode-theme-kind"); + // Apply the dark version of the AG Grid theme if the user is using a dark or high-contrast theme in VS Code. + const userTheme = getVsCodeTheme(); if (userTheme !== "vscode-light") { setBaseTheme("ag-theme-quartz-dark"); } + + // Set up event listener to handle data changes being sent to the webview. window.addEventListener("message", (event: any): void => { if (!isSecureOrigin(event.origin)) { return; @@ -50,23 +53,47 @@ export function App() { } const eventInfo = event.data; - switch (eventInfo.command) { case "ondatachanged": - setTableData(eventInfo.data); + const tableData: Table.Data = eventInfo.data; + if (tableData.actions && tableData.actions.row) { + // Add an extra column to the end of the table + const rows = tableData.rows?.map((row) => { + return { ...row, actions: "" }; + }); + const columns = [ + ...(tableData.columns ?? []), + { + field: "actions", + sortable: false, + cellRenderer: (_params: any) => { + return custom cell renderer; + }, + }, + ]; + setTableData({ ...tableData, rows, columns }); + } else { + setTableData(eventInfo.data); + } break; default: break; } }); + + // Once the listener is in place, send a "ready signal" to the TableView instance to handle new data. vscodeApi.postMessage({ command: "ready" }); + }, []); - const mutationObserver = new MutationObserver((_mutations, _observer) => { - const themeAttr = document.body.getAttribute("data-vscode-theme-kind"); + // Observe attributes of the `body` element to detect VS Code theme changes. + useMutableObserver( + document.body, + (_mutations, _observer) => { + const themeAttr = getVsCodeTheme(); setBaseTheme(themeAttr === "vscode-light" ? "ag-theme-quartz" : "ag-theme-quartz-dark"); - }); - mutationObserver.observe(document.body, { attributes: true }); - }, []); + }, + { attributes: true } + ); return ( <> diff --git a/packages/zowe-explorer/src/webviews/src/utils.ts b/packages/zowe-explorer/src/webviews/src/utils.ts index 63e1cdab76..e6c79b0b12 100644 --- a/packages/zowe-explorer/src/webviews/src/utils.ts +++ b/packages/zowe-explorer/src/webviews/src/utils.ts @@ -9,16 +9,30 @@ * */ +import { useEffect } from "preact/hooks"; + +export function getVsCodeTheme(): string | null { + return document.body.getAttribute("data-vscode-theme-kind"); +} + +export const useMutableObserver = (target: Node, callback: MutationCallback, options: MutationObserverInit | undefined = undefined): void => { + useEffect(() => { + const mutationObserver = new MutationObserver((mutations, observer) => callback(mutations, observer)); + mutationObserver.observe(target, options); + return (): void => mutationObserver.disconnect(); + }, [callback, options]); +}; + export function isSecureOrigin(origin: string): boolean { - const eventUrl = new URL(origin); - const isWebUser = - (eventUrl.protocol === document.location.protocol && eventUrl.hostname === document.location.hostname) || - eventUrl.hostname.endsWith(".github.dev"); - const isLocalVSCodeUser = eventUrl.protocol === "vscode-webview:"; + const eventUrl = new URL(origin); + const isWebUser = + (eventUrl.protocol === document.location.protocol && eventUrl.hostname === document.location.hostname) || + eventUrl.hostname.endsWith(".github.dev"); + const isLocalVSCodeUser = eventUrl.protocol === "vscode-webview:"; - if (!isWebUser && !isLocalVSCodeUser) { - return false; - } + if (!isWebUser && !isLocalVSCodeUser) { + return false; + } - return true; + return true; } From 1a6452714f16d64af4b250c4a14fea666c854bad Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 20 Jun 2024 09:46:07 -0400 Subject: [PATCH 020/107] feat: Make TableView its own component, support row actions Signed-off-by: Trae Yelovich --- .../src/vscode/ui/TableView.ts | 29 ++--- .../src/vscode/ui/utils/TableBuilder.ts | 59 ++------- .../src/trees/shared/SharedInit.ts | 2 +- .../src/webviews/src/table-view/App.tsx | 90 +------------- .../src/webviews/src/table-view/TableView.tsx | 113 ++++++++++++++++++ .../src/webviews/src/table-view/types.ts | 8 ++ 6 files changed, 145 insertions(+), 156 deletions(-) create mode 100644 packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 4c84126fdc..419fbdb49f 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -14,21 +14,14 @@ import { Event, EventEmitter, ExtensionContext } from "vscode"; import { randomUUID } from "crypto"; export namespace Table { - export type Action = string; + export type Action = { title: string; command: string }; export type Axes = "row" | "column"; export type RowContent = Record; export type Column = { field: string; filter?: boolean }; - export type Dividers = { - rows: number[]; - columns: number[]; - }; export type Data = { // Actions to apply to the given row or column index - actions: { - column: Map; - row: Map; - }; + actions: Record; // Column headers for the top of the table columns: Column[] | null | undefined; // The row data for the table. Each row contains a set of variables corresponding to the data for each column in that row @@ -111,21 +104,19 @@ export namespace Table { } /** - * Add one or more actions to the given row or column index. + * Add one or more actions to the given row. * - * @param axis The axis to add an action to (either "row" or "column") - * @param index The index where the action should be displayed - * @param actions The actions to add to the given row/column index + * @param index The row index where the action should be displayed + * @param actions The actions to add to the given row * * @returns Whether the webview successfully received the new action(s) */ - public addAction(axis: Axes, index: number, ...actions: Action[]): Promise { - const actionMap = axis === "row" ? this.data.actions.row : this.data.actions.column; - if (actionMap.has(index)) { - const existingActions = actionMap.get(index)!; - actionMap.set(index, [...existingActions, ...actions]); + public addAction(index: number, ...actions: Action[]): Promise { + if (this.data.actions[index]) { + const existingActions = this.data.actions[index]; + this.data.actions[index] = [...existingActions, ...actions]; } else { - actionMap.set(index, actions); + this.data.actions[index] = actions; } return this.updateWebview(); } diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index 59d8e4b440..981330a31b 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -35,10 +35,7 @@ import { TableMediator } from "./TableMediator"; export class TableBuilder { private context: ExtensionContext; private data: Table.Data = { - actions: { - column: new Map(), - row: new Map(), - }, + actions: {}, columns: [], rows: [], title: "", @@ -81,51 +78,14 @@ export class TableBuilder { return this; } - /** - * Add an action for the next table. - * - * @param actionMap the map of indices to {@link Table.Action} arrays to add to for the table - * @param index the index of the row or column to add an action to - */ - private action(actionMap: Map, index: number, action: Table.Action): void { - if (actionMap.has(index)) { - const actions = actionMap.get(index); - actionMap.set(index, [...actions, action]); - } else { - actionMap.set(index, [action]); - } - } - - /** - * Add column actions for the next table. - * - * @param actionMap the map of indices to {@link Table.Action} arrays to use for the table - * @returns The same {@link TableBuilder} instance with the column actions added - */ - public columnActions(actionMap: Map): TableBuilder { - this.data.actions.column = actionMap; - return this; - } - /** * Add row actions for the next table. * - * @param actionMap the map of indices to {@link Table.Action} arrays to use for the table + * @param actions the record of indices to {@link Table.Action} arrays to use for the table * @returns The same {@link TableBuilder} instance with the row actions added */ - public rowActions(actionMap: Map): TableBuilder { - this.data.actions.row = actionMap; - return this; - } - - /** - * Add a column action to the next table. - * - * @param index The column index to add an action to - * @returns The same {@link TableBuilder} instance with the column action added - */ - public columnAction(index: number, action: Table.Action): TableBuilder { - this.action(this.data.actions.column, index, action); + public rowActions(actions: Record): TableBuilder { + this.data.actions = actions; return this; } @@ -136,7 +96,12 @@ export class TableBuilder { * @returns The same {@link TableBuilder} instance with the row action added */ public rowAction(index: number, action: Table.Action): TableBuilder { - this.action(this.data.actions.row, index, action); + if (this.data.actions[index]) { + const actions = this.data.actions[index]; + this.data.actions[index] = [...actions, action]; + } else { + this.data.actions[index] = [action]; + } return this; } @@ -164,9 +129,7 @@ export class TableBuilder { * Resets all data configured in the builder from previously-created table views. */ public reset(): void { - this.data.actions.row.clear(); - this.data.actions.column.clear(); - + this.data.actions = {}; this.data.columns = []; this.data.rows = []; this.data.title = ""; diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index 374ca5eb01..b4a8ab5597 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -259,7 +259,7 @@ export class SharedInit { { field: "name", filter: true }, { field: "value", filter: true }, ]) - .rowAction(0, "test") + .rowAction(1, { title: "Test Button", command: "test" }) .build(); }) ); diff --git a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx index 9a5e6c2f80..5b59fb2c6c 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx @@ -9,98 +9,12 @@ * */ -// Required CSS for AG Grid -import "ag-grid-community/styles/ag-grid.css"; -// AG Grid Quartz Theme (used as base theme) -import "ag-grid-community/styles/ag-theme-quartz.css"; -import { AgGridReact } from "ag-grid-react"; -import { useEffect, useState } from "preact/hooks"; -import { getVsCodeTheme, isSecureOrigin, useMutableObserver } from "../utils"; -import type { Table } from "@zowe/zowe-explorer-api"; -import { tableProps } from "./types"; -// Custom styling (font family, VS Code color scheme, etc.) -import "./style.css"; - -const vscodeApi = acquireVsCodeApi(); +import { TableView } from "./TableView"; export function App() { - const [tableData, setTableData] = useState({ - actions: { - column: new Map(), - row: new Map(), - }, - columns: null, - rows: null, - title: "", - }); - const [baseTheme, setBaseTheme] = useState("ag-theme-quartz"); - - useEffect(() => { - // Apply the dark version of the AG Grid theme if the user is using a dark or high-contrast theme in VS Code. - const userTheme = getVsCodeTheme(); - if (userTheme !== "vscode-light") { - setBaseTheme("ag-theme-quartz-dark"); - } - - // Set up event listener to handle data changes being sent to the webview. - window.addEventListener("message", (event: any): void => { - if (!isSecureOrigin(event.origin)) { - return; - } - - if (!("data" in event)) { - return; - } - - const eventInfo = event.data; - switch (eventInfo.command) { - case "ondatachanged": - const tableData: Table.Data = eventInfo.data; - if (tableData.actions && tableData.actions.row) { - // Add an extra column to the end of the table - const rows = tableData.rows?.map((row) => { - return { ...row, actions: "" }; - }); - const columns = [ - ...(tableData.columns ?? []), - { - field: "actions", - sortable: false, - cellRenderer: (_params: any) => { - return custom cell renderer; - }, - }, - ]; - setTableData({ ...tableData, rows, columns }); - } else { - setTableData(eventInfo.data); - } - break; - default: - break; - } - }); - - // Once the listener is in place, send a "ready signal" to the TableView instance to handle new data. - vscodeApi.postMessage({ command: "ready" }); - }, []); - - // Observe attributes of the `body` element to detect VS Code theme changes. - useMutableObserver( - document.body, - (_mutations, _observer) => { - const themeAttr = getVsCodeTheme(); - setBaseTheme(themeAttr === "vscode-light" ? "ag-theme-quartz" : "ag-theme-quartz-dark"); - }, - { attributes: true } - ); - return ( <> - {tableData.title ?

{tableData.title}

: null} -
- -
+ ); } diff --git a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx new file mode 100644 index 0000000000..6e240fabcc --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx @@ -0,0 +1,113 @@ +// Required CSS for AG Grid +import "ag-grid-community/styles/ag-grid.css"; +// AG Grid Quartz Theme (used as base theme) +import "ag-grid-community/styles/ag-theme-quartz.css"; +import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; +import { AgGridReact } from "ag-grid-react"; +import { useEffect, useState } from "preact/hooks"; +import { getVsCodeTheme, isSecureOrigin, useMutableObserver } from "../utils"; +import type { Table } from "@zowe/zowe-explorer-api"; +import { TableViewProps, tableProps } from "./types"; +// Custom styling (font family, VS Code color scheme, etc.) +import "./style.css"; + +const vscodeApi = acquireVsCodeApi(); + +export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewProps) => { + const [tableData, setTableData] = useState( + data ?? { + actions: {}, + columns: null, + rows: null, + title: "", + } + ); + const [theme, setTheme] = useState(baseTheme ?? "ag-theme-quartz"); + + useEffect(() => { + // Apply the dark version of the AG Grid theme if the user is using a dark or high-contrast theme in VS Code. + const userTheme = getVsCodeTheme(); + if (userTheme !== "vscode-light") { + setTheme("ag-theme-quartz-dark"); + } + + // Set up event listener to handle data changes being sent to the webview. + window.addEventListener("message", (event: any): void => { + if (!isSecureOrigin(event.origin)) { + return; + } + + if (!("data" in event)) { + return; + } + + const response = event.data; + switch (response.command) { + case "ondatachanged": + // Update received from a VS Code extender; update table state + const newData: Table.Data = response.data; + if (newData.actions) { + // Add an extra column to the end of each row if row actions are present + const rows = newData.rows?.map((row) => { + return { ...row, actions: "" }; + }); + const columns = [ + ...(newData.columns ?? []), + { + // Prevent cells from being selectable + cellStyle: { border: "none !important", outline: "none" }, + field: "actions", + sortable: false, + // Support a custom cell renderer for row actions + cellRenderer: + actionsCellRenderer ?? + ((params: any) => { + if (newData.actions[params.rowIndex]) { + return ( + + {newData.actions[params.rowIndex].map((action) => ( + vscodeApi.postMessage({ command: action.command })} style={{ marginRight: "0.25em" }}> + {action.title} + + ))} + + ); + } else { + return <>; + } + }), + }, + ]; + setTableData({ ...newData, rows, columns }); + } else { + setTableData(response.data); + } + break; + default: + break; + } + }); + + // Once the listener is in place, send a "ready signal" to the TableView instance to handle new data. + vscodeApi.postMessage({ command: "ready" }); + }, []); + + // Observe attributes of the `body` element to detect VS Code theme changes. + useMutableObserver( + document.body, + (_mutations, _observer) => { + const themeAttr = getVsCodeTheme(); + setTheme(themeAttr === "vscode-light" ? "ag-theme-quartz" : "ag-theme-quartz-dark"); + }, + { attributes: true } + ); + + return ( + <> + {tableData.title ?

{tableData.title}

: null} +
+ +
+ + ); +}; diff --git a/packages/zowe-explorer/src/webviews/src/table-view/types.ts b/packages/zowe-explorer/src/webviews/src/table-view/types.ts index 04a1dc13fd..bcd787fe29 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/types.ts +++ b/packages/zowe-explorer/src/webviews/src/table-view/types.ts @@ -11,6 +11,14 @@ import type { Table } from "@zowe/zowe-explorer-api"; import { AgGridReactProps } from "ag-grid-react"; +import { JSXInternal } from "preact/src/jsx"; + +type AgGridThemes = "ag-theme-quartz" | "ag-theme-balham" | "ag-theme-material" | "ag-theme-alpine"; +export type TableViewProps = { + actionsCellRenderer?: (params: any) => JSXInternal.Element; + baseTheme?: AgGridThemes | string; + data?: Table.Data; +}; // Define props for the AG Grid table here export const tableProps = (tableData: Table.Data): Partial => ({ From 12605100bb4a7c2d79d16d76afacde83247c2ca6 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 20 Jun 2024 10:55:45 -0400 Subject: [PATCH 021/107] wip: apply 'all' actions to all rows Signed-off-by: Trae Yelovich --- .../zowe-explorer-api/src/vscode/ui/TableView.ts | 6 +++--- .../src/vscode/ui/utils/TableBuilder.ts | 12 ++++++++---- .../zowe-explorer/src/trees/shared/SharedInit.ts | 5 ++++- .../src/webviews/src/table-view/TableView.tsx | 8 ++++++-- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 419fbdb49f..b533a61e41 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -14,14 +14,14 @@ import { Event, EventEmitter, ExtensionContext } from "vscode"; import { randomUUID } from "crypto"; export namespace Table { - export type Action = { title: string; command: string }; + export type Action = { title: string; command: string; type?: "primary" | "secondary" | "icon" }; export type Axes = "row" | "column"; - export type RowContent = Record; + export type RowContent = Record; export type Column = { field: string; filter?: boolean }; export type Data = { // Actions to apply to the given row or column index - actions: Record; + actions: Record; // Column headers for the top of the table columns: Column[] | null | undefined; // The row data for the table. Each row contains a set of variables corresponding to the data for each column in that row diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index 981330a31b..2dd2cdb16d 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -35,7 +35,9 @@ import { TableMediator } from "./TableMediator"; export class TableBuilder { private context: ExtensionContext; private data: Table.Data = { - actions: {}, + actions: { + all: [], + }, columns: [], rows: [], title: "", @@ -84,7 +86,7 @@ export class TableBuilder { * @param actions the record of indices to {@link Table.Action} arrays to use for the table * @returns The same {@link TableBuilder} instance with the row actions added */ - public rowActions(actions: Record): TableBuilder { + public rowActions(actions: Record): TableBuilder { this.data.actions = actions; return this; } @@ -95,7 +97,7 @@ export class TableBuilder { * @param index The column index to add an action to * @returns The same {@link TableBuilder} instance with the row action added */ - public rowAction(index: number, action: Table.Action): TableBuilder { + public rowAction(index: number | "all", action: Table.Action): TableBuilder { if (this.data.actions[index]) { const actions = this.data.actions[index]; this.data.actions[index] = [...actions, action]; @@ -129,7 +131,9 @@ export class TableBuilder { * Resets all data configured in the builder from previously-created table views. */ public reset(): void { - this.data.actions = {}; + this.data.actions = { + all: [], + }; this.data.columns = []; this.data.rows = []; this.data.title = ""; diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index b4a8ab5597..7e6ad49456 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -259,7 +259,6 @@ export class SharedInit { { field: "name", filter: true }, { field: "value", filter: true }, ]) - .rowAction(1, { title: "Test Button", command: "test" }) .build(); }) ); @@ -271,12 +270,16 @@ export class SharedInit { ...Array.from({ length: 1024 }, (val) => ({ name: randomUUID(), value: randomInt(2147483647), + string: (Math.random() + 1).toString(36), })) ) .columns([ { field: "name", filter: true }, { field: "value", filter: true }, + { field: "string", filter: true }, ]) + .rowAction(1, { title: "Info", command: "test", type: "secondary" }) + .rowAction(1, { title: "Start", command: "test2", type: "primary" }) .build(); }) ); diff --git a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx index 6e240fabcc..61bda1c178 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx @@ -55,7 +55,7 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro ...(newData.columns ?? []), { // Prevent cells from being selectable - cellStyle: { border: "none !important", outline: "none" }, + cellStyle: { border: "none", outline: "none" }, field: "actions", sortable: false, // Support a custom cell renderer for row actions @@ -66,7 +66,11 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro return ( {newData.actions[params.rowIndex].map((action) => ( - vscodeApi.postMessage({ command: action.command })} style={{ marginRight: "0.25em" }}> + vscodeApi.postMessage({ command: action.command, row: newData.rows?.at(params.rowIndex) })} + style={{ marginRight: "0.25em" }} + > {action.title} ))} From b24f9e83d15feba9675782c38dec69fca7837b2e Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 20 Jun 2024 15:30:25 -0400 Subject: [PATCH 022/107] wip(table): Support right-click context menu options Signed-off-by: Trae Yelovich --- .../src/vscode/ui/TableView.ts | 19 +++++++++++++++++++ .../src/vscode/ui/utils/TableBuilder.ts | 9 +-------- .../src/webviews/src/table-view/TableView.tsx | 17 +++++++++++++---- .../src/webviews/src/table-view/style.css | 3 ++- .../src/webviews/src/table-view/types.ts | 1 + 5 files changed, 36 insertions(+), 13 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index b533a61e41..0f3a602b9d 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -15,6 +15,7 @@ import { randomUUID } from "crypto"; export namespace Table { export type Action = { title: string; command: string; type?: "primary" | "secondary" | "icon" }; + export type ContextMenuOption = { title: string; command: string }; export type Axes = "row" | "column"; export type RowContent = Record; @@ -24,6 +25,7 @@ export namespace Table { actions: Record; // Column headers for the top of the table columns: Column[] | null | undefined; + contextOpts: Record; // The row data for the table. Each row contains a set of variables corresponding to the data for each column in that row rows: RowContent[] | null | undefined; // The display title for the table @@ -121,6 +123,23 @@ export namespace Table { return this.updateWebview(); } + /** + * Add one or more context menu options to the given row. + * + * @param id The row index or column ID where the action should be displayed + * @param actions The actions to add to the given row + * @returns Whether the webview successfully received the new context menu option(s) + */ + public addContextOption(id: number | string, ...options: ContextMenuOption[]): Promise { + if (this.data.contextOpts[id]) { + const existingOpts = this.data.contextOpts[id]; + this.data.contextOpts[id] = [...existingOpts, ...options]; + } else { + this.data.contextOpts[id] = options; + } + return this.updateWebview(); + } + /** * Add rows of content to the table view. * @param rows The rows of data to add to the table diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index 2dd2cdb16d..d675027ded 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -38,18 +38,17 @@ export class TableBuilder { actions: { all: [], }, + contextOpts: {}, columns: [], rows: [], title: "", }; - public constructor(context: ExtensionContext) { this.context = context; } /** * Set the title for the next table. - * * @param name The name of the table * @returns The same {@link TableBuilder} instance with the title added */ @@ -60,7 +59,6 @@ export class TableBuilder { /** * Set the rows for the next table. - * * @param rows The rows of content to use for the table * @returns The same {@link TableBuilder} instance with the rows added */ @@ -71,7 +69,6 @@ export class TableBuilder { /** * Set the headers for the next table. - * * @param rows The headers to use for the table * @returns The same {@link TableBuilder} instance with the headers added */ @@ -82,7 +79,6 @@ export class TableBuilder { /** * Add row actions for the next table. - * * @param actions the record of indices to {@link Table.Action} arrays to use for the table * @returns The same {@link TableBuilder} instance with the row actions added */ @@ -93,7 +89,6 @@ export class TableBuilder { /** * Add a row action to the next table. - * * @param index The column index to add an action to * @returns The same {@link TableBuilder} instance with the row action added */ @@ -109,7 +104,6 @@ export class TableBuilder { /** * Builds the table with the given data. - * * @returns A new {@link Table.Instance} with the given data/options */ public build(): Table.Instance { @@ -118,7 +112,6 @@ export class TableBuilder { /** * Builds the table with the given data and shares it with the TableMediator singleton. - * * @returns A new, **shared** {@link Table.Instance} with the given data/options */ public buildAndShare(): Table.Instance { diff --git a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx index 61bda1c178..31aad107c8 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx @@ -16,7 +16,12 @@ const vscodeApi = acquireVsCodeApi(); export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewProps) => { const [tableData, setTableData] = useState( data ?? { - actions: {}, + actions: { + all: [], + }, + contextOpts: { + all: [], + }, columns: null, rows: null, title: "", @@ -31,6 +36,10 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro setTheme("ag-theme-quartz-dark"); } + // Add an event listener for the context menu to prevent VS Code from showing its right-click menu. + // Source: https://github.com/microsoft/vscode/issues/139824 + window.addEventListener("contextmenu", (e) => e.stopImmediatePropagation(), true); + // Set up event listener to handle data changes being sent to the webview. window.addEventListener("message", (event: any): void => { if (!isSecureOrigin(event.origin)) { @@ -48,7 +57,7 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro const newData: Table.Data = response.data; if (newData.actions) { // Add an extra column to the end of each row if row actions are present - const rows = newData.rows?.map((row) => { + const rows = newData.rows?.map((row: Table.RowContent) => { return { ...row, actions: "" }; }); const columns = [ @@ -62,10 +71,10 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro cellRenderer: actionsCellRenderer ?? ((params: any) => { - if (newData.actions[params.rowIndex]) { + if (newData.actions[params.rowIndex] || newData.actions["all"]) { return ( - {newData.actions[params.rowIndex].map((action) => ( + {[...(newData.actions[params.rowIndex] || []), ...(newData.actions["all"] || [])].map((action) => ( vscodeApi.postMessage({ command: action.command, row: newData.rows?.at(params.rowIndex) })} diff --git a/packages/zowe-explorer/src/webviews/src/table-view/style.css b/packages/zowe-explorer/src/webviews/src/table-view/style.css index 58ffa738ee..64b3afcec7 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/style.css +++ b/packages/zowe-explorer/src/webviews/src/table-view/style.css @@ -1,5 +1,6 @@ .ag-theme-vsc { - height: 50vh; + min-height: 50vh; + max-height: 85vh; margin-top: 1em; --ag-icon-font-family: "agGridQuartz"; --ag-row-hover-color: var(--vscode-list-hoverBackground); diff --git a/packages/zowe-explorer/src/webviews/src/table-view/types.ts b/packages/zowe-explorer/src/webviews/src/table-view/types.ts index bcd787fe29..9f07d1619a 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/types.ts +++ b/packages/zowe-explorer/src/webviews/src/table-view/types.ts @@ -22,6 +22,7 @@ export type TableViewProps = { // Define props for the AG Grid table here export const tableProps = (tableData: Table.Data): Partial => ({ + domLayout: "autoHeight", enableCellTextSelection: true, ensureDomOrder: true, rowData: tableData.rows, From 9468e27d176c8f14045b02ccfb46a84b6dad2ed5 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Fri, 21 Jun 2024 10:15:41 -0400 Subject: [PATCH 023/107] wip(table): comprehensive USS example w/ actions Signed-off-by: Trae Yelovich --- .../src/trees/shared/SharedInit.ts | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index 7e6ad49456..8b072535f6 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -10,7 +10,7 @@ */ import * as vscode from "vscode"; -import { FileManagement, IZoweTree, IZoweTreeNode, TableBuilder, Validation, ZoweScheme } from "@zowe/zowe-explorer-api"; +import { FileManagement, Gui, IZoweTree, IZoweTreeNode, TableBuilder, Validation, ZoweScheme } from "@zowe/zowe-explorer-api"; import { SharedActions } from "./SharedActions"; import { SharedHistoryView } from "./SharedHistoryView"; import { SharedTreeProviders } from "./SharedTreeProviders"; @@ -263,23 +263,41 @@ export class SharedInit { }) ); context.subscriptions.push( - vscode.commands.registerCommand("zowe.tableView3", () => { + vscode.commands.registerCommand("zowe.tableView3", async () => { + const uriPath = await Gui.showInputBox({ + title: "Enter a URI path to list USS files", + }); + if (uriPath == null) { + return; + } + + const files = await UssFSProvider.instance.listFiles( + Profiles.getInstance().getDefaultProfile(), + vscode.Uri.from({ + scheme: "zowe-uss", + path: uriPath, + }) + ); new TableBuilder(context) .title("Comprehensive table with actions") .rows( - ...Array.from({ length: 1024 }, (val) => ({ - name: randomUUID(), - value: randomInt(2147483647), - string: (Math.random() + 1).toString(36), + ...files.apiResponse.items.map((item) => ({ + name: item.name, + gid: item.gid, + uid: item.uid, + group: item.group, + perms: item.mode, + owner: item.user, })) ) .columns([ { field: "name", filter: true }, - { field: "value", filter: true }, - { field: "string", filter: true }, + { field: "gid", filter: true }, + { field: "uid", filter: true }, + { field: "group", filter: true }, + { field: "perms", filter: true }, + { field: "owner", filter: true }, ]) - .rowAction(1, { title: "Info", command: "test", type: "secondary" }) - .rowAction(1, { title: "Start", command: "test2", type: "primary" }) .build(); }) ); From 454416f739ef20205536f930294a64cbdb627085 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Fri, 21 Jun 2024 11:26:40 -0400 Subject: [PATCH 024/107] refactor(table): clean up rendering of row actions Signed-off-by: Trae Yelovich --- .../src/webviews/src/table-view/TableView.tsx | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx index 31aad107c8..31da705a61 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx @@ -70,25 +70,21 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro // Support a custom cell renderer for row actions cellRenderer: actionsCellRenderer ?? - ((params: any) => { - if (newData.actions[params.rowIndex] || newData.actions["all"]) { - return ( - - {[...(newData.actions[params.rowIndex] || []), ...(newData.actions["all"] || [])].map((action) => ( - vscodeApi.postMessage({ command: action.command, row: newData.rows?.at(params.rowIndex) })} - style={{ marginRight: "0.25em" }} - > - {action.title} - - ))} - - ); - } else { - return <>; - } - }), + ((params: any) => + // Render any actions for the given row and actions that apply to all rows + newData.actions[params.rowIndex] || newData.actions["all"] ? ( + + {[...(newData.actions[params.rowIndex] || []), ...(newData.actions["all"] || [])].map((action) => ( + vscodeApi.postMessage({ command: action.command, row: newData.rows!.at(params.rowIndex) })} + style={{ marginRight: "0.25em" }} + > + {action.title} + + ))} + + ) : null), }, ]; setTableData({ ...newData, rows, columns }); From 8934adf66cf352bbb71fa3bc5dabed38b89547a5 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Fri, 21 Jun 2024 14:53:17 -0400 Subject: [PATCH 025/107] wip: Context menu workaround for AG Grid Signed-off-by: Trae Yelovich --- .../src/webviews/src/table-view/ContextMenu.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx diff --git a/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx new file mode 100644 index 0000000000..d476e1fe19 --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx @@ -0,0 +1,14 @@ +import { useState } from "preact/hooks"; + +export const ContextMenu = ({ menuItems }: { menuItems?: string[] }) => { + const [items, setItems] = useState(menuItems ?? []); + return items.length == 0 ? null : ( +
+
    + {items.map((item, i) => ( +
  • {item}
  • + ))} +
+
+ ); +}; From b5b60c7f4b7b8c4db281d3f4452dfc068f58eb7a Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Fri, 21 Jun 2024 15:55:57 -0400 Subject: [PATCH 026/107] refactor: remove redundant state from ContextMenu Signed-off-by: Trae Yelovich --- .../src/webviews/src/table-view/ContextMenu.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx index d476e1fe19..1519fdffef 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx @@ -1,11 +1,8 @@ -import { useState } from "preact/hooks"; - export const ContextMenu = ({ menuItems }: { menuItems?: string[] }) => { - const [items, setItems] = useState(menuItems ?? []); - return items.length == 0 ? null : ( + return menuItems?.length == 0 ? null : (
    - {items.map((item, i) => ( + {menuItems!.map((item, i) => (
  • {item}
  • ))}
From 93cef75e5c354116ecf70d8f5efb01089ad2db03 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Fri, 21 Jun 2024 16:06:45 -0400 Subject: [PATCH 027/107] refactor(table): Update ContextMenu colors to match VSC Signed-off-by: Trae Yelovich --- .../zowe-explorer-api/src/vscode/ui/TableView.ts | 2 +- .../src/webviews/src/table-view/ContextMenu.tsx | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 0f3a602b9d..d505baad92 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -15,7 +15,7 @@ import { randomUUID } from "crypto"; export namespace Table { export type Action = { title: string; command: string; type?: "primary" | "secondary" | "icon" }; - export type ContextMenuOption = { title: string; command: string }; + export type ContextMenuOption = Omit; export type Axes = "row" | "column"; export type RowContent = Record; diff --git a/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx index 1519fdffef..8547ce1736 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx @@ -1,9 +1,15 @@ -export const ContextMenu = ({ menuItems }: { menuItems?: string[] }) => { +import type { Table } from "@zowe/zowe-explorer-api"; + +const vscodeApi = acquireVsCodeApi(); + +export const ContextMenu = ({ menuItems }: { menuItems?: Table.ContextMenuOption[] }) => { return menuItems?.length == 0 ? null : ( -
+
    {menuItems!.map((item, i) => ( -
  • {item}
  • +
  • vscodeApi.postMessage({ command: item.command })} style={{ borderBottom: "var(--vscode-menu-border)" }}> + {item.title} +
  • ))}
From 448e560d8207a8135efe77cec336603d247566c6 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 24 Jun 2024 15:35:48 -0400 Subject: [PATCH 028/107] wip: context menu component & hook Signed-off-by: Trae Yelovich --- .../webviews/src/table-view/ContextMenu.tsx | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx index 8547ce1736..a177db1584 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx @@ -1,7 +1,42 @@ import type { Table } from "@zowe/zowe-explorer-api"; +import { useState } from "preact/hooks"; +import { JSX } from "preact/jsx-runtime"; +import { ColDef } from "ag-grid-community"; const vscodeApi = acquireVsCodeApi(); +type MousePt = { x: number; y: number }; +export type ContextMenuState = { + open: boolean; + callback: Function; + component: JSX.Element | null; +}; + +export type ContextMenuProps = { + selectedRows: Table.RowContent[] | null | undefined; + clickedRow: Table.RowContent; + options: Table.ContextMenuOption[]; + colDef: ColDef; + close: () => void; +}; + +/** + * React hook that returns a prepared context menu component and its related states. + * + * @param contextMenu The props for the context menu component (options) + * @returns The result of the hook, with the component to render, open state and cell callback + */ +export const useContextMenu = (contextMenu: ContextMenuProps) => { + const [open, setOpen] = useState(false); + const [anchor, setAnchor] = useState({ x: 0, y: 0 }); + + return { + open, + callback: () => {}, + component: open ? : null, + }; +}; + export const ContextMenu = ({ menuItems }: { menuItems?: Table.ContextMenuOption[] }) => { return menuItems?.length == 0 ? null : (
From 556bd6fc4f5dac8a64f127a690aca9807185b270 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 25 Jun 2024 16:12:32 -0400 Subject: [PATCH 029/107] wip: Context Menu component; disable VS Code right-click menu Signed-off-by: Trae Yelovich --- .../src/vscode/ui/TableView.ts | 7 +- packages/zowe-explorer/.eslintignore | 1 + .../src/trees/shared/SharedInit.ts | 11 ++- .../zowe-explorer/src/webviews/package.json | 1 + .../webviews/src/table-view/ContextMenu.tsx | 82 ++++++++++++++----- .../src/webviews/src/table-view/TableView.tsx | 18 +++- .../src/webviews/src/table-view/types.ts | 4 +- packages/zowe-explorer/tsconfig.json | 2 +- pnpm-lock.yaml | 25 ++++++ 9 files changed, 119 insertions(+), 32 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index d505baad92..cd2122ccb2 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -12,6 +12,7 @@ import { UriPair, WebView } from "./WebView"; import { Event, EventEmitter, ExtensionContext } from "vscode"; import { randomUUID } from "crypto"; +import * as vscode from "vscode"; export namespace Table { export type Action = { title: string; command: string; type?: "primary" | "secondary" | "icon" }; @@ -55,8 +56,7 @@ export namespace Table { } public constructor(context: ExtensionContext, data?: Data) { - super(data.title ?? "Table view", "table-view", context); - this.panel.webview.onDidReceiveMessage((message) => this.onMessageReceived(message)); + super(data.title ?? "Table view", "table-view", context, (message) => this.onMessageReceived(message), true); if (data) { this.data = data; } @@ -79,6 +79,9 @@ export namespace Table { case "ready": await this.updateWebview(); break; + case "copy": + await vscode.env.clipboard.writeText(JSON.stringify(message.data)); + break; } } diff --git a/packages/zowe-explorer/.eslintignore b/packages/zowe-explorer/.eslintignore index 45bf8d268e..c9c90f1437 100644 --- a/packages/zowe-explorer/.eslintignore +++ b/packages/zowe-explorer/.eslintignore @@ -2,3 +2,4 @@ node_modules/**/* results/**/* out/**/* *.config.js +src/webviews \ No newline at end of file diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index 8b072535f6..1dce95d754 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -10,7 +10,7 @@ */ import * as vscode from "vscode"; -import { FileManagement, Gui, IZoweTree, IZoweTreeNode, TableBuilder, Validation, ZoweScheme } from "@zowe/zowe-explorer-api"; +import { FileManagement, FsAbstractUtils, Gui, IZoweTree, IZoweTreeNode, TableBuilder, Validation, ZoweScheme } from "@zowe/zowe-explorer-api"; import { SharedActions } from "./SharedActions"; import { SharedHistoryView } from "./SharedHistoryView"; import { SharedTreeProviders } from "./SharedTreeProviders"; @@ -271,8 +271,15 @@ export class SharedInit { return; } + const uriInfo = FsAbstractUtils.getInfoForUri( + vscode.Uri.from({ + scheme: "zowe-uss", + path: uriPath, + }) + ); + const files = await UssFSProvider.instance.listFiles( - Profiles.getInstance().getDefaultProfile(), + Profiles.getInstance().loadNamedProfile(uriInfo.profileName), vscode.Uri.from({ scheme: "zowe-uss", path: uriPath, diff --git a/packages/zowe-explorer/src/webviews/package.json b/packages/zowe-explorer/src/webviews/package.json index 1b382d861d..d677d8e5a1 100644 --- a/packages/zowe-explorer/src/webviews/package.json +++ b/packages/zowe-explorer/src/webviews/package.json @@ -19,6 +19,7 @@ "madge": "echo \"webviews: nothing to madge.\"" }, "dependencies": { + "@szhsin/react-menu": "^4.1.0", "@types/vscode-webview": "^1.57.1", "@vscode/webview-ui-toolkit": "^1.2.2", "ag-grid-community": "^31.3.2", diff --git a/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx index a177db1584..26a506cccb 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx @@ -1,23 +1,24 @@ import type { Table } from "@zowe/zowe-explorer-api"; -import { useState } from "preact/hooks"; -import { JSX } from "preact/jsx-runtime"; -import { ColDef } from "ag-grid-community"; - -const vscodeApi = acquireVsCodeApi(); +import { useCallback, useRef, useState } from "preact/hooks"; +import { CellContextMenuEvent, ColDef } from "ag-grid-community"; +import { JSXInternal } from "preact/src/jsx"; +import { ControlledMenu, MenuItem } from "@szhsin/react-menu"; +import "@szhsin/react-menu/dist/index.css"; type MousePt = { x: number; y: number }; export type ContextMenuState = { open: boolean; - callback: Function; - component: JSX.Element | null; + callback: (event: any) => void; + component: JSXInternal.Element | null; }; export type ContextMenuProps = { + selectRow: boolean; selectedRows: Table.RowContent[] | null | undefined; clickedRow: Table.RowContent; options: Table.ContextMenuOption[]; colDef: ColDef; - close: () => void; + vscodeApi: any; }; /** @@ -30,23 +31,60 @@ export const useContextMenu = (contextMenu: ContextMenuProps) => { const [open, setOpen] = useState(false); const [anchor, setAnchor] = useState({ x: 0, y: 0 }); + const clickedColDef = useRef(null!); + const selectedRows = useRef([]); + const clickedRow = useRef(null); + + const openMenu = useCallback((e: PointerEvent | null | undefined) => { + if (!e) { + return; + } + + setAnchor({ x: e.clientX, y: e.clientY }); + setOpen(true); + }, []); + + const cellMenu = useCallback( + (event: CellContextMenuEvent) => { + const cell = event.api.getFocusedCell(); + if (contextMenu.selectRow && cell) { + event.api.setFocusedCell(cell.rowIndex, cell.column, cell.rowPinned); + } + + clickedColDef.current = event.colDef; + selectedRows.current = event.api.getSelectedRows(); + clickedRow.current = event.data; + + openMenu(event.event as PointerEvent); + event.event?.stopImmediatePropagation(); + }, + [contextMenu.selectRow, selectedRows] + ); + return { open, - callback: () => {}, - component: open ? : null, + callback: cellMenu, + component: open ? ( + setOpen(false)}> + {ContextMenu(clickedRow.current, contextMenu.options, contextMenu.vscodeApi)} + + ) : null, }; }; -export const ContextMenu = ({ menuItems }: { menuItems?: Table.ContextMenuOption[] }) => { - return menuItems?.length == 0 ? null : ( -
-
    - {menuItems!.map((item, i) => ( -
  • vscodeApi.postMessage({ command: item.command })} style={{ borderBottom: "var(--vscode-menu-border)" }}> - {item.title} -
  • - ))} -
-
- ); +export type ContextMenuElemProps = { + anchor: MousePt; + menuItems: Table.ContextMenuOption[]; + vscodeApi: any; +}; + +export const ContextMenu = (clickedRow: any, menuItems: Table.ContextMenuOption[], vscodeApi: any) => { + return menuItems?.map((item, _i) => ( + vscodeApi.postMessage({ command: item.command, data: { ...clickedRow, actions: undefined } })} + style={{ borderBottom: "var(--vscode-menu-border)" }} + > + {item.title} + + )); }; diff --git a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx index 31da705a61..d7e1ef6433 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx @@ -8,6 +8,7 @@ import { useEffect, useState } from "preact/hooks"; import { getVsCodeTheme, isSecureOrigin, useMutableObserver } from "../utils"; import type { Table } from "@zowe/zowe-explorer-api"; import { TableViewProps, tableProps } from "./types"; +import { useContextMenu } from "./ContextMenu"; // Custom styling (font family, VS Code color scheme, etc.) import "./style.css"; @@ -29,6 +30,15 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro ); const [theme, setTheme] = useState(baseTheme ?? "ag-theme-quartz"); + const contextMenu = useContextMenu({ + options: [{ title: "Copy", command: "copy" }], + selectRow: true, + selectedRows: [], + clickedRow: undefined as any, + colDef: undefined as any, + vscodeApi, + }); + useEffect(() => { // Apply the dark version of the AG Grid theme if the user is using a dark or high-contrast theme in VS Code. const userTheme = getVsCodeTheme(); @@ -36,9 +46,8 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro setTheme("ag-theme-quartz-dark"); } - // Add an event listener for the context menu to prevent VS Code from showing its right-click menu. - // Source: https://github.com/microsoft/vscode/issues/139824 - window.addEventListener("contextmenu", (e) => e.stopImmediatePropagation(), true); + // Disable the event listener for the context menu in the active iframe to prevent VS Code from showing its right-click menu. + window.addEventListener("contextmenu", (e) => e.preventDefault(), true); // Set up event listener to handle data changes being sent to the webview. window.addEventListener("message", (event: any): void => { @@ -115,7 +124,8 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro <> {tableData.title ?

{tableData.title}

: null}
- + {contextMenu.component} +
); diff --git a/packages/zowe-explorer/src/webviews/src/table-view/types.ts b/packages/zowe-explorer/src/webviews/src/table-view/types.ts index 9f07d1619a..79264fcb67 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/types.ts +++ b/packages/zowe-explorer/src/webviews/src/table-view/types.ts @@ -12,6 +12,7 @@ import type { Table } from "@zowe/zowe-explorer-api"; import { AgGridReactProps } from "ag-grid-react"; import { JSXInternal } from "preact/src/jsx"; +import { ContextMenuState } from "./ContextMenu"; type AgGridThemes = "ag-theme-quartz" | "ag-theme-balham" | "ag-theme-material" | "ag-theme-alpine"; export type TableViewProps = { @@ -21,11 +22,12 @@ export type TableViewProps = { }; // Define props for the AG Grid table here -export const tableProps = (tableData: Table.Data): Partial => ({ +export const tableProps = (contextMenu: ContextMenuState, tableData: Table.Data): Partial => ({ domLayout: "autoHeight", enableCellTextSelection: true, ensureDomOrder: true, rowData: tableData.rows, columnDefs: tableData.columns, pagination: true, + onCellContextMenu: contextMenu.callback, }); diff --git a/packages/zowe-explorer/tsconfig.json b/packages/zowe-explorer/tsconfig.json index 388007e878..bf6def4144 100644 --- a/packages/zowe-explorer/tsconfig.json +++ b/packages/zowe-explorer/tsconfig.json @@ -23,7 +23,7 @@ } // "types": ["node", "jest", "mocha"] }, - "exclude": ["node_modules", ".vscode-test"], + "exclude": ["node_modules", "src/webviews/**", ".vscode-test"], "include": [ "src/**/*.ts", // Needed in production for vscode-nls localization to work: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6fb4ffd104..86a720565f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -291,6 +291,9 @@ importers: packages/zowe-explorer/src/webviews: dependencies: + '@szhsin/react-menu': + specifier: ^4.1.0 + version: 4.1.0(react-dom@18.3.1)(react@18.3.1) '@types/vscode-webview': specifier: ^1.57.1 version: 1.57.5 @@ -2023,6 +2026,18 @@ packages: resolution: {integrity: sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==} dev: true + /@szhsin/react-menu@4.1.0(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-lYYGUxqJxM2b/jD2Cn5a9RVOvHl9VBMX8qOnHZuX1w08cO2jslykpz5P75D7WnqudLnXsJ4k4+tI+q2U8XIFYw==} + peerDependencies: + react: '>=16.14.0' + react-dom: '>=16.14.0' + dependencies: + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-state: 2.1.1(react-dom@18.3.1)(react@18.3.1) + dev: false + /@tootallnate/once@1.1.2: resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} engines: {node: '>= 6'} @@ -8267,6 +8282,16 @@ packages: /react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + /react-transition-state@2.1.1(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-kQx5g1FVu9knoz1T1WkapjUgFz08qQ/g1OmuWGi3/AoEFfS0kStxrPlZx81urjCXdz2d+1DqLpU6TyLW/Ro04Q==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} From 83e82ce4782331c3a2bbff888f74b1ec6f16ee12 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 26 Jun 2024 10:00:24 -0400 Subject: [PATCH 030/107] feat(table): Action and Context Menu callbacks Signed-off-by: Trae Yelovich --- .../src/vscode/ui/TableView.ts | 31 +++++++++++++++--- .../src/vscode/ui/utils/TableBuilder.ts | 32 ++++++++++++++++++- .../src/trees/shared/SharedInit.ts | 26 +++++++++++++++ .../src/webviews/src/table-view/TableView.tsx | 10 ++++-- 4 files changed, 92 insertions(+), 7 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index cd2122ccb2..6c7ac86339 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -15,7 +15,13 @@ import { randomUUID } from "crypto"; import * as vscode from "vscode"; export namespace Table { - export type Action = { title: string; command: string; type?: "primary" | "secondary" | "icon" }; + export type Callback = (data: RowContent) => void | PromiseLike; + export type Action = { + title: string; + command: string; + type?: "primary" | "secondary" | "icon"; + callback: Callback; + }; export type ContextMenuOption = Omit; export type Axes = "row" | "column"; @@ -26,7 +32,7 @@ export namespace Table { actions: Record; // Column headers for the top of the table columns: Column[] | null | undefined; - contextOpts: Record; + contextOpts: Record; // The row data for the table. Each row contains a set of variables corresponding to the data for each column in that row rows: RowContent[] | null | undefined; // The display title for the table @@ -73,16 +79,33 @@ export namespace Table { return; } switch (message.command) { + // "ondisplaychanged" command: The table's layout was updated by the user from within the webview. case "ondisplaychanged": this.onTableDisplayChanged.fire(message.data); - break; + return; + // "ready" command: The table view has attached its message listener and is ready to receive data. case "ready": await this.updateWebview(); - break; + return; + // "copy" command: Copy the data for the row that was right-clicked. case "copy": + case "copy-cell": await vscode.env.clipboard.writeText(JSON.stringify(message.data)); + return; + default: break; } + + const row: number = message.rowIndex ?? 0; + const matchingActionable = [ + ...(this.data.actions[row] ?? []), + ...this.data.actions.all, + ...(this.data.contextOpts[row] ?? []), + ...this.data.contextOpts.all, + ].find((action) => action.command === message.command); + if (matchingActionable != null) { + await matchingActionable.callback(message.data); + } } /** diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index d675027ded..bcc348bea3 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -38,7 +38,9 @@ export class TableBuilder { actions: { all: [], }, - contextOpts: {}, + contextOpts: { + all: [], + }, columns: [], rows: [], title: "", @@ -77,6 +79,31 @@ export class TableBuilder { return this; } + /** + * Add context options for the next table. + * @param actions the record of indices to {@link Table.Action} arrays to use for the table + * @returns The same {@link TableBuilder} instance with the row actions added + */ + public contextOptions(opts: Record): TableBuilder { + this.data.contextOpts = opts; + return this; + } + + /** + * Add a row action to the next table. + * @param index The column index to add an action to + * @returns The same {@link TableBuilder} instance with the row action added + */ + public contextOption(index: number | "all", option: Table.ContextMenuOption): TableBuilder { + if (this.data.contextOpts[index]) { + const opts = this.data.contextOpts[index]; + this.data.contextOpts[index] = [...opts, option]; + } else { + this.data.contextOpts[index] = [option]; + } + return this; + } + /** * Add row actions for the next table. * @param actions the record of indices to {@link Table.Action} arrays to use for the table @@ -127,6 +154,9 @@ export class TableBuilder { this.data.actions = { all: [], }; + this.data.contextOpts = { + all: [], + }; this.data.columns = []; this.data.rows = []; this.data.title = ""; diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index 1dce95d754..04b7fe17e8 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -33,6 +33,7 @@ import { DatasetFSProvider } from "../dataset/DatasetFSProvider"; import { ExtensionUtils } from "../../utils/ExtensionUtils"; import type { Definitions } from "../../configuration/Definitions"; import { randomInt, randomUUID } from "crypto"; +import * as path from "path"; export class SharedInit { public static registerRefreshCommand( @@ -259,6 +260,13 @@ export class SharedInit { { field: "name", filter: true }, { field: "value", filter: true }, ]) + .contextOption("all", { + title: "Show as dialog", + command: "print-dialog", + callback: async (data) => { + await Gui.showMessage(JSON.stringify(data)); + }, + }) .build(); }) ); @@ -305,6 +313,24 @@ export class SharedInit { { field: "perms", filter: true }, { field: "owner", filter: true }, ]) + .rowAction("all", { + title: "Open in editor", + type: "primary", + command: "edit", + callback: async (data) => { + const filename = data.name as string; + const uri = vscode.Uri.from({ + scheme: "zowe-uss", + path: path.posix.join(uriPath, filename), + }); + + if (!UssFSProvider.instance.exists(uri)) { + await UssFSProvider.instance.writeFile(uri, new Uint8Array(), { create: true, overwrite: false }); + } + + await vscode.commands.executeCommand("vscode.open", uri); + }, + }) .build(); }) ); diff --git a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx index d7e1ef6433..1d1c6c9668 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx @@ -31,7 +31,7 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro const [theme, setTheme] = useState(baseTheme ?? "ag-theme-quartz"); const contextMenu = useContextMenu({ - options: [{ title: "Copy", command: "copy" }], + options: [{ title: "Copy", command: "copy", callback: () => {} }, ...(tableData.contextOpts.all ?? [])], selectRow: true, selectedRows: [], clickedRow: undefined as any, @@ -86,7 +86,13 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro {[...(newData.actions[params.rowIndex] || []), ...(newData.actions["all"] || [])].map((action) => ( vscodeApi.postMessage({ command: action.command, row: newData.rows!.at(params.rowIndex) })} + onClick={(_e: any) => + vscodeApi.postMessage({ + command: action.command, + data: { ...params.data, actions: undefined }, + row: newData.rows!.at(params.rowIndex), + }) + } style={{ marginRight: "0.25em" }} > {action.title} From 2c060cc16e51dd0d2edca5882e275dc300bc6932 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 26 Jun 2024 13:42:48 -0400 Subject: [PATCH 031/107] feat: Adopt VS Code color scheme in context menu Signed-off-by: Trae Yelovich --- .../src/trees/shared/SharedInit.ts | 4 +-- .../src/webviews/src/table-view/style.css | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index 04b7fe17e8..f6ef0894d8 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -10,7 +10,7 @@ */ import * as vscode from "vscode"; -import { FileManagement, FsAbstractUtils, Gui, IZoweTree, IZoweTreeNode, TableBuilder, Validation, ZoweScheme } from "@zowe/zowe-explorer-api"; +import { FileManagement, FsAbstractUtils, Gui, IZoweTree, IZoweTreeNode, Table, TableBuilder, Validation, ZoweScheme } from "@zowe/zowe-explorer-api"; import { SharedActions } from "./SharedActions"; import { SharedHistoryView } from "./SharedHistoryView"; import { SharedTreeProviders } from "./SharedTreeProviders"; @@ -317,7 +317,7 @@ export class SharedInit { title: "Open in editor", type: "primary", command: "edit", - callback: async (data) => { + callback: async (data: Table.RowContent) => { const filename = data.name as string; const uri = vscode.Uri.from({ scheme: "zowe-uss", diff --git a/packages/zowe-explorer/src/webviews/src/table-view/style.css b/packages/zowe-explorer/src/webviews/src/table-view/style.css index 64b3afcec7..9b34a4a5d2 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/style.css +++ b/packages/zowe-explorer/src/webviews/src/table-view/style.css @@ -19,3 +19,34 @@ .ag-theme-vsc.ag-popup { position: absolute; } + +.szh-menu { + background-color: var(--vscode-menu-background); + border: 1px solid var(--vscode-menu-border); +} + +.szh-menu__item { + color: var(--vscode-menu-foreground); +} + +.szh-menu__item--hover { + background-color: var(--vscode-menu-selectionBackground); + color: var(--vscode-menu-selectionForeground); +} + +.szh-menu__item--disabled { + color: var(--vscode-debugIcon-breakpointDisabledForeground); +} + +.szh-menu__divider { + background-color: var(--vscode-menu-separatorBackground); +} + +.szh-menu-button { + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); +} + +.szh-menu-button:hover { + background-color: var(--vscode-button-hoverBackground); +} From 8bf6f7ede94c41f45ce6adabef1963889c73d0fd Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 26 Jun 2024 15:55:54 -0400 Subject: [PATCH 032/107] table: add context menu option for USS demo table Signed-off-by: Trae Yelovich --- packages/zowe-explorer/src/trees/shared/SharedInit.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index f6ef0894d8..72ea29c8a3 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -313,6 +313,13 @@ export class SharedInit { { field: "perms", filter: true }, { field: "owner", filter: true }, ]) + .contextOption("all", { + title: "Copy path", + command: "copy-path", + callback: async (data: Table.RowContent) => { + await vscode.env.clipboard.writeText(path.posix.join(uriPath, data.name as string)); + }, + }) .rowAction("all", { title: "Open in editor", type: "primary", From 3f225a1a0dd5fba67f3b6f7b706e6b7b971d3207 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 27 Jun 2024 14:14:59 -0400 Subject: [PATCH 033/107] wip: jobs view, conditionally render actions Signed-off-by: Trae Yelovich --- packages/zowe-explorer/l10n/bundle.l10n.json | 36 +++++++++---------- packages/zowe-explorer/l10n/poeditor.json | 15 ++++---- packages/zowe-explorer/package.json | 12 ++++++- packages/zowe-explorer/package.nls.json | 3 +- .../zowe-explorer/src/trees/job/JobInit.ts | 23 +++++++++++- .../src/trees/shared/SharedInit.ts | 16 +++++---- .../src/webviews/src/table-view/TableView.tsx | 2 +- .../src/webviews/src/table-view/style.css | 2 +- .../src/webviews/src/table-view/types.ts | 2 +- 9 files changed, 74 insertions(+), 37 deletions(-) diff --git a/packages/zowe-explorer/l10n/bundle.l10n.json b/packages/zowe-explorer/l10n/bundle.l10n.json index cd64664529..792bcf2d2e 100644 --- a/packages/zowe-explorer/l10n/bundle.l10n.json +++ b/packages/zowe-explorer/l10n/bundle.l10n.json @@ -165,24 +165,6 @@ "Required API functions for pasting (fileList and copy/uploadFromBuffer) were not found.": "Required API functions for pasting (fileList and copy/uploadFromBuffer) were not found.", "Uploading USS files...": "Uploading USS files...", "Error uploading files": "Error uploading files", - "The 'move' function is not implemented for this USS API.": "The 'move' function is not implemented for this USS API.", - "Could not list USS files: Empty path provided in URI": "Could not list USS files: Empty path provided in URI", - "Profile does not exist for this file.": "Profile does not exist for this file.", - "$(sync~spin) Saving USS file...": "$(sync~spin) Saving USS file...", - "Renaming {0} failed due to API error: {1}/File pathError message": { - "message": "Renaming {0} failed due to API error: {1}", - "comment": [ - "File path", - "Error message" - ] - }, - "Deleting {0} failed due to API error: {1}/File nameError message": { - "message": "Deleting {0} failed due to API error: {1}", - "comment": [ - "File name", - "Error message" - ] - }, "Downloaded: {0}/Download time": { "message": "Downloaded: {0}", "comment": [ @@ -252,6 +234,24 @@ }, "initializeUSSFavorites.error.buttonRemove": "initializeUSSFavorites.error.buttonRemove", "File does not exist. It may have been deleted.": "File does not exist. It may have been deleted.", + "The 'move' function is not implemented for this USS API.": "The 'move' function is not implemented for this USS API.", + "Could not list USS files: Empty path provided in URI": "Could not list USS files: Empty path provided in URI", + "Profile does not exist for this file.": "Profile does not exist for this file.", + "$(sync~spin) Saving USS file...": "$(sync~spin) Saving USS file...", + "Renaming {0} failed due to API error: {1}/File pathError message": { + "message": "Renaming {0} failed due to API error: {1}", + "comment": [ + "File path", + "Error message" + ] + }, + "Deleting {0} failed due to API error: {1}/File nameError message": { + "message": "Deleting {0} failed due to API error: {1}", + "comment": [ + "File name", + "Error message" + ] + }, "{0} location/Node type": { "message": "{0} location", "comment": [ diff --git a/packages/zowe-explorer/l10n/poeditor.json b/packages/zowe-explorer/l10n/poeditor.json index 2dacc21900..4883873ce1 100644 --- a/packages/zowe-explorer/l10n/poeditor.json +++ b/packages/zowe-explorer/l10n/poeditor.json @@ -461,6 +461,9 @@ "openWithEncoding": { "Open with Encoding": "" }, + "jobsTableView": { + "Show as table": "" + }, "Refresh": "", "Delete Selected": "", "Select an item before deleting": "", @@ -531,12 +534,6 @@ "Required API functions for pasting (fileList and copy/uploadFromBuffer) were not found.": "", "Uploading USS files...": "", "Error uploading files": "", - "The 'move' function is not implemented for this USS API.": "", - "Could not list USS files: Empty path provided in URI": "", - "Profile does not exist for this file.": "", - "$(sync~spin) Saving USS file...": "", - "Renaming {0} failed due to API error: {1}": "", - "Deleting {0} failed due to API error: {1}": "", "Downloaded: {0}": "", "Encoding: {0}": "", "Binary": "", @@ -564,6 +561,12 @@ "Error: You have Zowe USS favorites that refer to a non-existent CLI profile named: {0}.\n To resolve this, you can remove {0} from the Favorites section of Zowe Explorer's USS view.\n Would you like to do this now? {1}": "", "initializeUSSFavorites.error.buttonRemove": "", "File does not exist. It may have been deleted.": "", + "The 'move' function is not implemented for this USS API.": "", + "Could not list USS files: Empty path provided in URI": "", + "Profile does not exist for this file.": "", + "$(sync~spin) Saving USS file...": "", + "Renaming {0} failed due to API error: {1}": "", + "Deleting {0} failed due to API error: {1}": "", "{0} location": "", "Choose a location to create the {0}": "", "Name of file or directory": "", diff --git a/packages/zowe-explorer/package.json b/packages/zowe-explorer/package.json index 7fb3cdecb8..77dadd8e5f 100644 --- a/packages/zowe-explorer/package.json +++ b/packages/zowe-explorer/package.json @@ -224,6 +224,11 @@ "title": "%ssoLogout%", "category": "Zowe Explorer" }, + { + "command": "zowe.jobs.tableView", + "title": "%jobsTableView%", + "category": "Zowe Explorer" + }, { "command": "zowe.uss.copyPath", "title": "%uss.copyPath%", @@ -1441,10 +1446,15 @@ "command": "zowe.profileManagement", "group": "099_zowe_jobsProfileAuthentication" }, + { + "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", + "command": "zowe.jobs.tableView", + "group": "100_zowe_tableview" + }, { "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", "command": "zowe.editHistory", - "group": "100_zowe_editHistory" + "group": "101_zowe_editHistory" }, { "when": "viewItem =~ /^(textFile|member.*|ds.*)/ && !listMultiSelection", diff --git a/packages/zowe-explorer/package.nls.json b/packages/zowe-explorer/package.nls.json index 5fd9f3b3d1..edd127e74a 100644 --- a/packages/zowe-explorer/package.nls.json +++ b/packages/zowe-explorer/package.nls.json @@ -152,5 +152,6 @@ "compareWithSelected": "Compare with Selected", "compareWithSelectedReadOnly": "Compare with Selected (Read-Only)", "compareFileStarted": "A file has been chosen for compare", - "openWithEncoding": "Open with Encoding" + "openWithEncoding": "Open with Encoding", + "jobsTableView": "Show as table" } diff --git a/packages/zowe-explorer/src/trees/job/JobInit.ts b/packages/zowe-explorer/src/trees/job/JobInit.ts index 2b3c32eb49..587ec75875 100644 --- a/packages/zowe-explorer/src/trees/job/JobInit.ts +++ b/packages/zowe-explorer/src/trees/job/JobInit.ts @@ -10,7 +10,7 @@ */ import * as vscode from "vscode"; -import { IZoweJobTreeNode, IZoweTreeNode, ZoweScheme, imperative } from "@zowe/zowe-explorer-api"; +import { IZoweJobTreeNode, IZoweTreeNode, TableBuilder, ZoweScheme, imperative } from "@zowe/zowe-explorer-api"; import { JobTree } from "./JobTree"; import { JobActions } from "./JobActions"; import { ZoweJobNode } from "./ZoweJobNode"; @@ -195,6 +195,27 @@ export class JobInit { async (job: IZoweJobTreeNode): Promise => jobsProvider.filterJobsDialog(job) ) ); + context.subscriptions.push( + vscode.commands.registerCommand("zowe.jobs.tableView", (jobSession: IZoweJobTreeNode) => { + const children = jobSession.children; + if (children) { + const jobObjects = children.map((c) => c.job); + new TableBuilder(context) + .title("Jobs view") + .rows( + ...jobObjects.map((job) => ({ + jobid: job.jobid, + jobname: job.jobname, + owner: job.owner, + status: job.status, + class: job.class, + retcode: job.retcode, + })) + ) + .build(); + } + }) + ); context.subscriptions.push( vscode.workspace.onDidOpenTextDocument((doc) => { if (doc.uri.scheme !== ZoweScheme.Jobs) { diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index 72ea29c8a3..bd2f621abd 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -227,6 +227,7 @@ export class SharedInit { ); context.subscriptions.push( vscode.commands.registerCommand("zowe.tableView", async () => { + // Build the table instance const table = new TableBuilder(context) .title("Cars for sale") .rows( @@ -243,11 +244,13 @@ export class SharedInit { ) .columns([{ field: "make" }, { field: "model" }, { field: "year" }]) .build(); + // Add content to the existing table view await table.addContent({ make: "Toyota", model: "Corolla", year: 2007 }); }) ); context.subscriptions.push( vscode.commands.registerCommand("zowe.tableView2", () => { + // Context option demo new TableBuilder(context) .title("Random data") .rows( @@ -272,6 +275,7 @@ export class SharedInit { ); context.subscriptions.push( vscode.commands.registerCommand("zowe.tableView3", async () => { + // Example use case: Listing USS files const uriPath = await Gui.showInputBox({ title: "Enter a URI path to list USS files", }); @@ -294,24 +298,22 @@ export class SharedInit { }) ); new TableBuilder(context) - .title("Comprehensive table with actions") + .title(uriPath.substring(uriPath.indexOf("/", 1))) .rows( ...files.apiResponse.items.map((item) => ({ name: item.name, - gid: item.gid, - uid: item.uid, + size: item.size, + owner: item.user, group: item.group, perms: item.mode, - owner: item.user, })) ) .columns([ { field: "name", filter: true }, - { field: "gid", filter: true }, - { field: "uid", filter: true }, + { field: "size", filter: true }, + { field: "owner", filter: true }, { field: "group", filter: true }, { field: "perms", filter: true }, - { field: "owner", filter: true }, ]) .contextOption("all", { title: "Copy path", diff --git a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx index 1d1c6c9668..06e311aaf8 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx @@ -64,7 +64,7 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro case "ondatachanged": // Update received from a VS Code extender; update table state const newData: Table.Data = response.data; - if (newData.actions) { + if (Object.keys(newData.actions).length > 1 || newData.actions.all?.length > 0) { // Add an extra column to the end of each row if row actions are present const rows = newData.rows?.map((row: Table.RowContent) => { return { ...row, actions: "" }; diff --git a/packages/zowe-explorer/src/webviews/src/table-view/style.css b/packages/zowe-explorer/src/webviews/src/table-view/style.css index 9b34a4a5d2..78936e047f 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/style.css +++ b/packages/zowe-explorer/src/webviews/src/table-view/style.css @@ -1,5 +1,5 @@ .ag-theme-vsc { - min-height: 50vh; + height: 50vh; max-height: 85vh; margin-top: 1em; --ag-icon-font-family: "agGridQuartz"; diff --git a/packages/zowe-explorer/src/webviews/src/table-view/types.ts b/packages/zowe-explorer/src/webviews/src/table-view/types.ts index 79264fcb67..50a19c8ad5 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/types.ts +++ b/packages/zowe-explorer/src/webviews/src/table-view/types.ts @@ -23,7 +23,7 @@ export type TableViewProps = { // Define props for the AG Grid table here export const tableProps = (contextMenu: ContextMenuState, tableData: Table.Data): Partial => ({ - domLayout: "autoHeight", + // domLayout: "autoHeight", enableCellTextSelection: true, ensureDomOrder: true, rowData: tableData.rows, From bca0a166169d9d5f6392dc63e24740b8653d681a Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 27 Jun 2024 15:34:02 -0400 Subject: [PATCH 034/107] feat(table): Conditional actions & context menu options Signed-off-by: Trae Yelovich --- .../src/vscode/ui/TableView.ts | 2 + .../src/vscode/ui/utils/HTMLTemplate.ts | 2 +- .../src/trees/shared/SharedInit.ts | 16 ++++++++ .../webviews/src/table-view/ContextMenu.tsx | 28 +++++++++---- .../src/webviews/src/table-view/TableView.tsx | 41 ++++++++++++------- .../src/webviews/src/table-view/types.ts | 2 + 6 files changed, 67 insertions(+), 24 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 6c7ac86339..d3079b69bb 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -16,10 +16,12 @@ import * as vscode from "vscode"; export namespace Table { export type Callback = (data: RowContent) => void | PromiseLike; + export type Conditional = (data: RowContent) => boolean; export type Action = { title: string; command: string; type?: "primary" | "secondary" | "icon"; + condition?: string; callback: Callback; }; export type ContextMenuOption = Omit; diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/HTMLTemplate.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/HTMLTemplate.ts index aeedf2a920..355df0c1d1 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/HTMLTemplate.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/HTMLTemplate.ts @@ -21,7 +21,7 @@ const HTMLTemplate: string = ` diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index bd2f621abd..8c467976d3 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -322,6 +322,22 @@ export class SharedInit { await vscode.env.clipboard.writeText(path.posix.join(uriPath, data.name as string)); }, }) + .contextOption("all", { + title: "Citrus-specific option", + command: "citrus-dialog", + condition: `(data) => { + for (const citrus of ["lemon", "grapefruit", "lime", "tangerine"]) { + if (data.name && data.name.startsWith(citrus)) { + return true; + } + } + + return false; + }`, + callback: async (data: Table.RowContent) => { + await vscode.env.clipboard.writeText(path.posix.join(uriPath, data.name as string)); + }, + }) .rowAction("all", { title: "Open in editor", type: "primary", diff --git a/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx index 26a506cccb..a8c12aa08c 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx @@ -4,6 +4,7 @@ import { CellContextMenuEvent, ColDef } from "ag-grid-community"; import { JSXInternal } from "preact/src/jsx"; import { ControlledMenu, MenuItem } from "@szhsin/react-menu"; import "@szhsin/react-menu/dist/index.css"; +import { wrapFn } from "./types"; type MousePt = { x: number; y: number }; export type ContextMenuState = { @@ -79,12 +80,23 @@ export type ContextMenuElemProps = { }; export const ContextMenu = (clickedRow: any, menuItems: Table.ContextMenuOption[], vscodeApi: any) => { - return menuItems?.map((item, _i) => ( - vscodeApi.postMessage({ command: item.command, data: { ...clickedRow, actions: undefined } })} - style={{ borderBottom: "var(--vscode-menu-border)" }} - > - {item.title} - - )); + return menuItems + ?.filter((item) => { + if (item.condition == null) { + return true; + } + + // Wrap function to properly handle named parameters + const cond = new Function(wrapFn(item.condition)); + // Invoke the wrapped function once to get the built function, then invoke it again with the parameters + return cond.call(null).call(null, clickedRow); + }) + .map((item, _i) => ( + vscodeApi.postMessage({ command: item.command, data: { ...clickedRow, actions: undefined } })} + style={{ borderBottom: "var(--vscode-menu-border)" }} + > + {item.title} + + )); }; diff --git a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx index 06e311aaf8..30f6166bd9 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx @@ -7,7 +7,7 @@ import { AgGridReact } from "ag-grid-react"; import { useEffect, useState } from "preact/hooks"; import { getVsCodeTheme, isSecureOrigin, useMutableObserver } from "../utils"; import type { Table } from "@zowe/zowe-explorer-api"; -import { TableViewProps, tableProps } from "./types"; +import { TableViewProps, tableProps, wrapFn } from "./types"; import { useContextMenu } from "./ContextMenu"; // Custom styling (font family, VS Code color scheme, etc.) import "./style.css"; @@ -83,21 +83,32 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro // Render any actions for the given row and actions that apply to all rows newData.actions[params.rowIndex] || newData.actions["all"] ? ( - {[...(newData.actions[params.rowIndex] || []), ...(newData.actions["all"] || [])].map((action) => ( - - vscodeApi.postMessage({ - command: action.command, - data: { ...params.data, actions: undefined }, - row: newData.rows!.at(params.rowIndex), - }) + {[...(newData.actions[params.rowIndex] || []), ...(newData.actions["all"] || [])] + .filter((action) => { + if (action.condition == null) { + return true; } - style={{ marginRight: "0.25em" }} - > - {action.title} - - ))} + + // Wrap function to properly handle named parameters + const cond = new Function(wrapFn(action.condition)); + // Invoke the wrapped function once to get the built function, then invoke it again with the parameters + return cond.call(null).call(null, params.data); + }) + .map((action) => ( + + vscodeApi.postMessage({ + command: action.command, + data: { ...params.data, actions: undefined }, + row: newData.rows!.at(params.rowIndex), + }) + } + style={{ marginRight: "0.25em" }} + > + {action.title} + + ))} ) : null), }, diff --git a/packages/zowe-explorer/src/webviews/src/table-view/types.ts b/packages/zowe-explorer/src/webviews/src/table-view/types.ts index 50a19c8ad5..38ab10e83a 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/types.ts +++ b/packages/zowe-explorer/src/webviews/src/table-view/types.ts @@ -14,6 +14,8 @@ import { AgGridReactProps } from "ag-grid-react"; import { JSXInternal } from "preact/src/jsx"; import { ContextMenuState } from "./ContextMenu"; +export const wrapFn = (s: string) => `{ return ${s} };`; + type AgGridThemes = "ag-theme-quartz" | "ag-theme-balham" | "ag-theme-material" | "ag-theme-alpine"; export type TableViewProps = { actionsCellRenderer?: (params: any) => JSXInternal.Element; From 01d693dc87dfb1a89ca9821167a25d4915a2de99 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 27 Jun 2024 16:10:14 -0400 Subject: [PATCH 035/107] refactor(table): Transform conditional functions internally Signed-off-by: Trae Yelovich --- .../src/vscode/ui/TableView.ts | 14 +++++++----- .../src/vscode/ui/utils/TableBuilder.ts | 22 +++++++++++-------- .../src/trees/shared/SharedInit.ts | 6 ++--- .../webviews/src/table-view/ContextMenu.tsx | 6 ----- .../src/webviews/src/table-view/types.ts | 7 +++++- 5 files changed, 30 insertions(+), 25 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index d3079b69bb..4f327c74b6 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -24,7 +24,9 @@ export namespace Table { condition?: string; callback: Callback; }; + export type ActionOpts = Omit & { condition?: Conditional }; export type ContextMenuOption = Omit; + export type ContextMenuOpts = Omit & { condition?: Conditional }; export type Axes = "row" | "column"; export type RowContent = Record; @@ -141,12 +143,12 @@ export namespace Table { * * @returns Whether the webview successfully received the new action(s) */ - public addAction(index: number, ...actions: Action[]): Promise { + public addAction(index: number, ...actions: ActionOpts[]): Promise { if (this.data.actions[index]) { const existingActions = this.data.actions[index]; - this.data.actions[index] = [...existingActions, ...actions]; + this.data.actions[index] = [...existingActions, ...actions.map((action) => ({ ...action, condition: action.condition?.toString() }))]; } else { - this.data.actions[index] = actions; + this.data.actions[index] = actions.map((action) => ({ ...action, condition: action.condition?.toString() })); } return this.updateWebview(); } @@ -158,12 +160,12 @@ export namespace Table { * @param actions The actions to add to the given row * @returns Whether the webview successfully received the new context menu option(s) */ - public addContextOption(id: number | string, ...options: ContextMenuOption[]): Promise { + public addContextOption(id: number | string, ...options: ContextMenuOpts[]): Promise { if (this.data.contextOpts[id]) { const existingOpts = this.data.contextOpts[id]; - this.data.contextOpts[id] = [...existingOpts, ...options]; + this.data.contextOpts[id] = [...existingOpts, ...options.map((option) => ({ ...option, condition: option.condition?.toString() }))]; } else { - this.data.contextOpts[id] = options; + this.data.contextOpts[id] = options.map((option) => ({ ...option, condition: option.condition?.toString() })); } return this.updateWebview(); } diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index bcc348bea3..0addcff18c 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -94,12 +94,12 @@ export class TableBuilder { * @param index The column index to add an action to * @returns The same {@link TableBuilder} instance with the row action added */ - public contextOption(index: number | "all", option: Table.ContextMenuOption): TableBuilder { + public contextOption(index: number | "all", option: Table.ContextMenuOpts): TableBuilder { if (this.data.contextOpts[index]) { const opts = this.data.contextOpts[index]; - this.data.contextOpts[index] = [...opts, option]; + this.data.contextOpts[index] = [...opts, { ...option, condition: option.condition?.toString() }]; } else { - this.data.contextOpts[index] = [option]; + this.data.contextOpts[index] = [{ ...option, condition: option.condition?.toString() }]; } return this; } @@ -109,8 +109,12 @@ export class TableBuilder { * @param actions the record of indices to {@link Table.Action} arrays to use for the table * @returns The same {@link TableBuilder} instance with the row actions added */ - public rowActions(actions: Record): TableBuilder { - this.data.actions = actions; + public rowActions(actions: Record): TableBuilder { + //this.data.actions = actions; + for (const key of Object.keys(actions)) { + const actionList = actions[key] as Table.ActionOpts[]; + this.data.actions[key] = actionList.map((action) => ({ ...action, condition: action.condition?.toString() })); + } return this; } @@ -119,12 +123,12 @@ export class TableBuilder { * @param index The column index to add an action to * @returns The same {@link TableBuilder} instance with the row action added */ - public rowAction(index: number | "all", action: Table.Action): TableBuilder { + public rowAction(index: number | "all", action: Table.ActionOpts): TableBuilder { if (this.data.actions[index]) { - const actions = this.data.actions[index]; - this.data.actions[index] = [...actions, action]; + const actionList = this.data.actions[index]; + this.data.actions[index] = [...actionList, { ...action, condition: action.condition?.toString() }]; } else { - this.data.actions[index] = [action]; + this.data.actions[index] = [{ ...action, condition: action.condition?.toString() }]; } return this; } diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index 8c467976d3..142784a54c 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -325,15 +325,15 @@ export class SharedInit { .contextOption("all", { title: "Citrus-specific option", command: "citrus-dialog", - condition: `(data) => { + condition: (data) => { for (const citrus of ["lemon", "grapefruit", "lime", "tangerine"]) { - if (data.name && data.name.startsWith(citrus)) { + if (data.name && (data.name as string).startsWith(citrus)) { return true; } } return false; - }`, + }, callback: async (data: Table.RowContent) => { await vscode.env.clipboard.writeText(path.posix.join(uriPath, data.name as string)); }, diff --git a/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx index a8c12aa08c..5fcdb070d6 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx @@ -1,17 +1,11 @@ import type { Table } from "@zowe/zowe-explorer-api"; import { useCallback, useRef, useState } from "preact/hooks"; import { CellContextMenuEvent, ColDef } from "ag-grid-community"; -import { JSXInternal } from "preact/src/jsx"; import { ControlledMenu, MenuItem } from "@szhsin/react-menu"; import "@szhsin/react-menu/dist/index.css"; import { wrapFn } from "./types"; type MousePt = { x: number; y: number }; -export type ContextMenuState = { - open: boolean; - callback: (event: any) => void; - component: JSXInternal.Element | null; -}; export type ContextMenuProps = { selectRow: boolean; diff --git a/packages/zowe-explorer/src/webviews/src/table-view/types.ts b/packages/zowe-explorer/src/webviews/src/table-view/types.ts index 38ab10e83a..a1c7a6b7e4 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/types.ts +++ b/packages/zowe-explorer/src/webviews/src/table-view/types.ts @@ -12,7 +12,12 @@ import type { Table } from "@zowe/zowe-explorer-api"; import { AgGridReactProps } from "ag-grid-react"; import { JSXInternal } from "preact/src/jsx"; -import { ContextMenuState } from "./ContextMenu"; + +export type ContextMenuState = { + open: boolean; + callback: (event: any) => void; + component: JSXInternal.Element | null; +}; export const wrapFn = (s: string) => `{ return ${s} };`; From 9da36c13c67c211f317de2daca00ffad4000c144 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 2 Jul 2024 09:44:22 -0400 Subject: [PATCH 036/107] feat(webviews): build for 'package' script if folder missing Signed-off-by: Trae Yelovich --- packages/zowe-explorer/src/webviews/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/zowe-explorer/src/webviews/package.json b/packages/zowe-explorer/src/webviews/package.json index d677d8e5a1..9b6195d829 100644 --- a/packages/zowe-explorer/src/webviews/package.json +++ b/packages/zowe-explorer/src/webviews/package.json @@ -11,7 +11,7 @@ "preview": "vite preview", "fresh-clone": "pnpm clean && rimraf node_modules", "clean": "rimraf dist || true", - "package": "echo \"webviews: nothing to package.\"", + "package": "node -e \"fs.accessSync(path.join(__dirname, 'dist'))\" && echo \"webviews: nothing to package.\" || pnpm build", "test": "echo \"webviews: nothing to test\"", "lint": "echo \"webviews: nothing to lint.\"", "lint:html": "echo \"webviews: nothing to lint.\"", From 83c85ed14ec2417c1c6ba0143ee860d668a324c6 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 2 Jul 2024 14:37:06 -0400 Subject: [PATCH 037/107] wip: callback data types for contextmenu opts Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/src/vscode/ui/TableView.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 4f327c74b6..a45ad5c09f 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -17,15 +17,17 @@ import * as vscode from "vscode"; export namespace Table { export type Callback = (data: RowContent) => void | PromiseLike; export type Conditional = (data: RowContent) => boolean; + export type ActionKind = "primary" | "secondary" | "icon"; + export type CallbackDataType = "row" | "column" | "cell"; export type Action = { title: string; command: string; - type?: "primary" | "secondary" | "icon"; + type?: ActionKind; condition?: string; callback: Callback; }; export type ActionOpts = Omit & { condition?: Conditional }; - export type ContextMenuOption = Omit; + export type ContextMenuOption = Omit & { dataType?: CallbackDataType }; export type ContextMenuOpts = Omit & { condition?: Conditional }; export type Axes = "row" | "column"; From d5cc516a10448b037c1579d120f02fde9fb72b98 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 3 Jul 2024 15:20:01 -0400 Subject: [PATCH 038/107] wip: Support several other column options Signed-off-by: Trae Yelovich --- .../src/vscode/ui/TableView.ts | 69 ++++++++++++++++++- .../src/trees/shared/SharedInit.ts | 2 +- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index a45ad5c09f..8133d72467 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -32,7 +32,74 @@ export namespace Table { export type Axes = "row" | "column"; export type RowContent = Record; - export type Column = { field: string; filter?: boolean }; + export type ValueFormatter = (data: any) => string; + export type SortDirection = "asc" | "desc"; + export type Column = { + field: string; + type?: string | string[]; + cellDataType?: boolean | string; + valueFormatter?: string | ValueFormatter; + checkboxSelection?: boolean; + icons?: { [key: string]: string }; + suppressNavigable?: boolean; + context?: any; + + // Locking and edit variables + hide?: boolean; + lockVisible?: boolean; + lockPosition?: boolean | "left" | "right"; + suppressMovable?: boolean; + editable?: boolean; + valueSetter?: any; // TODO: stronger type here + singleClickEdit?: boolean; + + filter?: boolean; + floatingFilter?: boolean; + + // Headers + + // "field" variable will be used as header name if not provided + headerName?: string; + headerTooltip?: string; + headerClass?: string | string[]; + weapHeaderText?: boolean; + autoHeaderHeight?: boolean; + headerCheckboxSelection?: boolean; + + // Pinning + pinned?: boolean | "left" | "right" | null; + initialPinned?: boolean | "left" | "right"; + lockPinned?: boolean; + + // Row dragging + rowDrag?: boolean; + dndSource?: boolean; + + // Sorting + sortable?: boolean; + sort?: SortDirection; + initialSort?: "asc" | "desc"; + sortIndex?: number | null; + initialSortIndex?: number; + sortingOrder?: SortDirection[]; + comparator?: any; // TODO: Stronger type + unSortIcon?: boolean; + + // Column/row spanning (TODO: stronger types, add params to fn) + colSpan?: any; + rowSpan?: any; + + // Sizing + width?: number; + initialWidth?: number; + minWidth?: number; + maxWidth?: number; + flex?: number; + initialFlex?: number; + resizable?: boolean; + suppressSizeToFit?: boolean; + suppressAutoSize?: boolean; + }; export type Data = { // Actions to apply to the given row or column index actions: Record; diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index 0af9f11548..799f33c1bd 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -399,7 +399,7 @@ export class SharedInit { })) ) .columns([ - { field: "name", filter: true }, + { field: "name", filter: true, sort: "asc" }, { field: "size", filter: true }, { field: "owner", filter: true }, { field: "group", filter: true }, From bdc87679e7052811c9d7cb8f79ef72fd687d9509 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 3 Jul 2024 15:24:36 -0400 Subject: [PATCH 039/107] lint: resolve failing stage Signed-off-by: Trae Yelovich --- .../src/edit-history/components/PersistentUtils.ts | 10 +++++----- .../src/webviews/src/table-view/index.html | 12 ++++++------ packages/zowe-explorer/src/webviews/vite.config.ts | 8 ++++---- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentUtils.ts b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentUtils.ts index 2d9578eb46..7e5880c439 100644 --- a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentUtils.ts +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentUtils.ts @@ -16,9 +16,9 @@ import { useContext } from "preact/hooks"; export const DataPanelContext = createContext(null); export function useDataPanelContext(): DataPanelContextType { - const dataPanelContext = useContext(DataPanelContext); - if (!dataPanelContext) { - throw new Error("DataPanelContext has to be used within "); - } - return dataPanelContext; + const dataPanelContext = useContext(DataPanelContext); + if (!dataPanelContext) { + throw new Error("DataPanelContext has to be used within "); + } + return dataPanelContext; } diff --git a/packages/zowe-explorer/src/webviews/src/table-view/index.html b/packages/zowe-explorer/src/webviews/src/table-view/index.html index ad45961f7b..3648f8e806 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/index.html +++ b/packages/zowe-explorer/src/webviews/src/table-view/index.html @@ -4,13 +4,13 @@ - + Table View - - -
- - + + +
+ + diff --git a/packages/zowe-explorer/src/webviews/vite.config.ts b/packages/zowe-explorer/src/webviews/vite.config.ts index bc213a9308..7fe3cf20df 100644 --- a/packages/zowe-explorer/src/webviews/vite.config.ts +++ b/packages/zowe-explorer/src/webviews/vite.config.ts @@ -58,8 +58,8 @@ export default defineConfig({ }, resolve: { alias: { - "react": "preact/compat", - "react-dom": "preact/compat" - } - } + react: "preact/compat", + "react-dom": "preact/compat", + }, + }, }); From 94c89ad8ec53aa454fe12f69ec717ae2434d41fa Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 10 Jul 2024 14:18:42 -0400 Subject: [PATCH 040/107] feat: context menu UX improvements; support more props - support `comparator, rowSpan, colSpan` props - prevent scrolling when context menu is open - workaround for AG grid removing focus from a right-clicked/focused cell Signed-off-by: Trae Yelovich --- .../src/vscode/ui/TableView.ts | 41 ++++++++++---- .../src/vscode/ui/utils/TableBuilder.ts | 10 +++- .../webviews/src/table-view/ContextMenu.tsx | 54 +++++++++++++------ .../src/webviews/src/table-view/TableView.tsx | 2 +- .../src/webviews/src/table-view/style.css | 8 +++ .../src/webviews/src/table-view/types.ts | 7 ++- 6 files changed, 92 insertions(+), 30 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 8133d72467..694f2b2c9d 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -29,7 +29,6 @@ export namespace Table { export type ActionOpts = Omit & { condition?: Conditional }; export type ContextMenuOption = Omit & { dataType?: CallbackDataType }; export type ContextMenuOpts = Omit & { condition?: Conditional }; - export type Axes = "row" | "column"; export type RowContent = Record; export type ValueFormatter = (data: any) => string; @@ -38,7 +37,7 @@ export namespace Table { field: string; type?: string | string[]; cellDataType?: boolean | string; - valueFormatter?: string | ValueFormatter; + valueFormatter?: string; checkboxSelection?: boolean; icons?: { [key: string]: string }; suppressNavigable?: boolean; @@ -50,7 +49,7 @@ export namespace Table { lockPosition?: boolean | "left" | "right"; suppressMovable?: boolean; editable?: boolean; - valueSetter?: any; // TODO: stronger type here + valueSetter?: string; singleClickEdit?: boolean; filter?: boolean; @@ -82,12 +81,12 @@ export namespace Table { sortIndex?: number | null; initialSortIndex?: number; sortingOrder?: SortDirection[]; - comparator?: any; // TODO: Stronger type + comparator?: string; unSortIcon?: boolean; - // Column/row spanning (TODO: stronger types, add params to fn) - colSpan?: any; - rowSpan?: any; + // Column/row spanning + colSpan?: string; + rowSpan?: string; // Sizing width?: number; @@ -100,6 +99,12 @@ export namespace Table { suppressSizeToFit?: boolean; suppressAutoSize?: boolean; }; + export type ColumnOpts = Omit & { + comparator?: (valueA: any, valueB: any, nodeA: any, nodeB: any, isDescending: boolean) => number; + colSpan?: (params: any) => number; + rowSpan?: (params: any) => number; + valueSetter?: string | ((params: any) => boolean); + }; export type Data = { // Actions to apply to the given row or column index actions: Record; @@ -255,8 +260,16 @@ export namespace Table { * @param headers The headers to add to the existing header list * @returns Whether the webview successfully received the list of headers */ - public async addColumns(...columns: Column[]): Promise { - this.data.columns.push(...columns); + public async addColumns(...columns: ColumnOpts[]): Promise { + this.data.columns.push( + ...columns.map((col) => ({ + ...col, + comparator: col.comparator?.toString(), + colSpan: col.colSpan?.toString(), + rowSpan: col.rowSpan?.toString(), + valueSetter: col.valueSetter?.toString(), + })) + ); return this.updateWebview(); } @@ -277,8 +290,14 @@ export namespace Table { * @param headers The new headers to use for the table * @returns Whether the webview successfully received the new headers */ - public async setColumns(columns: Column[]): Promise { - this.data.columns = columns; + public async setColumns(columns: ColumnOpts[]): Promise { + this.data.columns = columns.map((col) => ({ + ...col, + comparator: col.comparator?.toString(), + colSpan: col.colSpan?.toString(), + rowSpan: col.rowSpan?.toString(), + valueSetter: col.valueSetter?.toString(), + })); return this.updateWebview(); } diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index 0addcff18c..c73388b035 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -74,8 +74,14 @@ export class TableBuilder { * @param rows The headers to use for the table * @returns The same {@link TableBuilder} instance with the headers added */ - public columns(newColumns: Table.Column[]): TableBuilder { - this.data.columns = newColumns; + public columns(newColumns: Table.ColumnOpts[]): TableBuilder { + this.data.columns = newColumns.map((col) => ({ + ...col, + comparator: col.comparator?.toString(), + colSpan: col.colSpan?.toString(), + rowSpan: col.rowSpan?.toString(), + valueSetter: col.valueSetter?.toString(), + })); return this; } diff --git a/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx index 5fcdb070d6..a90e736b27 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx @@ -26,42 +26,66 @@ export const useContextMenu = (contextMenu: ContextMenuProps) => { const [open, setOpen] = useState(false); const [anchor, setAnchor] = useState({ x: 0, y: 0 }); - const clickedColDef = useRef(null!); - const selectedRows = useRef([]); - const clickedRow = useRef(null); + const gridRefs = useRef({ + colDef: null, + selectedRows: [], + clickedRow: null, + }); - const openMenu = useCallback((e: PointerEvent | null | undefined) => { + /* Opens the context menu and sets the anchor point to mouse coordinates */ + const openMenu = (e: PointerEvent | null | undefined) => { if (!e) { return; } setAnchor({ x: e.clientX, y: e.clientY }); setOpen(true); - }, []); + }; + + /* Removes 'focused-ctx-menu' class name from other grid cells when context menu is closed. */ + const removeContextMenuClass = () => { + const elems = document.querySelectorAll("div[role='gridcell']"); + elems.forEach((elem) => elem.classList.remove("focused-ctx-menu")); + }; const cellMenu = useCallback( (event: CellContextMenuEvent) => { - const cell = event.api.getFocusedCell(); - if (contextMenu.selectRow && cell) { - event.api.setFocusedCell(cell.rowIndex, cell.column, cell.rowPinned); + // Check if a cell is focused. If so, keep the border around the grid cell by adding a "focused cell" class. + const focusedCell = event.api.getFocusedCell(); + if (contextMenu.selectRow && focusedCell) { + // Only apply the border to grid cell divs contained in valid cells + if (event.event?.target && (event.event?.target as Element).classList.contains("ag-cell-value")) { + const lastCell = (event.event?.target as Element).parentElement?.parentElement!; + lastCell.classList.add("focused-ctx-menu"); + } } - clickedColDef.current = event.colDef; - selectedRows.current = event.api.getSelectedRows(); - clickedRow.current = event.data; + // Cache the current column, selected rows and clicked row for later use + gridRefs.current = { + colDef: event.colDef, + selectedRows: event.api.getSelectedRows(), + clickedRow: event.data, + }; openMenu(event.event as PointerEvent); - event.event?.stopImmediatePropagation(); }, - [contextMenu.selectRow, selectedRows] + [contextMenu.selectRow, gridRefs.current.selectedRows] ); return { open, callback: cellMenu, component: open ? ( - setOpen(false)}> - {ContextMenu(clickedRow.current, contextMenu.options, contextMenu.vscodeApi)} + { + removeContextMenuClass(); + setOpen(false); + }} + > + {ContextMenu(gridRefs.current.clickedRow, contextMenu.options, contextMenu.vscodeApi)} ) : null, }; diff --git a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx index 30f6166bd9..91a754a59f 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx @@ -140,7 +140,7 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro return ( <> {tableData.title ?

{tableData.title}

: null} -
+
{contextMenu.component}
diff --git a/packages/zowe-explorer/src/webviews/src/table-view/style.css b/packages/zowe-explorer/src/webviews/src/table-view/style.css index 78936e047f..953300bd18 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/style.css +++ b/packages/zowe-explorer/src/webviews/src/table-view/style.css @@ -16,6 +16,14 @@ --ag-selected-row-background-color: var(--vscode-notebook-selectedCellBackground); } +.ctx-menu-open .ag-body-viewport { + overflow: hidden !important; +} + +.focused-ctx-menu { + border: 1px solid var(--vscode-menu-foreground) !important; +} + .ag-theme-vsc.ag-popup { position: absolute; } diff --git a/packages/zowe-explorer/src/webviews/src/table-view/types.ts b/packages/zowe-explorer/src/webviews/src/table-view/types.ts index a1c7a6b7e4..b9aa77ef03 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/types.ts +++ b/packages/zowe-explorer/src/webviews/src/table-view/types.ts @@ -34,7 +34,12 @@ export const tableProps = (contextMenu: ContextMenuState, tableData: Table.Data) enableCellTextSelection: true, ensureDomOrder: true, rowData: tableData.rows, - columnDefs: tableData.columns, + columnDefs: tableData.columns?.map((col) => ({ + ...col, + comparator: col.comparator ? new Function(wrapFn(col.comparator)).call(null) : undefined, + colSpan: col.colSpan ? new Function(wrapFn(col.colSpan)).call(null) : undefined, + rowSpan: col.rowSpan ? new Function(wrapFn(col.rowSpan)).call(null) : undefined, + })), pagination: true, onCellContextMenu: contextMenu.callback, }); From e58c646037e510c2715459491374a2f16952ea56 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 15 Jul 2024 13:52:23 -0400 Subject: [PATCH 041/107] feat: valueFormatter support, 'Copy cell', 'Copy row' context opts Signed-off-by: Trae Yelovich --- .../src/vscode/ui/TableView.ts | 77 +++++++++++++------ .../src/vscode/ui/utils/TableBuilder.ts | 4 +- .../src/trees/shared/SharedInit.ts | 62 ++++++++++----- .../webviews/src/table-view/ContextMenu.tsx | 25 ++++-- .../src/webviews/src/table-view/TableView.tsx | 22 +++++- .../src/webviews/src/table-view/types.ts | 1 + 6 files changed, 137 insertions(+), 54 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 694f2b2c9d..734e2d14e5 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -15,24 +15,50 @@ import { randomUUID } from "crypto"; import * as vscode from "vscode"; export namespace Table { - export type Callback = (data: RowContent) => void | PromiseLike; - export type Conditional = (data: RowContent) => boolean; + /* The types of supported content for the table and how they are represented in callback functions. */ + export type ContentTypes = string | number | boolean | string[]; + export type RowData = Record; + export type ColData = RowData; + export type CellData = ContentTypes; + + /* Defines the supported callbacks and related types. */ + export type CallbackTypes = "row" | "column" | "cell"; + export type Callback = { + // The type of callback + typ: CallbackTypes; + // The callback function itself - called from within the webview container. + fn: + | ((data: RowData) => void | PromiseLike) + | ((data: ColData) => void | PromiseLike) + | ((data: CellData) => void | PromiseLike); + }; + + /* Conditional callback function - whether an action or option should be rendered. */ + export type Conditional = (data: RowData | CellData) => boolean; + + /* Defines the supported actions and related types. */ export type ActionKind = "primary" | "secondary" | "icon"; - export type CallbackDataType = "row" | "column" | "cell"; export type Action = { title: string; command: string; type?: ActionKind; + // Stringified function will be called from within the webview container condition?: string; callback: Callback; }; + export type ContextMenuOption = Omit & { dataType?: CallbackTypes }; + + /* Helper types to allow passing function properties to builder/view functions. */ export type ActionOpts = Omit & { condition?: Conditional }; - export type ContextMenuOption = Omit & { dataType?: CallbackDataType }; export type ContextMenuOpts = Omit & { condition?: Conditional }; - export type RowContent = Record; - export type ValueFormatter = (data: any) => string; - export type SortDirection = "asc" | "desc"; + // -- Misc types -- + /* Value formatter callback. Expects the exact display value to be returned. */ + export type ValueFormatter = (data: { value: CellData }) => string; + export type Positions = "left" | "right"; + export type SortDirections = "asc" | "desc"; + + /* The column type definition. All available properties are offered for AG Grid columns. */ export type Column = { field: string; type?: string | string[]; @@ -46,10 +72,9 @@ export namespace Table { // Locking and edit variables hide?: boolean; lockVisible?: boolean; - lockPosition?: boolean | "left" | "right"; + lockPosition?: boolean | Positions; suppressMovable?: boolean; editable?: boolean; - valueSetter?: string; singleClickEdit?: boolean; filter?: boolean; @@ -66,8 +91,8 @@ export namespace Table { headerCheckboxSelection?: boolean; // Pinning - pinned?: boolean | "left" | "right" | null; - initialPinned?: boolean | "left" | "right"; + pinned?: boolean | Positions | null; + initialPinned?: boolean | Positions; lockPinned?: boolean; // Row dragging @@ -76,11 +101,11 @@ export namespace Table { // Sorting sortable?: boolean; - sort?: SortDirection; - initialSort?: "asc" | "desc"; + sort?: SortDirections; + initialSort?: SortDirections; sortIndex?: number | null; initialSortIndex?: number; - sortingOrder?: SortDirection[]; + sortingOrder?: SortDirections[]; comparator?: string; unSortIcon?: boolean; @@ -99,11 +124,11 @@ export namespace Table { suppressSizeToFit?: boolean; suppressAutoSize?: boolean; }; - export type ColumnOpts = Omit & { + export type ColumnOpts = Omit & { comparator?: (valueA: any, valueB: any, nodeA: any, nodeB: any, isDescending: boolean) => number; colSpan?: (params: any) => number; rowSpan?: (params: any) => number; - valueSetter?: string | ((params: any) => boolean); + valueFormatter?: ValueFormatter; }; export type Data = { // Actions to apply to the given row or column index @@ -112,7 +137,7 @@ export namespace Table { columns: Column[] | null | undefined; contextOpts: Record; // The row data for the table. Each row contains a set of variables corresponding to the data for each column in that row - rows: RowContent[] | null | undefined; + rows: RowData[] | null | undefined; // The display title for the table title?: string; }; @@ -128,8 +153,8 @@ export namespace Table { */ export class View extends WebView { private data: Data; - private onTableDataReceived: Event; - private onTableDisplayChanged: EventEmitter; + private onTableDataReceived: Event; + private onTableDisplayChanged: EventEmitter; public getUris(): UriPair { return this.uris; @@ -167,8 +192,10 @@ export namespace Table { return; // "copy" command: Copy the data for the row that was right-clicked. case "copy": + await vscode.env.clipboard.writeText(JSON.stringify(message.data.row)); + return; case "copy-cell": - await vscode.env.clipboard.writeText(JSON.stringify(message.data)); + await vscode.env.clipboard.writeText(message.data.cell); return; default: break; @@ -182,7 +209,7 @@ export namespace Table { ...this.data.contextOpts.all, ].find((action) => action.command === message.command); if (matchingActionable != null) { - await matchingActionable.callback(message.data); + await matchingActionable.callback.fn(message.data); } } @@ -249,7 +276,7 @@ export namespace Table { * @param rows The rows of data to add to the table * @returns Whether the webview successfully received the new content */ - public async addContent(...rows: RowContent[]): Promise { + public async addContent(...rows: RowData[]): Promise { this.data.rows.push(...rows); return this.updateWebview(); } @@ -267,7 +294,7 @@ export namespace Table { comparator: col.comparator?.toString(), colSpan: col.colSpan?.toString(), rowSpan: col.rowSpan?.toString(), - valueSetter: col.valueSetter?.toString(), + valueFormatter: col.valueFormatter?.toString(), })) ); return this.updateWebview(); @@ -279,7 +306,7 @@ export namespace Table { * @param rows The rows of data to apply to the table * @returns Whether the webview successfully received the new content */ - public async setContent(rows: RowContent[]): Promise { + public async setContent(rows: RowData[]): Promise { this.data.rows = rows; return this.updateWebview(); } @@ -296,7 +323,7 @@ export namespace Table { comparator: col.comparator?.toString(), colSpan: col.colSpan?.toString(), rowSpan: col.rowSpan?.toString(), - valueSetter: col.valueSetter?.toString(), + valueFormatter: col.valueFormatter?.toString(), })); return this.updateWebview(); } diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index c73388b035..3cf528a38a 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -64,7 +64,7 @@ export class TableBuilder { * @param rows The rows of content to use for the table * @returns The same {@link TableBuilder} instance with the rows added */ - public rows(...rows: Table.RowContent[]): TableBuilder { + public rows(...rows: Table.RowData[]): TableBuilder { this.data.rows = rows; return this; } @@ -80,7 +80,7 @@ export class TableBuilder { comparator: col.comparator?.toString(), colSpan: col.colSpan?.toString(), rowSpan: col.rowSpan?.toString(), - valueSetter: col.valueSetter?.toString(), + valueFormatter: col.valueFormatter?.toString(), })); return this; } diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index 9f3496b0e4..0a8dd81413 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -315,14 +315,26 @@ export class SharedInit { make: "Ford", model: "Model T", year: 1908, + price: 4000, }, { make: "Tesla", model: "Model Y", year: 2022, + price: 40000, } ) - .columns([{ field: "make" }, { field: "model" }, { field: "year" }]) + .columns([ + { field: "make" }, + { field: "model" }, + { field: "year" }, + { + field: "price", + valueFormatter: (data: { value: Table.CellData }): string => { + return data.value ? `$${data.value.toString()}` : "No price available"; + }, + }, + ]) .build(); // Add content to the existing table view await table.addContent({ make: "Toyota", model: "Corolla", year: 2007 }); @@ -346,8 +358,11 @@ export class SharedInit { .contextOption("all", { title: "Show as dialog", command: "print-dialog", - callback: async (data) => { - await Gui.showMessage(JSON.stringify(data)); + callback: { + typ: "row", + fn: async (data) => { + await Gui.showMessage(JSON.stringify(data)); + }, }, }) .build(); @@ -390,7 +405,7 @@ export class SharedInit { ) .columns([ { field: "name", filter: true, sort: "asc" }, - { field: "size", filter: true }, + { field: "size", filter: true, valueFormatter: (value: any) => `${value} bytes` }, { field: "owner", filter: true }, { field: "group", filter: true }, { field: "perms", filter: true }, @@ -398,14 +413,17 @@ export class SharedInit { .contextOption("all", { title: "Copy path", command: "copy-path", - callback: async (data: Table.RowContent) => { - await vscode.env.clipboard.writeText(path.posix.join(uriPath, data.name as string)); + callback: { + fn: async (data: Table.RowData) => { + await vscode.env.clipboard.writeText(path.posix.join(uriPath, data.name as string)); + }, + typ: "row", }, }) .contextOption("all", { title: "Citrus-specific option", command: "citrus-dialog", - condition: (data) => { + condition: (data: any) => { for (const citrus of ["lemon", "grapefruit", "lime", "tangerine"]) { if (data.name && (data.name as string).startsWith(citrus)) { return true; @@ -414,26 +432,32 @@ export class SharedInit { return false; }, - callback: async (data: Table.RowContent) => { - await vscode.env.clipboard.writeText(path.posix.join(uriPath, data.name as string)); + callback: { + fn: async (data: Table.RowData) => { + await vscode.env.clipboard.writeText(path.posix.join(uriPath, data.name as string)); + }, + typ: "row", }, }) .rowAction("all", { title: "Open in editor", type: "primary", command: "edit", - callback: async (data: Table.RowContent) => { - const filename = data.name as string; - const uri = vscode.Uri.from({ - scheme: "zowe-uss", - path: path.posix.join(uriPath, filename), - }); + callback: { + fn: async (data: Table.RowData) => { + const filename = data.name as string; + const uri = vscode.Uri.from({ + scheme: "zowe-uss", + path: path.posix.join(uriPath, filename), + }); - if (!UssFSProvider.instance.exists(uri)) { - await UssFSProvider.instance.writeFile(uri, new Uint8Array(), { create: true, overwrite: false }); - } + if (!UssFSProvider.instance.exists(uri)) { + await UssFSProvider.instance.writeFile(uri, new Uint8Array(), { create: true, overwrite: false }); + } - await vscode.commands.executeCommand("vscode.open", uri); + await vscode.commands.executeCommand("vscode.open", uri); + }, + typ: "row", }, }) .build(); diff --git a/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx index a90e736b27..e3ae602822 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx @@ -9,8 +9,8 @@ type MousePt = { x: number; y: number }; export type ContextMenuProps = { selectRow: boolean; - selectedRows: Table.RowContent[] | null | undefined; - clickedRow: Table.RowContent; + selectedRows: Table.RowData[] | null | undefined; + clickedRow: Table.RowData; options: Table.ContextMenuOption[]; colDef: ColDef; vscodeApi: any; @@ -30,6 +30,7 @@ export const useContextMenu = (contextMenu: ContextMenuProps) => { colDef: null, selectedRows: [], clickedRow: null, + field: undefined, }); /* Opens the context menu and sets the anchor point to mouse coordinates */ @@ -65,6 +66,7 @@ export const useContextMenu = (contextMenu: ContextMenuProps) => { colDef: event.colDef, selectedRows: event.api.getSelectedRows(), clickedRow: event.data, + field: event.colDef.field, }; openMenu(event.event as PointerEvent); @@ -85,7 +87,7 @@ export const useContextMenu = (contextMenu: ContextMenuProps) => { setOpen(false); }} > - {ContextMenu(gridRefs.current.clickedRow, contextMenu.options, contextMenu.vscodeApi)} + {ContextMenu(gridRefs.current, contextMenu.options, contextMenu.vscodeApi)} ) : null, }; @@ -97,7 +99,7 @@ export type ContextMenuElemProps = { vscodeApi: any; }; -export const ContextMenu = (clickedRow: any, menuItems: Table.ContextMenuOption[], vscodeApi: any) => { +export const ContextMenu = (gridRefs: any, menuItems: Table.ContextMenuOption[], vscodeApi: any) => { return menuItems ?.filter((item) => { if (item.condition == null) { @@ -107,11 +109,22 @@ export const ContextMenu = (clickedRow: any, menuItems: Table.ContextMenuOption[ // Wrap function to properly handle named parameters const cond = new Function(wrapFn(item.condition)); // Invoke the wrapped function once to get the built function, then invoke it again with the parameters - return cond.call(null).call(null, clickedRow); + return cond.call(null).call(null, gridRefs.clickedRow); }) .map((item, _i) => ( vscodeApi.postMessage({ command: item.command, data: { ...clickedRow, actions: undefined } })} + onClick={(_e: any) => { + vscodeApi.postMessage({ + command: item.command, + data: { + row: { ...gridRefs.clickedRow, actions: undefined }, + field: gridRefs.field, + cell: gridRefs.colDef.valueFormatter + ? gridRefs.colDef.valueFormatter({ value: gridRefs.clickedRow[gridRefs.field] }) + : gridRefs.clickedRow[gridRefs.field], + }, + }); + }} style={{ borderBottom: "var(--vscode-menu-border)" }} > {item.title} diff --git a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx index 91a754a59f..fdd3331084 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx @@ -31,7 +31,25 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro const [theme, setTheme] = useState(baseTheme ?? "ag-theme-quartz"); const contextMenu = useContextMenu({ - options: [{ title: "Copy", command: "copy", callback: () => {} }, ...(tableData.contextOpts.all ?? [])], + options: [ + { + title: "Copy cell", + command: "copy-cell", + callback: { + typ: "cell", + fn: () => {}, + }, + }, + { + title: "Copy row", + command: "copy", + callback: { + typ: "row", + fn: () => {}, + }, + }, + ...(tableData.contextOpts.all ?? []), + ], selectRow: true, selectedRows: [], clickedRow: undefined as any, @@ -66,7 +84,7 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro const newData: Table.Data = response.data; if (Object.keys(newData.actions).length > 1 || newData.actions.all?.length > 0) { // Add an extra column to the end of each row if row actions are present - const rows = newData.rows?.map((row: Table.RowContent) => { + const rows = newData.rows?.map((row: Table.RowData) => { return { ...row, actions: "" }; }); const columns = [ diff --git a/packages/zowe-explorer/src/webviews/src/table-view/types.ts b/packages/zowe-explorer/src/webviews/src/table-view/types.ts index b9aa77ef03..c09a7f8f62 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/types.ts +++ b/packages/zowe-explorer/src/webviews/src/table-view/types.ts @@ -39,6 +39,7 @@ export const tableProps = (contextMenu: ContextMenuState, tableData: Table.Data) comparator: col.comparator ? new Function(wrapFn(col.comparator)).call(null) : undefined, colSpan: col.colSpan ? new Function(wrapFn(col.colSpan)).call(null) : undefined, rowSpan: col.rowSpan ? new Function(wrapFn(col.rowSpan)).call(null) : undefined, + valueFormatter: col.valueFormatter ? new Function(wrapFn(col.valueFormatter)).call(null) : undefined, })), pagination: true, onCellContextMenu: contextMenu.callback, From 1bacfb76c7e2dc2b8def93f93e82f16c57c1c09c Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 15 Jul 2024 14:40:02 -0400 Subject: [PATCH 042/107] feat(table): Fire events; refactor: Table.Data -> Table.ViewOpts Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/package.json | 1 + .../src/vscode/ui/TableView.ts | 33 ++++++++++++------- .../src/vscode/ui/utils/TableBuilder.ts | 2 +- .../src/trees/shared/SharedInit.ts | 3 +- .../src/webviews/src/table-view/TableView.tsx | 6 ++-- .../src/webviews/src/table-view/types.ts | 14 ++++++-- pnpm-lock.yaml | 7 ++++ 7 files changed, 47 insertions(+), 19 deletions(-) diff --git a/packages/zowe-explorer-api/package.json b/packages/zowe-explorer-api/package.json index a99e9bdab4..98517f70ce 100644 --- a/packages/zowe-explorer-api/package.json +++ b/packages/zowe-explorer-api/package.json @@ -31,6 +31,7 @@ "@zowe/zos-tso-for-zowe-sdk": "8.0.0-next.202404032038", "@zowe/zos-uss-for-zowe-sdk": "8.0.0-next.202404032038", "@zowe/zosmf-for-zowe-sdk": "8.0.0-next.202404032038", + "deep-object-diff": "^1.1.9", "mustache": "^4.2.0", "preact": "^10.16.0", "semver": "^7.6.0" diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 734e2d14e5..a03ab0d2ee 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -10,9 +10,9 @@ */ import { UriPair, WebView } from "./WebView"; -import { Event, EventEmitter, ExtensionContext } from "vscode"; +import { Event, EventEmitter, ExtensionContext, env } from "vscode"; import { randomUUID } from "crypto"; -import * as vscode from "vscode"; +import { addedDiff } from "deep-object-diff"; export namespace Table { /* The types of supported content for the table and how they are represented in callback functions. */ @@ -130,7 +130,7 @@ export namespace Table { rowSpan?: (params: any) => number; valueFormatter?: ValueFormatter; }; - export type Data = { + export type ViewOpts = { // Actions to apply to the given row or column index actions: Record; // Column headers for the top of the table @@ -152,9 +152,12 @@ export namespace Table { * use the `TableBuilder` class to prepare table data and build an instance. */ export class View extends WebView { - private data: Data; - private onTableDataReceived: Event; - private onTableDisplayChanged: EventEmitter; + private lastUpdated: ViewOpts; + private data: ViewOpts; + private onTableDataReceivedEmitter: EventEmitter> = new EventEmitter(); + private onTableDisplayChangedEmitter: EventEmitter = new EventEmitter(); + public onTableDisplayChanged: Event = this.onTableDisplayChangedEmitter.event; + public onTableDataReceived: Event> = this.onTableDataReceivedEmitter.event; public getUris(): UriPair { return this.uris; @@ -164,7 +167,7 @@ export namespace Table { return this.htmlContent; } - public constructor(context: ExtensionContext, data?: Data) { + public constructor(context: ExtensionContext, data?: ViewOpts) { super(data.title ?? "Table view", "table-view", context, (message) => this.onMessageReceived(message), true); if (data) { this.data = data; @@ -184,7 +187,7 @@ export namespace Table { switch (message.command) { // "ondisplaychanged" command: The table's layout was updated by the user from within the webview. case "ondisplaychanged": - this.onTableDisplayChanged.fire(message.data); + this.onTableDisplayChangedEmitter.fire(message.data); return; // "ready" command: The table view has attached its message listener and is ready to receive data. case "ready": @@ -192,10 +195,10 @@ export namespace Table { return; // "copy" command: Copy the data for the row that was right-clicked. case "copy": - await vscode.env.clipboard.writeText(JSON.stringify(message.data.row)); + await env.clipboard.writeText(JSON.stringify(message.data.row)); return; case "copy-cell": - await vscode.env.clipboard.writeText(message.data.cell); + await env.clipboard.writeText(message.data.cell); return; default: break; @@ -220,10 +223,16 @@ export namespace Table { * @returns Whether the webview received the update that was sent */ private async updateWebview(): Promise { - return this.panel.webview.postMessage({ + const result = await this.panel.webview.postMessage({ command: "ondatachanged", data: this.data, }); + + if (result) { + this.onTableDataReceivedEmitter.fire(this.lastUpdated ? addedDiff(this.data, this.lastUpdated) : this.data); + this.lastUpdated = this.data; + } + return result; } /** @@ -341,7 +350,7 @@ export namespace Table { } export class Instance extends View { - public constructor(context: ExtensionContext, data: Table.Data) { + public constructor(context: ExtensionContext, data: Table.ViewOpts) { super(context, data); } diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index 3cf528a38a..600aa0d2d1 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -34,7 +34,7 @@ import { TableMediator } from "./TableMediator"; */ export class TableBuilder { private context: ExtensionContext; - private data: Table.Data = { + private data: Table.ViewOpts = { actions: { all: [], }, diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index 0a8dd81413..cf4a4d68ed 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -366,6 +366,7 @@ export class SharedInit { }, }) .build(); + // Example of catching filter/sort changes: table.onTableDisplayChanged((data) => console.log(data)); }) ); context.subscriptions.push( @@ -405,7 +406,7 @@ export class SharedInit { ) .columns([ { field: "name", filter: true, sort: "asc" }, - { field: "size", filter: true, valueFormatter: (value: any) => `${value} bytes` }, + { field: "size", filter: true, valueFormatter: (data: { value: Table.CellData }) => `${data.value.toString()} bytes` }, { field: "owner", filter: true }, { field: "group", filter: true }, { field: "perms", filter: true }, diff --git a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx index fdd3331084..55ddf27005 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx @@ -15,7 +15,7 @@ import "./style.css"; const vscodeApi = acquireVsCodeApi(); export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewProps) => { - const [tableData, setTableData] = useState( + const [tableData, setTableData] = useState( data ?? { actions: { all: [], @@ -81,7 +81,7 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro switch (response.command) { case "ondatachanged": // Update received from a VS Code extender; update table state - const newData: Table.Data = response.data; + const newData: Table.ViewOpts = response.data; if (Object.keys(newData.actions).length > 1 || newData.actions.all?.length > 0) { // Add an extra column to the end of each row if row actions are present const rows = newData.rows?.map((row: Table.RowData) => { @@ -160,7 +160,7 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro {tableData.title ?

{tableData.title}

: null}
{contextMenu.component} - +
); diff --git a/packages/zowe-explorer/src/webviews/src/table-view/types.ts b/packages/zowe-explorer/src/webviews/src/table-view/types.ts index c09a7f8f62..262ea3e985 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/types.ts +++ b/packages/zowe-explorer/src/webviews/src/table-view/types.ts @@ -25,11 +25,11 @@ type AgGridThemes = "ag-theme-quartz" | "ag-theme-balham" | "ag-theme-material" export type TableViewProps = { actionsCellRenderer?: (params: any) => JSXInternal.Element; baseTheme?: AgGridThemes | string; - data?: Table.Data; + data?: Table.ViewOpts; }; // Define props for the AG Grid table here -export const tableProps = (contextMenu: ContextMenuState, tableData: Table.Data): Partial => ({ +export const tableProps = (contextMenu: ContextMenuState, tableData: Table.ViewOpts, vscodeApi: any): Partial => ({ // domLayout: "autoHeight", enableCellTextSelection: true, ensureDomOrder: true, @@ -43,4 +43,14 @@ export const tableProps = (contextMenu: ContextMenuState, tableData: Table.Data) })), pagination: true, onCellContextMenu: contextMenu.callback, + onFilterChanged: (event) => { + const rows: Table.RowData[] = []; + event.api.forEachNodeAfterFilterAndSort((row, _i) => rows.push(row.data)); + vscodeApi.postMessage({ command: "ondisplaychanged", data: rows }); + }, + onSortChanged: (event) => { + const rows: Table.RowData[] = []; + event.api.forEachNodeAfterFilterAndSort((row, _i) => rows.push(row.data)); + vscodeApi.postMessage({ command: "ondisplaychanged", data: rows }); + }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43f1c31d82..99696068de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -292,6 +292,9 @@ importers: '@zowe/zosmf-for-zowe-sdk': specifier: 8.0.0-next.202404032038 version: 8.0.0-next.202404032038(@zowe/core-for-zowe-sdk@8.0.0-next.202404032038)(@zowe/imperative@8.0.0-next.202404032038) + deep-object-diff: + specifier: ^1.1.9 + version: 1.1.9 mustache: specifier: ^4.2.0 version: 4.2.0 @@ -5115,6 +5118,10 @@ packages: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true + /deep-object-diff@1.1.9: + resolution: {integrity: sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==} + dev: false + /deepmerge-json@1.5.0: resolution: {integrity: sha512-jZRrDmBKjmGcqMFEUJ14FjMJwm05Qaked+1vxaALRtF0UAl7lPU8OLWXFxvoeg3jbQM249VPFVn8g2znaQkEtA==} engines: {node: '>=4.0.0'} From 3099705ee9d07c9c4a3c34478032f404967b1241 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 15 Jul 2024 14:41:46 -0400 Subject: [PATCH 043/107] fix(table): swap data objects in addedDiff call Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/src/vscode/ui/TableView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index a03ab0d2ee..fb7bdb92d1 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -229,7 +229,7 @@ export namespace Table { }); if (result) { - this.onTableDataReceivedEmitter.fire(this.lastUpdated ? addedDiff(this.data, this.lastUpdated) : this.data); + this.onTableDataReceivedEmitter.fire(this.lastUpdated ? addedDiff(this.lastUpdated, this.data) : this.data); this.lastUpdated = this.data; } return result; From 66e90dfbec8c38f7b5d1c7c99a4ec31827ddb1c5 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 15 Jul 2024 15:00:14 -0400 Subject: [PATCH 044/107] feat: Set optional table properties (list of properties: wip) Signed-off-by: Trae Yelovich --- .../src/vscode/ui/TableView.ts | 35 ++++++++++++------- .../src/vscode/ui/utils/TableBuilder.ts | 10 ++++++ .../src/trees/shared/SharedInit.ts | 6 +++- .../src/webviews/src/table-view/types.ts | 4 ++- 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index fb7bdb92d1..5e272537c7 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -24,41 +24,41 @@ export namespace Table { /* Defines the supported callbacks and related types. */ export type CallbackTypes = "row" | "column" | "cell"; export type Callback = { - // The type of callback + /** The type of callback */ typ: CallbackTypes; - // The callback function itself - called from within the webview container. + /** The callback function itself - called from within the webview container. */ fn: | ((data: RowData) => void | PromiseLike) | ((data: ColData) => void | PromiseLike) | ((data: CellData) => void | PromiseLike); }; - /* Conditional callback function - whether an action or option should be rendered. */ + /** Conditional callback function - whether an action or option should be rendered. */ export type Conditional = (data: RowData | CellData) => boolean; - /* Defines the supported actions and related types. */ + // Defines the supported actions and related types. export type ActionKind = "primary" | "secondary" | "icon"; export type Action = { title: string; command: string; type?: ActionKind; - // Stringified function will be called from within the webview container + /** Stringified function will be called from within the webview container. */ condition?: string; callback: Callback; }; export type ContextMenuOption = Omit & { dataType?: CallbackTypes }; - /* Helper types to allow passing function properties to builder/view functions. */ + // Helper types to allow passing function properties to builder/view functions. export type ActionOpts = Omit & { condition?: Conditional }; export type ContextMenuOpts = Omit & { condition?: Conditional }; // -- Misc types -- - /* Value formatter callback. Expects the exact display value to be returned. */ + /** Value formatter callback. Expects the exact display value to be returned. */ export type ValueFormatter = (data: { value: CellData }) => string; export type Positions = "left" | "right"; export type SortDirections = "asc" | "desc"; - /* The column type definition. All available properties are offered for AG Grid columns. */ + /** The column type definition. All available properties are offered for AG Grid columns. */ export type Column = { field: string; type?: string | string[]; @@ -131,15 +131,26 @@ export namespace Table { valueFormatter?: ValueFormatter; }; export type ViewOpts = { - // Actions to apply to the given row or column index + /** Actions to apply to the given row or column index */ actions: Record; - // Column headers for the top of the table + /** Column definitions for the top of the table */ columns: Column[] | null | undefined; + /** Context menu options for rows in the table */ contextOpts: Record; - // The row data for the table. Each row contains a set of variables corresponding to the data for each column in that row + /** The row data for the table. Each row contains a set of variables corresponding to the data for each column in that row. */ rows: RowData[] | null | undefined; - // The display title for the table + /** The display title for the table */ title?: string; + /** Whether the table should be split into pages. */ + pagination?: boolean; + /** How many rows to load per page */ + paginationPageSize?: number; + /** + * Set to an array of values to show the page size selector with custom list of possible page sizes. + * Set to `true` to show the page size selector with the default page sizes `[20, 50, 100]`. + * Set to `false` to hide the page size selector. + */ + paginationPageSizeSelector?: number[] | boolean; }; /** diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index 600aa0d2d1..368749736d 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -49,6 +49,16 @@ export class TableBuilder { this.context = context; } + /** + * Set optional properties for the table view. + * @param opts The options for the table + * @returns The same {@link TableBuilder} instance with the options added + */ + public options(opts: Omit): TableBuilder { + this.data = { ...this.data, ...opts }; + return this; + } + /** * Set the title for the next table. * @param name The name of the table diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index cf4a4d68ed..d5cc15c82b 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -344,6 +344,10 @@ export class SharedInit { vscode.commands.registerCommand("zowe.tableView2", () => { // Context option demo new TableBuilder(context) + .options({ + pagination: true, + paginationPageSizeSelector: [25, 50, 100, 250], + }) .title("Random data") .rows( ...Array.from({ length: 1024 }, (val) => ({ @@ -361,7 +365,7 @@ export class SharedInit { callback: { typ: "row", fn: async (data) => { - await Gui.showMessage(JSON.stringify(data)); + await Gui.showMessage(data.cell); }, }, }) diff --git a/packages/zowe-explorer/src/webviews/src/table-view/types.ts b/packages/zowe-explorer/src/webviews/src/table-view/types.ts index 262ea3e985..6c31be7209 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/types.ts +++ b/packages/zowe-explorer/src/webviews/src/table-view/types.ts @@ -41,7 +41,9 @@ export const tableProps = (contextMenu: ContextMenuState, tableData: Table.ViewO rowSpan: col.rowSpan ? new Function(wrapFn(col.rowSpan)).call(null) : undefined, valueFormatter: col.valueFormatter ? new Function(wrapFn(col.valueFormatter)).call(null) : undefined, })), - pagination: true, + pagination: tableData.pagination, + paginationPageSize: tableData.paginationPageSize, + paginationPageSizeSelector: tableData.paginationPageSizeSelector, onCellContextMenu: contextMenu.callback, onFilterChanged: (event) => { const rows: Table.RowData[] = []; From a95fd3d89869c39cb163f24f0cb6d916b6b13d3c Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 16 Jul 2024 10:55:32 -0400 Subject: [PATCH 045/107] feat(tables): add several AG Grid props. to options type Signed-off-by: Trae Yelovich --- .../src/vscode/ui/TableView.ts | 117 ++++++++++++++++-- .../src/vscode/ui/utils/TableBuilder.ts | 2 +- 2 files changed, 107 insertions(+), 12 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 5e272537c7..3626c8f394 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -130,19 +130,71 @@ export namespace Table { rowSpan?: (params: any) => number; valueFormatter?: ValueFormatter; }; - export type ViewOpts = { - /** Actions to apply to the given row or column index */ - actions: Record; - /** Column definitions for the top of the table */ - columns: Column[] | null | undefined; - /** Context menu options for rows in the table */ - contextOpts: Record; - /** The row data for the table. Each row contains a set of variables corresponding to the data for each column in that row. */ - rows: RowData[] | null | undefined; - /** The display title for the table */ - title?: string; + + export interface SizeColumnsToFitGridStrategy { + type: "fitGridWidth"; + // Default minimum width for every column (does not override the column minimum width). + defaultMinWidth?: number; + // Default maximum width for every column (does not override the column maximum width). + defaultMaxWidth?: number; + // Provide to limit specific column widths when sizing. + columnLimits?: SizeColumnsToFitGridColumnLimits[]; + } + + export interface SizeColumnsToFitGridColumnLimits { + colId: string; + // Minimum width for this column (does not override the column minimum width) + minWidth?: number; + // Maximum width for this column (does not override the column maximum width) + maxWidth?: number; + } + + export interface SizeColumnsToFitProvidedWidthStrategy { + type: "fitProvidedWidth"; + width: number; + } + + export interface SizeColumnsToContentStrategy { + type: "fitCellContents"; + // If true, the header won't be included when calculating the column widths. + skipHeader?: boolean; + // If not provided will auto-size all columns. Otherwise will size the specified columns. + colIds?: string[]; + } + + // AG Grid: Optional properties + export type GridProperties = { + /** Allow reordering and pinning columns by dragging columns from the Columns Tool Panel to the grid */ + allowDragFromColumnsToolPanel?: boolean; + /** Number of pixels to add a column width after the auto-sizing calculation */ + autoSizePadding?: number; + /** Auto-size the columns when the grid is loaded. Can size to fit the grid width, fit a provided width or fit the cell contents. Read once during initialization. */ + autoSizeStrategy?: SizeColumnsToFitGridStrategy | SizeColumnsToFitProvidedWidthStrategy | SizeColumnsToFitGridStrategy; + /** Set to 'shift' to have shift-resize as the default resize operation */ + colResizeDefault?: "shift"; + /** Changes the display type of the column menu. 'new' displays the main list of menu items; 'legacy' displays a tabbed menu */ + columnMenu?: "legacy" | "new"; + /** Set this to `true` to enable debugging information from the grid */ + debug?: boolean; + /** The height in pixels for the rows containing floating filters. */ + floatingFiltersHeight?: number; + /** The height in pixels for the rows containing header column groups. */ + groupHeaderHeight?: number; + /** The height in pixels for the row contianing the column label header. Default provided by the AG Grid theme. */ + headerHeight?: number; + /** Show/hide the "Loading" overlay */ + loading?: boolean; + /** Map of key:value pairs for localizing grid text. Read once during initialization. */ + localeText?: { [key: string]: string }; + /** Keeps the order of columns maintained after new Column Definitions are updated. */ + maintainColumnOrder?: boolean; /** Whether the table should be split into pages. */ pagination?: boolean; + /** + * Set to `true` so that the number of rows to load per page is automatically adjusted by the grid. + * If `false`, `paginationPageSize` is used. + */ + paginationAutoPageSize?: boolean; /** How many rows to load per page */ paginationPageSize?: number; /** @@ -151,8 +203,40 @@ export namespace Table { * Set to `false` to hide the page size selector. */ paginationPageSizeSelector?: number[] | boolean; + /** If defined, rows are filtered using this text as a Quick Filter. */ + quickFilterText?: string; + /** Set to `true` to skip the `headerName` when `autoSize` is called by default. Read once during initialization. */ + skipHeaderOnAutoSize?: boolean; + /** Suppresses auto-sizing columns for columns - when enabled, double-clicking a column's header's edge will not auto-size. Read once during initialization. */ + suppressAutoSize?: boolean; + suppressColumnMoveAnimation?: boolean; + /** If `true`, when you dreag a column out of the grid, the column is not hidden */ + suppressDragLeaveHidesColumns?: boolean; + /** If `true`, then dots in field names are not treated as deep references, allowing you to use dots in your field name if preferred. */ + suppressFieldDotNotation?: boolean; + /** + * When `true`, the column menu button will always be shown. + * When `false`, the column menu button will only show when the mouse is over the column header. + * If `columnMenu = 'legacy'`, this will default to `false` instead of `true`. + */ + suppressMenuHide?: boolean; + /** Set to `true` to suppress column moving (fixed position for columns) */ + suppressMovableColumns?: boolean; }; + export type ViewOpts = { + /** Actions to apply to the given row or column index */ + actions: Record; + /** Column definitions for the top of the table */ + columns: Column[] | null | undefined; + /** Context menu options for rows in the table */ + contextOpts: Record; + /** The row data for the table. Each row contains a set of variables corresponding to the data for each column in that row. */ + rows: RowData[] | null | undefined; + /** The display title for the table */ + title?: string; + } & GridProperties; + /** * A class that acts as a controller between the extension and the table view. Based off of the {@link WebView} class. * @@ -348,6 +432,17 @@ export namespace Table { return this.updateWebview(); } + /** + * Sets the options for the table. + * + * @param opts The optional grid properties for the table + * @returns Whether the webview successfully received the new options + */ + public setOptions(opts: GridProperties): Promise { + this.data = { ...this.data, ...opts }; + return this.updateWebview(); + } + /** * Sets the display title for the table view. * diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index 368749736d..33de118b81 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -54,7 +54,7 @@ export class TableBuilder { * @param opts The options for the table * @returns The same {@link TableBuilder} instance with the options added */ - public options(opts: Omit): TableBuilder { + public options(opts: Table.GridProperties): TableBuilder { this.data = { ...this.data, ...opts }; return this; } From e796ea6ca36fd01e77a85e509e7a6a5138e3f8a7 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 16 Jul 2024 16:08:54 -0400 Subject: [PATCH 046/107] refactor(table/mediator): Make 'removeTable' require instance; remove hard privacy Signed-off-by: Trae Yelovich --- .../src/vscode/ui/utils/TableMediator.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts index 6527c98cb8..6a6fc088e9 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts @@ -47,7 +47,7 @@ import { Table } from "../TableView"; export class TableMediator { private static instance: TableMediator; - #tables: Map; + private tables: Map = new Map(); private constructor() {} @@ -70,7 +70,7 @@ export class TableMediator { * @param table The {@link Table.View} instance to add to the mediator */ public addTable(table: Table.View): void { - this.#tables.set(table.getId(), table); + this.tables.set(table.getId(), table); } /** @@ -83,21 +83,20 @@ export class TableMediator { * * `undefined` if the instance was deleted or does not exist */ public getTable(id: string): Table.View | undefined { - return this.#tables.get(id); + return this.tables.get(id); } /** * Removes a table from the mediator. - * Note that the * * @param id The unique ID of the table to delete * @returns `true` if the table was deleted; `false` otherwise */ - public removeTable(id: string): boolean { - if (this.#tables.has(id)) { + public removeTable(instance: Table.Instance): boolean { + if (Array.from(this.tables.values()).find((table) => table.getId() === instance.getId()) == null) { return false; } - return this.#tables.delete(id); + return this.tables.delete(instance.getId()); } } From f5c0f7608f5c5b732c9202d81f0c33b309c83cab Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 16 Jul 2024 16:09:28 -0400 Subject: [PATCH 047/107] refactor(table/builder): Add 'addRows' and 'addColumns' helper fns Signed-off-by: Trae Yelovich --- .../src/vscode/ui/utils/TableBuilder.ts | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index 33de118b81..0f783bebae 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -80,18 +80,42 @@ export class TableBuilder { } /** - * Set the headers for the next table. - * @param rows The headers to use for the table - * @returns The same {@link TableBuilder} instance with the headers added + * Adds rows to the table. Does not replace existing rows. + * @param rows The rows of content to add to the table + * @returns The same {@link TableBuilder} instance with the new rows added */ - public columns(newColumns: Table.ColumnOpts[]): TableBuilder { - this.data.columns = newColumns.map((col) => ({ + public addRows(rows: Table.RowData[]): TableBuilder { + this.data.rows = [...this.data.rows, ...rows]; + return this; + } + + /** + * Set the columns for the next table. + * @param columns The columns to use for the table + * @returns The same {@link TableBuilder} instance with the columns added + */ + public columns(...columns: Table.ColumnOpts[]): TableBuilder { + this.data.columns = this.convertColumnOpts(columns); + return this; + } + + private convertColumnOpts(columns: Table.ColumnOpts[]): Table.Column[] { + return columns.map((col) => ({ ...col, comparator: col.comparator?.toString(), colSpan: col.colSpan?.toString(), rowSpan: col.rowSpan?.toString(), valueFormatter: col.valueFormatter?.toString(), })); + } + + /** + * Adds columns to the table. Does not replace existing columns. + * @param columns The column definitions to add to the table + * @returns The same {@link TableBuilder} instance with the new column definitions added + */ + public addColumns(columns: Table.ColumnOpts[]): TableBuilder { + this.data.columns = [...this.data.columns, ...this.convertColumnOpts(columns)]; return this; } From 93868a112d64939e93e8058b45d6a2df644f9bca Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 16 Jul 2024 16:09:52 -0400 Subject: [PATCH 048/107] fix(table/view): Generate UUID once per table Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/src/vscode/ui/TableView.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 3626c8f394..cb31b09860 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -254,6 +254,8 @@ export namespace Table { public onTableDisplayChanged: Event = this.onTableDisplayChangedEmitter.event; public onTableDataReceived: Event> = this.onTableDataReceivedEmitter.event; + private uuid: string; + public getUris(): UriPair { return this.uris; } @@ -336,8 +338,10 @@ export namespace Table { * @returns The unique ID for this table view */ public getId(): string { - const uuid = randomUUID(); - return `${this.data.title}-${uuid.substring(0, uuid.indexOf("-"))}##${this.context.extension.id}`; + if (this.uuid == null) { + this.uuid = randomUUID(); + } + return `${this.data.title}-${this.uuid.substring(0, this.uuid.indexOf("-"))}##${this.context.extension.id}`; } /** From 21995c375ddac428e0f2e080dffc51c4aee2125c Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 16 Jul 2024 16:10:23 -0400 Subject: [PATCH 049/107] wip(tests): TableBuilder, TableMediator test cases Signed-off-by: Trae Yelovich --- .../zowe-explorer-api/__mocks__/vscode.ts | 184 ++++++++++++++++++ .../vscode/ui/utils/TableBuilder.unit.test.ts | 99 ++++++++++ .../ui/utils/TableMediator.unit.test.ts | 62 ++++++ 3 files changed, 345 insertions(+) create mode 100644 packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts create mode 100644 packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableMediator.unit.test.ts diff --git a/packages/zowe-explorer-api/__mocks__/vscode.ts b/packages/zowe-explorer-api/__mocks__/vscode.ts index 009aa42aeb..701b7f6297 100644 --- a/packages/zowe-explorer-api/__mocks__/vscode.ts +++ b/packages/zowe-explorer-api/__mocks__/vscode.ts @@ -266,6 +266,181 @@ export interface TabGroups { close(tabGroup: TabGroup | readonly TabGroup[], preserveFocus?: boolean): Thenable; } +/** + * Content settings for a webview panel. + */ +export interface WebviewPanelOptions { + /** + * Controls if the find widget is enabled in the panel. + * + * Defaults to `false`. + */ + readonly enableFindWidget?: boolean; + + /** + * Controls if the webview panel's content (iframe) is kept around even when the panel + * is no longer visible. + * + * Normally the webview panel's html context is created when the panel becomes visible + * and destroyed when it is hidden. Extensions that have complex state + * or UI can set the `retainContextWhenHidden` to make the editor keep the webview + * context around, even when the webview moves to a background tab. When a webview using + * `retainContextWhenHidden` becomes hidden, its scripts and other dynamic content are suspended. + * When the panel becomes visible again, the context is automatically restored + * in the exact same state it was in originally. You cannot send messages to a + * hidden webview, even with `retainContextWhenHidden` enabled. + * + * `retainContextWhenHidden` has a high memory overhead and should only be used if + * your panel's context cannot be quickly saved and restored. + */ + readonly retainContextWhenHidden?: boolean; +} + +/** + * A panel that contains a webview. + */ +interface WebviewPanel { + /** + * Identifies the type of the webview panel, such as `'markdown.preview'`. + */ + readonly viewType: string; + + /** + * Title of the panel shown in UI. + */ + title: string; + + /** + * Icon for the panel shown in UI. + */ + iconPath?: + | Uri + | { + /** + * The icon path for the light theme. + */ + readonly light: Uri; + /** + * The icon path for the dark theme. + */ + readonly dark: Uri; + }; + + /** + * {@linkcode Webview} belonging to the panel. + */ + readonly webview: any; + + /** + * Content settings for the webview panel. + */ + readonly options: WebviewPanelOptions; + + /** + * Editor position of the panel. This property is only set if the webview is in + * one of the editor view columns. + */ + readonly viewColumn: ViewColumn | undefined; + + /** + * Whether the panel is active (focused by the user). + */ + readonly active: boolean; + + /** + * Whether the panel is visible. + */ + readonly visible: boolean; + + /** + * Fired when the panel's view state changes. + */ + readonly onDidChangeViewState: Event; + + /** + * Fired when the panel is disposed. + * + * This may be because the user closed the panel or because `.dispose()` was + * called on it. + * + * Trying to use the panel after it has been disposed throws an exception. + */ + readonly onDidDispose: Event; + + /** + * Show the webview panel in a given column. + * + * A webview panel may only show in a single column at a time. If it is already showing, this + * method moves it to a new column. + * + * @param viewColumn View column to show the panel in. Shows in the current `viewColumn` if undefined. + * @param preserveFocus When `true`, the webview will not take focus. + */ + reveal(viewColumn?: ViewColumn, preserveFocus?: boolean): void; + + /** + * Dispose of the webview panel. + * + * This closes the panel if it showing and disposes of the resources owned by the webview. + * Webview panels are also disposed when the user closes the webview panel. Both cases + * fire the `onDispose` event. + */ + dispose(): any; +} + +/** + * Content settings for a webview. + */ +export interface WebviewOptions { + /** + * Controls whether scripts are enabled in the webview content or not. + * + * Defaults to false (scripts-disabled). + */ + readonly enableScripts?: boolean; + + /** + * Controls whether forms are enabled in the webview content or not. + * + * Defaults to true if {@link WebviewOptions.enableScripts scripts are enabled}. Otherwise defaults to false. + * Explicitly setting this property to either true or false overrides the default. + */ + readonly enableForms?: boolean; + + /** + * Controls whether command uris are enabled in webview content or not. + * + * Defaults to `false` (command uris are disabled). + * + * If you pass in an array, only the commands in the array are allowed. + */ + readonly enableCommandUris?: boolean | readonly string[]; + + /** + * Root paths from which the webview can load local (filesystem) resources using uris from `asWebviewUri` + * + * Default to the root folders of the current workspace plus the extension's install directory. + * + * Pass in an empty array to disallow access to any local resources. + */ + readonly localResourceRoots?: readonly Uri[]; + + /** + * Mappings of localhost ports used inside the webview. + * + * Port mapping allow webviews to transparently define how localhost ports are resolved. This can be used + * to allow using a static localhost port inside the webview that is resolved to random port that a service is + * running on. + * + * If a webview accesses localhost content, we recommend that you specify port mappings even if + * the `webviewPort` and `extensionHostPort` ports are the same. + * + * *Note* that port mappings only work for `http` or `https` urls. Websocket urls (e.g. `ws://localhost:3000`) + * cannot be mapped to another port. + */ + readonly portMapping?: readonly any[]; +} + export namespace window { /** * Represents the grid widget within the main editor area @@ -309,6 +484,15 @@ export namespace window { return undefined; } + export function createWebviewPanel( + viewType: string, + title: string, + showOptions: ViewColumn | { preserveFocus: boolean; viewColumn: ViewColumn }, + options?: WebviewPanelOptions & WebviewOptions + ): WebviewPanel { + return undefined as any; + } + export function showQuickPick( items: readonly T[] | Thenable, options?: QuickPickOptions & { canPickMany: true }, diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts new file mode 100644 index 0000000000..7bdb7d7a7b --- /dev/null +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts @@ -0,0 +1,99 @@ +import { Table, TableBuilder } from "../../../../../src"; + +// TableBuilder unit tests + +function createGlobalMocks() { + return { + context: { + extensionPath: "/a/b/c/zowe-explorer", + extension: { + id: "Zowe.vscode-extension-for-zowe", + }, + }, + }; +} + +describe("TableBuilder::constructor", () => { + it("stores the extension context within the builder", () => { + const globalMocks = createGlobalMocks(); + const builder = new TableBuilder(globalMocks.context as any); + expect((builder as any).context).toBe(globalMocks.context); + }); +}); + +describe("TableBuilder::options", () => { + it("adds the given options to the table data, returning the same instance", () => { + const globalMocks = createGlobalMocks(); + let builder = new TableBuilder(globalMocks.context as any); + expect((builder as any).data).not.toHaveProperty("pagination"); + builder = builder.options({ + pagination: false, + }); + expect((builder as any).data).toHaveProperty("pagination"); + }); +}); + +describe("TableBuilder::title", () => { + it("sets the given title on the table data, returning the same instance", () => { + const globalMocks = createGlobalMocks(); + let builder = new TableBuilder(globalMocks.context as any); + expect((builder as any).data.title).toBe(""); + const title = "An incredulously long title for a table such that nobody should have to bear witness to such a tragedy"; + builder = builder.title(title); + expect((builder as any).data.title).toBe(title); + }); +}); + +describe("TableBuilder::rows", () => { + it("sets the given rows for the table, returning the same instance", () => { + const globalMocks = createGlobalMocks(); + let builder = new TableBuilder(globalMocks.context as any); + expect((builder as any).data.rows).toStrictEqual([]); + const newRows = [ + { a: 1, b: 2, c: 3, d: false }, + { a: 3, b: 2, c: 1, d: true }, + ]; + builder = builder.rows(...newRows); + expect((builder as any).data.rows).toStrictEqual(newRows); + }); +}); + +describe("TableBuilder::addRows", () => { + it("adds the given rows to the table, returning the same instance", () => { + const globalMocks = createGlobalMocks(); + let builder = new TableBuilder(globalMocks.context as any); + expect((builder as any).data.rows).toStrictEqual([]); + const newRows = [ + { a: 1, b: 2, c: 3, d: false }, + { a: 3, b: 2, c: 1, d: true }, + ]; + builder = builder.rows(...newRows); + newRows.push({ a: 2, b: 1, c: 3, d: false }); + builder = builder.addRows([newRows[newRows.length - 1]]); + expect((builder as any).data.rows).toStrictEqual(newRows); + }); +}); + +describe("TableBuilder::columns", () => { + it("sets the given columns for the table, returning the same instance", () => { + const globalMocks = createGlobalMocks(); + let builder = new TableBuilder(globalMocks.context as any); + expect((builder as any).data.columns).toStrictEqual([]); + const newCols: Table.ColumnOpts[] = [{ field: "cat" }, { field: "doge", filter: true }, { field: "parrot", sort: "asc" }]; + builder = builder.columns(...newCols); + expect(JSON.parse(JSON.stringify((builder as any).data.columns))).toStrictEqual(JSON.parse(JSON.stringify(newCols))); + }); +}); + +describe("TableBuilder::addColumns", () => { + it("adds the given columns to the table, returning the same instance", () => { + const globalMocks = createGlobalMocks(); + let builder = new TableBuilder(globalMocks.context as any); + expect((builder as any).data.columns).toStrictEqual([]); + const newCols: Table.ColumnOpts[] = [{ field: "cat" }, { field: "doge", filter: true }, { field: "parrot", sort: "asc" }]; + builder = builder.columns(...newCols); + newCols.push({ field: "parakeet", sort: "desc" }); + builder = builder.addColumns([newCols[newCols.length - 1]]); + expect(JSON.parse(JSON.stringify((builder as any).data.columns))).toStrictEqual(JSON.parse(JSON.stringify(newCols))); + }); +}); diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableMediator.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableMediator.unit.test.ts new file mode 100644 index 0000000000..792f54ae06 --- /dev/null +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableMediator.unit.test.ts @@ -0,0 +1,62 @@ +import { TableBuilder, TableMediator } from "../../../../../src/"; +import * as vscode from "vscode"; + +// TableMediator unit tests + +// Global mocks for building a table view and using it within test cases. +function createGlobalMocks() { + // Mock `vscode.window.createWebviewPanel` to return a usable panel object + const createWebviewPanelMock = jest.spyOn(vscode.window, "createWebviewPanel").mockReturnValueOnce({ + onDidDispose: (_fn) => {}, + webview: { asWebviewUri: (uri) => uri.toString(), onDidReceiveMessage: (_fn) => {} }, + } as any); + + // Example table for use with the mediator + const table = new TableBuilder({ + extensionPath: "/a/b/c/zowe-explorer", + extension: { id: "Zowe.vscode-extension-for-zowe" }, + } as any) + .title("SomeTable") + .build(); + + return { + createWebviewPanelMock, + table, + }; +} + +describe("TableMediator::getInstance", () => { + it("returns an instance of TableMediator", () => { + expect(TableMediator.getInstance()).toBeInstanceOf(TableMediator); + }); +}); + +describe("TableMediator::addTable", () => { + it("adds the given table object to its internal map", () => { + const globalMocks = createGlobalMocks(); + TableMediator.getInstance().addTable(globalMocks.table); + expect((TableMediator.getInstance() as any).tables.get(globalMocks.table.getId())).toBe(globalMocks.table); + (TableMediator.getInstance() as any).tables = new Map(); + }); +}); + +describe("TableMediator::getTable", () => { + it("retrieves the table by ID using its internal map", () => { + const globalMocks = createGlobalMocks(); + const tableId = globalMocks.table.getId(); + TableMediator.getInstance().addTable(globalMocks.table); + expect(TableMediator.getInstance().getTable(tableId)).toBe(globalMocks.table); + (TableMediator.getInstance() as any).tables = new Map(); + }); +}); + +describe("TableMediator::removeTable", () => { + it("removes a table view from its internal map", () => { + const globalMocks = createGlobalMocks(); + const tableId = globalMocks.table.getId(); + TableMediator.getInstance().addTable(globalMocks.table); + expect(TableMediator.getInstance().removeTable(globalMocks.table)).toBe(true); + expect((TableMediator.getInstance() as any).tables.get(globalMocks.table.getId())).toBe(undefined); + expect(TableMediator.getInstance().getTable(tableId)).toBe(undefined); + }); +}); From 59716dd0d02eef9e483aeda2a8290c3608aa0caa Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 16 Jul 2024 16:20:37 -0400 Subject: [PATCH 050/107] tests(TableBuilder): convertColumnOpts, reset test cases Signed-off-by: Trae Yelovich --- .../vscode/ui/utils/TableBuilder.unit.test.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts index 7bdb7d7a7b..7ed623ec8b 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts @@ -97,3 +97,50 @@ describe("TableBuilder::addColumns", () => { expect(JSON.parse(JSON.stringify((builder as any).data.columns))).toStrictEqual(JSON.parse(JSON.stringify(newCols))); }); }); + +describe("TableBuilder::convertColumnOpts", () => { + it("converts an array of ColumnOpts to an array of Column", () => { + const globalMocks = createGlobalMocks(); + let builder = new TableBuilder(globalMocks.context as any); + const newCols: Table.ColumnOpts[] = [ + { field: "cat", valueFormatter: (data: { value: Table.ContentTypes }) => `val: ${data.value.toString()}` }, + { field: "doge", filter: true }, + { field: "parrot", sort: "asc" }, + ]; + expect((builder as any).convertColumnOpts(newCols)).toStrictEqual( + newCols.map((col) => ({ + ...col, + comparator: col.comparator?.toString(), + colSpan: col.colSpan?.toString(), + rowSpan: col.rowSpan?.toString(), + valueFormatter: col.valueFormatter?.toString(), + })) + ); + }); +}); + +describe("TableBuilder::reset", () => { + it("resets all table data on the builder instance", () => { + const globalMocks = createGlobalMocks(); + const newRows = [ + { a: 1, b: 2, c: 3, d: false }, + { a: 3, b: 2, c: 1, d: true }, + ]; + const builder = new TableBuilder(globalMocks.context as any) + .rows(...newRows) + .title("A table") + .options({ pagination: false }); + builder.reset(); + expect((builder as any).data).toStrictEqual({ + actions: { + all: [], + }, + contextOpts: { + all: [], + }, + columns: [], + rows: [], + title: "", + }); + }); +}); From a9dc229ecf7caef05bc456c1598bae7ff9895f16 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 16 Jul 2024 16:20:53 -0400 Subject: [PATCH 051/107] refactor(TableBuilder): Update reset fn impl Signed-off-by: Trae Yelovich --- .../src/vscode/ui/utils/TableBuilder.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index 0f783bebae..6ff204ab9a 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -195,14 +195,16 @@ export class TableBuilder { * Resets all data configured in the builder from previously-created table views. */ public reset(): void { - this.data.actions = { - all: [], - }; - this.data.contextOpts = { - all: [], + this.data = { + actions: { + all: [], + }, + contextOpts: { + all: [], + }, + columns: [], + rows: [], + title: "", }; - this.data.columns = []; - this.data.rows = []; - this.data.title = ""; } } From abbdc9683d7ee51c4dc9e2a87b0b6f5d45832f54 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 17 Jul 2024 16:22:00 -0400 Subject: [PATCH 052/107] refactor: clean up types and functions for table Signed-off-by: Trae Yelovich --- .../src/vscode/ui/TableView.ts | 29 ++++++++++++++----- .../src/vscode/ui/utils/TableBuilder.ts | 19 ++++++------ 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index cb31b09860..33296c1c7f 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -56,7 +56,6 @@ export namespace Table { /** Value formatter callback. Expects the exact display value to be returned. */ export type ValueFormatter = (data: { value: CellData }) => string; export type Positions = "left" | "right"; - export type SortDirections = "asc" | "desc"; /** The column type definition. All available properties are offered for AG Grid columns. */ export type Column = { @@ -101,11 +100,11 @@ export namespace Table { // Sorting sortable?: boolean; - sort?: SortDirections; - initialSort?: SortDirections; + sort?: "asc" | "desc"; + initialSort?: "asc" | "desc"; sortIndex?: number | null; initialSortIndex?: number; - sortingOrder?: SortDirections[]; + sortingOrder?: ("asc" | "desc")[]; comparator?: string; unSortIcon?: boolean; @@ -168,7 +167,10 @@ export namespace Table { allowDragFromColumnsToolPanel?: boolean; /** Number of pixels to add a column width after the auto-sizing calculation */ autoSizePadding?: number; - /** Auto-size the columns when the grid is loaded. Can size to fit the grid width, fit a provided width or fit the cell contents. Read once during initialization. */ + /** + * Auto-size the columns when the grid is loaded. Can size to fit the grid width, fit a provided width or fit the cell contents. + * Read once during initialization. + */ autoSizeStrategy?: SizeColumnsToFitGridStrategy | SizeColumnsToFitProvidedWidthStrategy | SizeColumnsToFitGridStrategy; /** Set to 'shift' to have shift-resize as the default resize operation */ colResizeDefault?: "shift"; @@ -207,7 +209,10 @@ export namespace Table { quickFilterText?: string; /** Set to `true` to skip the `headerName` when `autoSize` is called by default. Read once during initialization. */ skipHeaderOnAutoSize?: boolean; - /** Suppresses auto-sizing columns for columns - when enabled, double-clicking a column's header's edge will not auto-size. Read once during initialization. */ + /** + * Suppresses auto-sizing columns for columns - when enabled, double-clicking a column's header's edge will not auto-size. + * Read once during initialization. + */ suppressAutoSize?: boolean; suppressColumnMoveAnimation?: boolean; /** If `true`, when you dreag a column out of the grid, the column is not hidden */ @@ -248,7 +253,17 @@ export namespace Table { */ export class View extends WebView { private lastUpdated: ViewOpts; - private data: ViewOpts; + private data: ViewOpts = { + actions: { + all: [], + }, + contextOpts: { + all: [], + }, + rows: [], + columns: [], + title: "", + }; private onTableDataReceivedEmitter: EventEmitter> = new EventEmitter(); private onTableDisplayChangedEmitter: EventEmitter = new EventEmitter(); public onTableDisplayChanged: Event = this.onTableDisplayChangedEmitter.event; diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index 6ff204ab9a..94f1a26e9d 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -124,17 +124,19 @@ export class TableBuilder { * @param actions the record of indices to {@link Table.Action} arrays to use for the table * @returns The same {@link TableBuilder} instance with the row actions added */ - public contextOptions(opts: Record): TableBuilder { - this.data.contextOpts = opts; + public contextOptions(opts: Record): TableBuilder { + for (const key of Object.keys(opts)) { + this.addContextOption(key as number | "all", opts[key]); + } return this; } /** - * Add a row action to the next table. - * @param index The column index to add an action to - * @returns The same {@link TableBuilder} instance with the row action added + * Add a context menu option to the table. + * @param index The row index to add an option to (or "all" for all rows) + * @returns The same {@link TableBuilder} instance with the context menu option added */ - public contextOption(index: number | "all", option: Table.ContextMenuOpts): TableBuilder { + public addContextOption(index: number | "all", option: Table.ContextMenuOpts): TableBuilder { if (this.data.contextOpts[index]) { const opts = this.data.contextOpts[index]; this.data.contextOpts[index] = [...opts, { ...option, condition: option.condition?.toString() }]; @@ -152,8 +154,7 @@ export class TableBuilder { public rowActions(actions: Record): TableBuilder { //this.data.actions = actions; for (const key of Object.keys(actions)) { - const actionList = actions[key] as Table.ActionOpts[]; - this.data.actions[key] = actionList.map((action) => ({ ...action, condition: action.condition?.toString() })); + this.addRowAction(key as number | "all", actions[key]); } return this; } @@ -163,7 +164,7 @@ export class TableBuilder { * @param index The column index to add an action to * @returns The same {@link TableBuilder} instance with the row action added */ - public rowAction(index: number | "all", action: Table.ActionOpts): TableBuilder { + public addRowAction(index: number | "all", action: Table.ActionOpts): TableBuilder { if (this.data.actions[index]) { const actionList = this.data.actions[index]; this.data.actions[index] = [...actionList, { ...action, condition: action.condition?.toString() }]; From 519171dcae98a4e9bb5aa24cb1692f0321a615a4 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 17 Jul 2024 16:22:35 -0400 Subject: [PATCH 053/107] tests: TableBuilder, TableMediator test cases Signed-off-by: Trae Yelovich --- .../vscode/ui/utils/TableBuilder.unit.test.ts | 159 +++++++++++++++++- .../ui/utils/TableMediator.unit.test.ts | 38 ++++- 2 files changed, 185 insertions(+), 12 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts index 7ed623ec8b..9f2282521f 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts @@ -1,9 +1,29 @@ -import { Table, TableBuilder } from "../../../../../src"; +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { window } from "vscode"; +import { Table, TableBuilder, TableMediator } from "../../../../../src"; // TableBuilder unit tests function createGlobalMocks() { + const mockPanel = { + onDidDispose: (_fn) => {}, + webview: { asWebviewUri: (uri) => uri.toString(), onDidReceiveMessage: (_fn) => {} }, + }; + // Mock `vscode.window.createWebviewPanel` to return a usable panel object + const createWebviewPanelMock = jest.spyOn(window, "createWebviewPanel").mockReturnValueOnce(mockPanel as any); + return { + createWebviewPanelMock, context: { extensionPath: "/a/b/c/zowe-explorer", extension: { @@ -101,11 +121,11 @@ describe("TableBuilder::addColumns", () => { describe("TableBuilder::convertColumnOpts", () => { it("converts an array of ColumnOpts to an array of Column", () => { const globalMocks = createGlobalMocks(); - let builder = new TableBuilder(globalMocks.context as any); + const builder = new TableBuilder(globalMocks.context as any); const newCols: Table.ColumnOpts[] = [ { field: "cat", valueFormatter: (data: { value: Table.ContentTypes }) => `val: ${data.value.toString()}` }, - { field: "doge", filter: true }, - { field: "parrot", sort: "asc" }, + { field: "doge", filter: true, comparator: (valueA, valueB, nodeA, nodeB, isDescending) => -1, colSpan: (params) => 2 }, + { field: "parrot", sort: "asc", rowSpan: (params) => 2 }, ]; expect((builder as any).convertColumnOpts(newCols)).toStrictEqual( newCols.map((col) => ({ @@ -119,6 +139,137 @@ describe("TableBuilder::convertColumnOpts", () => { }); }); +describe("TableBuilder::contextOptions", () => { + it("adds the given context options and returns the same instance", () => { + const globalMocks = createGlobalMocks(); + const builder = new TableBuilder(globalMocks.context as any); + const ctxOpts = { + all: [ + { + title: "Add to queue", + command: "add-to-queue", + callback: { + typ: "cell", + fn: (_cell: Table.CellData) => {}, + }, + condition: (_data) => true, + }, + ], + } as Record; + + const addCtxOptSpy = jest.spyOn(builder, "addContextOption"); + const builderCtxOpts = builder.contextOptions(ctxOpts); + expect(builderCtxOpts).toBeInstanceOf(TableBuilder); + expect(addCtxOptSpy).toHaveBeenCalledTimes(1); + }); +}); + +describe("TableBuilder::addContextOption", () => { + it("adds the given context option and returns the same instance", () => { + const globalMocks = createGlobalMocks(); + const builder = new TableBuilder(globalMocks.context as any); + const ctxOpt = { + title: "Delete", + command: "delete", + callback: { + typ: "row", + fn: (_row: Table.RowData) => {}, + }, + condition: (_data) => true, + } as Table.ContextMenuOpts; + + // case 0: adding context option to "all" rows + const builderCtxOpts = builder.addContextOption("all", ctxOpt); + expect(builderCtxOpts).toBeInstanceOf(TableBuilder); + expect((builderCtxOpts as any).data.contextOpts).toStrictEqual({ + all: [{ ...ctxOpt, condition: ctxOpt.condition?.toString() }], + }); + // case 1: adding context option to a specific row, no previous options existed + const finalBuilder = builderCtxOpts.addContextOption(0, ctxOpt); + expect((finalBuilder as any).data.contextOpts).toStrictEqual({ + 0: [{ ...ctxOpt, condition: ctxOpt.condition?.toString() }], + all: [{ ...ctxOpt, condition: ctxOpt.condition?.toString() }], + }); + }); +}); + +describe("TableBuilder::addRowAction", () => { + it("adds the given row action to all rows and returns the same instance", () => { + const globalMocks = createGlobalMocks(); + const builder = new TableBuilder(globalMocks.context as any); + const rowAction = { + title: "Move", + command: "move", + callback: { + typ: "cell", + fn: (_cell: Table.CellData) => {}, + }, + condition: (_data) => true, + } as Table.ActionOpts; + const builderAction = builder.addRowAction("all", rowAction); + expect(builderAction).toBeInstanceOf(TableBuilder); + expect((builderAction as any).data.actions).toStrictEqual({ + all: [{ ...rowAction, condition: rowAction.condition?.toString() }], + }); + const finalBuilder = builderAction.addRowAction(0, rowAction); + expect((finalBuilder as any).data.actions).toStrictEqual({ + 0: [{ ...rowAction, condition: rowAction.condition?.toString() }], + all: [{ ...rowAction, condition: rowAction.condition?.toString() }], + }); + }); +}); + +describe("TableBuilder::rowActions", () => { + it("calls rowAction for each action and returns the same instance", () => { + const globalMocks = createGlobalMocks(); + const builder = new TableBuilder(globalMocks.context as any); + const rowActions = { + 0: [ + { + title: "Move", + command: "move", + callback: { + typ: "cell", + fn: (_cell: Table.CellData) => {}, + }, + condition: (_data) => true, + }, + ] as Table.ActionOpts[], + all: [ + { + title: "Recall", + command: "recall", + callback: { + typ: "cell", + fn: (_cell: Table.CellData) => {}, + }, + condition: (_data) => true, + }, + ] as Table.ActionOpts[], + }; + const rowActionSpy = jest.spyOn(builder, "addRowAction"); + const builderAction = builder.rowActions(rowActions); + expect(rowActionSpy).toHaveBeenCalledTimes(2); + expect(builderAction).toBeInstanceOf(TableBuilder); + }); +}); + +describe("TableBuilder::buildAndShare", () => { + it("builds the table view and adds it to the table mediator", () => { + const globalMocks = createGlobalMocks(); + const newRows = [ + { a: 1, b: 2, c: 3, d: false, e: 5 }, + { a: 3, b: 2, c: 1, d: true, e: 6 }, + ]; + const addTableSpy = jest.spyOn(TableMediator.prototype, "addTable"); + const builder = new TableBuilder(globalMocks.context as any) + .addRows(newRows) + .addColumns([{ field: "a" }, { field: "b" }, { field: "c" }, { field: "d" }, { field: "e" }]); + const instance = builder.buildAndShare(); + expect(addTableSpy).toHaveBeenCalledWith(instance); + }); +}); + describe("TableBuilder::reset", () => { it("resets all table data on the builder instance", () => { const globalMocks = createGlobalMocks(); diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableMediator.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableMediator.unit.test.ts index 792f54ae06..23d3d6822d 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableMediator.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableMediator.unit.test.ts @@ -1,3 +1,14 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + import { TableBuilder, TableMediator } from "../../../../../src/"; import * as vscode from "vscode"; @@ -5,22 +16,25 @@ import * as vscode from "vscode"; // Global mocks for building a table view and using it within test cases. function createGlobalMocks() { - // Mock `vscode.window.createWebviewPanel` to return a usable panel object - const createWebviewPanelMock = jest.spyOn(vscode.window, "createWebviewPanel").mockReturnValueOnce({ + const mockPanel = { onDidDispose: (_fn) => {}, webview: { asWebviewUri: (uri) => uri.toString(), onDidReceiveMessage: (_fn) => {} }, - } as any); + }; + // Mock `vscode.window.createWebviewPanel` to return a usable panel object + const createWebviewPanelMock = jest.spyOn(vscode.window, "createWebviewPanel").mockReturnValueOnce(mockPanel as any); - // Example table for use with the mediator - const table = new TableBuilder({ + const extensionContext = { extensionPath: "/a/b/c/zowe-explorer", extension: { id: "Zowe.vscode-extension-for-zowe" }, - } as any) - .title("SomeTable") - .build(); + }; + + // Example table for use with the mediator + const table = new TableBuilder(extensionContext as any).title("SomeTable").build(); return { createWebviewPanelMock, + extensionContext, + mockPanel, table, }; } @@ -59,4 +73,12 @@ describe("TableMediator::removeTable", () => { expect((TableMediator.getInstance() as any).tables.get(globalMocks.table.getId())).toBe(undefined); expect(TableMediator.getInstance().getTable(tableId)).toBe(undefined); }); + + it("returns false if the table instance does not exist in the map", () => { + const globalMocks = createGlobalMocks(); + globalMocks.createWebviewPanelMock.mockReturnValueOnce(globalMocks.mockPanel as any); + const table2 = new TableBuilder(globalMocks.extensionContext as any).build(); + TableMediator.getInstance().addTable(globalMocks.table); + expect(TableMediator.getInstance().removeTable(table2)).toBe(false); + }); }); From ae2a43dc0b3c7f8a3879009ab066d78b7088a1a3 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 17 Jul 2024 16:22:46 -0400 Subject: [PATCH 054/107] tests: Table.View unit test cases Signed-off-by: Trae Yelovich --- .../__unit__/vscode/ui/TableView.unit.test.ts | 288 ++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts new file mode 100644 index 0000000000..3dbcd9a62f --- /dev/null +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts @@ -0,0 +1,288 @@ +import { join } from "path"; +import { Table } from "../../../../src"; +import { env, EventEmitter, Uri, window } from "vscode"; +import * as crypto from "crypto"; + +function createGlobalMocks() { + const mockPanel = { + onDidDispose: (_fn) => {}, + webview: { asWebviewUri: (uri) => uri.toString(), onDidReceiveMessage: (_fn) => {}, postMessage: (_message, _origin, _transfer) => {} }, + }; + // Mock `vscode.window.createWebviewPanel` to return a usable panel object + const createWebviewPanelMock = jest.spyOn(window, "createWebviewPanel").mockReturnValueOnce(mockPanel as any); + + return { + createWebviewPanelMock, + context: { + extensionPath: "/a/b/c/zowe-explorer", + extension: { + id: "Zowe.vscode-extension-for-zowe", + }, + }, + }; +} + +// Table.View unit tests +describe("Table.View", () => { + describe("getUris", () => { + it("returns the URIs from the WebView base class", () => { + const globalMocks = createGlobalMocks(); + const view = new Table.View(globalMocks.context as any, { title: "Table" } as any); + const buildPath = join(globalMocks.context.extensionPath, "src", "webviews"); + const scriptPath = join(buildPath, "dist", "table-view", "table-view.js"); + expect(view.getUris()).toStrictEqual({ + disk: { + build: Uri.parse(buildPath), + script: Uri.parse(scriptPath), + css: undefined, + }, + resource: { + build: buildPath, + script: scriptPath, + css: undefined, + }, + }); + }); + }); + + describe("getHtml", () => { + it("returns the HTML content generated by the WebView base class", () => { + const globalMocks = createGlobalMocks(); + const view = new Table.View(globalMocks.context as any, { title: "Table" } as any); + expect(view.getHtml()).toStrictEqual(view.panel.webview.html); + }); + }); + + describe("updateWebview", () => { + it("calls postMessage on the panel and sends the data to the webview", async () => { + const globalMocks = createGlobalMocks(); + const view = new Table.View(globalMocks.context as any, { title: "Table" } as any); + + // case 1: Post message was not successful; updateWebview returns false + const postMessageMock = jest.spyOn(view.panel.webview, "postMessage").mockResolvedValueOnce(false); + await expect((view as any).updateWebview()).resolves.toBe(false); + expect(postMessageMock).toHaveBeenCalledWith({ + command: "ondatachanged", + data: { title: "Table" }, + }); + + // case 2: Post message was successful; updateWebview returns true and event is fired + const emitterFireMock = jest.spyOn(EventEmitter.prototype, "fire"); + postMessageMock.mockResolvedValueOnce(true); + await expect((view as any).updateWebview()).resolves.toBe(true); + expect(postMessageMock).toHaveBeenCalledWith({ + command: "ondatachanged", + data: { title: "Table" }, + }); + expect(emitterFireMock).toHaveBeenCalledWith({ title: "Table" }); + postMessageMock.mockRestore(); + }); + }); + + describe("getId", () => { + it("returns a valid ID for the table view", () => { + const globalMocks = createGlobalMocks(); + const view = new Table.View(globalMocks.context as any, { title: "Table" } as any); + const randomUuidMock = jest.spyOn(crypto, "randomUUID").mockReturnValueOnce("foo-bar-baz-qux-quux"); + expect(view.getId()).toBe("Table-foo##Zowe.vscode-extension-for-zowe"); + expect(randomUuidMock).toHaveBeenCalled(); + }); + }); + + describe("setTitle", () => { + const updateWebviewMock = jest.spyOn((Table.View as any).prototype, "updateWebview"); + + it("returns false if it was unable to send the new title", async () => { + const globalMocks = createGlobalMocks(); + const view = new Table.View(globalMocks.context as any, { title: "Stable Table of Cables" } as any); + updateWebviewMock.mockResolvedValueOnce(false); + await expect(view.setTitle("Unstable Table of Cables")).resolves.toBe(false); + }); + + it("returns true if it successfully sent the new title", async () => { + const globalMocks = createGlobalMocks(); + const view = new Table.View(globalMocks.context as any, { title: "Stable Table of Cables" } as any); + updateWebviewMock.mockResolvedValueOnce(true); + await expect(view.setTitle("Unstable Table of Cables")).resolves.toBe(true); + expect((view as any).data.title).toBe("Unstable Table of Cables"); + }); + }); + + describe("setOptions", () => { + const updateWebviewMock = jest.spyOn((Table.View as any).prototype, "updateWebview"); + + it("returns false if it was unable to send the new options", async () => { + const globalMocks = createGlobalMocks(); + const view = new Table.View(globalMocks.context as any, { title: "Table" } as any); + updateWebviewMock.mockResolvedValueOnce(false); + await expect( + view.setOptions({ + debug: true, + pagination: false, + }) + ).resolves.toBe(false); + }); + + it("returns true if it successfully sent the new options", async () => { + const globalMocks = createGlobalMocks(); + const view = new Table.View(globalMocks.context as any, { title: "Stable Table of Cables" } as any); + updateWebviewMock.mockResolvedValueOnce(true); + await expect( + view.setOptions({ + debug: true, + pagination: false, + }) + ).resolves.toBe(true); + expect((view as any).data.debug).toBe(true); + expect((view as any).data.pagination).toBe(false); + }); + }); + + describe("setColumns", () => { + const updateWebviewMock = jest.spyOn((Table.View as any).prototype, "updateWebview"); + + it("returns false if it was unable to send the new columns", async () => { + const globalMocks = createGlobalMocks(); + const view = new Table.View(globalMocks.context as any, { title: "Table" } as any); + updateWebviewMock.mockResolvedValueOnce(false); + await expect(view.setColumns([{ field: "apple" }, { field: "banana" }, { field: "orange" }])).resolves.toBe(false); + }); + + it("returns true if it successfully sent the new options", async () => { + const globalMocks = createGlobalMocks(); + const view = new Table.View(globalMocks.context as any, { title: "Stable Table of Cables" } as any); + updateWebviewMock.mockResolvedValueOnce(true); + const cols = [ + { field: "apple", valueFormatter: (data: { value: Table.CellData }) => `${data.value.toString()} apples` }, + { field: "banana", comparator: (valueA, valueB, nodeA, nodeB, isDescending) => -1, colSpan: (params) => 2 }, + { field: "orange", rowSpan: (params) => 2 }, + ]; + await expect(view.setColumns(cols)).resolves.toBe(true); + expect((view as any).data.columns).toStrictEqual( + cols.map((col) => ({ + ...col, + colSpan: col.colSpan?.toString(), + comparator: col.comparator?.toString(), + rowSpan: col.rowSpan?.toString(), + valueFormatter: col.valueFormatter?.toString(), + })) + ); + }); + }); + + describe("onMessageReceived", () => { + it("fires the onTableDisplayChanged event when handling the 'ondisplaychanged' command", async () => { + const globalMocks = createGlobalMocks(); + const view = new Table.View(globalMocks.context as any, { title: "Table w/ changing display" } as any); + const onTableDisplayChangedFireMock = jest.spyOn((view as any).onTableDisplayChangedEmitter, "fire"); + const tableData = { rows: [{ a: 1, b: 1, c: 1 }] }; + await view.onMessageReceived({ + command: "ondisplaychanged", + data: tableData, + }); + expect(onTableDisplayChangedFireMock).toHaveBeenCalledWith(tableData); + }); + + it("calls updateWebview when handling the 'ready' command", async () => { + const globalMocks = createGlobalMocks(); + const view = new Table.View(globalMocks.context as any, { title: "Table w/ changing display" } as any); + const updateWebviewMock = jest.spyOn(view as any, "updateWebview").mockImplementation(); + await view.onMessageReceived({ + command: "ready", + }); + expect(updateWebviewMock).toHaveBeenCalled(); + updateWebviewMock.mockRestore(); + }); + + it("calls vscode.env.clipboard.writeText when handling the 'copy' command", async () => { + const globalMocks = createGlobalMocks(); + const view = new Table.View(globalMocks.context as any, { title: "Table w/ copy" } as any); + const writeTextMock = jest.spyOn(env.clipboard, "writeText").mockImplementation(); + const mockWebviewMsg = { + command: "copy", + data: { row: { a: 1, b: 1, c: 1 } }, + }; + await view.onMessageReceived(mockWebviewMsg); + expect(writeTextMock).toHaveBeenCalledWith(JSON.stringify(mockWebviewMsg.data.row)); + writeTextMock.mockRestore(); + }); + + it("calls vscode.env.clipboard.writeText when handling the 'copy-cell' command", async () => { + const globalMocks = createGlobalMocks(); + const view = new Table.View(globalMocks.context as any, { title: "Table w/ copy-cell" } as any); + const writeTextMock = jest.spyOn(env.clipboard, "writeText").mockImplementation(); + const mockWebviewMsg = { + command: "copy-cell", + data: { cell: 1, row: { a: 1, b: 1, c: 1 } }, + }; + await view.onMessageReceived(mockWebviewMsg); + expect(writeTextMock).toHaveBeenCalledWith(mockWebviewMsg.data.cell); + writeTextMock.mockRestore(); + }); + + it("does nothing for a command that doesn't exist as a context option or row action", async () => { + const globalMocks = createGlobalMocks(); + const data = { + title: "Some table", + rows: [{ a: 1, b: 1, c: 1 }], + columns: [], + contextOpts: { + all: [], + }, + actions: { + all: [], + }, + }; + const view = new Table.View(globalMocks.context as any, data); + const updateWebviewMock = jest.spyOn(view as any, "updateWebview"); + const writeTextMock = jest.spyOn(env.clipboard, "writeText"); + const mockWebviewMsg = { + command: "nonexistent-action", + data: { row: data.rows[0] }, + }; + await view.onMessageReceived(mockWebviewMsg); + expect(writeTextMock).not.toHaveBeenCalled(); + expect(updateWebviewMock).not.toHaveBeenCalled(); + }); + + it("runs the callback for an action that exists", async () => { + const globalMocks = createGlobalMocks(); + const callbackMock = jest.fn(); + const data = { + title: "Some table", + rows: [{ a: 1, b: 1, c: 1 }], + columns: [], + contextOpts: { + all: [], + }, + actions: { + all: [ + { + title: "Some action", + command: "some-action", + callback: { + typ: "cell", + fn: (_cell: Table.CellData) => { + callbackMock(); + }, + }, + } as Table.Action, + ], + }, + }; + const view = new Table.View(globalMocks.context as any, data); + const updateWebviewMock = jest.spyOn(view as any, "updateWebview"); + const writeTextMock = jest.spyOn(env.clipboard, "writeText"); + const mockWebviewMsg = { + command: "some-action", + data: { cell: data.rows[0].a, row: data.rows[0] }, + }; + await view.onMessageReceived(mockWebviewMsg); + expect(writeTextMock).not.toHaveBeenCalled(); + expect(updateWebviewMock).not.toHaveBeenCalled(); + expect(callbackMock).toHaveBeenCalled(); + }); + }); +}); + +// Table.Instance unit tests From f7deb470e06d5e0581b499a0bd38dd492c913bec Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 18 Jul 2024 10:25:24 -0400 Subject: [PATCH 055/107] refactor(Table.View): use 'diff' instead of 'addedDiff' for events Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/src/vscode/ui/TableView.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 33296c1c7f..6972a7f156 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -12,7 +12,7 @@ import { UriPair, WebView } from "./WebView"; import { Event, EventEmitter, ExtensionContext, env } from "vscode"; import { randomUUID } from "crypto"; -import { addedDiff } from "deep-object-diff"; +import { diff } from "deep-object-diff"; export namespace Table { /* The types of supported content for the table and how they are represented in callback functions. */ @@ -341,7 +341,7 @@ export namespace Table { }); if (result) { - this.onTableDataReceivedEmitter.fire(this.lastUpdated ? addedDiff(this.lastUpdated, this.data) : this.data); + this.onTableDataReceivedEmitter.fire(this.lastUpdated ? diff(this.lastUpdated, this.data) : this.data); this.lastUpdated = this.data; } return result; @@ -367,7 +367,7 @@ export namespace Table { * * @returns Whether the webview successfully received the new action(s) */ - public addAction(index: number, ...actions: ActionOpts[]): Promise { + public addAction(index: number | "all", ...actions: ActionOpts[]): Promise { if (this.data.actions[index]) { const existingActions = this.data.actions[index]; this.data.actions[index] = [...existingActions, ...actions.map((action) => ({ ...action, condition: action.condition?.toString() }))]; @@ -384,7 +384,7 @@ export namespace Table { * @param actions The actions to add to the given row * @returns Whether the webview successfully received the new context menu option(s) */ - public addContextOption(id: number | string, ...options: ContextMenuOpts[]): Promise { + public addContextOption(id: number | "all", ...options: ContextMenuOpts[]): Promise { if (this.data.contextOpts[id]) { const existingOpts = this.data.contextOpts[id]; this.data.contextOpts[id] = [...existingOpts, ...options.map((option) => ({ ...option, condition: option.condition?.toString() }))]; From 919510bc36f30221000f31339eab2c078109ac2f Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 18 Jul 2024 10:26:43 -0400 Subject: [PATCH 056/107] tests(Table.View): Bump patch coverage to 100% Signed-off-by: Trae Yelovich --- .../__unit__/vscode/ui/TableView.unit.test.ts | 250 ++++++++++++++++-- 1 file changed, 225 insertions(+), 25 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts index 3dbcd9a62f..a3a79b2cb7 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts @@ -1,12 +1,14 @@ import { join } from "path"; -import { Table } from "../../../../src"; +import { Table, TableBuilder, WebView } from "../../../../src"; import { env, EventEmitter, Uri, window } from "vscode"; import * as crypto from "crypto"; +import { diff } from "deep-object-diff"; function createGlobalMocks() { const mockPanel = { - onDidDispose: (_fn) => {}, - webview: { asWebviewUri: (uri) => uri.toString(), onDidReceiveMessage: (_fn) => {}, postMessage: (_message, _origin, _transfer) => {} }, + dispose: jest.fn(), + onDidDispose: jest.fn(), + webview: { asWebviewUri: (uri) => uri.toString(), onDidReceiveMessage: jest.fn(), postMessage: jest.fn() }, }; // Mock `vscode.window.createWebviewPanel` to return a usable panel object const createWebviewPanelMock = jest.spyOn(window, "createWebviewPanel").mockReturnValueOnce(mockPanel as any); @@ -19,6 +21,7 @@ function createGlobalMocks() { id: "Zowe.vscode-extension-for-zowe", }, }, + updateWebviewMock: jest.spyOn((Table.View as any).prototype, "updateWebview"), }; } @@ -76,6 +79,20 @@ describe("Table.View", () => { }); expect(emitterFireMock).toHaveBeenCalledWith({ title: "Table" }); postMessageMock.mockRestore(); + emitterFireMock.mockClear(); + + // case 2: Post message was successful; updateWebview was previously called + // result: Uses lastUpdated cache, returns true and fires the event + postMessageMock.mockResolvedValueOnce(true); + const mockNewRow = { a: 3, b: 2, c: 1 }; + (view as any).data.rows = [mockNewRow]; + await expect((view as any).updateWebview()).resolves.toBe(true); + expect(postMessageMock).toHaveBeenCalledWith({ + command: "ondatachanged", + data: { title: "Table", rows: [mockNewRow] }, + }); + expect(emitterFireMock).toHaveBeenCalledWith(diff((view as any).lastUpdated, (view as any).data)); + postMessageMock.mockRestore(); }); }); @@ -90,31 +107,27 @@ describe("Table.View", () => { }); describe("setTitle", () => { - const updateWebviewMock = jest.spyOn((Table.View as any).prototype, "updateWebview"); - it("returns false if it was unable to send the new title", async () => { const globalMocks = createGlobalMocks(); const view = new Table.View(globalMocks.context as any, { title: "Stable Table of Cables" } as any); - updateWebviewMock.mockResolvedValueOnce(false); + globalMocks.updateWebviewMock.mockResolvedValueOnce(false); await expect(view.setTitle("Unstable Table of Cables")).resolves.toBe(false); }); it("returns true if it successfully sent the new title", async () => { const globalMocks = createGlobalMocks(); const view = new Table.View(globalMocks.context as any, { title: "Stable Table of Cables" } as any); - updateWebviewMock.mockResolvedValueOnce(true); + globalMocks.updateWebviewMock.mockResolvedValueOnce(true); await expect(view.setTitle("Unstable Table of Cables")).resolves.toBe(true); expect((view as any).data.title).toBe("Unstable Table of Cables"); }); }); describe("setOptions", () => { - const updateWebviewMock = jest.spyOn((Table.View as any).prototype, "updateWebview"); - it("returns false if it was unable to send the new options", async () => { const globalMocks = createGlobalMocks(); const view = new Table.View(globalMocks.context as any, { title: "Table" } as any); - updateWebviewMock.mockResolvedValueOnce(false); + globalMocks.updateWebviewMock.mockResolvedValueOnce(false); await expect( view.setOptions({ debug: true, @@ -126,7 +139,7 @@ describe("Table.View", () => { it("returns true if it successfully sent the new options", async () => { const globalMocks = createGlobalMocks(); const view = new Table.View(globalMocks.context as any, { title: "Stable Table of Cables" } as any); - updateWebviewMock.mockResolvedValueOnce(true); + globalMocks.updateWebviewMock.mockResolvedValueOnce(true); await expect( view.setOptions({ debug: true, @@ -139,19 +152,17 @@ describe("Table.View", () => { }); describe("setColumns", () => { - const updateWebviewMock = jest.spyOn((Table.View as any).prototype, "updateWebview"); - it("returns false if it was unable to send the new columns", async () => { const globalMocks = createGlobalMocks(); const view = new Table.View(globalMocks.context as any, { title: "Table" } as any); - updateWebviewMock.mockResolvedValueOnce(false); + globalMocks.updateWebviewMock.mockResolvedValueOnce(false); await expect(view.setColumns([{ field: "apple" }, { field: "banana" }, { field: "orange" }])).resolves.toBe(false); }); it("returns true if it successfully sent the new options", async () => { const globalMocks = createGlobalMocks(); const view = new Table.View(globalMocks.context as any, { title: "Stable Table of Cables" } as any); - updateWebviewMock.mockResolvedValueOnce(true); + globalMocks.updateWebviewMock.mockResolvedValueOnce(true); const cols = [ { field: "apple", valueFormatter: (data: { value: Table.CellData }) => `${data.value.toString()} apples` }, { field: "banana", comparator: (valueA, valueB, nodeA, nodeB, isDescending) => -1, colSpan: (params) => 2 }, @@ -171,6 +182,19 @@ describe("Table.View", () => { }); describe("onMessageReceived", () => { + it("does nothing if no command is provided", async () => { + const globalMocks = createGlobalMocks(); + const view = new Table.View(globalMocks.context as any, { title: "Table w/ changing display" } as any); + const onTableDisplayChangedFireMock = jest.spyOn((view as any).onTableDisplayChangedEmitter, "fire"); + globalMocks.updateWebviewMock.mockClear(); + const tableData = { rows: [{ a: 1, b: 1, c: 1 }] }; + await view.onMessageReceived({ + data: tableData, + }); + expect(onTableDisplayChangedFireMock).not.toHaveBeenCalledWith(tableData); + expect(globalMocks.updateWebviewMock).not.toHaveBeenCalled(); + }); + it("fires the onTableDisplayChanged event when handling the 'ondisplaychanged' command", async () => { const globalMocks = createGlobalMocks(); const view = new Table.View(globalMocks.context as any, { title: "Table w/ changing display" } as any); @@ -186,12 +210,12 @@ describe("Table.View", () => { it("calls updateWebview when handling the 'ready' command", async () => { const globalMocks = createGlobalMocks(); const view = new Table.View(globalMocks.context as any, { title: "Table w/ changing display" } as any); - const updateWebviewMock = jest.spyOn(view as any, "updateWebview").mockImplementation(); + globalMocks.updateWebviewMock.mockImplementation(); await view.onMessageReceived({ command: "ready", }); - expect(updateWebviewMock).toHaveBeenCalled(); - updateWebviewMock.mockRestore(); + expect(globalMocks.updateWebviewMock).toHaveBeenCalled(); + globalMocks.updateWebviewMock.mockRestore(); }); it("calls vscode.env.clipboard.writeText when handling the 'copy' command", async () => { @@ -234,7 +258,6 @@ describe("Table.View", () => { }, }; const view = new Table.View(globalMocks.context as any, data); - const updateWebviewMock = jest.spyOn(view as any, "updateWebview"); const writeTextMock = jest.spyOn(env.clipboard, "writeText"); const mockWebviewMsg = { command: "nonexistent-action", @@ -242,12 +265,13 @@ describe("Table.View", () => { }; await view.onMessageReceived(mockWebviewMsg); expect(writeTextMock).not.toHaveBeenCalled(); - expect(updateWebviewMock).not.toHaveBeenCalled(); + expect(globalMocks.updateWebviewMock).not.toHaveBeenCalled(); }); it("runs the callback for an action that exists", async () => { const globalMocks = createGlobalMocks(); - const callbackMock = jest.fn(); + const allCallbackMock = jest.fn(); + const zeroCallbackMock = jest.fn(); const data = { title: "Some table", rows: [{ a: 1, b: 1, c: 1 }], @@ -263,7 +287,19 @@ describe("Table.View", () => { callback: { typ: "cell", fn: (_cell: Table.CellData) => { - callbackMock(); + allCallbackMock(); + }, + }, + } as Table.Action, + ], + 0: [ + { + title: "Zero action", + command: "zero-action", + callback: { + typ: "cell", + fn: (_cell: Table.CellData) => { + zeroCallbackMock(); }, }, } as Table.Action, @@ -271,18 +307,182 @@ describe("Table.View", () => { }, }; const view = new Table.View(globalMocks.context as any, data); - const updateWebviewMock = jest.spyOn(view as any, "updateWebview"); const writeTextMock = jest.spyOn(env.clipboard, "writeText"); + // case 1: An action that exists for all rows const mockWebviewMsg = { command: "some-action", data: { cell: data.rows[0].a, row: data.rows[0] }, + rowIndex: 0, }; await view.onMessageReceived(mockWebviewMsg); expect(writeTextMock).not.toHaveBeenCalled(); - expect(updateWebviewMock).not.toHaveBeenCalled(); - expect(callbackMock).toHaveBeenCalled(); + expect(globalMocks.updateWebviewMock).not.toHaveBeenCalled(); + expect(allCallbackMock).toHaveBeenCalled(); + // case 2: An action that exists for all rows + const mockNextWebviewMsg = { + command: "zero-action", + data: { cell: data.rows[0].a, row: data.rows[0] }, + rowIndex: 0, + }; + await view.onMessageReceived(mockNextWebviewMsg); + expect(writeTextMock).not.toHaveBeenCalled(); + expect(globalMocks.updateWebviewMock).not.toHaveBeenCalled(); + expect(zeroCallbackMock).toHaveBeenCalled(); + }); + }); + + describe("setContent", () => { + it("sets the rows on the internal data structure and calls updateWebview", async () => { + const globalMocks = createGlobalMocks(); + const mockRow = { red: 255, green: 0, blue: 255 }; + const data = { title: "Table w/ content", rows: [] }; + const view = new Table.View(globalMocks.context as any, data as any); + globalMocks.updateWebviewMock.mockImplementation(); + await view.setContent([mockRow]); + expect(globalMocks.updateWebviewMock).toHaveBeenCalled(); + expect((view as any).data.rows[0]).toStrictEqual(mockRow); + globalMocks.updateWebviewMock.mockRestore(); + }); + }); + + describe("addColumns", () => { + it("sets the columns on the internal data structure and calls updateWebview", async () => { + const globalMocks = createGlobalMocks(); + const mockCols = [ + { field: "name", sort: "desc", colSpan: (params) => 2, rowSpan: (params) => 2 }, + { + field: "address", + sort: "asc", + comparator: (valueA, valueB, nodeA, nodeB, isDescending) => 1, + valueFormatter: (data: { value }) => `Located at ${data.value.toString()}`, + }, + ] as Table.ColumnOpts[]; + const data = { title: "Table w/ cols", columns: [], rows: [] }; + const view = new Table.View(globalMocks.context as any, data as any); + globalMocks.updateWebviewMock.mockImplementation(); + await view.addColumns(...mockCols); + expect(globalMocks.updateWebviewMock).toHaveBeenCalled(); + expect((view as any).data.columns).toStrictEqual( + mockCols.map((col) => ({ + ...col, + colSpan: col.colSpan?.toString(), + comparator: col.comparator?.toString(), + rowSpan: col.rowSpan?.toString(), + valueFormatter: col.valueFormatter?.toString(), + })) + ); + globalMocks.updateWebviewMock.mockRestore(); + }); + }); + + describe("addContent", () => { + it("adds the rows to the internal data structure and calls updateWebview", async () => { + const globalMocks = createGlobalMocks(); + const mockRow = { blue: true, yellow: false, violet: true }; + const data = { title: "Table w/ no initial rows", rows: [] }; + const view = new Table.View(globalMocks.context as any, data as any); + globalMocks.updateWebviewMock.mockImplementation(); + await view.addContent(mockRow); + expect(globalMocks.updateWebviewMock).toHaveBeenCalled(); + expect((view as any).data.rows[0]).toStrictEqual(mockRow); + globalMocks.updateWebviewMock.mockRestore(); + }); + }); + + describe("addContextOption", () => { + it("adds the context option to the internal data structure and calls updateWebview", async () => { + const globalMocks = createGlobalMocks(); + const data = { title: "Table w/ no initial rows", contextOpts: { all: [] }, rows: [] }; + const view = new Table.View(globalMocks.context as any, data as any); + globalMocks.updateWebviewMock.mockImplementation(); + + // case 1: Adding a context menu option to all rows + const contextOpt = { + title: "Add to cart", + command: "add-to-cart", + callback: { + typ: "row", + fn: (_data) => {}, + }, + condition: (_data) => true, + } as Table.ContextMenuOpts; + await view.addContextOption("all", contextOpt); + expect(globalMocks.updateWebviewMock).toHaveBeenCalled(); + expect((view as any).data.contextOpts["all"]).toStrictEqual([{ ...contextOpt, condition: contextOpt.condition?.toString() }]); + + // case 2: Adding a context menu option to one row + const singleRowContextOpt = { + title: "Save for later", + command: "save-for-later", + callback: { + typ: "row", + fn: (_data) => {}, + }, + condition: (_data) => true, + } as Table.ContextMenuOpts; + await view.addContextOption(1, singleRowContextOpt); + expect(globalMocks.updateWebviewMock).toHaveBeenCalled(); + expect((view as any).data.contextOpts[1]).toStrictEqual([ + { ...singleRowContextOpt, condition: singleRowContextOpt.condition?.toString() }, + ]); + globalMocks.updateWebviewMock.mockRestore(); + }); + }); + + describe("addAction", () => { + it("adds the action to the internal data structure and calls updateWebview", async () => { + const globalMocks = createGlobalMocks(); + const data = { title: "Table w/ no initial rows", actions: { all: [] }, rows: [] }; + const view = new Table.View(globalMocks.context as any, data as any); + globalMocks.updateWebviewMock.mockImplementation(); + + // case 1: Adding an action to all rows + const action = { + title: "Add to wishlist", + command: "add-to-wishlist", + callback: { + typ: "row", + fn: (_data) => {}, + }, + condition: (_data) => true, + } as Table.ContextMenuOpts; + await view.addAction("all", action); + expect(globalMocks.updateWebviewMock).toHaveBeenCalled(); + expect((view as any).data.actions["all"]).toStrictEqual([{ ...action, condition: action.condition?.toString() }]); + + // case 2: Adding an action to one row + const singleRowAction = { + title: "Learn more", + command: "learn-more", + callback: { + typ: "row", + fn: (_data) => {}, + }, + condition: (_data) => true, + } as Table.ContextMenuOpts; + await view.addAction(2, singleRowAction); + expect(globalMocks.updateWebviewMock).toHaveBeenCalled(); + expect((view as any).data.actions[2]).toStrictEqual([{ ...singleRowAction, condition: singleRowAction.condition?.toString() }]); + globalMocks.updateWebviewMock.mockRestore(); }); }); }); // Table.Instance unit tests +describe("Table.Instance", () => { + describe("dispose", () => { + it("disposes of the table view using the function in the base class", () => { + const globalMocks = createGlobalMocks(); + const builder = new TableBuilder(globalMocks.context as any) + .addRows([ + { a: 1, b: 2, c: 3, d: false, e: 5 }, + { a: 3, b: 2, c: 1, d: true, e: 6 }, + ]) + .addColumns([{ field: "a" }, { field: "b" }, { field: "c" }, { field: "d" }, { field: "e" }]); + const instance = builder.build(); + const disposeMock = jest.spyOn((WebView as any).prototype, "dispose"); + instance.dispose(); + expect(disposeMock).toHaveBeenCalled(); + }); + }); +}); From db6c10cb1c8b974eb6220a2a2f7ec89b81687af5 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 18 Jul 2024 10:28:07 -0400 Subject: [PATCH 057/107] refactor(tests): Update TableBuilder test case format Signed-off-by: Trae Yelovich --- .../vscode/ui/utils/TableBuilder.unit.test.ts | 452 +++++++++--------- 1 file changed, 227 insertions(+), 225 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts index 9f2282521f..2fe857fd18 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts @@ -33,265 +33,267 @@ function createGlobalMocks() { }; } -describe("TableBuilder::constructor", () => { - it("stores the extension context within the builder", () => { - const globalMocks = createGlobalMocks(); - const builder = new TableBuilder(globalMocks.context as any); - expect((builder as any).context).toBe(globalMocks.context); +describe("TableBuilder", () => { + describe("constructor", () => { + it("stores the extension context within the builder", () => { + const globalMocks = createGlobalMocks(); + const builder = new TableBuilder(globalMocks.context as any); + expect((builder as any).context).toBe(globalMocks.context); + }); }); -}); -describe("TableBuilder::options", () => { - it("adds the given options to the table data, returning the same instance", () => { - const globalMocks = createGlobalMocks(); - let builder = new TableBuilder(globalMocks.context as any); - expect((builder as any).data).not.toHaveProperty("pagination"); - builder = builder.options({ - pagination: false, + describe("options", () => { + it("adds the given options to the table data, returning the same instance", () => { + const globalMocks = createGlobalMocks(); + let builder = new TableBuilder(globalMocks.context as any); + expect((builder as any).data).not.toHaveProperty("pagination"); + builder = builder.options({ + pagination: false, + }); + expect((builder as any).data).toHaveProperty("pagination"); }); - expect((builder as any).data).toHaveProperty("pagination"); }); -}); -describe("TableBuilder::title", () => { - it("sets the given title on the table data, returning the same instance", () => { - const globalMocks = createGlobalMocks(); - let builder = new TableBuilder(globalMocks.context as any); - expect((builder as any).data.title).toBe(""); - const title = "An incredulously long title for a table such that nobody should have to bear witness to such a tragedy"; - builder = builder.title(title); - expect((builder as any).data.title).toBe(title); + describe("title", () => { + it("sets the given title on the table data, returning the same instance", () => { + const globalMocks = createGlobalMocks(); + let builder = new TableBuilder(globalMocks.context as any); + expect((builder as any).data.title).toBe(""); + const title = "An incredulously long title for a table such that nobody should have to bear witness to such a tragedy"; + builder = builder.title(title); + expect((builder as any).data.title).toBe(title); + }); }); -}); -describe("TableBuilder::rows", () => { - it("sets the given rows for the table, returning the same instance", () => { - const globalMocks = createGlobalMocks(); - let builder = new TableBuilder(globalMocks.context as any); - expect((builder as any).data.rows).toStrictEqual([]); - const newRows = [ - { a: 1, b: 2, c: 3, d: false }, - { a: 3, b: 2, c: 1, d: true }, - ]; - builder = builder.rows(...newRows); - expect((builder as any).data.rows).toStrictEqual(newRows); + describe("rows", () => { + it("sets the given rows for the table, returning the same instance", () => { + const globalMocks = createGlobalMocks(); + let builder = new TableBuilder(globalMocks.context as any); + expect((builder as any).data.rows).toStrictEqual([]); + const newRows = [ + { a: 1, b: 2, c: 3, d: false }, + { a: 3, b: 2, c: 1, d: true }, + ]; + builder = builder.rows(...newRows); + expect((builder as any).data.rows).toStrictEqual(newRows); + }); }); -}); -describe("TableBuilder::addRows", () => { - it("adds the given rows to the table, returning the same instance", () => { - const globalMocks = createGlobalMocks(); - let builder = new TableBuilder(globalMocks.context as any); - expect((builder as any).data.rows).toStrictEqual([]); - const newRows = [ - { a: 1, b: 2, c: 3, d: false }, - { a: 3, b: 2, c: 1, d: true }, - ]; - builder = builder.rows(...newRows); - newRows.push({ a: 2, b: 1, c: 3, d: false }); - builder = builder.addRows([newRows[newRows.length - 1]]); - expect((builder as any).data.rows).toStrictEqual(newRows); + describe("addRows", () => { + it("adds the given rows to the table, returning the same instance", () => { + const globalMocks = createGlobalMocks(); + let builder = new TableBuilder(globalMocks.context as any); + expect((builder as any).data.rows).toStrictEqual([]); + const newRows = [ + { a: 1, b: 2, c: 3, d: false }, + { a: 3, b: 2, c: 1, d: true }, + ]; + builder = builder.rows(...newRows); + newRows.push({ a: 2, b: 1, c: 3, d: false }); + builder = builder.addRows([newRows[newRows.length - 1]]); + expect((builder as any).data.rows).toStrictEqual(newRows); + }); }); -}); -describe("TableBuilder::columns", () => { - it("sets the given columns for the table, returning the same instance", () => { - const globalMocks = createGlobalMocks(); - let builder = new TableBuilder(globalMocks.context as any); - expect((builder as any).data.columns).toStrictEqual([]); - const newCols: Table.ColumnOpts[] = [{ field: "cat" }, { field: "doge", filter: true }, { field: "parrot", sort: "asc" }]; - builder = builder.columns(...newCols); - expect(JSON.parse(JSON.stringify((builder as any).data.columns))).toStrictEqual(JSON.parse(JSON.stringify(newCols))); + describe("columns", () => { + it("sets the given columns for the table, returning the same instance", () => { + const globalMocks = createGlobalMocks(); + let builder = new TableBuilder(globalMocks.context as any); + expect((builder as any).data.columns).toStrictEqual([]); + const newCols: Table.ColumnOpts[] = [{ field: "cat" }, { field: "doge", filter: true }, { field: "parrot", sort: "asc" }]; + builder = builder.columns(...newCols); + expect(JSON.parse(JSON.stringify((builder as any).data.columns))).toStrictEqual(JSON.parse(JSON.stringify(newCols))); + }); }); -}); -describe("TableBuilder::addColumns", () => { - it("adds the given columns to the table, returning the same instance", () => { - const globalMocks = createGlobalMocks(); - let builder = new TableBuilder(globalMocks.context as any); - expect((builder as any).data.columns).toStrictEqual([]); - const newCols: Table.ColumnOpts[] = [{ field: "cat" }, { field: "doge", filter: true }, { field: "parrot", sort: "asc" }]; - builder = builder.columns(...newCols); - newCols.push({ field: "parakeet", sort: "desc" }); - builder = builder.addColumns([newCols[newCols.length - 1]]); - expect(JSON.parse(JSON.stringify((builder as any).data.columns))).toStrictEqual(JSON.parse(JSON.stringify(newCols))); + describe("addColumns", () => { + it("adds the given columns to the table, returning the same instance", () => { + const globalMocks = createGlobalMocks(); + let builder = new TableBuilder(globalMocks.context as any); + expect((builder as any).data.columns).toStrictEqual([]); + const newCols: Table.ColumnOpts[] = [{ field: "cat" }, { field: "doge", filter: true }, { field: "parrot", sort: "asc" }]; + builder = builder.columns(...newCols); + newCols.push({ field: "parakeet", sort: "desc" }); + builder = builder.addColumns([newCols[newCols.length - 1]]); + expect(JSON.parse(JSON.stringify((builder as any).data.columns))).toStrictEqual(JSON.parse(JSON.stringify(newCols))); + }); }); -}); -describe("TableBuilder::convertColumnOpts", () => { - it("converts an array of ColumnOpts to an array of Column", () => { - const globalMocks = createGlobalMocks(); - const builder = new TableBuilder(globalMocks.context as any); - const newCols: Table.ColumnOpts[] = [ - { field: "cat", valueFormatter: (data: { value: Table.ContentTypes }) => `val: ${data.value.toString()}` }, - { field: "doge", filter: true, comparator: (valueA, valueB, nodeA, nodeB, isDescending) => -1, colSpan: (params) => 2 }, - { field: "parrot", sort: "asc", rowSpan: (params) => 2 }, - ]; - expect((builder as any).convertColumnOpts(newCols)).toStrictEqual( - newCols.map((col) => ({ - ...col, - comparator: col.comparator?.toString(), - colSpan: col.colSpan?.toString(), - rowSpan: col.rowSpan?.toString(), - valueFormatter: col.valueFormatter?.toString(), - })) - ); + describe("convertColumnOpts", () => { + it("converts an array of ColumnOpts to an array of Column", () => { + const globalMocks = createGlobalMocks(); + const builder = new TableBuilder(globalMocks.context as any); + const newCols: Table.ColumnOpts[] = [ + { field: "cat", valueFormatter: (data: { value: Table.ContentTypes }) => `val: ${data.value.toString()}` }, + { field: "doge", filter: true, comparator: (valueA, valueB, nodeA, nodeB, isDescending) => -1, colSpan: (params) => 2 }, + { field: "parrot", sort: "asc", rowSpan: (params) => 2 }, + ]; + expect((builder as any).convertColumnOpts(newCols)).toStrictEqual( + newCols.map((col) => ({ + ...col, + comparator: col.comparator?.toString(), + colSpan: col.colSpan?.toString(), + rowSpan: col.rowSpan?.toString(), + valueFormatter: col.valueFormatter?.toString(), + })) + ); + }); }); -}); -describe("TableBuilder::contextOptions", () => { - it("adds the given context options and returns the same instance", () => { - const globalMocks = createGlobalMocks(); - const builder = new TableBuilder(globalMocks.context as any); - const ctxOpts = { - all: [ - { - title: "Add to queue", - command: "add-to-queue", - callback: { - typ: "cell", - fn: (_cell: Table.CellData) => {}, + describe("contextOptions", () => { + it("adds the given context options and returns the same instance", () => { + const globalMocks = createGlobalMocks(); + const builder = new TableBuilder(globalMocks.context as any); + const ctxOpts = { + all: [ + { + title: "Add to queue", + command: "add-to-queue", + callback: { + typ: "cell", + fn: (_cell: Table.CellData) => {}, + }, + condition: (_data) => true, }, - condition: (_data) => true, - }, - ], - } as Record; + ], + } as Record; - const addCtxOptSpy = jest.spyOn(builder, "addContextOption"); - const builderCtxOpts = builder.contextOptions(ctxOpts); - expect(builderCtxOpts).toBeInstanceOf(TableBuilder); - expect(addCtxOptSpy).toHaveBeenCalledTimes(1); + const addCtxOptSpy = jest.spyOn(builder, "addContextOption"); + const builderCtxOpts = builder.contextOptions(ctxOpts); + expect(builderCtxOpts).toBeInstanceOf(TableBuilder); + expect(addCtxOptSpy).toHaveBeenCalledTimes(1); + }); }); -}); -describe("TableBuilder::addContextOption", () => { - it("adds the given context option and returns the same instance", () => { - const globalMocks = createGlobalMocks(); - const builder = new TableBuilder(globalMocks.context as any); - const ctxOpt = { - title: "Delete", - command: "delete", - callback: { - typ: "row", - fn: (_row: Table.RowData) => {}, - }, - condition: (_data) => true, - } as Table.ContextMenuOpts; + describe("addContextOption", () => { + it("adds the given context option and returns the same instance", () => { + const globalMocks = createGlobalMocks(); + const builder = new TableBuilder(globalMocks.context as any); + const ctxOpt = { + title: "Delete", + command: "delete", + callback: { + typ: "row", + fn: (_row: Table.RowData) => {}, + }, + condition: (_data) => true, + } as Table.ContextMenuOpts; - // case 0: adding context option to "all" rows - const builderCtxOpts = builder.addContextOption("all", ctxOpt); - expect(builderCtxOpts).toBeInstanceOf(TableBuilder); - expect((builderCtxOpts as any).data.contextOpts).toStrictEqual({ - all: [{ ...ctxOpt, condition: ctxOpt.condition?.toString() }], - }); - // case 1: adding context option to a specific row, no previous options existed - const finalBuilder = builderCtxOpts.addContextOption(0, ctxOpt); - expect((finalBuilder as any).data.contextOpts).toStrictEqual({ - 0: [{ ...ctxOpt, condition: ctxOpt.condition?.toString() }], - all: [{ ...ctxOpt, condition: ctxOpt.condition?.toString() }], + // case 0: adding context option to "all" rows + const builderCtxOpts = builder.addContextOption("all", ctxOpt); + expect(builderCtxOpts).toBeInstanceOf(TableBuilder); + expect((builderCtxOpts as any).data.contextOpts).toStrictEqual({ + all: [{ ...ctxOpt, condition: ctxOpt.condition?.toString() }], + }); + // case 1: adding context option to a specific row, no previous options existed + const finalBuilder = builderCtxOpts.addContextOption(0, ctxOpt); + expect((finalBuilder as any).data.contextOpts).toStrictEqual({ + 0: [{ ...ctxOpt, condition: ctxOpt.condition?.toString() }], + all: [{ ...ctxOpt, condition: ctxOpt.condition?.toString() }], + }); }); }); -}); -describe("TableBuilder::addRowAction", () => { - it("adds the given row action to all rows and returns the same instance", () => { - const globalMocks = createGlobalMocks(); - const builder = new TableBuilder(globalMocks.context as any); - const rowAction = { - title: "Move", - command: "move", - callback: { - typ: "cell", - fn: (_cell: Table.CellData) => {}, - }, - condition: (_data) => true, - } as Table.ActionOpts; - const builderAction = builder.addRowAction("all", rowAction); - expect(builderAction).toBeInstanceOf(TableBuilder); - expect((builderAction as any).data.actions).toStrictEqual({ - all: [{ ...rowAction, condition: rowAction.condition?.toString() }], - }); - const finalBuilder = builderAction.addRowAction(0, rowAction); - expect((finalBuilder as any).data.actions).toStrictEqual({ - 0: [{ ...rowAction, condition: rowAction.condition?.toString() }], - all: [{ ...rowAction, condition: rowAction.condition?.toString() }], + describe("addRowAction", () => { + it("adds the given row action to all rows and returns the same instance", () => { + const globalMocks = createGlobalMocks(); + const builder = new TableBuilder(globalMocks.context as any); + const rowAction = { + title: "Move", + command: "move", + callback: { + typ: "cell", + fn: (_cell: Table.CellData) => {}, + }, + condition: (_data) => true, + } as Table.ActionOpts; + const builderAction = builder.addRowAction("all", rowAction); + expect(builderAction).toBeInstanceOf(TableBuilder); + expect((builderAction as any).data.actions).toStrictEqual({ + all: [{ ...rowAction, condition: rowAction.condition?.toString() }], + }); + const finalBuilder = builderAction.addRowAction(0, rowAction); + expect((finalBuilder as any).data.actions).toStrictEqual({ + 0: [{ ...rowAction, condition: rowAction.condition?.toString() }], + all: [{ ...rowAction, condition: rowAction.condition?.toString() }], + }); }); }); -}); -describe("TableBuilder::rowActions", () => { - it("calls rowAction for each action and returns the same instance", () => { - const globalMocks = createGlobalMocks(); - const builder = new TableBuilder(globalMocks.context as any); - const rowActions = { - 0: [ - { - title: "Move", - command: "move", - callback: { - typ: "cell", - fn: (_cell: Table.CellData) => {}, + describe("rowActions", () => { + it("calls rowAction for each action and returns the same instance", () => { + const globalMocks = createGlobalMocks(); + const builder = new TableBuilder(globalMocks.context as any); + const rowActions = { + 0: [ + { + title: "Move", + command: "move", + callback: { + typ: "cell", + fn: (_cell: Table.CellData) => {}, + }, + condition: (_data) => true, }, - condition: (_data) => true, - }, - ] as Table.ActionOpts[], - all: [ - { - title: "Recall", - command: "recall", - callback: { - typ: "cell", - fn: (_cell: Table.CellData) => {}, + ] as Table.ActionOpts[], + all: [ + { + title: "Recall", + command: "recall", + callback: { + typ: "cell", + fn: (_cell: Table.CellData) => {}, + }, + condition: (_data) => true, }, - condition: (_data) => true, - }, - ] as Table.ActionOpts[], - }; - const rowActionSpy = jest.spyOn(builder, "addRowAction"); - const builderAction = builder.rowActions(rowActions); - expect(rowActionSpy).toHaveBeenCalledTimes(2); - expect(builderAction).toBeInstanceOf(TableBuilder); + ] as Table.ActionOpts[], + }; + const rowActionSpy = jest.spyOn(builder, "addRowAction"); + const builderAction = builder.rowActions(rowActions); + expect(rowActionSpy).toHaveBeenCalledTimes(2); + expect(builderAction).toBeInstanceOf(TableBuilder); + }); }); -}); -describe("TableBuilder::buildAndShare", () => { - it("builds the table view and adds it to the table mediator", () => { - const globalMocks = createGlobalMocks(); - const newRows = [ - { a: 1, b: 2, c: 3, d: false, e: 5 }, - { a: 3, b: 2, c: 1, d: true, e: 6 }, - ]; - const addTableSpy = jest.spyOn(TableMediator.prototype, "addTable"); - const builder = new TableBuilder(globalMocks.context as any) - .addRows(newRows) - .addColumns([{ field: "a" }, { field: "b" }, { field: "c" }, { field: "d" }, { field: "e" }]); - const instance = builder.buildAndShare(); - expect(addTableSpy).toHaveBeenCalledWith(instance); + describe("buildAndShare", () => { + it("builds the table view and adds it to the table mediator", () => { + const globalMocks = createGlobalMocks(); + const newRows = [ + { a: 1, b: 2, c: 3, d: false, e: 5 }, + { a: 3, b: 2, c: 1, d: true, e: 6 }, + ]; + const addTableSpy = jest.spyOn(TableMediator.prototype, "addTable"); + const builder = new TableBuilder(globalMocks.context as any) + .addRows(newRows) + .addColumns([{ field: "a" }, { field: "b" }, { field: "c" }, { field: "d" }, { field: "e" }]); + const instance = builder.buildAndShare(); + expect(addTableSpy).toHaveBeenCalledWith(instance); + }); }); -}); -describe("TableBuilder::reset", () => { - it("resets all table data on the builder instance", () => { - const globalMocks = createGlobalMocks(); - const newRows = [ - { a: 1, b: 2, c: 3, d: false }, - { a: 3, b: 2, c: 1, d: true }, - ]; - const builder = new TableBuilder(globalMocks.context as any) - .rows(...newRows) - .title("A table") - .options({ pagination: false }); - builder.reset(); - expect((builder as any).data).toStrictEqual({ - actions: { - all: [], - }, - contextOpts: { - all: [], - }, - columns: [], - rows: [], - title: "", + describe("reset", () => { + it("resets all table data on the builder instance", () => { + const globalMocks = createGlobalMocks(); + const newRows = [ + { a: 1, b: 2, c: 3, d: false }, + { a: 3, b: 2, c: 1, d: true }, + ]; + const builder = new TableBuilder(globalMocks.context as any) + .rows(...newRows) + .title("A table") + .options({ pagination: false }); + builder.reset(); + expect((builder as any).data).toStrictEqual({ + actions: { + all: [], + }, + contextOpts: { + all: [], + }, + columns: [], + rows: [], + title: "", + }); }); }); }); From 1b7ec33a1d97129b9bbf331f8aeac180328ca79c Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 18 Jul 2024 10:33:01 -0400 Subject: [PATCH 058/107] refactor: Remove table examples and add license Signed-off-by: Trae Yelovich --- .../__unit__/vscode/ui/TableView.unit.test.ts | 11 ++ .../zowe-explorer/src/trees/job/JobInit.ts | 21 --- .../src/trees/shared/SharedInit.ts | 163 ------------------ 3 files changed, 11 insertions(+), 184 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts index a3a79b2cb7..1e4201a179 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts @@ -1,3 +1,14 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + import { join } from "path"; import { Table, TableBuilder, WebView } from "../../../../src"; import { env, EventEmitter, Uri, window } from "vscode"; diff --git a/packages/zowe-explorer/src/trees/job/JobInit.ts b/packages/zowe-explorer/src/trees/job/JobInit.ts index 62048a9ce7..840bbaf7a6 100644 --- a/packages/zowe-explorer/src/trees/job/JobInit.ts +++ b/packages/zowe-explorer/src/trees/job/JobInit.ts @@ -145,27 +145,6 @@ export class JobInit { ) ); context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.copyName", async (job: IZoweJobTreeNode) => JobActions.copyName(job))); - context.subscriptions.push( - vscode.commands.registerCommand("zowe.jobs.tableView", (jobSession: IZoweJobTreeNode) => { - const children = jobSession.children; - if (children) { - const jobObjects = children.map((c) => c.job); - new TableBuilder(context) - .title("Jobs view") - .rows( - ...jobObjects.map((job) => ({ - jobid: job.jobid, - jobname: job.jobname, - owner: job.owner, - status: job.status, - class: job.class, - retcode: job.retcode, - })) - ) - .build(); - } - }) - ); context.subscriptions.push( vscode.workspace.onDidOpenTextDocument((doc) => { if (doc.uri.scheme !== ZoweScheme.Jobs) { diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index d5cc15c82b..4550f737ba 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -305,169 +305,6 @@ export class SharedInit { // This command does nothing, its here to let us disable individual items in the tree view }) ); - context.subscriptions.push( - vscode.commands.registerCommand("zowe.tableView", async () => { - // Build the table instance - const table = new TableBuilder(context) - .title("Cars for sale") - .rows( - { - make: "Ford", - model: "Model T", - year: 1908, - price: 4000, - }, - { - make: "Tesla", - model: "Model Y", - year: 2022, - price: 40000, - } - ) - .columns([ - { field: "make" }, - { field: "model" }, - { field: "year" }, - { - field: "price", - valueFormatter: (data: { value: Table.CellData }): string => { - return data.value ? `$${data.value.toString()}` : "No price available"; - }, - }, - ]) - .build(); - // Add content to the existing table view - await table.addContent({ make: "Toyota", model: "Corolla", year: 2007 }); - }) - ); - context.subscriptions.push( - vscode.commands.registerCommand("zowe.tableView2", () => { - // Context option demo - new TableBuilder(context) - .options({ - pagination: true, - paginationPageSizeSelector: [25, 50, 100, 250], - }) - .title("Random data") - .rows( - ...Array.from({ length: 1024 }, (val) => ({ - name: randomUUID(), - value: randomInt(2147483647), - })) - ) - .columns([ - { field: "name", filter: true }, - { field: "value", filter: true }, - ]) - .contextOption("all", { - title: "Show as dialog", - command: "print-dialog", - callback: { - typ: "row", - fn: async (data) => { - await Gui.showMessage(data.cell); - }, - }, - }) - .build(); - // Example of catching filter/sort changes: table.onTableDisplayChanged((data) => console.log(data)); - }) - ); - context.subscriptions.push( - vscode.commands.registerCommand("zowe.tableView3", async () => { - // Example use case: Listing USS files - const uriPath = await Gui.showInputBox({ - title: "Enter a URI path to list USS files", - }); - if (uriPath == null) { - return; - } - - const uriInfo = FsAbstractUtils.getInfoForUri( - vscode.Uri.from({ - scheme: "zowe-uss", - path: uriPath, - }) - ); - - const files = await UssFSProvider.instance.listFiles( - Profiles.getInstance().loadNamedProfile(uriInfo.profileName), - vscode.Uri.from({ - scheme: "zowe-uss", - path: uriPath, - }) - ); - new TableBuilder(context) - .title(uriPath.substring(uriPath.indexOf("/", 1))) - .rows( - ...files.apiResponse.items.map((item) => ({ - name: item.name, - size: item.size, - owner: item.user, - group: item.group, - perms: item.mode, - })) - ) - .columns([ - { field: "name", filter: true, sort: "asc" }, - { field: "size", filter: true, valueFormatter: (data: { value: Table.CellData }) => `${data.value.toString()} bytes` }, - { field: "owner", filter: true }, - { field: "group", filter: true }, - { field: "perms", filter: true }, - ]) - .contextOption("all", { - title: "Copy path", - command: "copy-path", - callback: { - fn: async (data: Table.RowData) => { - await vscode.env.clipboard.writeText(path.posix.join(uriPath, data.name as string)); - }, - typ: "row", - }, - }) - .contextOption("all", { - title: "Citrus-specific option", - command: "citrus-dialog", - condition: (data: any) => { - for (const citrus of ["lemon", "grapefruit", "lime", "tangerine"]) { - if (data.name && (data.name as string).startsWith(citrus)) { - return true; - } - } - - return false; - }, - callback: { - fn: async (data: Table.RowData) => { - await vscode.env.clipboard.writeText(path.posix.join(uriPath, data.name as string)); - }, - typ: "row", - }, - }) - .rowAction("all", { - title: "Open in editor", - type: "primary", - command: "edit", - callback: { - fn: async (data: Table.RowData) => { - const filename = data.name as string; - const uri = vscode.Uri.from({ - scheme: "zowe-uss", - path: path.posix.join(uriPath, filename), - }); - - if (!UssFSProvider.instance.exists(uri)) { - await UssFSProvider.instance.writeFile(uri, new Uint8Array(), { create: true, overwrite: false }); - } - - await vscode.commands.executeCommand("vscode.open", uri); - }, - typ: "row", - }, - }) - .build(); - }) - ); // initialize the Constants.filesToCompare array during initialization LocalFileManagement.resetCompareSelection(); } From cfc3a4bf6e8c116afe1d152c314ef1caa5bdbe96 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 18 Jul 2024 10:38:20 -0400 Subject: [PATCH 059/107] deps: Update AG Grid to resolve audit warnings Signed-off-by: Trae Yelovich --- .../zowe-explorer/src/webviews/package.json | 4 +-- pnpm-lock.yaml | 26 ++++++++++++------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/zowe-explorer/src/webviews/package.json b/packages/zowe-explorer/src/webviews/package.json index 9b6195d829..0c4740cad1 100644 --- a/packages/zowe-explorer/src/webviews/package.json +++ b/packages/zowe-explorer/src/webviews/package.json @@ -22,8 +22,8 @@ "@szhsin/react-menu": "^4.1.0", "@types/vscode-webview": "^1.57.1", "@vscode/webview-ui-toolkit": "^1.2.2", - "ag-grid-community": "^31.3.2", - "ag-grid-react": "^31.3.2", + "ag-grid-community": "^32.0.2", + "ag-grid-react": "^32.0.2", "lodash": "^4.17.21", "preact": "^10.16.0", "preact-render-to-string": "^6.5.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99696068de..f5f46bbd75 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -349,11 +349,11 @@ importers: specifier: ^1.2.2 version: 1.4.0(react@18.3.1) ag-grid-community: - specifier: ^31.3.2 - version: 31.3.2 + specifier: ^32.0.2 + version: 32.0.2 ag-grid-react: - specifier: ^31.3.2 - version: 31.3.2(react-dom@18.3.1)(react@18.3.1) + specifier: ^32.0.2 + version: 32.0.2(react-dom@18.3.1)(react@18.3.1) lodash: specifier: ^4.17.21 version: 4.17.21 @@ -3542,17 +3542,23 @@ packages: hasBin: true dev: true - /ag-grid-community@31.3.2: - resolution: {integrity: sha512-GxqFRD0OcjaVRE1gwLgoP0oERNPH8Lk8wKJ1txulsxysEQ5dZWHhiIoXXSiHjvOCVMkK/F5qzY6HNrn6VeDMTQ==} + /ag-charts-types@10.0.2: + resolution: {integrity: sha512-Nxo5slHOXlaeg0gRIsVnovAosQzzlYfWJtdDy0Aq/VvpJru/PJ+5i2c9aCyEhgRxhBjImsoegwkgRj7gNOWV6Q==} dev: false - /ag-grid-react@31.3.2(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-SFHN05bsXp901rIT00Fa6iQLCtyavoJiKaXEDUtAU5LMu+GTkjs/FPQBQ8754omgdDFr4NsS3Ri6QbqBne3rug==} + /ag-grid-community@32.0.2: + resolution: {integrity: sha512-vLJJUjnsG9hNK41GNuW2EHu1W264kxA/poOpcX4kmyrjU5Uzvelsbj3HdKAO9POV28iqyRdKGYfAWdn8QzA7KA==} + dependencies: + ag-charts-types: 10.0.2 + dev: false + + /ag-grid-react@32.0.2(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-IWYsoyJ/Z763rWbE5/9SaT1n5xwIKrm/QzOG14l7i8z5J6JdJwfV0aQFATmEE8Xws2H48vlLcLdW1cv4hwV3eg==} peerDependencies: react: ^16.3.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.3.0 || ^17.0.0 || ^18.0.0 dependencies: - ag-grid-community: 31.3.2 + ag-grid-community: 32.0.2 prop-types: 15.8.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -8871,11 +8877,13 @@ packages: /loglevel-plugin-prefix@0.8.4: resolution: {integrity: sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==} + requiresBuild: true dev: true /loglevel@1.9.1: resolution: {integrity: sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==} engines: {node: '>= 0.6.0'} + requiresBuild: true dev: true /loose-envify@1.4.0: From 4c4eb15999f28344c0ea2a954014154b58ccb9d0 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 18 Jul 2024 10:47:42 -0400 Subject: [PATCH 060/107] style: fix unused imports & lint errors Signed-off-by: Trae Yelovich --- packages/zowe-explorer/src/trees/job/JobInit.ts | 2 +- .../zowe-explorer/src/trees/shared/SharedInit.ts | 15 +-------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/packages/zowe-explorer/src/trees/job/JobInit.ts b/packages/zowe-explorer/src/trees/job/JobInit.ts index 840bbaf7a6..b3dd615503 100644 --- a/packages/zowe-explorer/src/trees/job/JobInit.ts +++ b/packages/zowe-explorer/src/trees/job/JobInit.ts @@ -10,7 +10,7 @@ */ import * as vscode from "vscode"; -import { IZoweJobTreeNode, IZoweTreeNode, TableBuilder, ZoweScheme, imperative, Gui } from "@zowe/zowe-explorer-api"; +import { IZoweJobTreeNode, IZoweTreeNode, ZoweScheme, imperative, Gui } from "@zowe/zowe-explorer-api"; import { JobTree } from "./JobTree"; import { JobActions } from "./JobActions"; import { ZoweJobNode } from "./ZoweJobNode"; diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index 4550f737ba..37f2a09f35 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -10,18 +10,7 @@ */ import * as vscode from "vscode"; -import { - FileManagement, - FsAbstractUtils, - Gui, - IZoweTree, - IZoweTreeNode, - Table, - TableBuilder, - Validation, - ZosEncoding, - ZoweScheme, -} from "@zowe/zowe-explorer-api"; +import { FileManagement, IZoweTree, IZoweTreeNode, Validation, ZosEncoding, ZoweScheme } from "@zowe/zowe-explorer-api"; import { SharedActions } from "./SharedActions"; import { SharedHistoryView } from "./SharedHistoryView"; import { SharedTreeProviders } from "./SharedTreeProviders"; @@ -45,8 +34,6 @@ import { SharedUtils } from "./SharedUtils"; import { SharedContext } from "./SharedContext"; import { TreeViewUtils } from "../../utils/TreeViewUtils"; import { CertificateWizard } from "../../utils/CertificateWizard"; -import { randomInt, randomUUID } from "crypto"; -import * as path from "path"; export class SharedInit { public static registerRefreshCommand( From f4d904d6e38578e9c42716146c18643eddca8182 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 22 Jul 2024 10:33:33 -0400 Subject: [PATCH 061/107] fix: temporarily resolve webview chunk warning Signed-off-by: Trae Yelovich --- packages/zowe-explorer/src/webviews/vite.config.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/zowe-explorer/src/webviews/vite.config.ts b/packages/zowe-explorer/src/webviews/vite.config.ts index 7fe3cf20df..aefe95f88f 100644 --- a/packages/zowe-explorer/src/webviews/vite.config.ts +++ b/packages/zowe-explorer/src/webviews/vite.config.ts @@ -41,9 +41,9 @@ export default defineConfig({ typescript: true, }), ], - root: path.resolve(__dirname, "src"), build: { + chunkSizeWarningLimit: 1000, cssCodeSplit: false, emptyOutDir: true, outDir: path.resolve(__dirname, "dist"), @@ -53,6 +53,9 @@ export default defineConfig({ entryFileNames: `[name]/[name].js`, chunkFileNames: `[name]/[name].js`, assetFileNames: `[name]/[name].[ext]`, + manualChunks: { + "ag-grid-react": ["ag-grid-react"], + }, }, }, }, From 2999572e66e913e232c9c45fc6e57ad23e1d9733 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 22 Jul 2024 11:34:35 -0400 Subject: [PATCH 062/107] refactor(webview): Consolidate webview options into type Signed-off-by: Trae Yelovich --- .../__unit__/vscode/ui/WebView.unit.test.ts | 18 ++++++---------- .../src/vscode/ui/TableView.ts | 5 ++++- .../src/vscode/ui/WebView.ts | 21 +++++++++---------- .../src/trees/shared/SharedHistoryView.ts | 5 ++++- .../zowe-explorer/src/trees/uss/USSActions.ts | 6 +++--- .../src/trees/uss/USSAttributeView.ts | 6 ++++-- .../src/utils/CertificateWizard.ts | 4 +++- 7 files changed, 34 insertions(+), 31 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/WebView.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/WebView.unit.test.ts index ab7e4f225b..2df27e7d48 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/WebView.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/WebView.unit.test.ts @@ -44,12 +44,9 @@ describe("WebView unit tests", () => { const createWebviewPanelSpy = jest.spyOn(vscode.window, "createWebviewPanel"); const renderSpy = jest.spyOn(Mustache, "render"); - const testView = new WebView( - "Test Webview Title", - "example-folder", - { extensionPath: "test/path" } as vscode.ExtensionContext, - async (_message: any) => {} - ); + const testView = new WebView("Test Webview Title", "example-folder", { extensionPath: "test/path" } as vscode.ExtensionContext, { + onDidReceiveMessage: async (_message: any) => {}, + }); expect(createWebviewPanelSpy).toHaveBeenCalled(); expect(renderSpy).toHaveBeenCalled(); (testView as any).dispose(); @@ -57,12 +54,9 @@ describe("WebView unit tests", () => { }); it("returns HTML content from WebView", () => { - const testView = new WebView( - "Test Webview Title", - "example-folder", - { extensionPath: "test/path" } as vscode.ExtensionContext, - async (_message: any) => {} - ); + const testView = new WebView("Test Webview Title", "example-folder", { extensionPath: "test/path" } as vscode.ExtensionContext, { + onDidReceiveMessage: async (_message: any) => {}, + }); expect(testView.htmlContent).toBe(testView.panel.webview.html); }); }); diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 6972a7f156..0b82569c5d 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -280,7 +280,10 @@ export namespace Table { } public constructor(context: ExtensionContext, data?: ViewOpts) { - super(data.title ?? "Table view", "table-view", context, (message) => this.onMessageReceived(message), true); + super(data.title ?? "Table view", "table-view", context, { + onDidReceiveMessage: (message) => this.onMessageReceived(message), + retainContext: true, + }); if (data) { this.data = data; } diff --git a/packages/zowe-explorer-api/src/vscode/ui/WebView.ts b/packages/zowe-explorer-api/src/vscode/ui/WebView.ts index 6c349d7616..141ae0eee4 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/WebView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/WebView.ts @@ -18,11 +18,16 @@ import { join as joinPath } from "path"; import { randomUUID } from "crypto"; export type WebViewOpts = { - retainContext: boolean; + /** Callback function that is called when the extension has received a message from the webview. */ + onDidReceiveMessage?: (message: object) => void | Promise; + /** Retains context of the webview even after it is hidden. */ + retainContext?: boolean; }; export type UriPair = { + /** The paths for the webview on-disk. */ disk?: Types.WebviewUris; + /** The paths for the webview resources, before transformation by the `asWebviewUri` function. */ resource?: Types.WebviewUris; }; @@ -51,13 +56,7 @@ export class WebView { * @param context The VSCode extension context * @param onDidReceiveMessage Event callback: called when messages are received from the webview */ - public constructor( - title: string, - webviewName: string, - context: ExtensionContext, - onDidReceiveMessage?: (message: object) => void | Promise, - retainContext?: boolean - ) { + public constructor(title: string, webviewName: string, context: ExtensionContext, opts?: WebViewOpts) { this.context = context; this.disposables = []; @@ -78,7 +77,7 @@ export class WebView { this.panel = window.createWebviewPanel("ZEAPIWebview", this.title, ViewColumn.Beside, { enableScripts: true, localResourceRoots: [this.uris.disk.build], - retainContextWhenHidden: retainContext ?? false, + retainContextWhenHidden: opts?.retainContext ?? false, }); // Associate URI resources with webview @@ -94,8 +93,8 @@ export class WebView { title: this.title, }); this.webviewContent = builtHtml; - if (onDidReceiveMessage) { - this.panel.webview.onDidReceiveMessage(async (message) => onDidReceiveMessage(message)); + if (opts?.onDidReceiveMessage) { + this.panel.webview.onDidReceiveMessage(async (message) => opts.onDidReceiveMessage(message)); } this.panel.onDidDispose(() => this.dispose(), null, this.disposables); this.panel.webview.html = this.webviewContent; diff --git a/packages/zowe-explorer/src/trees/shared/SharedHistoryView.ts b/packages/zowe-explorer/src/trees/shared/SharedHistoryView.ts index c31df9ad00..c1ac2382a2 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedHistoryView.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedHistoryView.ts @@ -25,7 +25,10 @@ export class SharedHistoryView extends WebView { public constructor(context: ExtensionContext, treeProviders: Definitions.IZoweProviders) { const label = "Edit History"; - super(label, "edit-history", context, (message: object) => this.onDidReceiveMessage(message), true); + super(label, "edit-history", context, { + onDidReceiveMessage: (message: object) => this.onDidReceiveMessage(message), + retainContext: true, + }); this.treeProviders = treeProviders; this.currentSelection = { ds: "search", uss: "search", jobs: "search" }; } diff --git a/packages/zowe-explorer/src/trees/uss/USSActions.ts b/packages/zowe-explorer/src/trees/uss/USSActions.ts index df07f9ec48..ef365dac02 100644 --- a/packages/zowe-explorer/src/trees/uss/USSActions.ts +++ b/packages/zowe-explorer/src/trees/uss/USSActions.ts @@ -15,7 +15,7 @@ import * as path from "path"; import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; import { Gui, imperative, Validation, IZoweUSSTreeNode, Types } from "@zowe/zowe-explorer-api"; import { isBinaryFileSync } from "isbinaryfile"; -import { USSAtributeView } from "./USSAttributeView"; +import { USSAttributeView } from "./USSAttributeView"; import { USSFileStructure } from "./USSFileStructure"; import { ZoweUSSNode } from "./ZoweUSSNode"; import { Constants } from "../../configuration/Constants"; @@ -231,8 +231,8 @@ export class USSActions { } } - public static editAttributes(context: vscode.ExtensionContext, fileProvider: Types.IZoweUSSTreeType, node: IZoweUSSTreeNode): USSAtributeView { - return new USSAtributeView(context, fileProvider, node); + public static editAttributes(context: vscode.ExtensionContext, fileProvider: Types.IZoweUSSTreeType, node: IZoweUSSTreeNode): USSAttributeView { + return new USSAttributeView(context, fileProvider, node); } /** diff --git a/packages/zowe-explorer/src/trees/uss/USSAttributeView.ts b/packages/zowe-explorer/src/trees/uss/USSAttributeView.ts index 4c0e1c3394..8c84c610e1 100644 --- a/packages/zowe-explorer/src/trees/uss/USSAttributeView.ts +++ b/packages/zowe-explorer/src/trees/uss/USSAttributeView.ts @@ -14,7 +14,7 @@ import { Disposable, ExtensionContext } from "vscode"; import { ZoweExplorerApiRegister } from "../../extending/ZoweExplorerApiRegister"; import { SharedContext } from "../shared/SharedContext"; -export class USSAtributeView extends WebView { +export class USSAttributeView extends WebView { private treeProvider: Types.IZoweUSSTreeType; private readonly ussNode: IZoweUSSTreeNode; private readonly ussApi: MainframeInteraction.IUss; @@ -24,7 +24,9 @@ export class USSAtributeView extends WebView { public constructor(context: ExtensionContext, treeProvider: Types.IZoweUSSTreeType, node: IZoweUSSTreeNode) { const label = node.label ? `Edit Attributes: ${node.label as string}` : "Edit Attributes"; - super(label, "edit-attributes", context, (message: object) => this.onDidReceiveMessage(message)); + super(label, "edit-attributes", context, { + onDidReceiveMessage: (message: object) => this.onDidReceiveMessage(message), + }); this.treeProvider = treeProvider; this.ussNode = node; this.canUpdate = node.onUpdate != null; diff --git a/packages/zowe-explorer/src/utils/CertificateWizard.ts b/packages/zowe-explorer/src/utils/CertificateWizard.ts index f35cc31d20..ed550267d7 100644 --- a/packages/zowe-explorer/src/utils/CertificateWizard.ts +++ b/packages/zowe-explorer/src/utils/CertificateWizard.ts @@ -43,7 +43,9 @@ export class CertificateWizard extends WebView { }> = new DeferredPromise(); public constructor(context: vscode.ExtensionContext, opts: CertWizardOpts) { - super(vscode.l10n.t("Certificate Wizard"), "certificate-wizard", context, (message: object) => this.onDidReceiveMessage(message)); + super(vscode.l10n.t("Certificate Wizard"), "certificate-wizard", context, { + onDidReceiveMessage: (message: object) => this.onDidReceiveMessage(message), + }); this.opts = opts; this.panel.onDidDispose(() => { this.userSubmission.reject(userDismissed); From 47ca5ca2eea726315900128fba22ffa111d32a6c Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 22 Jul 2024 11:35:09 -0400 Subject: [PATCH 063/107] tests: Branch coverage for TableBuilder Signed-off-by: Trae Yelovich --- .../vscode/ui/utils/TableBuilder.unit.test.ts | 36 +++++++++++++++++-- .../src/vscode/ui/utils/TableBuilder.ts | 6 ++-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts index 2fe857fd18..6c4f1c24ed 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts @@ -166,9 +166,10 @@ describe("TableBuilder", () => { }); describe("addContextOption", () => { - it("adds the given context option and returns the same instance", () => { + it("adds the given context option with conditional and returns the same instance", () => { const globalMocks = createGlobalMocks(); const builder = new TableBuilder(globalMocks.context as any); + (builder as any).data.contextOpts = { all: [] }; const ctxOpt = { title: "Delete", command: "delete", @@ -179,19 +180,48 @@ describe("TableBuilder", () => { condition: (_data) => true, } as Table.ContextMenuOpts; - // case 0: adding context option to "all" rows + // case 0: adding context option w/ conditional to "all" rows, index previously existed const builderCtxOpts = builder.addContextOption("all", ctxOpt); expect(builderCtxOpts).toBeInstanceOf(TableBuilder); expect((builderCtxOpts as any).data.contextOpts).toStrictEqual({ all: [{ ...ctxOpt, condition: ctxOpt.condition?.toString() }], }); - // case 1: adding context option to a specific row, no previous options existed + + // case 1: adding context option w/ conditional to a specific row, index did not already exist const finalBuilder = builderCtxOpts.addContextOption(0, ctxOpt); expect((finalBuilder as any).data.contextOpts).toStrictEqual({ 0: [{ ...ctxOpt, condition: ctxOpt.condition?.toString() }], all: [{ ...ctxOpt, condition: ctxOpt.condition?.toString() }], }); }); + + it("adds the given context option without conditional and returns the same instance", () => { + const globalMocks = createGlobalMocks(); + const builder = new TableBuilder(globalMocks.context as any); + (builder as any).data.contextOpts = { all: [] }; + const ctxOptNoCond = { + title: "Add", + command: "add", + callback: { + typ: "row", + fn: (_row: Table.RowData) => {}, + }, + } as Table.ContextMenuOpts; + + // case 0: adding context option w/ condition to "all" rows, index previously existed + const builderCtxOpts = builder.addContextOption("all", ctxOptNoCond); + expect(builderCtxOpts).toBeInstanceOf(TableBuilder); + expect((builderCtxOpts as any).data.contextOpts).toStrictEqual({ + all: [{ ...ctxOptNoCond, condition: ctxOptNoCond.condition?.toString() }], + }); + + // case 1: adding context option w/ condition to a specific row, index did not already exist + const finalBuilder = builderCtxOpts.addContextOption(0, ctxOptNoCond); + expect((finalBuilder as any).data.contextOpts).toStrictEqual({ + 0: [{ ...ctxOptNoCond, condition: ctxOptNoCond.condition?.toString() }], + all: [{ ...ctxOptNoCond, condition: ctxOptNoCond.condition?.toString() }], + }); + }); }); describe("addRowAction", () => { diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index 94f1a26e9d..e6d139136b 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -125,8 +125,10 @@ export class TableBuilder { * @returns The same {@link TableBuilder} instance with the row actions added */ public contextOptions(opts: Record): TableBuilder { - for (const key of Object.keys(opts)) { - this.addContextOption(key as number | "all", opts[key]); + for (const [key, optsForKey] of Object.entries(opts)) { + for (const opt of optsForKey) { + this.addContextOption(key as number | "all", opt); + } } return this; } From 651df0ea824e394eee8e7417c5b326e73d8699d5 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 22 Jul 2024 11:35:42 -0400 Subject: [PATCH 064/107] tests: Branch coverage for Table.View Signed-off-by: Trae Yelovich --- .../__unit__/vscode/ui/TableView.unit.test.ts | 94 +++++++++++++++++-- 1 file changed, 86 insertions(+), 8 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts index 1e4201a179..8243445fe8 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts @@ -38,6 +38,14 @@ function createGlobalMocks() { // Table.View unit tests describe("Table.View", () => { + describe("constructor", () => { + it("handles a missing title in the data object", () => { + const globalMocks = createGlobalMocks(); + const view = new Table.View(globalMocks.context as any, {} as any); + expect((view as any).title).toBe("Table view"); + }); + }); + describe("getUris", () => { it("returns the URIs from the WebView base class", () => { const globalMocks = createGlobalMocks(); @@ -303,7 +311,7 @@ describe("Table.View", () => { }, } as Table.Action, ], - 0: [ + 1: [ { title: "Zero action", command: "zero-action", @@ -323,17 +331,17 @@ describe("Table.View", () => { const mockWebviewMsg = { command: "some-action", data: { cell: data.rows[0].a, row: data.rows[0] }, - rowIndex: 0, + rowIndex: 1, }; await view.onMessageReceived(mockWebviewMsg); expect(writeTextMock).not.toHaveBeenCalled(); expect(globalMocks.updateWebviewMock).not.toHaveBeenCalled(); expect(allCallbackMock).toHaveBeenCalled(); - // case 2: An action that exists for all rows + // case 2: An action that exists for one row const mockNextWebviewMsg = { command: "zero-action", data: { cell: data.rows[0].a, row: data.rows[0] }, - rowIndex: 0, + rowIndex: 1, }; await view.onMessageReceived(mockNextWebviewMsg); expect(writeTextMock).not.toHaveBeenCalled(); @@ -401,13 +409,13 @@ describe("Table.View", () => { }); describe("addContextOption", () => { - it("adds the context option to the internal data structure and calls updateWebview", async () => { + it("adds the context option with conditional to the internal data structure and calls updateWebview", async () => { const globalMocks = createGlobalMocks(); const data = { title: "Table w/ no initial rows", contextOpts: { all: [] }, rows: [] }; const view = new Table.View(globalMocks.context as any, data as any); globalMocks.updateWebviewMock.mockImplementation(); - // case 1: Adding a context menu option to all rows + // case 1: Adding a context menu option with conditional to all rows const contextOpt = { title: "Add to cart", command: "add-to-cart", @@ -421,7 +429,7 @@ describe("Table.View", () => { expect(globalMocks.updateWebviewMock).toHaveBeenCalled(); expect((view as any).data.contextOpts["all"]).toStrictEqual([{ ...contextOpt, condition: contextOpt.condition?.toString() }]); - // case 2: Adding a context menu option to one row + // case 2: Adding a context menu option with conditional to one row const singleRowContextOpt = { title: "Save for later", command: "save-for-later", @@ -438,10 +446,46 @@ describe("Table.View", () => { ]); globalMocks.updateWebviewMock.mockRestore(); }); + + it("adds the context option without conditional to the internal data structure and calls updateWebview", async () => { + const globalMocks = createGlobalMocks(); + const data = { title: "Table w/ no initial rows", contextOpts: { all: [] }, rows: [] }; + const view = new Table.View(globalMocks.context as any, data as any); + globalMocks.updateWebviewMock.mockImplementation(); + + // case 1: Adding a context menu option without conditional to all rows + const contextOpt = { + title: "Remove from cart", + command: "rm-from-cart", + callback: { + typ: "row", + fn: (_data) => {}, + }, + } as Table.ContextMenuOpts; + await view.addContextOption("all", contextOpt); + expect(globalMocks.updateWebviewMock).toHaveBeenCalled(); + expect((view as any).data.contextOpts["all"]).toStrictEqual([{ ...contextOpt, condition: contextOpt.condition?.toString() }]); + + // case 2: Adding a context menu option without conditional to one row + const singleRowContextOpt = { + title: "Add to wishlist", + command: "add-to-wishlist", + callback: { + typ: "row", + fn: (_data) => {}, + }, + } as Table.ContextMenuOpts; + await view.addContextOption(1, singleRowContextOpt); + expect(globalMocks.updateWebviewMock).toHaveBeenCalled(); + expect((view as any).data.contextOpts[1]).toStrictEqual([ + { ...singleRowContextOpt, condition: singleRowContextOpt.condition?.toString() }, + ]); + globalMocks.updateWebviewMock.mockRestore(); + }); }); describe("addAction", () => { - it("adds the action to the internal data structure and calls updateWebview", async () => { + it("adds the action with conditional to the internal data structure and calls updateWebview", async () => { const globalMocks = createGlobalMocks(); const data = { title: "Table w/ no initial rows", actions: { all: [] }, rows: [] }; const view = new Table.View(globalMocks.context as any, data as any); @@ -476,6 +520,40 @@ describe("Table.View", () => { expect((view as any).data.actions[2]).toStrictEqual([{ ...singleRowAction, condition: singleRowAction.condition?.toString() }]); globalMocks.updateWebviewMock.mockRestore(); }); + + it("adds the action without conditional to the internal data structure and calls updateWebview", async () => { + const globalMocks = createGlobalMocks(); + const data = { title: "Table w/ no initial rows", actions: { all: [] }, rows: [] }; + const view = new Table.View(globalMocks.context as any, data as any); + globalMocks.updateWebviewMock.mockImplementation(); + + // case 1: Adding an action without conditional to all rows + const action = { + title: "Remove from wishlist", + command: "rm-from-wishlist", + callback: { + typ: "row", + fn: (_data) => {}, + }, + } as Table.ContextMenuOpts; + await view.addAction("all", action); + expect(globalMocks.updateWebviewMock).toHaveBeenCalled(); + expect((view as any).data.actions["all"]).toStrictEqual([{ ...action, condition: action.condition?.toString() }]); + + // case 2: Adding an action without conditional to one row + const singleRowAction = { + title: "Learn less", + command: "learn-less", + callback: { + typ: "row", + fn: (_data) => {}, + }, + } as Table.ContextMenuOpts; + await view.addAction(2, singleRowAction); + expect(globalMocks.updateWebviewMock).toHaveBeenCalled(); + expect((view as any).data.actions[2]).toStrictEqual([{ ...singleRowAction, condition: singleRowAction.condition?.toString() }]); + globalMocks.updateWebviewMock.mockRestore(); + }); }); }); From f89e798eaeb87de07e4db7dece2ce0cf7cbf81f2 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 22 Jul 2024 11:43:19 -0400 Subject: [PATCH 065/107] fix(zedc): Allow unused var in prepare::extract_code_zip Signed-off-by: Trae Yelovich --- zedc/src/code/prepare.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/zedc/src/code/prepare.rs b/zedc/src/code/prepare.rs index e1ceea8b25..5b917e1cfb 100644 --- a/zedc/src/code/prepare.rs +++ b/zedc/src/code/prepare.rs @@ -81,6 +81,7 @@ fn code_cli_binary(dir: &Path) -> PathBuf { /// * `zip_path` - The path of the ZIP file to extract /// * `vsc_path` - The path where the archive should be extracted into /// +#[allow(unused_variables)] async fn extract_code_zip(file: &std::fs::File, zip_path: &Path, vsc_path: &Path) -> anyhow::Result<()> { cfg_if::cfg_if! { if #[cfg(target_os = "macos")] { From a5bcb17658a89dd2d84287f93ee0d6c2dfbbc1d6 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 22 Jul 2024 12:00:46 -0400 Subject: [PATCH 066/107] feat(table): Support editable tables and handle edit events Signed-off-by: Trae Yelovich --- .../__unit__/vscode/ui/TableView.unit.test.ts | 19 +++++++++++++++++++ .../src/vscode/ui/TableView.ts | 14 ++++++++++++++ .../src/webviews/src/table-view/types.ts | 13 +++++++++++++ 3 files changed, 46 insertions(+) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts index 8243445fe8..ae2da94fe1 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts @@ -226,6 +226,25 @@ describe("Table.View", () => { expect(onTableDisplayChangedFireMock).toHaveBeenCalledWith(tableData); }); + it("fires the onTableDataEdited event when handling the 'ontableedited' command", async () => { + const globalMocks = createGlobalMocks(); + const view = new Table.View(globalMocks.context as any, { title: "Table w/ editable columns" } as any); + await view.setColumns([{ field: "a", editable: true }, { field: "b" }, { field: "c" }]); + const onTableDataEditedFireMock = jest.spyOn((view as any).onTableDataEditedEmitter, "fire"); + const tableData = { rows: [{ a: 1, b: 1, c: 1 }] }; + const editData = { + value: 2, + oldValue: tableData.rows[0].a, + field: "a", + rowIndex: 1, + }; + await view.onMessageReceived({ + command: "ontableedited", + data: editData, + }); + expect(onTableDataEditedFireMock).toHaveBeenCalledWith(editData); + }); + it("calls updateWebview when handling the 'ready' command", async () => { const globalMocks = createGlobalMocks(); const view = new Table.View(globalMocks.context as any, { title: "Table w/ changing display" } as any); diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 0b82569c5d..530db8cc61 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -242,6 +242,13 @@ export namespace Table { title?: string; } & GridProperties; + export type EditEvent = { + rowIndex: number; + field: string; + value: ContentTypes; + oldValue?: ContentTypes; + }; + /** * A class that acts as a controller between the extension and the table view. Based off of the {@link WebView} class. * @@ -266,8 +273,10 @@ export namespace Table { }; private onTableDataReceivedEmitter: EventEmitter> = new EventEmitter(); private onTableDisplayChangedEmitter: EventEmitter = new EventEmitter(); + private onTableDataEditedEmitter: EventEmitter = new EventEmitter(); public onTableDisplayChanged: Event = this.onTableDisplayChangedEmitter.event; public onTableDataReceived: Event> = this.onTableDataReceivedEmitter.event; + public onTableDataEdited: Event = this.onTableDataEditedEmitter.event; private uuid: string; @@ -300,6 +309,11 @@ export namespace Table { return; } switch (message.command) { + // "ontableedited" command: The table's contents were updated by the user from within the webview. + // Fires for editable columns only. + case "ontableedited": + this.onTableDataEditedEmitter.fire(message.data); + return; // "ondisplaychanged" command: The table's layout was updated by the user from within the webview. case "ondisplaychanged": this.onTableDisplayChangedEmitter.fire(message.data); diff --git a/packages/zowe-explorer/src/webviews/src/table-view/types.ts b/packages/zowe-explorer/src/webviews/src/table-view/types.ts index 6c31be7209..013f7ce618 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/types.ts +++ b/packages/zowe-explorer/src/webviews/src/table-view/types.ts @@ -45,6 +45,19 @@ export const tableProps = (contextMenu: ContextMenuState, tableData: Table.ViewO paginationPageSize: tableData.paginationPageSize, paginationPageSizeSelector: tableData.paginationPageSizeSelector, onCellContextMenu: contextMenu.callback, + onCellValueChanged: tableData.columns?.some((col) => col.editable) + ? (event) => { + vscodeApi.postMessage({ + command: "ontableedit", + data: { + rowIndex: event.rowIndex, + field: event.colDef.field, + value: event.value, + oldValue: event.oldValue, + }, + }); + } + : undefined, onFilterChanged: (event) => { const rows: Table.RowData[] = []; event.api.forEachNodeAfterFilterAndSort((row, _i) => rows.push(row.data)); From d91dfef4f04562d1cdb71862c7e709a0e58338f3 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 22 Jul 2024 12:26:30 -0400 Subject: [PATCH 067/107] fix(tests): Update references to USSAttributeView Signed-off-by: Trae Yelovich --- .../__tests__/__unit__/trees/uss/USSActions.unit.test.ts | 4 ++-- .../__unit__/trees/uss/USSAttributeView.unit.test.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSActions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSActions.unit.test.ts index d4f1c8b97e..5d4fc60f78 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSActions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSActions.unit.test.ts @@ -39,7 +39,7 @@ import { USSFileStructure } from "../../../../src/trees/uss/USSFileStructure"; import { AuthUtils } from "../../../../src/utils/AuthUtils"; import { IZoweTree } from "../../../../../zowe-explorer-api/src/tree/IZoweTree"; import { IZoweUSSTreeNode } from "../../../../../zowe-explorer-api/src/tree"; -import { USSAtributeView } from "../../../../src/trees/uss/USSAttributeView"; +import { USSAttributeView } from "../../../../src/trees/uss/USSAttributeView"; import { mocked } from "../../../__mocks__/mockUtils"; import { USSTree } from "../../../../src/trees/uss/USSTree"; @@ -842,7 +842,7 @@ describe("USS Action Unit Tests - function editAttributes", () => { {} as IZoweTree, { label: "some/node", getProfile: jest.fn() } as unknown as IZoweUSSTreeNode ); - expect(view).toBeInstanceOf(USSAtributeView); + expect(view).toBeInstanceOf(USSAttributeView); }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSAttributeView.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSAttributeView.unit.test.ts index 43998c0851..5ea37ccb43 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSAttributeView.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSAttributeView.unit.test.ts @@ -11,7 +11,7 @@ import * as vscode from "vscode"; import { MockedProperty } from "../../../__mocks__/mockUtils"; -import { USSAtributeView } from "../../../../src/trees/uss/USSAttributeView"; +import { USSAttributeView } from "../../../../src/trees/uss/USSAttributeView"; import { UssFSProvider } from "../../../../src/trees/uss/UssFSProvider"; import { IZoweTree } from "../../../../../zowe-explorer-api/src/tree/IZoweTree"; import { IZoweUSSTreeNode } from "../../../../../zowe-explorer-api/src/tree"; @@ -21,7 +21,7 @@ import { MainframeInteraction } from "../../../../../zowe-explorer-api/src/exten import { SharedContext } from "../../../../src/trees/shared/SharedContext"; describe("AttributeView unit tests", () => { - let view: USSAtributeView; + let view: USSAttributeView; const context = { extensionPath: "some/fake/ext/path" } as unknown as vscode.ExtensionContext; const treeProvider = { refreshElement: jest.fn(), refresh: jest.fn() } as unknown as IZoweTree; const createDirMock = jest.spyOn(UssFSProvider.instance, "createDirectory").mockImplementation(); @@ -41,7 +41,7 @@ describe("AttributeView unit tests", () => { getTag: () => Promise.resolve("UTF-8"), } as unknown as MainframeInteraction.IUss); jest.spyOn(SharedContext, "isUssDirectory").mockReturnValue(false); - view = new USSAtributeView(context, treeProvider, node); + view = new USSAttributeView(context, treeProvider, node); }); afterAll(() => { From 77e155465f6939082ffbf62515e9daf697582205 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 22 Jul 2024 15:17:57 -0400 Subject: [PATCH 068/107] refactor: Improved AG Grid opt. handling; wait to render table Signed-off-by: Trae Yelovich --- .../__unit__/vscode/ui/TableView.unit.test.ts | 4 ++-- .../vscode/ui/utils/TableBuilder.unit.test.ts | 4 ++-- .../src/vscode/ui/TableView.ts | 8 ++++--- .../src/vscode/ui/utils/TableBuilder.ts | 2 +- .../src/webviews/src/table-view/TableView.tsx | 21 +++++-------------- .../src/webviews/src/table-view/types.ts | 4 +--- 6 files changed, 16 insertions(+), 27 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts index ae2da94fe1..45eb2c6cbc 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts @@ -165,8 +165,8 @@ describe("Table.View", () => { pagination: false, }) ).resolves.toBe(true); - expect((view as any).data.debug).toBe(true); - expect((view as any).data.pagination).toBe(false); + expect((view as any).data.options.debug).toBe(true); + expect((view as any).data.options.pagination).toBe(false); }); }); diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts index 6c4f1c24ed..d2c7c3054d 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts @@ -46,11 +46,11 @@ describe("TableBuilder", () => { it("adds the given options to the table data, returning the same instance", () => { const globalMocks = createGlobalMocks(); let builder = new TableBuilder(globalMocks.context as any); - expect((builder as any).data).not.toHaveProperty("pagination"); + expect((builder as any).data?.options).toBe(undefined); builder = builder.options({ pagination: false, }); - expect((builder as any).data).toHaveProperty("pagination"); + expect((builder as any).data.options).toHaveProperty("pagination"); }); }); diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 530db8cc61..d95a1a9695 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -171,7 +171,7 @@ export namespace Table { * Auto-size the columns when the grid is loaded. Can size to fit the grid width, fit a provided width or fit the cell contents. * Read once during initialization. */ - autoSizeStrategy?: SizeColumnsToFitGridStrategy | SizeColumnsToFitProvidedWidthStrategy | SizeColumnsToFitGridStrategy; + autoSizeStrategy?: SizeColumnsToFitGridStrategy | SizeColumnsToFitProvidedWidthStrategy | SizeColumnsToContentStrategy; /** Set to 'shift' to have shift-resize as the default resize operation */ colResizeDefault?: "shift"; /** Changes the display type of the column menu. 'new' displays the main list of menu items; 'legacy' displays a tabbed menu */ @@ -240,7 +240,9 @@ export namespace Table { rows: RowData[] | null | undefined; /** The display title for the table */ title?: string; - } & GridProperties; + /** AG Grid-specific properties */ + options?: GridProperties; + }; export type EditEvent = { rowIndex: number; @@ -475,7 +477,7 @@ export namespace Table { * @returns Whether the webview successfully received the new options */ public setOptions(opts: GridProperties): Promise { - this.data = { ...this.data, ...opts }; + this.data = { ...this.data, options: this.data.options ? { ...this.data.options, ...opts } : opts }; return this.updateWebview(); } diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index e6d139136b..16dc163efe 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -55,7 +55,7 @@ export class TableBuilder { * @returns The same {@link TableBuilder} instance with the options added */ public options(opts: Table.GridProperties): TableBuilder { - this.data = { ...this.data, ...opts }; + this.data = { ...this.data, options: this.data.options ? { ...this.data.options, ...opts } : opts }; return this; } diff --git a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx index 55ddf27005..5ef7e19147 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx @@ -15,19 +15,7 @@ import "./style.css"; const vscodeApi = acquireVsCodeApi(); export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewProps) => { - const [tableData, setTableData] = useState( - data ?? { - actions: { - all: [], - }, - contextOpts: { - all: [], - }, - columns: null, - rows: null, - title: "", - } - ); + const [tableData, setTableData] = useState(data); const [theme, setTheme] = useState(baseTheme ?? "ag-theme-quartz"); const contextMenu = useContextMenu({ @@ -48,7 +36,7 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro fn: () => {}, }, }, - ...(tableData.contextOpts.all ?? []), + ...(tableData?.contextOpts?.all ?? []), ], selectRow: true, selectedRows: [], @@ -94,6 +82,7 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro cellStyle: { border: "none", outline: "none" }, field: "actions", sortable: false, + suppressSizeToFit: true, // Support a custom cell renderer for row actions cellRenderer: actionsCellRenderer ?? @@ -157,10 +146,10 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro return ( <> - {tableData.title ?

{tableData.title}

: null} + {tableData?.title ?

{tableData.title}

: null}
{contextMenu.component} - + {tableData ? : null}
); diff --git a/packages/zowe-explorer/src/webviews/src/table-view/types.ts b/packages/zowe-explorer/src/webviews/src/table-view/types.ts index 013f7ce618..d07a670a3a 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/types.ts +++ b/packages/zowe-explorer/src/webviews/src/table-view/types.ts @@ -41,9 +41,6 @@ export const tableProps = (contextMenu: ContextMenuState, tableData: Table.ViewO rowSpan: col.rowSpan ? new Function(wrapFn(col.rowSpan)).call(null) : undefined, valueFormatter: col.valueFormatter ? new Function(wrapFn(col.valueFormatter)).call(null) : undefined, })), - pagination: tableData.pagination, - paginationPageSize: tableData.paginationPageSize, - paginationPageSizeSelector: tableData.paginationPageSizeSelector, onCellContextMenu: contextMenu.callback, onCellValueChanged: tableData.columns?.some((col) => col.editable) ? (event) => { @@ -68,4 +65,5 @@ export const tableProps = (contextMenu: ContextMenuState, tableData: Table.ViewO event.api.forEachNodeAfterFilterAndSort((row, _i) => rows.push(row.data)); vscodeApi.postMessage({ command: "ondisplaychanged", data: rows }); }, + ...(tableData.options ?? {}), }); From 3f4ff8b15852708308a8c9373fee4e31dfb6aa03 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 22 Jul 2024 15:28:03 -0400 Subject: [PATCH 069/107] lint: Add webview ignore pattern to eslintrc.yaml Signed-off-by: Trae Yelovich --- .eslintrc.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 94c2494c45..45d0f56ca1 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -6,7 +6,7 @@ extends: env: node: true es6: true -ignorePatterns: ["**/scripts", "**/__mocks__", "**/lib", "wdio.conf.ts", "**/features/", "samples/__integration__/"] +ignorePatterns: ["**/scripts", "**/__mocks__", "**/lib", "wdio.conf.ts", "**/features/", "samples/__integration__/", "**/src/webviews"] overrides: - files: "**/__tests__/**" rules: From 121deddcbed8110f80dc6e71c8245740b11ada6c Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 22 Jul 2024 15:46:54 -0400 Subject: [PATCH 070/107] refactor(lint): Remove .eslintignore(s) in favor of .eslintrc.yaml Signed-off-by: Trae Yelovich --- .eslintrc.yaml | 14 +++++++++++++- packages/zowe-explorer-api/.eslintignore | 4 ---- packages/zowe-explorer-ftp-extension/.eslintignore | 5 ----- packages/zowe-explorer/.eslintignore | 7 ------- 4 files changed, 13 insertions(+), 17 deletions(-) delete mode 100644 packages/zowe-explorer-api/.eslintignore delete mode 100644 packages/zowe-explorer-ftp-extension/.eslintignore delete mode 100644 packages/zowe-explorer/.eslintignore diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 45d0f56ca1..fcec84a821 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -6,7 +6,19 @@ extends: env: node: true es6: true -ignorePatterns: ["**/scripts", "**/__mocks__", "**/lib", "wdio.conf.ts", "**/features/", "samples/__integration__/", "**/src/webviews"] +ignorePatterns: + [ + "**/scripts", + "**/__mocks__", + "**/lib", + "webpack.config.js", + "**/*wdio.conf.ts", + "**/features/", + "samples/__integration__/", + "**/out", + "**/results", + "**/src/webviews", + ] overrides: - files: "**/__tests__/**" rules: diff --git a/packages/zowe-explorer-api/.eslintignore b/packages/zowe-explorer-api/.eslintignore deleted file mode 100644 index 0997e433bc..0000000000 --- a/packages/zowe-explorer-api/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -__mocks__/** -node_modules/** -lib/** -*.js diff --git a/packages/zowe-explorer-ftp-extension/.eslintignore b/packages/zowe-explorer-ftp-extension/.eslintignore deleted file mode 100644 index 246b4930e4..0000000000 --- a/packages/zowe-explorer-ftp-extension/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules/** -out/** -__mocks__/** -__tests__/** -*.js \ No newline at end of file diff --git a/packages/zowe-explorer/.eslintignore b/packages/zowe-explorer/.eslintignore deleted file mode 100644 index ce46446935..0000000000 --- a/packages/zowe-explorer/.eslintignore +++ /dev/null @@ -1,7 +0,0 @@ -node_modules/**/* -results/**/* -out/**/* -*.config.js -**/.wdio-vscode-service/ -**/*.wdio.conf.ts -src/webviews \ No newline at end of file From fbd65fa6ac32a06af1240a4da08858fd2acdf15a Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 22 Jul 2024 15:53:13 -0400 Subject: [PATCH 071/107] fix(sample): Update vue sample for webview changes Signed-off-by: Trae Yelovich --- samples/vue-webview-sample/src/extension.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/samples/vue-webview-sample/src/extension.ts b/samples/vue-webview-sample/src/extension.ts index ffc2940289..a2c546db56 100644 --- a/samples/vue-webview-sample/src/extension.ts +++ b/samples/vue-webview-sample/src/extension.ts @@ -5,8 +5,10 @@ export function activate(context: vscode.ExtensionContext) { console.log('Congratulations, your extension "helloworld-sample" is now active!'); const disposable = vscode.commands.registerCommand("extension.helloWorld", () => { - const webview = new WebView("Sample Webview", "vue-sample", context, (message: Record) => { - vscode.window.showInformationMessage(message.text); + const webview = new WebView("Sample Webview", "vue-sample", context, { + onDidReceiveMessage: (message: Record) => { + vscode.window.showInformationMessage(message.text); + }, }); }); From 94d17cacfe5d16af49d4835be3ff3ef0a547788f Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 22 Jul 2024 16:18:03 -0400 Subject: [PATCH 072/107] refactor(table): Make types stricter; handle empty column defs Signed-off-by: Trae Yelovich --- .../__unit__/vscode/ui/TableView.unit.test.ts | 15 ++++++++ .../vscode/ui/utils/TableBuilder.unit.test.ts | 37 +++++++++++++++++++ .../src/vscode/ui/TableView.ts | 4 +- .../src/vscode/ui/utils/TableBuilder.ts | 7 +++- 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts index 45eb2c6cbc..0c1c2deac8 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts @@ -159,6 +159,8 @@ describe("Table.View", () => { const globalMocks = createGlobalMocks(); const view = new Table.View(globalMocks.context as any, { title: "Stable Table of Cables" } as any); globalMocks.updateWebviewMock.mockResolvedValueOnce(true); + + // case 1: No options were previously defined await expect( view.setOptions({ debug: true, @@ -167,6 +169,19 @@ describe("Table.View", () => { ).resolves.toBe(true); expect((view as any).data.options.debug).toBe(true); expect((view as any).data.options.pagination).toBe(false); + + globalMocks.updateWebviewMock.mockResolvedValueOnce(true); + // case 2: Options were previously specified + await expect( + view.setOptions({ + paginationPageSize: 0, + }) + ).resolves.toBe(true); + expect((view as any).data.options).toStrictEqual({ + debug: true, + pagination: false, + paginationPageSize: 0, + }); }); }); diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts index d2c7c3054d..3045d3b88e 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts @@ -52,6 +52,23 @@ describe("TableBuilder", () => { }); expect((builder as any).data.options).toHaveProperty("pagination"); }); + + it("keeps the existing options on the table data if called multiple times", () => { + const globalMocks = createGlobalMocks(); + let builder = new TableBuilder(globalMocks.context as any); + expect((builder as any).data?.options).toBe(undefined); + builder = builder.options({ + suppressAutoSize: true, + }); + expect((builder as any).data.options).toHaveProperty("suppressAutoSize"); + builder = builder.options({ + paginationPageSize: 50, + }); + expect((builder as any).data.options).toStrictEqual({ + suppressAutoSize: true, + paginationPageSize: 50, + }); + }); }); describe("title", () => { @@ -285,6 +302,26 @@ describe("TableBuilder", () => { }); }); + describe("build", () => { + it("builds the table view and constructs column definitions if needed", () => { + const globalMocks = createGlobalMocks(); + const newRows = [ + { a: 1, b: 2, c: 3, d: false, e: 5 }, + { a: 3, b: 2, c: 1, d: true, e: 6 }, + ]; + const builder = new TableBuilder(globalMocks.context as any).addRows(newRows); + const instance = builder.build(); + expect((instance as any).data.columns).toStrictEqual([{ field: "a" }, { field: "b" }, { field: "c" }, { field: "d" }, { field: "e" }]); + }); + + it("builds the table view", () => { + const globalMocks = createGlobalMocks(); + const builder = new TableBuilder(globalMocks.context as any); + const instance = builder.build(); + expect((instance as any).data.columns).toHaveLength(0); + }); + }); + describe("buildAndShare", () => { it("builds the table view and adds it to the table mediator", () => { const globalMocks = createGlobalMocks(); diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index d95a1a9695..ec8bff4eb5 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -233,11 +233,11 @@ export namespace Table { /** Actions to apply to the given row or column index */ actions: Record; /** Column definitions for the top of the table */ - columns: Column[] | null | undefined; + columns: Column[]; /** Context menu options for rows in the table */ contextOpts: Record; /** The row data for the table. Each row contains a set of variables corresponding to the data for each column in that row. */ - rows: RowData[] | null | undefined; + rows: RowData[]; /** The display title for the table */ title?: string; /** AG Grid-specific properties */ diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index 16dc163efe..aac37d19cf 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -181,6 +181,11 @@ export class TableBuilder { * @returns A new {@link Table.Instance} with the given data/options */ public build(): Table.Instance { + // Construct column definitions if rows were provided, but no columns are specified at time of build + if (this.data.columns.length === 0 && this.data.rows.length > 0) { + this.data.columns = Object.keys(this.data.rows[0]).map((k) => ({ field: k })); + } + return new Table.Instance(this.context, this.data); } @@ -189,7 +194,7 @@ export class TableBuilder { * @returns A new, **shared** {@link Table.Instance} with the given data/options */ public buildAndShare(): Table.Instance { - const table = new Table.Instance(this.context, this.data); + const table = this.build(); TableMediator.getInstance().addTable(table); return table; } From 1a5c42a493de82e6ded7239d4f9853ea665e5c3c Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 23 Jul 2024 14:33:54 -0400 Subject: [PATCH 073/107] refactor(table): Remove table from mediator during disposal Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/src/vscode/ui/TableView.ts | 3 +++ .../zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index ec8bff4eb5..b4294a7071 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -13,6 +13,7 @@ import { UriPair, WebView } from "./WebView"; import { Event, EventEmitter, ExtensionContext, env } from "vscode"; import { randomUUID } from "crypto"; import { diff } from "deep-object-diff"; +import { TableMediator } from "./utils/TableMediator"; export namespace Table { /* The types of supported content for the table and how they are represented in callback functions. */ @@ -500,8 +501,10 @@ export namespace Table { /** * Closes the table view and marks it as disposed. + * Removes the table instance from the mediator if it exists. */ public dispose(): void { + TableMediator.getInstance().removeTable(this); super.dispose(); } } diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts index 6a6fc088e9..2bd2514b1c 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts @@ -9,7 +9,7 @@ * */ -import { Table } from "../TableView"; +import type { Table } from "../TableView"; /** * Mediator class for managing and accessing shared tables in Zowe Explorer. @@ -42,7 +42,7 @@ import { Table } from "../TableView"; * Tables can only be removed by the extender that has contributed them using {@link TableMediator.deleteTable}. * This establishes a read-only relationship between the mediator and extenders that have not contributed the table they are trying to access. * - * **Note** that this does not prevent an extender with access to the ID from disposing the table once they've received access to the instance. + * **Note** that this does not prevent an extender with access to the ID from disposing the table, once they've received access to the instance. */ export class TableMediator { From 7a6e3fd02d2fa66c8a993eb3c55664e7bacef507 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 24 Jul 2024 15:41:26 -0400 Subject: [PATCH 074/107] fix: update job & spool for existing nodes; optimize 'Cancel job' Signed-off-by: Trae Yelovich --- packages/zowe-explorer/src/trees/job/JobActions.ts | 12 ++++++------ packages/zowe-explorer/src/trees/job/ZoweJobNode.ts | 2 ++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/zowe-explorer/src/trees/job/JobActions.ts b/packages/zowe-explorer/src/trees/job/JobActions.ts index 873cb3c6ce..729c4de809 100644 --- a/packages/zowe-explorer/src/trees/job/JobActions.ts +++ b/packages/zowe-explorer/src/trees/job/JobActions.ts @@ -421,7 +421,7 @@ export class JobActions { const failedJobs: { job: zosjobs.IJob; error: string }[] = []; // Build list of common sessions from node selection - const sessionNodes = []; + const sessionNodes = new Set(); for (const jobNode of nodes) { if (!jobNode.job) { continue; @@ -444,11 +444,8 @@ export class JobActions { const cancelled = await jesApis[sesLabel].cancelJob(jobNode.job); if (!cancelled) { failedJobs.push({ job: jobNode.job, error: vscode.l10n.t("The job was not cancelled.") }); - } else if (!sessionNodes.includes(sesNode)) { - setImmediate(() => { - jobsProvider.refreshElement(sesNode); - }); - sessionNodes.push(sesNode); + } else { + sessionNodes.add(sesNode); } } catch (err) { if (err instanceof Error) { @@ -472,6 +469,9 @@ export class JobActions { } else { await Gui.showMessage(vscode.l10n.t("Cancelled selected jobs successfully.")); } + for (const session of sessionNodes) { + jobsProvider.refreshElement(session); + } } public static async sortJobs(session: IZoweJobTreeNode, jobsProvider: JobTree): Promise { const selection = await Gui.showQuickPick( diff --git a/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts b/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts index 023ce56137..2b2fbd487d 100644 --- a/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts +++ b/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts @@ -175,6 +175,7 @@ export class ZoweJobNode extends ZoweTreeNode implements IZoweJobTreeNode { ); if (existing) { existing.tooltip = existing.label = newLabel; + (existing as ZoweSpoolNode).spool = spool; elementChildren[newLabel] = existing; } else { const spoolNode = new ZoweSpoolNode({ @@ -237,6 +238,7 @@ export class ZoweJobNode extends ZoweTreeNode implements IZoweJobTreeNode { if (existing) { // If matched, update the label to reflect latest retcode/status existing.tooltip = existing.label = nodeTitle; + existing.job = job; elementChildren[nodeTitle] = existing; } else { const jobNode = new ZoweJobNode({ From 6c1422784400a751bd997e2f99afa36f7e9a5fb3 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 25 Jul 2024 14:59:57 -0400 Subject: [PATCH 075/107] refactor: Fully support WebviewViews, adjust table CSS Signed-off-by: Trae Yelovich --- .../src/vscode/ui/TableViewProvider.ts | 34 +++++++--- .../src/vscode/ui/WebView.ts | 62 +++++++++++++++---- .../zowe-explorer-api/src/vscode/ui/index.ts | 1 + .../src/vscode/ui/utils/TableBuilder.ts | 9 ++- .../webviews/src/table-view/ContextMenu.tsx | 3 + .../src/webviews/src/table-view/style.css | 3 +- .../src/webviews/src/table-view/types.ts | 2 +- 7 files changed, 88 insertions(+), 26 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts b/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts index a448365224..48ddcf5620 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts @@ -16,10 +16,28 @@ export class TableViewProvider implements WebviewViewProvider { private view: WebviewView; private tableView: Table.View; - public setTableView(tableView: Table.View): void { + private static instance: TableViewProvider; + + private constructor() {} + + public static getInstance(): TableViewProvider { + if (!this.instance) { + this.instance = new TableViewProvider(); + } + + return this.instance; + } + + public setTableView(tableView: Table.View | null): void { this.tableView = tableView; - if (this.view && this.view.webview.html !== this.tableView.getHtml()) { - this.view.webview.html = this.tableView.getHtml(); + + if (tableView == null) { + this.view.webview.html = ""; + return; + } + + if (this.view) { + this.tableView.resolveForView(this.view); } } @@ -34,12 +52,8 @@ export class TableViewProvider implements WebviewViewProvider { ): void | Thenable { this.view = webviewView; - this.view.webview.options = { - enableScripts: true, - localResourceRoots: [this.tableView.getUris().disk.build], - }; - - this.view.webview.html = this.tableView.getHtml(); - this.view.webview.onDidReceiveMessage((data) => this.tableView.onMessageReceived(data)); + if (this.tableView != null) { + this.tableView.resolveForView(this.view); + } } } diff --git a/packages/zowe-explorer-api/src/vscode/ui/WebView.ts b/packages/zowe-explorer-api/src/vscode/ui/WebView.ts index 141ae0eee4..8f4ab10ab0 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/WebView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/WebView.ts @@ -13,7 +13,7 @@ import * as Mustache from "mustache"; import * as fs from "fs"; import HTMLTemplate from "./utils/HTMLTemplate"; import { Types } from "../../Types"; -import { Disposable, ExtensionContext, Uri, ViewColumn, WebviewPanel, window } from "vscode"; +import { Disposable, ExtensionContext, Uri, ViewColumn, WebviewPanel, WebviewView, window } from "vscode"; import { join as joinPath } from "path"; import { randomUUID } from "crypto"; @@ -22,6 +22,8 @@ export type WebViewOpts = { onDidReceiveMessage?: (message: object) => void | Promise; /** Retains context of the webview even after it is hidden. */ retainContext?: boolean; + /** Whether the webview should be prepared for a WebviewViewProvider. */ + isView?: boolean; }; export type UriPair = { @@ -37,6 +39,7 @@ export class WebView { // The webview HTML content to render after filling the HTML template. protected webviewContent: string; public panel: WebviewPanel; + public view: WebviewView; // Resource identifiers for the on-disk content and vscode-webview resource. protected uris: UriPair = {}; @@ -47,6 +50,8 @@ export class WebView { protected context: ExtensionContext; + private webviewOpts: WebViewOpts; + /** * Constructs a webview for use with bundled assets. * The webview entrypoint must be located at src//dist//index.js. @@ -64,6 +69,8 @@ export class WebView { this.nonce = randomUUID(); this.title = title; + this.webviewOpts = opts; + const cssPath = joinPath(context.extensionPath, "src", "webviews", "dist", "style", "style.css"); const cssExists = fs.existsSync(cssPath); @@ -74,17 +81,45 @@ export class WebView { css: cssExists ? Uri.file(cssPath) : undefined, }; - this.panel = window.createWebviewPanel("ZEAPIWebview", this.title, ViewColumn.Beside, { + if (!(opts?.isView ?? false)) { + this.panel = window.createWebviewPanel("ZEAPIWebview", this.title, ViewColumn.Beside, { + enableScripts: true, + localResourceRoots: [this.uris.disk.build], + retainContextWhenHidden: opts?.retainContext ?? false, + }); + + // Associate URI resources with webview + this.uris.resource = { + build: this.panel.webview.asWebviewUri(this.uris.disk.build), + script: this.panel.webview.asWebviewUri(this.uris.disk.script), + css: this.uris.disk.css ? this.panel.webview.asWebviewUri(this.uris.disk.css) : undefined, + }; + + const builtHtml = Mustache.render(HTMLTemplate, { + uris: this.uris, + nonce: this.nonce, + title: this.title, + }); + this.webviewContent = builtHtml; + if (opts?.onDidReceiveMessage) { + this.panel.webview.onDidReceiveMessage(async (message) => opts.onDidReceiveMessage(message)); + } + this.panel.onDidDispose(() => this.dispose(), null, this.disposables); + this.panel.webview.html = this.webviewContent; + } + } + + public resolveForView(webviewView: WebviewView): void { + webviewView.webview.options = { enableScripts: true, localResourceRoots: [this.uris.disk.build], - retainContextWhenHidden: opts?.retainContext ?? false, - }); + }; // Associate URI resources with webview this.uris.resource = { - build: this.panel.webview.asWebviewUri(this.uris.disk.build), - script: this.panel.webview.asWebviewUri(this.uris.disk.script), - css: this.uris.disk.css ? this.panel.webview.asWebviewUri(this.uris.disk.css) : undefined, + build: webviewView.webview.asWebviewUri(this.uris.disk.build), + script: webviewView.webview.asWebviewUri(this.uris.disk.script), + css: this.uris.disk.css ? webviewView.webview.asWebviewUri(this.uris.disk.css) : undefined, }; const builtHtml = Mustache.render(HTMLTemplate, { @@ -93,18 +128,21 @@ export class WebView { title: this.title, }); this.webviewContent = builtHtml; - if (opts?.onDidReceiveMessage) { - this.panel.webview.onDidReceiveMessage(async (message) => opts.onDidReceiveMessage(message)); + if (this.webviewOpts?.onDidReceiveMessage) { + webviewView.webview.onDidReceiveMessage(async (message) => this.webviewOpts.onDidReceiveMessage(message)); } - this.panel.onDidDispose(() => this.dispose(), null, this.disposables); - this.panel.webview.html = this.webviewContent; + webviewView.onDidDispose(() => this.dispose(), null, this.disposables); + webviewView.webview.html = this.webviewContent; + this.view = webviewView; } /** * Disposes of the webview instance */ protected dispose(): void { - this.panel.dispose(); + if (this.panel) { + this.panel.dispose(); + } for (const disp of this.disposables) { disp.dispose(); diff --git a/packages/zowe-explorer-api/src/vscode/ui/index.ts b/packages/zowe-explorer-api/src/vscode/ui/index.ts index 11f482ea44..5d275adf11 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/index.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/index.ts @@ -12,4 +12,5 @@ export * from "./utils/TableBuilder"; export * from "./utils/TableMediator"; export * from "./TableView"; +export * from "./TableViewProvider"; export * from "./WebView"; diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index aac37d19cf..8d9b08e88a 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -45,10 +45,17 @@ export class TableBuilder { rows: [], title: "", }; + private forWebviewView = false; + public constructor(context: ExtensionContext) { this.context = context; } + public isView(): TableBuilder { + this.forWebviewView = true; + return this; + } + /** * Set optional properties for the table view. * @param opts The options for the table @@ -186,7 +193,7 @@ export class TableBuilder { this.data.columns = Object.keys(this.data.rows[0]).map((k) => ({ field: k })); } - return new Table.Instance(this.context, this.data); + return new Table.Instance(this.context, this.forWebviewView, this.data); } /** diff --git a/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx index e3ae602822..1771c9deaf 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx @@ -31,6 +31,7 @@ export const useContextMenu = (contextMenu: ContextMenuProps) => { selectedRows: [], clickedRow: null, field: undefined, + rowIndex: null, }); /* Opens the context menu and sets the anchor point to mouse coordinates */ @@ -67,6 +68,7 @@ export const useContextMenu = (contextMenu: ContextMenuProps) => { selectedRows: event.api.getSelectedRows(), clickedRow: event.data, field: event.colDef.field, + rowIndex: event.rowIndex, }; openMenu(event.event as PointerEvent); @@ -117,6 +119,7 @@ export const ContextMenu = (gridRefs: any, menuItems: Table.ContextMenuOption[], vscodeApi.postMessage({ command: item.command, data: { + rowIndex: gridRefs.rowIndex, row: { ...gridRefs.clickedRow, actions: undefined }, field: gridRefs.field, cell: gridRefs.colDef.valueFormatter diff --git a/packages/zowe-explorer/src/webviews/src/table-view/style.css b/packages/zowe-explorer/src/webviews/src/table-view/style.css index 953300bd18..bfeb0a7bde 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/style.css +++ b/packages/zowe-explorer/src/webviews/src/table-view/style.css @@ -1,6 +1,5 @@ .ag-theme-vsc { - height: 50vh; - max-height: 85vh; + max-height: 95vh; margin-top: 1em; --ag-icon-font-family: "agGridQuartz"; --ag-row-hover-color: var(--vscode-list-hoverBackground); diff --git a/packages/zowe-explorer/src/webviews/src/table-view/types.ts b/packages/zowe-explorer/src/webviews/src/table-view/types.ts index d07a670a3a..35a830ebcd 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/types.ts +++ b/packages/zowe-explorer/src/webviews/src/table-view/types.ts @@ -30,7 +30,7 @@ export type TableViewProps = { // Define props for the AG Grid table here export const tableProps = (contextMenu: ContextMenuState, tableData: Table.ViewOpts, vscodeApi: any): Partial => ({ - // domLayout: "autoHeight", + domLayout: "autoHeight", enableCellTextSelection: true, ensureDomOrder: true, rowData: tableData.rows, From a725433ec58bb9e08e42b153463e4b84572315bf Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 25 Jul 2024 15:00:40 -0400 Subject: [PATCH 076/107] fix(jobs): Do not pass session to job nodes Signed-off-by: Trae Yelovich --- packages/zowe-explorer/src/trees/job/ZoweJobNode.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts b/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts index 2b2fbd487d..f1191c1dce 100644 --- a/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts +++ b/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts @@ -245,7 +245,6 @@ export class ZoweJobNode extends ZoweTreeNode implements IZoweJobTreeNode { label: nodeTitle, collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, parentNode: this, - session: this.session, profile: this.getProfile(), job, }); From 78aa6f339346a76241a5659224e7cb02f256ed9f Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 25 Jul 2024 15:01:30 -0400 Subject: [PATCH 077/107] fix(jobs): 'Cancel job' refresh/UI logic Signed-off-by: Trae Yelovich --- packages/zowe-explorer/src/trees/job/JobActions.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/zowe-explorer/src/trees/job/JobActions.ts b/packages/zowe-explorer/src/trees/job/JobActions.ts index 729c4de809..b8602c8dd1 100644 --- a/packages/zowe-explorer/src/trees/job/JobActions.ts +++ b/packages/zowe-explorer/src/trees/job/JobActions.ts @@ -454,9 +454,15 @@ export class JobActions { } } + for (const session of sessionNodes) { + session.dirty = true; + await session.getChildren(); + jobsProvider.refreshElement(session); + } + if (failedJobs.length > 0) { // Display any errors from the API - await Gui.warningMessage( + Gui.warningMessage( vscode.l10n.t({ message: "One or more jobs failed to cancel: {0}", args: [failedJobs.reduce((prev, j) => prev.concat(`\n${j.job.jobname}(${j.job.jobid}): ${j.error}`), "\n")], @@ -467,10 +473,7 @@ export class JobActions { } ); } else { - await Gui.showMessage(vscode.l10n.t("Cancelled selected jobs successfully.")); - } - for (const session of sessionNodes) { - jobsProvider.refreshElement(session); + Gui.showMessage(vscode.l10n.t("Cancelled selected jobs successfully.")); } } public static async sortJobs(session: IZoweJobTreeNode, jobsProvider: JobTree): Promise { From e87c02c3b674156c3fad6cef037f91b39ce13a65 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 25 Jul 2024 15:02:51 -0400 Subject: [PATCH 078/107] refactor: Handle table callback types; add updateRow table fn Signed-off-by: Trae Yelovich --- .../src/vscode/ui/TableView.ts | 69 +++++++++++++++---- .../src/webviews/src/table-view/TableView.tsx | 17 +++-- 2 files changed, 68 insertions(+), 18 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index b4294a7071..416b9f7544 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -22,18 +22,34 @@ export namespace Table { export type ColData = RowData; export type CellData = ContentTypes; + export type RowInfo = { + index: number; + row: RowData; + }; + /* Defines the supported callbacks and related types. */ export type CallbackTypes = "row" | "column" | "cell"; - export type Callback = { + export type RowCallback = { + /** The type of callback */ + typ: "row"; + /** The callback function itself - called from within the webview container. */ + fn: (view: Table.View, row: RowInfo) => void | PromiseLike; + }; + export type CellCallback = { + /** The type of callback */ + typ: "cell"; + /** The callback function itself - called from within the webview container. */ + fn: (view: Table.View, cell: CellData) => void | PromiseLike; + }; + export type ColumnCallback = { /** The type of callback */ - typ: CallbackTypes; + typ: "column"; /** The callback function itself - called from within the webview container. */ - fn: - | ((data: RowData) => void | PromiseLike) - | ((data: ColData) => void | PromiseLike) - | ((data: CellData) => void | PromiseLike); + fn: (view: Table.View, col: ColData) => void | PromiseLike; }; + export type Callback = RowCallback | CellCallback | ColumnCallback; + /** Conditional callback function - whether an action or option should be rendered. */ export type Conditional = (data: RowData | CellData) => boolean; @@ -208,6 +224,8 @@ export namespace Table { paginationPageSizeSelector?: number[] | boolean; /** If defined, rows are filtered using this text as a Quick Filter. */ quickFilterText?: string; + /** Enable selection of rows in table */ + rowSelection?: "single" | "multiple"; /** Set to `true` to skip the `headerName` when `autoSize` is called by default. Read once during initialization. */ skipHeaderOnAutoSize?: boolean; /** @@ -291,10 +309,10 @@ export namespace Table { return this.htmlContent; } - public constructor(context: ExtensionContext, data?: ViewOpts) { + public constructor(context: ExtensionContext, isView?: boolean, data?: ViewOpts) { super(data.title ?? "Table view", "table-view", context, { onDidReceiveMessage: (message) => this.onMessageReceived(message), - retainContext: true, + isView, }); if (data) { this.data = data; @@ -344,7 +362,17 @@ export namespace Table { ...this.data.contextOpts.all, ].find((action) => action.command === message.command); if (matchingActionable != null) { - await matchingActionable.callback.fn(message.data); + switch (matchingActionable.callback.typ) { + case "row": + await matchingActionable.callback.fn(this, { index: message.data.rowIndex, row: message.data.row } as RowInfo); + break; + case "cell": + await matchingActionable.callback.fn(this, message.data.cell); + break; + case "column": + // TODO + break; + } } } @@ -355,7 +383,7 @@ export namespace Table { * @returns Whether the webview received the update that was sent */ private async updateWebview(): Promise { - const result = await this.panel.webview.postMessage({ + const result = await (this.panel ?? this.view).webview.postMessage({ command: "ondatachanged", data: this.data, }); @@ -424,6 +452,21 @@ export namespace Table { return this.updateWebview(); } + /** + * Update an existing row in the table view. + * @param index The AG GRID row index to update within the table + * @param row The new row content + * @returns Whether the webview successfully updated the new row + */ + public async updateRow(index: number, row: RowData | null): Promise { + if (row == null) { + this.data.rows.splice(index, 1); + } else { + this.data.rows[index] = row; + } + return this.updateWebview(); + } + /** * Adds headers to the end of the existing header list in the table view. * @@ -495,13 +538,13 @@ export namespace Table { } export class Instance extends View { - public constructor(context: ExtensionContext, data: Table.ViewOpts) { - super(context, data); + public constructor(context: ExtensionContext, isView: boolean, data: Table.ViewOpts) { + super(context, isView, data); } /** * Closes the table view and marks it as disposed. - * Removes the table instance from the mediator if it exists. + * Removes the table instance from the mediator if it exists. */ public dispose(): void { TableMediator.getInstance().removeTable(this); diff --git a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx index 5ef7e19147..170271e30e 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx @@ -81,6 +81,7 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro // Prevent cells from being selectable cellStyle: { border: "none", outline: "none" }, field: "actions", + minWidth: 360, sortable: false, suppressSizeToFit: true, // Support a custom cell renderer for row actions @@ -89,7 +90,7 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro ((params: any) => // Render any actions for the given row and actions that apply to all rows newData.actions[params.rowIndex] || newData.actions["all"] ? ( - + {[...(newData.actions[params.rowIndex] || []), ...(newData.actions["all"] || [])] .filter((action) => { if (action.condition == null) { @@ -107,11 +108,17 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro onClick={(_e: any) => vscodeApi.postMessage({ command: action.command, - data: { ...params.data, actions: undefined }, - row: newData.rows!.at(params.rowIndex), + data: { + rowIndex: params.node.rowIndex, + row: { ...params.data, actions: undefined }, + field: params.colDef.field, + cell: params.colDef.valueFormatter + ? params.colDef.valueFormatter({ value: params.data[params.colDef.field] }) + : params.data[params.colDef.field], + }, }) } - style={{ marginRight: "0.25em" }} + style={{ marginRight: "0.25em", width: "fit-content" }} > {action.title} @@ -122,7 +129,7 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro ]; setTableData({ ...newData, rows, columns }); } else { - setTableData(response.data); + setTableData(newData); } break; default: From 9f2e0e896428aa7f756e5c4eb45b2ce87d244f84 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 25 Jul 2024 16:00:10 -0400 Subject: [PATCH 079/107] fix(tests): Update table tests to handle changes Signed-off-by: Trae Yelovich --- .../__unit__/vscode/ui/TableView.unit.test.ts | 16 ++++++++-------- .../zowe-explorer-api/src/vscode/ui/TableView.ts | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts index 0c1c2deac8..47f583ec26 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts @@ -49,7 +49,7 @@ describe("Table.View", () => { describe("getUris", () => { it("returns the URIs from the WebView base class", () => { const globalMocks = createGlobalMocks(); - const view = new Table.View(globalMocks.context as any, { title: "Table" } as any); + const view = new Table.View(globalMocks.context as any, false, { title: "Table" } as any); const buildPath = join(globalMocks.context.extensionPath, "src", "webviews"); const scriptPath = join(buildPath, "dist", "table-view", "table-view.js"); expect(view.getUris()).toStrictEqual({ @@ -70,7 +70,7 @@ describe("Table.View", () => { describe("getHtml", () => { it("returns the HTML content generated by the WebView base class", () => { const globalMocks = createGlobalMocks(); - const view = new Table.View(globalMocks.context as any, { title: "Table" } as any); + const view = new Table.View(globalMocks.context as any, false, { title: "Table" } as any); expect(view.getHtml()).toStrictEqual(view.panel.webview.html); }); }); @@ -78,7 +78,7 @@ describe("Table.View", () => { describe("updateWebview", () => { it("calls postMessage on the panel and sends the data to the webview", async () => { const globalMocks = createGlobalMocks(); - const view = new Table.View(globalMocks.context as any, { title: "Table" } as any); + const view = new Table.View(globalMocks.context as any, false, { title: "Table" } as any); // case 1: Post message was not successful; updateWebview returns false const postMessageMock = jest.spyOn(view.panel.webview, "postMessage").mockResolvedValueOnce(false); @@ -118,7 +118,7 @@ describe("Table.View", () => { describe("getId", () => { it("returns a valid ID for the table view", () => { const globalMocks = createGlobalMocks(); - const view = new Table.View(globalMocks.context as any, { title: "Table" } as any); + const view = new Table.View(globalMocks.context as any, false, { title: "Table" } as any); const randomUuidMock = jest.spyOn(crypto, "randomUUID").mockReturnValueOnce("foo-bar-baz-qux-quux"); expect(view.getId()).toBe("Table-foo##Zowe.vscode-extension-for-zowe"); expect(randomUuidMock).toHaveBeenCalled(); @@ -243,7 +243,7 @@ describe("Table.View", () => { it("fires the onTableDataEdited event when handling the 'ontableedited' command", async () => { const globalMocks = createGlobalMocks(); - const view = new Table.View(globalMocks.context as any, { title: "Table w/ editable columns" } as any); + const view = new Table.View(globalMocks.context as any, false, { title: "Table w/ editable columns" } as any); await view.setColumns([{ field: "a", editable: true }, { field: "b" }, { field: "c" }]); const onTableDataEditedFireMock = jest.spyOn((view as any).onTableDataEditedEmitter, "fire"); const tableData = { rows: [{ a: 1, b: 1, c: 1 }] }; @@ -339,7 +339,7 @@ describe("Table.View", () => { command: "some-action", callback: { typ: "cell", - fn: (_cell: Table.CellData) => { + fn: (_view: Table.View, _cell: Table.CellData) => { allCallbackMock(); }, }, @@ -351,7 +351,7 @@ describe("Table.View", () => { command: "zero-action", callback: { typ: "cell", - fn: (_cell: Table.CellData) => { + fn: (_view: Table.View, _cell: Table.CellData) => { zeroCallbackMock(); }, }, @@ -359,7 +359,7 @@ describe("Table.View", () => { ], }, }; - const view = new Table.View(globalMocks.context as any, data); + const view = new Table.View(globalMocks.context as any, false, data); const writeTextMock = jest.spyOn(env.clipboard, "writeText"); // case 1: An action that exists for all rows const mockWebviewMsg = { diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 416b9f7544..3b4d442dd0 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -310,7 +310,7 @@ export namespace Table { } public constructor(context: ExtensionContext, isView?: boolean, data?: ViewOpts) { - super(data.title ?? "Table view", "table-view", context, { + super(data?.title ?? "Table view", "table-view", context, { onDidReceiveMessage: (message) => this.onMessageReceived(message), isView, }); From e008bb01c33a78db98711f65f3bf0639bd6d1d96 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 25 Jul 2024 16:05:42 -0400 Subject: [PATCH 080/107] feat: Tabular jobs view Signed-off-by: Trae Yelovich --- packages/zowe-explorer/l10n/poeditor.json | 12 +- packages/zowe-explorer/package.json | 33 +++--- packages/zowe-explorer/package.nls.json | 2 + packages/zowe-explorer/src/extension.ts | 3 + .../zowe-explorer/src/trees/job/JobInit.ts | 112 +++++++++++++++++- 5 files changed, 140 insertions(+), 22 deletions(-) diff --git a/packages/zowe-explorer/l10n/poeditor.json b/packages/zowe-explorer/l10n/poeditor.json index aab3edfc57..c0ebd8ce56 100644 --- a/packages/zowe-explorer/l10n/poeditor.json +++ b/packages/zowe-explorer/l10n/poeditor.json @@ -8,6 +8,12 @@ "viewsContainers.activitybar": { "Zowe Explorer": "" }, + "viewsContainers.panel.tableView": { + "Zowe": "" + }, + "zowe.resources.name": { + "Zowe Resources": "" + }, "zowe.placeholderCommand": { "Placeholder": "" }, @@ -395,12 +401,12 @@ "openWithEncoding": { "Open with Encoding": "" }, - "zowe.history.deprecationMsg": { - "Changes made here will not be reflected in Zowe Explorer, use right-click Edit History option to access information from local storage.": "" - }, "jobsTableView": { "Show as table": "" }, + "zowe.history.deprecationMsg": { + "Changes made here will not be reflected in Zowe Explorer, use right-click Edit History option to access information from local storage.": "" + }, "Refresh": "", "Delete Selected": "", "Select an item before deleting": "", diff --git a/packages/zowe-explorer/package.json b/packages/zowe-explorer/package.json index bfb59c7713..cd1a4da834 100644 --- a/packages/zowe-explorer/package.json +++ b/packages/zowe-explorer/package.json @@ -45,6 +45,13 @@ "title": "%viewsContainers.activitybar%", "icon": "resources/zowe.svg" } + ], + "panel": [ + { + "id": "zowe-panel", + "icon": "resources/zowe.svg", + "title": "%viewsContainers.panel.tableView%" + } ] }, "views": { @@ -61,6 +68,13 @@ "id": "zowe.jobs.explorer", "name": "%zowe.jobs.explorer%" } + ], + "zowe-panel": [ + { + "type": "webview", + "id": "zowe-resources", + "name": "%zowe.resources.name%" + } ] }, "keybindings": [ @@ -165,7 +179,7 @@ } }, { - "command": "zowe.jobs.tableView", + "command": "zowe.jobs.tabularView", "title": "%jobsTableView%", "category": "Zowe Explorer" }, @@ -654,21 +668,6 @@ "command": "zowe.placeholderCommand", "title": "%zowe.placeholderCommand%", "enablement": "false" - }, - { - "command": "zowe.tableView", - "title": "Show table view (basic)", - "category": "Zowe Explorer" - }, - { - "command": "zowe.tableView2", - "title": "Show table view (several entries)", - "category": "Zowe Explorer" - }, - { - "command": "zowe.tableView3", - "title": "Show table view (all features)", - "category": "Zowe Explorer" } ], "menus": { @@ -1263,7 +1262,7 @@ }, { "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", - "command": "zowe.jobs.tableView", + "command": "zowe.jobs.tabularView", "group": "100_zowe_tableview" }, { diff --git a/packages/zowe-explorer/package.nls.json b/packages/zowe-explorer/package.nls.json index 99d072dac9..1d4fdd99ac 100644 --- a/packages/zowe-explorer/package.nls.json +++ b/packages/zowe-explorer/package.nls.json @@ -2,6 +2,8 @@ "displayName": "Zowe Explorer", "description": "VS Code extension, powered by Zowe CLI, that streamlines interaction with mainframe data sets, USS files, and jobs", "viewsContainers.activitybar": "Zowe Explorer", + "viewsContainers.panel.tableView": "Zowe Resources", + "zowe.resources.name": "Zowe Resources", "zowe.placeholderCommand": "Placeholder", "zowe.promptCredentials": "Update Credentials", "zowe.profileManagement": "Manage Profile", diff --git a/packages/zowe-explorer/src/extension.ts b/packages/zowe-explorer/src/extension.ts index ce0d6d60c6..9d5b2406ba 100644 --- a/packages/zowe-explorer/src/extension.ts +++ b/packages/zowe-explorer/src/extension.ts @@ -23,6 +23,7 @@ import { SharedInit } from "./trees/shared/SharedInit"; import { SharedTreeProviders } from "./trees/shared/SharedTreeProviders"; import { USSInit } from "./trees/uss/USSInit"; import { ProfilesUtils } from "./utils/ProfilesUtils"; +import { TableViewProvider } from "@zowe/zowe-explorer-api"; /** * The function that runs when the extension is loaded @@ -43,6 +44,8 @@ export async function activate(context: vscode.ExtensionContext): Promise USSInit.initUSSProvider(context), job: () => JobInit.initJobsProvider(context), }); + + context.subscriptions.push(vscode.window.registerWebviewViewProvider("zowe-resources", TableViewProvider.getInstance())); SharedInit.registerCommonCommands(context, providers); SharedInit.registerRefreshCommand(context, activate, deactivate); ZoweExplorerExtender.createInstance(providers.ds, providers.uss, providers.job); diff --git a/packages/zowe-explorer/src/trees/job/JobInit.ts b/packages/zowe-explorer/src/trees/job/JobInit.ts index b3dd615503..dda33311ce 100644 --- a/packages/zowe-explorer/src/trees/job/JobInit.ts +++ b/packages/zowe-explorer/src/trees/job/JobInit.ts @@ -10,7 +10,7 @@ */ import * as vscode from "vscode"; -import { IZoweJobTreeNode, IZoweTreeNode, ZoweScheme, imperative, Gui } from "@zowe/zowe-explorer-api"; +import { IZoweJobTreeNode, IZoweTreeNode, ZoweScheme, imperative, Gui, TableBuilder, Table, TableViewProvider } from "@zowe/zowe-explorer-api"; import { JobTree } from "./JobTree"; import { JobActions } from "./JobActions"; import { ZoweJobNode } from "./ZoweJobNode"; @@ -21,7 +21,7 @@ import { SharedInit } from "../shared/SharedInit"; import { SharedUtils } from "../shared/SharedUtils"; import { JobFSProvider } from "./JobFSProvider"; import { PollProvider } from "./JobPollProvider"; - +import { SharedTreeProviders } from "../shared/SharedTreeProviders"; export class JobInit { /** * Creates the Job tree that contains nodes of sessions, jobs and spool items @@ -145,6 +145,114 @@ export class JobInit { ) ); context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.copyName", async (job: IZoweJobTreeNode) => JobActions.copyName(job))); + context.subscriptions.push( + vscode.commands.registerCommand("zowe.jobs.tabularView", async (node, nodeList) => { + const selectedNodes = SharedUtils.getSelectedNodeList(node, nodeList) as IZoweJobTreeNode[]; + if (selectedNodes.length !== 1) { + return; + } + + const profileNode = selectedNodes[0]; + const children = await profileNode.getChildren(); + + TableViewProvider.getInstance().setTableView( + new TableBuilder(context) + .options({ + autoSizeStrategy: { type: "fitCellContents", colIds: ["name", "class", "owner", "id", "retcode", "status"] }, + rowSelection: "multiple", + }) + .isView() + .title(`Jobs view: ${profileNode.owner} | ${profileNode.prefix} | ${profileNode.status}`) + .rows( + ...children.map((item) => ({ + name: item.job.jobname, + class: item.job.class, + owner: item.job.owner, + id: item.job.jobid, + retcode: item.job.retcode, + status: item.job.status, + })) + ) + .columns( + ...[ + { field: "name", checkboxSelection: true, filter: true, sort: "asc" } as Table.ColumnOpts, + { + field: "class", + filter: true, + }, + { field: "owner", filter: true }, + { field: "id", headerName: "ID", filter: true }, + { field: "retcode", headerName: "Return Code", filter: true }, + { field: "status", filter: true }, + ] + ) + .addRowAction("all", { + title: "Get JCL", + command: "get-jcl", + callback: { + fn: async (view: Table.View, data: Table.RowInfo) => { + const child = children.find((c) => data.row.id === c.job?.jobid); + if (child != null) { + await JobActions.downloadJcl(child as ZoweJobNode); + } + }, + typ: "row", + }, + }) + .addRowAction("all", { + title: "Reveal in tree", + type: "primary", + command: "edit", + callback: { + fn: async (view: Table.View, data: Table.RowInfo) => { + const child = children.find((c) => data.row.id === c.job?.jobid); + if (child) { + await jobsProvider.getTreeView().reveal(child, { expand: true }); + } + }, + typ: "row", + }, + }) + .addContextOption("all", { + title: "Cancel job", + command: "cancel-job", + callback: { + fn: async (view: Table.View, data: Table.RowInfo) => { + const child = children.find((c) => data.row.id === c.job?.jobid); + if (child) { + await JobActions.cancelJobs(SharedTreeProviders.job, [child]); + await view.updateRow(data.index, { + name: child.job.jobname, + class: child.job.class, + owner: child.job.owner, + id: child.job.jobid, + retcode: child.job.retcode, + status: child.job.status, + }); + } + }, + typ: "row", + }, + condition: (data: Table.RowData) => data["status"] === "ACTIVE", + }) + .addContextOption("all", { + title: "Delete job", + command: "delete-job", + callback: { + fn: async (view: Table.View, data: Table.RowInfo) => { + const child = children.find((c) => data.row.id === c.job?.jobid); + if (child) { + await JobActions.deleteCommand(jobsProvider, child); + await view.updateRow(data.index, null); + } + }, + typ: "row", + }, + }) + .build() + ); + }) + ); context.subscriptions.push( vscode.workspace.onDidOpenTextDocument((doc) => { if (doc.uri.scheme !== ZoweScheme.Jobs) { From 38b35f4f4e7c7b3b54172d08a245873d888ebf48 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 25 Jul 2024 17:14:54 -0400 Subject: [PATCH 081/107] fix(tests): Resolve failing unit tests Signed-off-by: Trae Yelovich --- .../__tests__/__mocks__/mockCreators/jobs.ts | 1 - .../__tests__/__mocks__/vscode.ts | 169 ++++++++++++++++++ .../__tests__/__unit__/extension.unit.test.ts | 1 + .../trees/job/JobActions.unit.test.ts | 32 +++- .../src/configuration/Constants.ts | 2 +- 5 files changed, 195 insertions(+), 10 deletions(-) diff --git a/packages/zowe-explorer/__tests__/__mocks__/mockCreators/jobs.ts b/packages/zowe-explorer/__tests__/__mocks__/mockCreators/jobs.ts index 461372ad2f..97d29c3d92 100644 --- a/packages/zowe-explorer/__tests__/__mocks__/mockCreators/jobs.ts +++ b/packages/zowe-explorer/__tests__/__mocks__/mockCreators/jobs.ts @@ -145,7 +145,6 @@ export function createJobNode(session: any, profile: imperative.IProfileLoaded) label: "sampleJob", collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, parentNode: session.getSessionNode(), - session, profile, job: createIJobObject(), }); diff --git a/packages/zowe-explorer/__tests__/__mocks__/vscode.ts b/packages/zowe-explorer/__tests__/__mocks__/vscode.ts index b7028bcc7b..2d3aa4dbfd 100644 --- a/packages/zowe-explorer/__tests__/__mocks__/vscode.ts +++ b/packages/zowe-explorer/__tests__/__mocks__/vscode.ts @@ -389,7 +389,176 @@ export interface FileDecorationProvider { provideFileDecoration(uri: Uri, token: CancellationToken): ProviderResult; } +/** + * Additional information the webview view being resolved. + * + * @param T Type of the webview's state. + */ +interface WebviewViewResolveContext { + /** + * Persisted state from the webview content. + * + * To save resources, the editor normally deallocates webview documents (the iframe content) that are not visible. + * For example, when the user collapse a view or switches to another top level activity in the sidebar, the + * `WebviewView` itself is kept alive but the webview's underlying document is deallocated. It is recreated when + * the view becomes visible again. + * + * You can prevent this behavior by setting `retainContextWhenHidden` in the `WebviewOptions`. However this + * increases resource usage and should be avoided wherever possible. Instead, you can use persisted state to + * save off a webview's state so that it can be quickly recreated as needed. + * + * To save off a persisted state, inside the webview call `acquireVsCodeApi().setState()` with + * any json serializable object. To restore the state again, call `getState()`. For example: + * + * ```js + * // Within the webview + * const vscode = acquireVsCodeApi(); + * + * // Get existing state + * const oldState = vscode.getState() || { value: 0 }; + * + * // Update state + * setState({ value: oldState.value + 1 }) + * ``` + * + * The editor ensures that the persisted state is saved correctly when a webview is hidden and across + * editor restarts. + */ + readonly state: T | undefined; +} + +/** + * A webview based view. + */ +export interface WebviewView { + /** + * Identifies the type of the webview view, such as `'hexEditor.dataView'`. + */ + readonly viewType: string; + + /** + * The underlying webview for the view. + */ + readonly webview: any; + + /** + * View title displayed in the UI. + * + * The view title is initially taken from the extension `package.json` contribution. + */ + title?: string; + + /** + * Human-readable string which is rendered less prominently in the title. + */ + description?: string; + + /** + * The badge to display for this webview view. + * To remove the badge, set to undefined. + */ + badge?: any; + + /** + * Event fired when the view is disposed. + * + * Views are disposed when they are explicitly hidden by a user (this happens when a user + * right clicks in a view and unchecks the webview view). + * + * Trying to use the view after it has been disposed throws an exception. + */ + readonly onDidDispose: Event; + + /** + * Tracks if the webview is currently visible. + * + * Views are visible when they are on the screen and expanded. + */ + readonly visible: boolean; + + /** + * Event fired when the visibility of the view changes. + * + * Actions that trigger a visibility change: + * + * - The view is collapsed or expanded. + * - The user switches to a different view group in the sidebar or panel. + * + * Note that hiding a view using the context menu instead disposes of the view and fires `onDidDispose`. + */ + readonly onDidChangeVisibility: Event; + + /** + * Reveal the view in the UI. + * + * If the view is collapsed, this will expand it. + * + * @param preserveFocus When `true` the view will not take focus. + */ + show(preserveFocus?: boolean): void; +} + +/** + * Provider for creating `WebviewView` elements. + */ +export interface WebviewViewProvider { + /** + * Resolves a webview view. + * + * `resolveWebviewView` is called when a view first becomes visible. This may happen when the view is + * first loaded or when the user hides and then shows a view again. + * + * @param webviewView Webview view to restore. The provider should take ownership of this view. The + * provider must set the webview's `.html` and hook up all webview events it is interested in. + * @param context Additional metadata about the view being resolved. + * @param token Cancellation token indicating that the view being provided is no longer needed. + * + * @returns Optional thenable indicating that the view has been fully resolved. + */ + resolveWebviewView(webviewView: WebviewView, context: WebviewViewResolveContext, token: CancellationToken): Thenable | void; +} + export namespace window { + /** + * Register a new provider for webview views. + * + * @param viewId Unique id of the view. This should match the `id` from the + * `views` contribution in the package.json. + * @param provider Provider for the webview views. + * + * @returns Disposable that unregisters the provider. + */ + export function registerWebviewViewProvider( + viewId: string, + provider: WebviewViewProvider, + options?: { + /** + * Content settings for the webview created for this view. + */ + readonly webviewOptions?: { + /** + * Controls if the webview element itself (iframe) is kept around even when the view + * is no longer visible. + * + * Normally the webview's html context is created when the view becomes visible + * and destroyed when it is hidden. Extensions that have complex state + * or UI can set the `retainContextWhenHidden` to make the editor keep the webview + * context around, even when the webview moves to a background tab. When a webview using + * `retainContextWhenHidden` becomes hidden, its scripts and other dynamic content are suspended. + * When the view becomes visible again, the context is automatically restored + * in the exact same state it was in originally. You cannot send messages to a + * hidden webview, even with `retainContextWhenHidden` enabled. + * + * `retainContextWhenHidden` has a high memory overhead and should only be used if + * your view's context cannot be quickly saved and restored. + */ + readonly retainContextWhenHidden?: boolean; + }; + } + ): Disposable { + return new Disposable(); + } + export const visibleTextEditors = []; /** * Options for creating a {@link TreeView} diff --git a/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts index 34ba67d079..6ad3ed7a75 100644 --- a/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts @@ -209,6 +209,7 @@ function createGlobalMocks() { "zowe.jobs.sortBy", "zowe.jobs.filterJobs", "zowe.jobs.copyName", + "zowe.jobs.tabularView", "zowe.updateSecureCredentials", "zowe.manualPoll", "zowe.editHistory", diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobActions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobActions.unit.test.ts index 5a10debfd4..e13f60b095 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobActions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobActions.unit.test.ts @@ -111,7 +111,7 @@ function createGlobalMocks() { get: activeTextEditorDocument, configurable: true, }); - Object.defineProperty(Profiles, "getInstance", { value: jest.fn().mockResolvedValue(newMocks.mockProfileInstance), configurable: true }); + Object.defineProperty(Profiles, "getInstance", { value: jest.fn().mockReturnValue(newMocks.mockProfileInstance), configurable: true }); const executeCommand = jest.fn(); Object.defineProperty(vscode.commands, "executeCommand", { value: executeCommand, configurable: true }); Object.defineProperty(ZoweLogger, "error", { value: jest.fn(), configurable: true }); @@ -1130,9 +1130,20 @@ describe("cancelJob", () => { createGlobalMocks(); const session = createISession(); const profile = createIProfile(); - const jobSessionNode = createJobSessionNode(session, profile); - const jobNode = createJobNode(jobSessionNode, profile); - const jobsProvider = createJobsTree(session, jobNode.job, profile, createTreeView()); + + const createBlockMocks = () => { + const jobSessionNode = createJobSessionNode(session, profile); + const jobNode = createJobNode(jobSessionNode, profile); + + return { + session: createISession(), + profile, + jobSessionNode, + jobNode, + jobsProvider: createJobsTree(session, jobNode.job, profile, createTreeView()), + }; + }; + const jesCancelJobMock = jest.fn(); const mockJesApi = (mockFn?: jest.Mock): void => { @@ -1155,17 +1166,20 @@ describe("cancelJob", () => { }); it("returns early if no nodes are specified", async () => { + const { jobsProvider } = createBlockMocks(); await JobActions.cancelJobs(jobsProvider, []); expect(Gui.showMessage).not.toHaveBeenCalled(); }); it("returns early if all nodes in selection have been cancelled", async () => { + const { jobNode, jobsProvider } = createBlockMocks(); jobNode.job.retcode = "CANCELED"; await JobActions.cancelJobs(jobsProvider, [jobNode]); expect(Gui.showMessage).toHaveBeenCalledWith("The selected jobs were already cancelled."); }); it("shows a warning message if one or more jobs failed to cancel", async () => { + const { jobNode, jobsProvider } = createBlockMocks(); jobNode.job.retcode = "ACTIVE"; jesCancelJobMock.mockResolvedValueOnce(false); await JobActions.cancelJobs(jobsProvider, [jobNode]); @@ -1175,6 +1189,7 @@ describe("cancelJob", () => { }); it("shows a warning message if one or more APIs do not support cancelJob", async () => { + const { jobNode, jobsProvider } = createBlockMocks(); // Make cancelJob undefined mockJesApi(); jobNode.job.retcode = "ACTIVE"; @@ -1188,6 +1203,7 @@ describe("cancelJob", () => { }); it("shows matching error messages for one or more failed jobs", async () => { + const { jobNode, jobsProvider } = createBlockMocks(); jobNode.job.retcode = "ACTIVE"; jesCancelJobMock.mockRejectedValueOnce(new Error("Failed to cancel job... something went wrong.")); await JobActions.cancelJobs(jobsProvider, [jobNode]); @@ -1200,18 +1216,18 @@ describe("cancelJob", () => { }); it("shows a message confirming the jobs were cancelled", async () => { + const { jobNode, jobsProvider, jobSessionNode } = createBlockMocks(); jobNode.job.retcode = "ACTIVE"; jesCancelJobMock.mockResolvedValueOnce(true); - const setImmediateSpy = jest.spyOn(global, "setImmediate"); + const getChildrenMock = jest.spyOn(jobSessionNode, "getChildren"); await JobActions.cancelJobs(jobsProvider, [jobNode]); - + expect(getChildrenMock).toHaveBeenCalled(); // Check that refreshElement was called through setImmediate - expect(setImmediateSpy).toHaveBeenCalled(); - expect(Gui.showMessage).toHaveBeenCalledWith("Cancelled selected jobs successfully."); }); it("does not work for job session nodes", async () => { + const { jobsProvider, jobSessionNode } = createBlockMocks(); await JobActions.cancelJobs(jobsProvider, [jobSessionNode]); expect(jesCancelJobMock).not.toHaveBeenCalled(); }); diff --git a/packages/zowe-explorer/src/configuration/Constants.ts b/packages/zowe-explorer/src/configuration/Constants.ts index 6e796f1175..7db56f034b 100644 --- a/packages/zowe-explorer/src/configuration/Constants.ts +++ b/packages/zowe-explorer/src/configuration/Constants.ts @@ -17,7 +17,7 @@ import type { Profiles } from "./Profiles"; export class Constants { public static CONFIG_PATH: string; - public static readonly COMMAND_COUNT = 99; + public static readonly COMMAND_COUNT = 100; public static readonly MAX_SEARCH_HISTORY = 5; public static readonly MAX_FILE_HISTORY = 10; public static readonly MS_PER_SEC = 1000; From fd0770fb65bce7c9a77215b01e67450dae18b5c2 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 25 Jul 2024 18:06:48 -0400 Subject: [PATCH 082/107] feat(tables): Separate row callbacks into single/multiple Signed-off-by: Trae Yelovich --- .../__unit__/vscode/ui/TableView.unit.test.ts | 18 +++---- .../src/vscode/ui/TableView.ts | 32 ++++++++--- .../zowe-explorer/src/trees/job/JobInit.ts | 53 +++++++++++-------- 3 files changed, 66 insertions(+), 37 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts index 47f583ec26..796f6624f6 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts @@ -310,7 +310,7 @@ describe("Table.View", () => { all: [], }, }; - const view = new Table.View(globalMocks.context as any, data); + const view = new Table.View(globalMocks.context as any, false, data); const writeTextMock = jest.spyOn(env.clipboard, "writeText"); const mockWebviewMsg = { command: "nonexistent-action", @@ -454,7 +454,7 @@ describe("Table.View", () => { title: "Add to cart", command: "add-to-cart", callback: { - typ: "row", + typ: "single-row", fn: (_data) => {}, }, condition: (_data) => true, @@ -468,7 +468,7 @@ describe("Table.View", () => { title: "Save for later", command: "save-for-later", callback: { - typ: "row", + typ: "single-row", fn: (_data) => {}, }, condition: (_data) => true, @@ -492,7 +492,7 @@ describe("Table.View", () => { title: "Remove from cart", command: "rm-from-cart", callback: { - typ: "row", + typ: "single-row", fn: (_data) => {}, }, } as Table.ContextMenuOpts; @@ -505,7 +505,7 @@ describe("Table.View", () => { title: "Add to wishlist", command: "add-to-wishlist", callback: { - typ: "row", + typ: "single-row", fn: (_data) => {}, }, } as Table.ContextMenuOpts; @@ -530,7 +530,7 @@ describe("Table.View", () => { title: "Add to wishlist", command: "add-to-wishlist", callback: { - typ: "row", + typ: "single-row", fn: (_data) => {}, }, condition: (_data) => true, @@ -544,7 +544,7 @@ describe("Table.View", () => { title: "Learn more", command: "learn-more", callback: { - typ: "row", + typ: "single-row", fn: (_data) => {}, }, condition: (_data) => true, @@ -566,7 +566,7 @@ describe("Table.View", () => { title: "Remove from wishlist", command: "rm-from-wishlist", callback: { - typ: "row", + typ: "single-row", fn: (_data) => {}, }, } as Table.ContextMenuOpts; @@ -579,7 +579,7 @@ describe("Table.View", () => { title: "Learn less", command: "learn-less", callback: { - typ: "row", + typ: "single-row", fn: (_data) => {}, }, } as Table.ContextMenuOpts; diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 3b4d442dd0..075e5f25a6 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -23,18 +23,24 @@ export namespace Table { export type CellData = ContentTypes; export type RowInfo = { - index: number; + index?: number; row: RowData; }; /* Defines the supported callbacks and related types. */ - export type CallbackTypes = "row" | "column" | "cell"; - export type RowCallback = { + export type CallbackTypes = "single-row" | "multi-row" | "column" | "cell"; + export type SingleRowCallback = { /** The type of callback */ - typ: "row"; + typ: "single-row"; /** The callback function itself - called from within the webview container. */ fn: (view: Table.View, row: RowInfo) => void | PromiseLike; }; + export type MultiRowCallback = { + /** The type of callback */ + typ: "multi-row"; + /** The callback function itself - called from within the webview container. */ + fn: (view: Table.View, rows: Record) => void | PromiseLike; + }; export type CellCallback = { /** The type of callback */ typ: "cell"; @@ -48,7 +54,7 @@ export namespace Table { fn: (view: Table.View, col: ColData) => void | PromiseLike; }; - export type Callback = RowCallback | CellCallback | ColumnCallback; + export type Callback = SingleRowCallback | MultiRowCallback | CellCallback | ColumnCallback; /** Conditional callback function - whether an action or option should be rendered. */ export type Conditional = (data: RowData | CellData) => boolean; @@ -363,8 +369,11 @@ export namespace Table { ].find((action) => action.command === message.command); if (matchingActionable != null) { switch (matchingActionable.callback.typ) { - case "row": - await matchingActionable.callback.fn(this, { index: message.data.rowIndex, row: message.data.row } as RowInfo); + case "single-row": + await matchingActionable.callback.fn(this, { index: message.data.rowIndex, row: message.data.row }); + break; + case "multi-row": + await matchingActionable.callback.fn(this, message.data.rows); break; case "cell": await matchingActionable.callback.fn(this, message.data.cell); @@ -442,6 +451,15 @@ export namespace Table { return this.updateWebview(); } + /** + * Get rows of content from the table view. + * @param rows The rows of data in the table + * @returns Whether the webview successfully received the new content + */ + public getContent(): RowData[] { + return this.data.rows; + } + /** * Add rows of content to the table view. * @param rows The rows of data to add to the table diff --git a/packages/zowe-explorer/src/trees/job/JobInit.ts b/packages/zowe-explorer/src/trees/job/JobInit.ts index dda33311ce..646c82080e 100644 --- a/packages/zowe-explorer/src/trees/job/JobInit.ts +++ b/packages/zowe-explorer/src/trees/job/JobInit.ts @@ -196,7 +196,7 @@ export class JobInit { await JobActions.downloadJcl(child as ZoweJobNode); } }, - typ: "row", + typ: "single-row", }, }) .addRowAction("all", { @@ -210,28 +210,33 @@ export class JobInit { await jobsProvider.getTreeView().reveal(child, { expand: true }); } }, - typ: "row", + typ: "single-row", }, }) .addContextOption("all", { title: "Cancel job", command: "cancel-job", callback: { - fn: async (view: Table.View, data: Table.RowInfo) => { - const child = children.find((c) => data.row.id === c.job?.jobid); - if (child) { - await JobActions.cancelJobs(SharedTreeProviders.job, [child]); - await view.updateRow(data.index, { - name: child.job.jobname, - class: child.job.class, - owner: child.job.owner, - id: child.job.jobid, - retcode: child.job.retcode, - status: child.job.status, - }); + fn: async (view: Table.View, data: Record) => { + const childrenToCancel = Object.values(data) + .map((row) => children.find((c) => row.id === c.job?.jobid)) + .filter((child) => child); + if (childrenToCancel.length > 0) { + await JobActions.cancelJobs(SharedTreeProviders.job, childrenToCancel); + const profNode = childrenToCancel[0].getSessionNode() as ZoweJobNode; + await view.setContent( + profNode.children.map((item) => ({ + name: item.job.jobname, + class: item.job.class, + owner: item.job.owner, + id: item.job.jobid, + retcode: item.job.retcode, + status: item.job.status, + })) + ); } }, - typ: "row", + typ: "multi-row", }, condition: (data: Table.RowData) => data["status"] === "ACTIVE", }) @@ -239,14 +244,20 @@ export class JobInit { title: "Delete job", command: "delete-job", callback: { - fn: async (view: Table.View, data: Table.RowInfo) => { - const child = children.find((c) => data.row.id === c.job?.jobid); - if (child) { - await JobActions.deleteCommand(jobsProvider, child); - await view.updateRow(data.index, null); + fn: async (view: Table.View, data: Record) => { + const childrenToDelete = Object.values(data) + .map((row) => children.find((c) => row.id === c.job?.jobid)) + .filter((child) => child); + if (childrenToDelete.length > 0) { + await JobActions.deleteCommand(jobsProvider, undefined, childrenToDelete); + const newData = view.getContent(); + for (const index of Object.keys(data).map(Number)) { + newData.splice(index, 1); + } + await view.setContent(newData); } }, - typ: "row", + typ: "multi-row", }, }) .build() From 75490341e634e1941136f088cb9545dc4eea4d48 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 25 Jul 2024 18:08:03 -0400 Subject: [PATCH 083/107] wip(table): implement Actions Bar at top of table view Signed-off-by: Trae Yelovich --- .../webviews/src/table-view/ActionsBar.tsx | 55 +++++++++++++++++++ .../src/webviews/src/table-view/TableView.tsx | 24 ++++++-- .../src/webviews/src/table-view/types.ts | 10 +++- 3 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 packages/zowe-explorer/src/webviews/src/table-view/ActionsBar.tsx diff --git a/packages/zowe-explorer/src/webviews/src/table-view/ActionsBar.tsx b/packages/zowe-explorer/src/webviews/src/table-view/ActionsBar.tsx new file mode 100644 index 0000000000..f50b918711 --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/table-view/ActionsBar.tsx @@ -0,0 +1,55 @@ +import { Ref } from "preact/hooks"; +import type { Table } from "@zowe/zowe-explorer-api"; +import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; +import { GridApi } from "ag-grid-community"; + +export const ActionsBar = ({ + actions, + gridRef, + itemCount, + vscodeApi, +}: { + actions: Table.Action[]; + gridRef: Ref; + itemCount: number; + vscodeApi: any; +}) => { + return ( +
+
+ {itemCount === 0 ? "No" : itemCount} item{itemCount < 1 ? "" : "s"} selected +
+ + {actions + .filter((action) => (itemCount > 1 ? action.callback.typ === "multi-row" : action.callback.typ.endsWith("row"))) + .map((action) => ( + { + const selectedRows = (gridRef.current.api as GridApi).getSelectedNodes(); + vscodeApi.postMessage({ + command: action.command, + data: { + rows: selectedRows.map((row) => ({ [row.rowIndex!]: row.data })), + }, + }); + }} + > + {action.title} + + ))} + +
+ ); +}; diff --git a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx index 170271e30e..4cc8494bf4 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx @@ -4,19 +4,22 @@ import "ag-grid-community/styles/ag-grid.css"; import "ag-grid-community/styles/ag-theme-quartz.css"; import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; import { AgGridReact } from "ag-grid-react"; -import { useEffect, useState } from "preact/hooks"; +import { useEffect, useRef, useState } from "preact/hooks"; import { getVsCodeTheme, isSecureOrigin, useMutableObserver } from "../utils"; import type { Table } from "@zowe/zowe-explorer-api"; import { TableViewProps, tableProps, wrapFn } from "./types"; import { useContextMenu } from "./ContextMenu"; // Custom styling (font family, VS Code color scheme, etc.) import "./style.css"; +import { ActionsBar } from "./ActionsBar"; const vscodeApi = acquireVsCodeApi(); export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewProps) => { const [tableData, setTableData] = useState(data); const [theme, setTheme] = useState(baseTheme ?? "ag-theme-quartz"); + const [selectionCount, setSelectionCount] = useState(0); + const gridRef = useRef(); const contextMenu = useContextMenu({ options: [ @@ -32,7 +35,7 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro title: "Copy row", command: "copy", callback: { - typ: "row", + typ: "single-row", fn: () => {}, }, }, @@ -90,7 +93,15 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro ((params: any) => // Render any actions for the given row and actions that apply to all rows newData.actions[params.rowIndex] || newData.actions["all"] ? ( - + {[...(newData.actions[params.rowIndex] || []), ...(newData.actions["all"] || [])] .filter((action) => { if (action.condition == null) { @@ -113,7 +124,9 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro row: { ...params.data, actions: undefined }, field: params.colDef.field, cell: params.colDef.valueFormatter - ? params.colDef.valueFormatter({ value: params.data[params.colDef.field] }) + ? params.colDef.valueFormatter({ + value: params.data[params.colDef.field], + }) : params.data[params.colDef.field], }, }) @@ -156,7 +169,8 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro {tableData?.title ?

{tableData.title}

: null}
{contextMenu.component} - {tableData ? : null} + + {tableData ? : null}
); diff --git a/packages/zowe-explorer/src/webviews/src/table-view/types.ts b/packages/zowe-explorer/src/webviews/src/table-view/types.ts index 35a830ebcd..498fbeb9ee 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/types.ts +++ b/packages/zowe-explorer/src/webviews/src/table-view/types.ts @@ -29,7 +29,12 @@ export type TableViewProps = { }; // Define props for the AG Grid table here -export const tableProps = (contextMenu: ContextMenuState, tableData: Table.ViewOpts, vscodeApi: any): Partial => ({ +export const tableProps = ( + contextMenu: ContextMenuState, + setSelectionCount: React.Dispatch, + tableData: Table.ViewOpts, + vscodeApi: any +): Partial => ({ domLayout: "autoHeight", enableCellTextSelection: true, ensureDomOrder: true, @@ -60,6 +65,9 @@ export const tableProps = (contextMenu: ContextMenuState, tableData: Table.ViewO event.api.forEachNodeAfterFilterAndSort((row, _i) => rows.push(row.data)); vscodeApi.postMessage({ command: "ondisplaychanged", data: rows }); }, + onSelectionChanged: (event) => { + setSelectionCount(event.api.getSelectedRows().length); + }, onSortChanged: (event) => { const rows: Table.RowData[] = []; event.api.forEachNodeAfterFilterAndSort((row, _i) => rows.push(row.data)); From a52b5acdf724152e03ba184e967d62a17fa42a89 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 25 Jul 2024 18:51:34 -0400 Subject: [PATCH 084/107] feat(table): Support merging options for 'actions' column Signed-off-by: Trae Yelovich --- packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx index 4cc8494bf4..4edcdd18bb 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx @@ -81,6 +81,7 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro const columns = [ ...(newData.columns ?? []), { + ...(newData.columns.find((col) => col.field === "actions") ?? {}), // Prevent cells from being selectable cellStyle: { border: "none", outline: "none" }, field: "actions", From 103eb6ec83db5d5e76d87e86c2c419daf1a53f1d Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 25 Jul 2024 18:52:54 -0400 Subject: [PATCH 085/107] refactor: Fix actions bar row handling, update job view opts Signed-off-by: Trae Yelovich --- .../zowe-explorer/src/trees/job/JobInit.ts | 16 +++++----- .../webviews/src/table-view/ActionsBar.tsx | 31 +++++++++++++------ 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/packages/zowe-explorer/src/trees/job/JobInit.ts b/packages/zowe-explorer/src/trees/job/JobInit.ts index 646c82080e..b309324a12 100644 --- a/packages/zowe-explorer/src/trees/job/JobInit.ts +++ b/packages/zowe-explorer/src/trees/job/JobInit.ts @@ -184,9 +184,10 @@ export class JobInit { { field: "id", headerName: "ID", filter: true }, { field: "retcode", headerName: "Return Code", filter: true }, { field: "status", filter: true }, + { field: "actions", hide: true }, ] ) - .addRowAction("all", { + .addContextOption("all", { title: "Get JCL", command: "get-jcl", callback: { @@ -199,9 +200,8 @@ export class JobInit { typ: "single-row", }, }) - .addRowAction("all", { + .addContextOption("all", { title: "Reveal in tree", - type: "primary", command: "edit", callback: { fn: async (view: Table.View, data: Table.RowInfo) => { @@ -213,8 +213,8 @@ export class JobInit { typ: "single-row", }, }) - .addContextOption("all", { - title: "Cancel job", + .addRowAction("all", { + title: "Cancel", command: "cancel-job", callback: { fn: async (view: Table.View, data: Record) => { @@ -225,7 +225,7 @@ export class JobInit { await JobActions.cancelJobs(SharedTreeProviders.job, childrenToCancel); const profNode = childrenToCancel[0].getSessionNode() as ZoweJobNode; await view.setContent( - profNode.children.map((item) => ({ + (await profNode.getChildren()).map((item) => ({ name: item.job.jobname, class: item.job.class, owner: item.job.owner, @@ -240,8 +240,8 @@ export class JobInit { }, condition: (data: Table.RowData) => data["status"] === "ACTIVE", }) - .addContextOption("all", { - title: "Delete job", + .addRowAction("all", { + title: "Delete", command: "delete-job", callback: { fn: async (view: Table.View, data: Record) => { diff --git a/packages/zowe-explorer/src/webviews/src/table-view/ActionsBar.tsx b/packages/zowe-explorer/src/webviews/src/table-view/ActionsBar.tsx index f50b918711..43501f9d13 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/ActionsBar.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/ActionsBar.tsx @@ -17,31 +17,42 @@ export const ActionsBar = ({ return (
-
- {itemCount === 0 ? "No" : itemCount} item{itemCount < 1 ? "" : "s"} selected -
- +
+ {itemCount === 0 ? "No" : itemCount} item{itemCount === 1 ? "" : "s"} selected +
+ {actions .filter((action) => (itemCount > 1 ? action.callback.typ === "multi-row" : action.callback.typ.endsWith("row"))) .map((action) => ( { const selectedRows = (gridRef.current.api as GridApi).getSelectedNodes(); + if (selectedRows.length === 0) { + return; + } + vscodeApi.postMessage({ command: action.command, data: { - rows: selectedRows.map((row) => ({ [row.rowIndex!]: row.data })), + row: action.callback.typ === "single-row" ? selectedRows[0].data : undefined, + rows: + action.callback.typ === "multi-row" + ? selectedRows.reduce((all, row) => ({ ...all, [row.rowIndex!]: row.data }), {}) + : undefined, }, }); }} From 4b0532a54c5bb4e90df2911374654ddd8566ba30 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Fri, 26 Jul 2024 10:11:50 -0400 Subject: [PATCH 086/107] fix(tables): Remove CSS style on filter popups Signed-off-by: Trae Yelovich --- packages/zowe-explorer/src/webviews/src/table-view/style.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/zowe-explorer/src/webviews/src/table-view/style.css b/packages/zowe-explorer/src/webviews/src/table-view/style.css index bfeb0a7bde..a404e6ac7c 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/style.css +++ b/packages/zowe-explorer/src/webviews/src/table-view/style.css @@ -23,10 +23,6 @@ border: 1px solid var(--vscode-menu-foreground) !important; } -.ag-theme-vsc.ag-popup { - position: absolute; -} - .szh-menu { background-color: var(--vscode-menu-background); border: 1px solid var(--vscode-menu-border); From aed27360e24ec3bd2ee813c0f8354ffa1c44fb0c Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 30 Jul 2024 13:43:05 -0400 Subject: [PATCH 087/107] fix(TableViewProvider): dispose pre-existing table Signed-off-by: Trae Yelovich --- .../zowe-explorer-api/src/vscode/ui/TableViewProvider.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts b/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts index 48ddcf5620..e6862e31b9 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts @@ -14,7 +14,7 @@ import { Table } from "./TableView"; export class TableViewProvider implements WebviewViewProvider { private view: WebviewView; - private tableView: Table.View; + private tableView: Table.Instance = null; private static instance: TableViewProvider; @@ -28,7 +28,10 @@ export class TableViewProvider implements WebviewViewProvider { return this.instance; } - public setTableView(tableView: Table.View | null): void { + public setTableView(tableView: Table.Instance | null): void { + if (this.tableView != null) { + this.tableView.dispose(); + } this.tableView = tableView; if (tableView == null) { From 6bdd24de5cd36cd73f95bde62670db5a731234e7 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 30 Jul 2024 13:52:09 -0400 Subject: [PATCH 088/107] chore: separate jobs view into diff branch Signed-off-by: Trae Yelovich --- .../__tests__/__unit__/extension.unit.test.ts | 1 - .../src/configuration/Constants.ts | 2 +- .../zowe-explorer/src/trees/job/JobInit.ts | 123 +----------------- 3 files changed, 3 insertions(+), 123 deletions(-) diff --git a/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts index 6ad3ed7a75..34ba67d079 100644 --- a/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts @@ -209,7 +209,6 @@ function createGlobalMocks() { "zowe.jobs.sortBy", "zowe.jobs.filterJobs", "zowe.jobs.copyName", - "zowe.jobs.tabularView", "zowe.updateSecureCredentials", "zowe.manualPoll", "zowe.editHistory", diff --git a/packages/zowe-explorer/src/configuration/Constants.ts b/packages/zowe-explorer/src/configuration/Constants.ts index 7db56f034b..6e796f1175 100644 --- a/packages/zowe-explorer/src/configuration/Constants.ts +++ b/packages/zowe-explorer/src/configuration/Constants.ts @@ -17,7 +17,7 @@ import type { Profiles } from "./Profiles"; export class Constants { public static CONFIG_PATH: string; - public static readonly COMMAND_COUNT = 100; + public static readonly COMMAND_COUNT = 99; public static readonly MAX_SEARCH_HISTORY = 5; public static readonly MAX_FILE_HISTORY = 10; public static readonly MS_PER_SEC = 1000; diff --git a/packages/zowe-explorer/src/trees/job/JobInit.ts b/packages/zowe-explorer/src/trees/job/JobInit.ts index b309324a12..b3dd615503 100644 --- a/packages/zowe-explorer/src/trees/job/JobInit.ts +++ b/packages/zowe-explorer/src/trees/job/JobInit.ts @@ -10,7 +10,7 @@ */ import * as vscode from "vscode"; -import { IZoweJobTreeNode, IZoweTreeNode, ZoweScheme, imperative, Gui, TableBuilder, Table, TableViewProvider } from "@zowe/zowe-explorer-api"; +import { IZoweJobTreeNode, IZoweTreeNode, ZoweScheme, imperative, Gui } from "@zowe/zowe-explorer-api"; import { JobTree } from "./JobTree"; import { JobActions } from "./JobActions"; import { ZoweJobNode } from "./ZoweJobNode"; @@ -21,7 +21,7 @@ import { SharedInit } from "../shared/SharedInit"; import { SharedUtils } from "../shared/SharedUtils"; import { JobFSProvider } from "./JobFSProvider"; import { PollProvider } from "./JobPollProvider"; -import { SharedTreeProviders } from "../shared/SharedTreeProviders"; + export class JobInit { /** * Creates the Job tree that contains nodes of sessions, jobs and spool items @@ -145,125 +145,6 @@ export class JobInit { ) ); context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.copyName", async (job: IZoweJobTreeNode) => JobActions.copyName(job))); - context.subscriptions.push( - vscode.commands.registerCommand("zowe.jobs.tabularView", async (node, nodeList) => { - const selectedNodes = SharedUtils.getSelectedNodeList(node, nodeList) as IZoweJobTreeNode[]; - if (selectedNodes.length !== 1) { - return; - } - - const profileNode = selectedNodes[0]; - const children = await profileNode.getChildren(); - - TableViewProvider.getInstance().setTableView( - new TableBuilder(context) - .options({ - autoSizeStrategy: { type: "fitCellContents", colIds: ["name", "class", "owner", "id", "retcode", "status"] }, - rowSelection: "multiple", - }) - .isView() - .title(`Jobs view: ${profileNode.owner} | ${profileNode.prefix} | ${profileNode.status}`) - .rows( - ...children.map((item) => ({ - name: item.job.jobname, - class: item.job.class, - owner: item.job.owner, - id: item.job.jobid, - retcode: item.job.retcode, - status: item.job.status, - })) - ) - .columns( - ...[ - { field: "name", checkboxSelection: true, filter: true, sort: "asc" } as Table.ColumnOpts, - { - field: "class", - filter: true, - }, - { field: "owner", filter: true }, - { field: "id", headerName: "ID", filter: true }, - { field: "retcode", headerName: "Return Code", filter: true }, - { field: "status", filter: true }, - { field: "actions", hide: true }, - ] - ) - .addContextOption("all", { - title: "Get JCL", - command: "get-jcl", - callback: { - fn: async (view: Table.View, data: Table.RowInfo) => { - const child = children.find((c) => data.row.id === c.job?.jobid); - if (child != null) { - await JobActions.downloadJcl(child as ZoweJobNode); - } - }, - typ: "single-row", - }, - }) - .addContextOption("all", { - title: "Reveal in tree", - command: "edit", - callback: { - fn: async (view: Table.View, data: Table.RowInfo) => { - const child = children.find((c) => data.row.id === c.job?.jobid); - if (child) { - await jobsProvider.getTreeView().reveal(child, { expand: true }); - } - }, - typ: "single-row", - }, - }) - .addRowAction("all", { - title: "Cancel", - command: "cancel-job", - callback: { - fn: async (view: Table.View, data: Record) => { - const childrenToCancel = Object.values(data) - .map((row) => children.find((c) => row.id === c.job?.jobid)) - .filter((child) => child); - if (childrenToCancel.length > 0) { - await JobActions.cancelJobs(SharedTreeProviders.job, childrenToCancel); - const profNode = childrenToCancel[0].getSessionNode() as ZoweJobNode; - await view.setContent( - (await profNode.getChildren()).map((item) => ({ - name: item.job.jobname, - class: item.job.class, - owner: item.job.owner, - id: item.job.jobid, - retcode: item.job.retcode, - status: item.job.status, - })) - ); - } - }, - typ: "multi-row", - }, - condition: (data: Table.RowData) => data["status"] === "ACTIVE", - }) - .addRowAction("all", { - title: "Delete", - command: "delete-job", - callback: { - fn: async (view: Table.View, data: Record) => { - const childrenToDelete = Object.values(data) - .map((row) => children.find((c) => row.id === c.job?.jobid)) - .filter((child) => child); - if (childrenToDelete.length > 0) { - await JobActions.deleteCommand(jobsProvider, undefined, childrenToDelete); - const newData = view.getContent(); - for (const index of Object.keys(data).map(Number)) { - newData.splice(index, 1); - } - await view.setContent(newData); - } - }, - typ: "multi-row", - }, - }) - .build() - ); - }) - ); context.subscriptions.push( vscode.workspace.onDidOpenTextDocument((doc) => { if (doc.uri.scheme !== ZoweScheme.Jobs) { From 4bc4c8b252b0e9c3fb457ab593b3b13b3c52cd23 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 30 Jul 2024 14:41:00 -0400 Subject: [PATCH 089/107] tests: Add patch coverage for TableViewProvider Signed-off-by: Trae Yelovich --- .../vscode/ui/TableViewProvider.unit.test.ts | 109 ++++++++++++++++++ .../src/vscode/ui/TableViewProvider.ts | 6 +- 2 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableViewProvider.unit.test.ts diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableViewProvider.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableViewProvider.unit.test.ts new file mode 100644 index 0000000000..6d3f97429a --- /dev/null +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableViewProvider.unit.test.ts @@ -0,0 +1,109 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { EventEmitter, ExtensionContext, WebviewView } from "vscode"; +import { TableBuilder, TableViewProvider } from "../../../../lib"; + +describe("TableViewProvider", () => { + const fakeExtContext = { + extensionPath: "/a/b/c/zowe-explorer", + extension: { + id: "Zowe.vscode-extension-for-zowe", + }, + } as ExtensionContext; + + describe("getInstance", () => { + it("returns a singleton instance for the TableViewProvider", () => { + expect(TableViewProvider.getInstance()).toBeInstanceOf(TableViewProvider); + }); + }); + + describe("setTableView", () => { + it("sets the table to the given table view", () => { + // case 1: table did not previously exist + const builder = new TableBuilder(fakeExtContext); + + const tableOne = builder + .isView() + .addColumns([ + { field: "apple", headerName: "Apple" }, + { field: "orange", headerName: "Orange" }, + { field: "apple", headerName: "Banana" }, + ]) + .addRows([ + { apple: 0, banana: 1, orange: 2 }, + { apple: 3, banana: 4, orange: 5 }, + { apple: 6, banana: 7, orange: 8 }, + { apple: 9, banana: 10, orange: 11 }, + ]) + .build(); + TableViewProvider.getInstance().setTableView(tableOne); + expect((TableViewProvider.getInstance() as any).tableView).toBe(tableOne); + + const disposeSpy = jest.spyOn(tableOne, "dispose"); + + // case 2: table previously existed, dispose called on old table + const tableTwo = builder.options({ pagination: false }).build(); + TableViewProvider.getInstance().setTableView(tableTwo); + expect((TableViewProvider.getInstance() as any).tableView).toBe(tableTwo); + expect(disposeSpy).toHaveBeenCalled(); + }); + }); + + describe("getTableView", () => { + beforeEach(() => { + TableViewProvider.getInstance().setTableView(null); + }); + + it("returns null if no table view has been provided", () => { + expect(TableViewProvider.getInstance().getTableView()).toBe(null); + }); + + it("returns a valid table view if one has been provided", () => { + expect(TableViewProvider.getInstance().getTableView()).toBe(null); + const table = new TableBuilder(fakeExtContext) + .isView() + .addColumns([ + { field: "a", headerName: "A" }, + { field: "b", headerName: "B" }, + { field: "c", headerName: "C" }, + ]) + .addRows([ + { a: 0, b: 1, c: 2 }, + { a: 3, b: 4, c: 5 }, + ]) + .build(); + TableViewProvider.getInstance().setTableView(table); + expect(TableViewProvider.getInstance().getTableView()).toBe(table); + }); + }); + + describe("resolveWebviewView", () => { + it("correctly resolves the view and calls resolveForView on the table", async () => { + const table = new TableBuilder(fakeExtContext).isView().build(); + TableViewProvider.getInstance().setTableView(table); + const resolveForViewSpy = jest.spyOn(table, "resolveForView"); + const fakeView = { + onDidDispose: jest.fn(), + viewType: "zowe.panel", + title: "SomeWebviewView", + webview: { asWebviewUri: jest.fn(), onDidReceiveMessage: jest.fn(), options: {} }, + } as unknown as WebviewView; + const fakeEventEmitter = new EventEmitter(); + await TableViewProvider.getInstance().resolveWebviewView( + fakeView, + { state: undefined }, + { isCancellationRequested: false, onCancellationRequested: fakeEventEmitter.event } + ); + expect(resolveForViewSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts b/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts index e6862e31b9..897465e170 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts @@ -13,7 +13,7 @@ import { CancellationToken, WebviewView, WebviewViewProvider, WebviewViewResolve import { Table } from "./TableView"; export class TableViewProvider implements WebviewViewProvider { - private view: WebviewView; + private view?: WebviewView; private tableView: Table.Instance = null; private static instance: TableViewProvider; @@ -35,7 +35,9 @@ export class TableViewProvider implements WebviewViewProvider { this.tableView = tableView; if (tableView == null) { - this.view.webview.html = ""; + if (this.view != null) { + this.view.webview.html = ""; + } return; } From 79f661786ef3cce76fe39e86dae89ee3c41d8e52 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 30 Jul 2024 15:12:24 -0400 Subject: [PATCH 090/107] fix(test): Update import for TableViewProvider Signed-off-by: Trae Yelovich --- .../__tests__/__unit__/vscode/ui/TableViewProvider.unit.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableViewProvider.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableViewProvider.unit.test.ts index 6d3f97429a..4bc7d06b79 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableViewProvider.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableViewProvider.unit.test.ts @@ -10,7 +10,7 @@ */ import { EventEmitter, ExtensionContext, WebviewView } from "vscode"; -import { TableBuilder, TableViewProvider } from "../../../../lib"; +import { TableBuilder, TableViewProvider } from "../../../../src/vscode/ui"; describe("TableViewProvider", () => { const fakeExtContext = { From e1d7262a1d6cb0a4e2de054632b0fa83e0e609db Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 30 Jul 2024 15:22:35 -0400 Subject: [PATCH 091/107] chore: Update changelog for info on tables Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index 26fe8157c6..37dca87643 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -36,6 +36,7 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t - Added PEM certificate support as an authentication method for logging into the API ML. [#2621](https://github.com/zowe/zowe-explorer-vscode/issues/2621) - Deprecated the `getUSSDocumentFilePath` function on the `IZoweTreeNode` interface as Zowe Explorer no longer uses the local file system for storing USS files. **No replacement is planned**; please access data from tree nodes using their [resource URIs](https://github.com/zowe/zowe-explorer-vscode/wiki/FileSystemProvider#operations-for-extenders) instead. [#2968](https://github.com/zowe/zowe-explorer-vscode/pull/2968) - **Next Breaking:** Changed `ProfilesCache.convertV1ProfToConfig` method to be a static method that requires `ProfileInfo` instance as a parameter. +- Implemented support for building, exposing and displaying table views within Zowe Explorer. Tables can be customized and exposed using the helper facilities (`TableBuilder` and `TableMediator`) for an extender's specific use case. For more information on how to configure and show tables, please refer to the [wiki article on Table Views](https://github.com/zowe/zowe-explorer-vscode/wiki/Table-Views). [#2258](https://github.com/zowe/zowe-explorer-vscode/issues/2258) ### Bug fixes From 82f3032da6190461bd474b36fda0904badef8822 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 30 Jul 2024 15:40:10 -0400 Subject: [PATCH 092/107] test: update TableView coverage; refactor: disable column callbacks for now Signed-off-by: Trae Yelovich --- .../__unit__/vscode/ui/TableView.unit.test.ts | 61 +++++++++++++++++-- .../src/vscode/ui/TableView.ts | 6 +- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts index 796f6624f6..33188e2c60 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts @@ -325,6 +325,7 @@ describe("Table.View", () => { const globalMocks = createGlobalMocks(); const allCallbackMock = jest.fn(); const zeroCallbackMock = jest.fn(); + const multiCallbackMock = jest.fn(); const data = { title: "Some table", rows: [{ a: 1, b: 1, c: 1 }], @@ -344,14 +345,24 @@ describe("Table.View", () => { }, }, } as Table.Action, + { + title: "Multi action", + command: "multi-action", + callback: { + typ: "multi-row", + fn: (_view: Table.View, row: Record) => { + multiCallbackMock(); + }, + }, + } as Table.Action, ], 1: [ { title: "Zero action", command: "zero-action", callback: { - typ: "cell", - fn: (_view: Table.View, _cell: Table.CellData) => { + typ: "single-row", + fn: (_view: Table.View, row: Table.RowInfo) => { zeroCallbackMock(); }, }, @@ -361,7 +372,8 @@ describe("Table.View", () => { }; const view = new Table.View(globalMocks.context as any, false, data); const writeTextMock = jest.spyOn(env.clipboard, "writeText"); - // case 1: An action that exists for all rows + + // case 1: A cell action that exists for all rows const mockWebviewMsg = { command: "some-action", data: { cell: data.rows[0].a, row: data.rows[0] }, @@ -371,7 +383,8 @@ describe("Table.View", () => { expect(writeTextMock).not.toHaveBeenCalled(); expect(globalMocks.updateWebviewMock).not.toHaveBeenCalled(); expect(allCallbackMock).toHaveBeenCalled(); - // case 2: An action that exists for one row + + // case 2: A single-row action that exists for one row const mockNextWebviewMsg = { command: "zero-action", data: { cell: data.rows[0].a, row: data.rows[0] }, @@ -381,6 +394,17 @@ describe("Table.View", () => { expect(writeTextMock).not.toHaveBeenCalled(); expect(globalMocks.updateWebviewMock).not.toHaveBeenCalled(); expect(zeroCallbackMock).toHaveBeenCalled(); + + // case 2: A multi-row action that exists for all rows + const mockFinalWebviewMsg = { + command: "multi-action", + data: { cell: data.rows[0].a, rows: data.rows }, + rowIndex: 2, + }; + await view.onMessageReceived(mockFinalWebviewMsg); + expect(writeTextMock).not.toHaveBeenCalled(); + expect(globalMocks.updateWebviewMock).not.toHaveBeenCalled(); + expect(multiCallbackMock).toHaveBeenCalled(); }); }); @@ -589,6 +613,35 @@ describe("Table.View", () => { globalMocks.updateWebviewMock.mockRestore(); }); }); + + describe("getContent", () => { + it("returns the content provided to the table view", () => { + const globalMocks = createGlobalMocks(); + const view = new Table.View(globalMocks.context as any, false, { rows: [{ d: true, e: false, f: true }], title: "Table" } as any); + expect(view.getContent()).toStrictEqual([{ d: true, e: false, f: true }]); + }); + }); + + describe("updateRow", () => { + it("updates the rows on the internal data structure and calls updateWebview", async () => { + const globalMocks = createGlobalMocks(); + const mockRow = { a: 2, b: 2, c: 2 }; + + // case 1: Update the contents of a single row with new contents + const data = { title: "Table w/ content", rows: [{ a: 1, b: 2, c: 3 }] }; + const view = new Table.View(globalMocks.context as any, data as any); + globalMocks.updateWebviewMock.mockImplementation(); + await view.updateRow(0, mockRow); + expect(globalMocks.updateWebviewMock).toHaveBeenCalled(); + globalMocks.updateWebviewMock.mockClear(); + + // case 2: Remove a row from the table + await view.updateRow(0, null); + expect((view as any).data.rows.length).toBe(0); + expect(globalMocks.updateWebviewMock).toHaveBeenCalled(); + globalMocks.updateWebviewMock.mockRestore(); + }); + }); }); // Table.Instance unit tests diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 075e5f25a6..8c387b2fa8 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -54,7 +54,7 @@ export namespace Table { fn: (view: Table.View, col: ColData) => void | PromiseLike; }; - export type Callback = SingleRowCallback | MultiRowCallback | CellCallback | ColumnCallback; + export type Callback = SingleRowCallback | MultiRowCallback | CellCallback; /** Conditional callback function - whether an action or option should be rendered. */ export type Conditional = (data: RowData | CellData) => boolean; @@ -378,8 +378,8 @@ export namespace Table { case "cell": await matchingActionable.callback.fn(this, message.data.cell); break; - case "column": - // TODO + // TODO: Support column callbacks? (if there's enough interest) + default: break; } } From 1da28958012153ffaf8d9c3e631d8ec1ccf2fa66 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 30 Jul 2024 15:50:52 -0400 Subject: [PATCH 093/107] chore: add typedoc to TableViewProvider; standardize TableMediator test layout Signed-off-by: Trae Yelovich --- .../ui/utils/TableMediator.unit.test.ts | 70 ++++++++++--------- .../src/vscode/ui/TableViewProvider.ts | 41 +++++++++++ 2 files changed, 77 insertions(+), 34 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableMediator.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableMediator.unit.test.ts index 23d3d6822d..bbca697f6f 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableMediator.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableMediator.unit.test.ts @@ -39,46 +39,48 @@ function createGlobalMocks() { }; } -describe("TableMediator::getInstance", () => { - it("returns an instance of TableMediator", () => { - expect(TableMediator.getInstance()).toBeInstanceOf(TableMediator); +describe("TableMediator", () => { + describe("getInstance", () => { + it("returns an instance of TableMediator", () => { + expect(TableMediator.getInstance()).toBeInstanceOf(TableMediator); + }); }); -}); -describe("TableMediator::addTable", () => { - it("adds the given table object to its internal map", () => { - const globalMocks = createGlobalMocks(); - TableMediator.getInstance().addTable(globalMocks.table); - expect((TableMediator.getInstance() as any).tables.get(globalMocks.table.getId())).toBe(globalMocks.table); - (TableMediator.getInstance() as any).tables = new Map(); + describe("addTable", () => { + it("adds the given table object to its internal map", () => { + const globalMocks = createGlobalMocks(); + TableMediator.getInstance().addTable(globalMocks.table); + expect((TableMediator.getInstance() as any).tables.get(globalMocks.table.getId())).toBe(globalMocks.table); + (TableMediator.getInstance() as any).tables = new Map(); + }); }); -}); -describe("TableMediator::getTable", () => { - it("retrieves the table by ID using its internal map", () => { - const globalMocks = createGlobalMocks(); - const tableId = globalMocks.table.getId(); - TableMediator.getInstance().addTable(globalMocks.table); - expect(TableMediator.getInstance().getTable(tableId)).toBe(globalMocks.table); - (TableMediator.getInstance() as any).tables = new Map(); + describe("getTable", () => { + it("retrieves the table by ID using its internal map", () => { + const globalMocks = createGlobalMocks(); + const tableId = globalMocks.table.getId(); + TableMediator.getInstance().addTable(globalMocks.table); + expect(TableMediator.getInstance().getTable(tableId)).toBe(globalMocks.table); + (TableMediator.getInstance() as any).tables = new Map(); + }); }); -}); -describe("TableMediator::removeTable", () => { - it("removes a table view from its internal map", () => { - const globalMocks = createGlobalMocks(); - const tableId = globalMocks.table.getId(); - TableMediator.getInstance().addTable(globalMocks.table); - expect(TableMediator.getInstance().removeTable(globalMocks.table)).toBe(true); - expect((TableMediator.getInstance() as any).tables.get(globalMocks.table.getId())).toBe(undefined); - expect(TableMediator.getInstance().getTable(tableId)).toBe(undefined); - }); + describe("removeTable", () => { + it("removes a table view from its internal map", () => { + const globalMocks = createGlobalMocks(); + const tableId = globalMocks.table.getId(); + TableMediator.getInstance().addTable(globalMocks.table); + expect(TableMediator.getInstance().removeTable(globalMocks.table)).toBe(true); + expect((TableMediator.getInstance() as any).tables.get(globalMocks.table.getId())).toBe(undefined); + expect(TableMediator.getInstance().getTable(tableId)).toBe(undefined); + }); - it("returns false if the table instance does not exist in the map", () => { - const globalMocks = createGlobalMocks(); - globalMocks.createWebviewPanelMock.mockReturnValueOnce(globalMocks.mockPanel as any); - const table2 = new TableBuilder(globalMocks.extensionContext as any).build(); - TableMediator.getInstance().addTable(globalMocks.table); - expect(TableMediator.getInstance().removeTable(table2)).toBe(false); + it("returns false if the table instance does not exist in the map", () => { + const globalMocks = createGlobalMocks(); + globalMocks.createWebviewPanelMock.mockReturnValueOnce(globalMocks.mockPanel as any); + const table2 = new TableBuilder(globalMocks.extensionContext as any).build(); + TableMediator.getInstance().addTable(globalMocks.table); + expect(TableMediator.getInstance().removeTable(table2)).toBe(false); + }); }); }); diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts b/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts index 897465e170..5e94f833fd 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableViewProvider.ts @@ -12,6 +12,30 @@ import { CancellationToken, WebviewView, WebviewViewProvider, WebviewViewResolveContext } from "vscode"; import { Table } from "./TableView"; +/** + * View provider class for rendering table views in the "Zowe Resources" panel. + * Registered during initialization of Zowe Explorer. + * + * @remarks + * ## Usage + * + * ### Setting the current table view + * + * Use the {@link setTableView} function to set a table view for the "Zowe Resources" panel. + * _Note_ that setting another table view on the view provider will dispose of the last table instance that was provided. + * + * ### Getting the current table view + * + * Use the {@link getTableView} function to get the current table view in the "Zowe Resources" panel. + * + * ### resolveWebviewView + * + * VS Code uses this function to resolve the instance of the table view to render. **Please do not use this function directly** - + * use {@link setTableView} instead to provide the table view. + * + * Calling the function directly will interfere with the intended behavior of + * the "Zowe Resources" panel and is therefore not supported. + */ export class TableViewProvider implements WebviewViewProvider { private view?: WebviewView; private tableView: Table.Instance = null; @@ -20,6 +44,10 @@ export class TableViewProvider implements WebviewViewProvider { private constructor() {} + /** + * Retrieve the singleton instance of the TableViewProvider. + * @returns the TableViewProvider instance used by Zowe Explorer + */ public static getInstance(): TableViewProvider { if (!this.instance) { this.instance = new TableViewProvider(); @@ -28,6 +56,10 @@ export class TableViewProvider implements WebviewViewProvider { return this.instance; } + /** + * Provide a table view to display in the "Zowe Resources" view. + * @param tableView The table view to prepare for rendering + */ public setTableView(tableView: Table.Instance | null): void { if (this.tableView != null) { this.tableView.dispose(); @@ -46,10 +78,19 @@ export class TableViewProvider implements WebviewViewProvider { } } + /** + * Retrieve the current table view for the "Zowe Resources" panel, if one exists. + */ public getTableView(): Table.View { return this.tableView; } + /** + * VS Code internal function used to resolve the webview view from the view provider. + * @param webviewView The webview view object for the panel + * @param context Additional context for the webview view + * @param token (unused) cancellation token for the webview view resolution process + */ public resolveWebviewView( webviewView: WebviewView, context: WebviewViewResolveContext, From 67617f0e2948270f7fadea0eed8aef3722d5759b Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 30 Jul 2024 15:51:46 -0400 Subject: [PATCH 094/107] fix: remove remaining refs for jobs tabular view Signed-off-by: Trae Yelovich --- packages/zowe-explorer/package.json | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/zowe-explorer/package.json b/packages/zowe-explorer/package.json index 5ca5b8a528..5df4929448 100644 --- a/packages/zowe-explorer/package.json +++ b/packages/zowe-explorer/package.json @@ -178,11 +178,6 @@ "dark": "./resources/dark/history.svg" } }, - { - "command": "zowe.jobs.tabularView", - "title": "%jobsTableView%", - "category": "Zowe Explorer" - }, { "command": "zowe.uss.copyPath", "title": "%uss.copyPath%", @@ -1260,15 +1255,10 @@ "command": "zowe.profileManagement", "group": "099_zowe_jobsProfileAuthentication" }, - { - "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", - "command": "zowe.jobs.tabularView", - "group": "100_zowe_tableview" - }, { "when": "view == zowe.jobs.explorer && viewItem =~ /^(?!.*_fav.*)server.*/ && !listMultiSelection", "command": "zowe.editHistory", - "group": "101_zowe_editHistory" + "group": "100_zowe_editHistory" }, { "when": "viewItem =~ /^(textFile|member.*|ds.*)/ && !listMultiSelection", From fc9b311402bf5506c8aa67e18d48298eca2691f8 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 30 Jul 2024 15:57:51 -0400 Subject: [PATCH 095/107] chore: update strings after removing job table refs Signed-off-by: Trae Yelovich --- packages/zowe-explorer/l10n/bundle.l10n.json | 36 ++++++++++---------- packages/zowe-explorer/l10n/poeditor.json | 17 ++++----- packages/zowe-explorer/package.nls.json | 1 - 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/packages/zowe-explorer/l10n/bundle.l10n.json b/packages/zowe-explorer/l10n/bundle.l10n.json index 9e84a07d99..e05c45f180 100644 --- a/packages/zowe-explorer/l10n/bundle.l10n.json +++ b/packages/zowe-explorer/l10n/bundle.l10n.json @@ -172,24 +172,6 @@ "Required API functions for pasting (fileList and copy/uploadFromBuffer) were not found.": "Required API functions for pasting (fileList and copy/uploadFromBuffer) were not found.", "Uploading USS files...": "Uploading USS files...", "Error uploading files": "Error uploading files", - "The 'move' function is not implemented for this USS API.": "The 'move' function is not implemented for this USS API.", - "Could not list USS files: Empty path provided in URI": "Could not list USS files: Empty path provided in URI", - "Profile does not exist for this file.": "Profile does not exist for this file.", - "$(sync~spin) Saving USS file...": "$(sync~spin) Saving USS file...", - "Renaming {0} failed due to API error: {1}/File pathError message": { - "message": "Renaming {0} failed due to API error: {1}", - "comment": [ - "File path", - "Error message" - ] - }, - "Deleting {0} failed due to API error: {1}/File nameError message": { - "message": "Deleting {0} failed due to API error: {1}", - "comment": [ - "File name", - "Error message" - ] - }, "Downloaded: {0}/Download time": { "message": "Downloaded: {0}", "comment": [ @@ -260,6 +242,24 @@ "initializeUSSFavorites.error.buttonRemove": "initializeUSSFavorites.error.buttonRemove", "File does not exist. It may have been deleted.": "File does not exist. It may have been deleted.", "$(sync~spin) Pulling from Mainframe...": "$(sync~spin) Pulling from Mainframe...", + "The 'move' function is not implemented for this USS API.": "The 'move' function is not implemented for this USS API.", + "Could not list USS files: Empty path provided in URI": "Could not list USS files: Empty path provided in URI", + "Profile does not exist for this file.": "Profile does not exist for this file.", + "$(sync~spin) Saving USS file...": "$(sync~spin) Saving USS file...", + "Renaming {0} failed due to API error: {1}/File pathError message": { + "message": "Renaming {0} failed due to API error: {1}", + "comment": [ + "File path", + "Error message" + ] + }, + "Deleting {0} failed due to API error: {1}/File nameError message": { + "message": "Deleting {0} failed due to API error: {1}", + "comment": [ + "File name", + "Error message" + ] + }, "{0} location/Node type": { "message": "{0} location", "comment": [ diff --git a/packages/zowe-explorer/l10n/poeditor.json b/packages/zowe-explorer/l10n/poeditor.json index 228ee3f74f..94b86c110d 100644 --- a/packages/zowe-explorer/l10n/poeditor.json +++ b/packages/zowe-explorer/l10n/poeditor.json @@ -9,7 +9,7 @@ "Zowe Explorer": "" }, "viewsContainers.panel.tableView": { - "Zowe": "" + "Zowe Resources": "" }, "zowe.resources.name": { "Zowe Resources": "" @@ -401,9 +401,6 @@ "openWithEncoding": { "Open with Encoding": "" }, - "jobsTableView": { - "Show as table": "" - }, "zowe.history.deprecationMsg": { "Changes made here will not be reflected in Zowe Explorer, use right-click Edit History option to access information from local storage.": "" }, @@ -489,12 +486,6 @@ "Required API functions for pasting (fileList and copy/uploadFromBuffer) were not found.": "", "Uploading USS files...": "", "Error uploading files": "", - "The 'move' function is not implemented for this USS API.": "", - "Could not list USS files: Empty path provided in URI": "", - "Profile does not exist for this file.": "", - "$(sync~spin) Saving USS file...": "", - "Renaming {0} failed due to API error: {1}": "", - "Deleting {0} failed due to API error: {1}": "", "Downloaded: {0}": "", "Encoding: {0}": "", "Binary": "", @@ -523,6 +514,12 @@ "initializeUSSFavorites.error.buttonRemove": "", "File does not exist. It may have been deleted.": "", "$(sync~spin) Pulling from Mainframe...": "", + "The 'move' function is not implemented for this USS API.": "", + "Could not list USS files: Empty path provided in URI": "", + "Profile does not exist for this file.": "", + "$(sync~spin) Saving USS file...": "", + "Renaming {0} failed due to API error: {1}": "", + "Deleting {0} failed due to API error: {1}": "", "{0} location": "", "Choose a location to create the {0}": "", "Name of file or directory": "", diff --git a/packages/zowe-explorer/package.nls.json b/packages/zowe-explorer/package.nls.json index 1d4fdd99ac..c8128c205d 100644 --- a/packages/zowe-explorer/package.nls.json +++ b/packages/zowe-explorer/package.nls.json @@ -133,6 +133,5 @@ "compareWithSelected": "Compare with Selected", "compareWithSelectedReadOnly": "Compare with Selected (Read-Only)", "openWithEncoding": "Open with Encoding", - "jobsTableView": "Show as table", "zowe.history.deprecationMsg": "Changes made here will not be reflected in Zowe Explorer, use right-click Edit History option to access information from local storage." } From e44bb966668672fae48148f76db16d2ccd92c150 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 5 Aug 2024 11:52:30 -0400 Subject: [PATCH 096/107] wip: address SonarCloud issues (1/3) Signed-off-by: Trae Yelovich --- .../__unit__/vscode/ui/TableView.unit.test.ts | 4 ++-- .../vscode/ui/utils/TableBuilder.unit.test.ts | 8 +++---- .../src/vscode/ui/TableView.ts | 7 +++--- .../src/vscode/ui/utils/TableBuilder.ts | 23 +++++++++---------- .../webviews/src/table-view/ActionsBar.tsx | 5 ++-- .../src/webviews/src/table-view/App.tsx | 6 +---- .../webviews/src/table-view/ContextMenu.tsx | 3 ++- .../src/webviews/src/table-view/TableView.tsx | 5 ++-- .../src/webviews/src/table-view/types.ts | 2 +- 9 files changed, 30 insertions(+), 33 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts index 33188e2c60..70a9bb0c6f 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/TableView.unit.test.ts @@ -198,7 +198,7 @@ describe("Table.View", () => { const view = new Table.View(globalMocks.context as any, { title: "Stable Table of Cables" } as any); globalMocks.updateWebviewMock.mockResolvedValueOnce(true); const cols = [ - { field: "apple", valueFormatter: (data: { value: Table.CellData }) => `${data.value.toString()} apples` }, + { field: "apple", valueFormatter: (data: { value: Table.ContentTypes }) => `${data.value.toString()} apples` }, { field: "banana", comparator: (valueA, valueB, nodeA, nodeB, isDescending) => -1, colSpan: (params) => 2 }, { field: "orange", rowSpan: (params) => 2 }, ]; @@ -340,7 +340,7 @@ describe("Table.View", () => { command: "some-action", callback: { typ: "cell", - fn: (_view: Table.View, _cell: Table.CellData) => { + fn: (_view: Table.View, _cell: Table.ContentTypes) => { allCallbackMock(); }, }, diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts index 3045d3b88e..14d8768236 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/utils/TableBuilder.unit.test.ts @@ -168,7 +168,7 @@ describe("TableBuilder", () => { command: "add-to-queue", callback: { typ: "cell", - fn: (_cell: Table.CellData) => {}, + fn: (_cell: Table.ContentTypes) => {}, }, condition: (_data) => true, }, @@ -250,7 +250,7 @@ describe("TableBuilder", () => { command: "move", callback: { typ: "cell", - fn: (_cell: Table.CellData) => {}, + fn: (_cell: Table.ContentTypes) => {}, }, condition: (_data) => true, } as Table.ActionOpts; @@ -278,7 +278,7 @@ describe("TableBuilder", () => { command: "move", callback: { typ: "cell", - fn: (_cell: Table.CellData) => {}, + fn: (_cell: Table.ContentTypes) => {}, }, condition: (_data) => true, }, @@ -289,7 +289,7 @@ describe("TableBuilder", () => { command: "recall", callback: { typ: "cell", - fn: (_cell: Table.CellData) => {}, + fn: (_cell: Table.ContentTypes) => {}, }, condition: (_data) => true, }, diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 8c387b2fa8..dfe247ddd1 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -20,7 +20,6 @@ export namespace Table { export type ContentTypes = string | number | boolean | string[]; export type RowData = Record; export type ColData = RowData; - export type CellData = ContentTypes; export type RowInfo = { index?: number; @@ -45,7 +44,7 @@ export namespace Table { /** The type of callback */ typ: "cell"; /** The callback function itself - called from within the webview container. */ - fn: (view: Table.View, cell: CellData) => void | PromiseLike; + fn: (view: Table.View, cell: ContentTypes) => void | PromiseLike; }; export type ColumnCallback = { /** The type of callback */ @@ -57,7 +56,7 @@ export namespace Table { export type Callback = SingleRowCallback | MultiRowCallback | CellCallback; /** Conditional callback function - whether an action or option should be rendered. */ - export type Conditional = (data: RowData | CellData) => boolean; + export type Conditional = (data: RowData | ContentTypes) => boolean; // Defines the supported actions and related types. export type ActionKind = "primary" | "secondary" | "icon"; @@ -77,7 +76,7 @@ export namespace Table { // -- Misc types -- /** Value formatter callback. Expects the exact display value to be returned. */ - export type ValueFormatter = (data: { value: CellData }) => string; + export type ValueFormatter = (data: { value: ContentTypes }) => string; export type Positions = "left" | "right"; /** The column type definition. All available properties are offered for AG Grid columns. */ diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index 8d9b08e88a..ec3b5dbca2 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -51,7 +51,7 @@ export class TableBuilder { this.context = context; } - public isView(): TableBuilder { + public isView(): this { this.forWebviewView = true; return this; } @@ -61,7 +61,7 @@ export class TableBuilder { * @param opts The options for the table * @returns The same {@link TableBuilder} instance with the options added */ - public options(opts: Table.GridProperties): TableBuilder { + public options(opts: Table.GridProperties): this { this.data = { ...this.data, options: this.data.options ? { ...this.data.options, ...opts } : opts }; return this; } @@ -71,7 +71,7 @@ export class TableBuilder { * @param name The name of the table * @returns The same {@link TableBuilder} instance with the title added */ - public title(name: string): TableBuilder { + public title(name: string): this { this.data.title = name; return this; } @@ -81,7 +81,7 @@ export class TableBuilder { * @param rows The rows of content to use for the table * @returns The same {@link TableBuilder} instance with the rows added */ - public rows(...rows: Table.RowData[]): TableBuilder { + public rows(...rows: Table.RowData[]): this { this.data.rows = rows; return this; } @@ -91,7 +91,7 @@ export class TableBuilder { * @param rows The rows of content to add to the table * @returns The same {@link TableBuilder} instance with the new rows added */ - public addRows(rows: Table.RowData[]): TableBuilder { + public addRows(rows: Table.RowData[]): this { this.data.rows = [...this.data.rows, ...rows]; return this; } @@ -101,7 +101,7 @@ export class TableBuilder { * @param columns The columns to use for the table * @returns The same {@link TableBuilder} instance with the columns added */ - public columns(...columns: Table.ColumnOpts[]): TableBuilder { + public columns(...columns: Table.ColumnOpts[]): this { this.data.columns = this.convertColumnOpts(columns); return this; } @@ -121,7 +121,7 @@ export class TableBuilder { * @param columns The column definitions to add to the table * @returns The same {@link TableBuilder} instance with the new column definitions added */ - public addColumns(columns: Table.ColumnOpts[]): TableBuilder { + public addColumns(columns: Table.ColumnOpts[]): this { this.data.columns = [...this.data.columns, ...this.convertColumnOpts(columns)]; return this; } @@ -131,7 +131,7 @@ export class TableBuilder { * @param actions the record of indices to {@link Table.Action} arrays to use for the table * @returns The same {@link TableBuilder} instance with the row actions added */ - public contextOptions(opts: Record): TableBuilder { + public contextOptions(opts: Record): this { for (const [key, optsForKey] of Object.entries(opts)) { for (const opt of optsForKey) { this.addContextOption(key as number | "all", opt); @@ -145,7 +145,7 @@ export class TableBuilder { * @param index The row index to add an option to (or "all" for all rows) * @returns The same {@link TableBuilder} instance with the context menu option added */ - public addContextOption(index: number | "all", option: Table.ContextMenuOpts): TableBuilder { + public addContextOption(index: number | "all", option: Table.ContextMenuOpts): this { if (this.data.contextOpts[index]) { const opts = this.data.contextOpts[index]; this.data.contextOpts[index] = [...opts, { ...option, condition: option.condition?.toString() }]; @@ -160,8 +160,7 @@ export class TableBuilder { * @param actions the record of indices to {@link Table.Action} arrays to use for the table * @returns The same {@link TableBuilder} instance with the row actions added */ - public rowActions(actions: Record): TableBuilder { - //this.data.actions = actions; + public rowActions(actions: Record): this { for (const key of Object.keys(actions)) { this.addRowAction(key as number | "all", actions[key]); } @@ -173,7 +172,7 @@ export class TableBuilder { * @param index The column index to add an action to * @returns The same {@link TableBuilder} instance with the row action added */ - public addRowAction(index: number | "all", action: Table.ActionOpts): TableBuilder { + public addRowAction(index: number | "all", action: Table.ActionOpts): this { if (this.data.actions[index]) { const actionList = this.data.actions[index]; this.data.actions[index] = [...actionList, { ...action, condition: action.condition?.toString() }]; diff --git a/packages/zowe-explorer/src/webviews/src/table-view/ActionsBar.tsx b/packages/zowe-explorer/src/webviews/src/table-view/ActionsBar.tsx index 43501f9d13..7f65786449 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/ActionsBar.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/ActionsBar.tsx @@ -10,7 +10,7 @@ export const ActionsBar = ({ vscodeApi, }: { actions: Table.Action[]; - gridRef: Ref; + gridRef: Ref; itemCount: number; vscodeApi: any; }) => { @@ -35,8 +35,9 @@ export const ActionsBar = ({ {actions .filter((action) => (itemCount > 1 ? action.callback.typ === "multi-row" : action.callback.typ.endsWith("row"))) - .map((action) => ( + .map((action, i) => ( { diff --git a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx index 5b59fb2c6c..5e097549aa 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/App.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/App.tsx @@ -12,9 +12,5 @@ import { TableView } from "./TableView"; export function App() { - return ( - <> - - - ); + return ; } diff --git a/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx index 1771c9deaf..1542c6ef34 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx @@ -113,8 +113,9 @@ export const ContextMenu = (gridRefs: any, menuItems: Table.ContextMenuOption[], // Invoke the wrapped function once to get the built function, then invoke it again with the parameters return cond.call(null).call(null, gridRefs.clickedRow); }) - .map((item, _i) => ( + .map((item, i) => ( { vscodeApi.postMessage({ command: item.command, diff --git a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx index 4edcdd18bb..fb68b82371 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx @@ -19,7 +19,7 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro const [tableData, setTableData] = useState(data); const [theme, setTheme] = useState(baseTheme ?? "ag-theme-quartz"); const [selectionCount, setSelectionCount] = useState(0); - const gridRef = useRef(); + const gridRef = useRef(); const contextMenu = useContextMenu({ options: [ @@ -114,8 +114,9 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro // Invoke the wrapped function once to get the built function, then invoke it again with the parameters return cond.call(null).call(null, params.data); }) - .map((action) => ( + .map((action, i) => ( vscodeApi.postMessage({ diff --git a/packages/zowe-explorer/src/webviews/src/table-view/types.ts b/packages/zowe-explorer/src/webviews/src/table-view/types.ts index 498fbeb9ee..0ba060a14f 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/types.ts +++ b/packages/zowe-explorer/src/webviews/src/table-view/types.ts @@ -24,7 +24,7 @@ export const wrapFn = (s: string) => `{ return ${s} };`; type AgGridThemes = "ag-theme-quartz" | "ag-theme-balham" | "ag-theme-material" | "ag-theme-alpine"; export type TableViewProps = { actionsCellRenderer?: (params: any) => JSXInternal.Element; - baseTheme?: AgGridThemes | string; + baseTheme?: AgGridThemes; data?: Table.ViewOpts; }; From 0a38ea6758f6ef230f5e3d92bb6a01e509f50c23 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 5 Aug 2024 12:06:13 -0400 Subject: [PATCH 097/107] wip: address SonarCloud issues (2/3) Signed-off-by: Trae Yelovich --- .../webviews/src/table-view/ContextMenu.tsx | 2 +- .../src/webviews/src/table-view/TableView.tsx | 154 +++++++++--------- .../src/webviews/src/table-view/types.ts | 8 +- 3 files changed, 80 insertions(+), 84 deletions(-) diff --git a/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx index 1542c6ef34..8e99e3eabe 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/ContextMenu.tsx @@ -111,7 +111,7 @@ export const ContextMenu = (gridRefs: any, menuItems: Table.ContextMenuOption[], // Wrap function to properly handle named parameters const cond = new Function(wrapFn(item.condition)); // Invoke the wrapped function once to get the built function, then invoke it again with the parameters - return cond.call(null).call(null, gridRefs.clickedRow); + return cond()(null, gridRefs.clickedRow); }) .map((item, i) => ( 1 || newData.actions.all?.length > 0) { - // Add an extra column to the end of each row if row actions are present - const rows = newData.rows?.map((row: Table.RowData) => { - return { ...row, actions: "" }; - }); - const columns = [ - ...(newData.columns ?? []), - { - ...(newData.columns.find((col) => col.field === "actions") ?? {}), - // Prevent cells from being selectable - cellStyle: { border: "none", outline: "none" }, - field: "actions", - minWidth: 360, - sortable: false, - suppressSizeToFit: true, - // Support a custom cell renderer for row actions - cellRenderer: - actionsCellRenderer ?? - ((params: any) => - // Render any actions for the given row and actions that apply to all rows - newData.actions[params.rowIndex] || newData.actions["all"] ? ( - - {[...(newData.actions[params.rowIndex] || []), ...(newData.actions["all"] || [])] - .filter((action) => { - if (action.condition == null) { - return true; - } + if (response.command === "ondatachanged") { + // Update received from a VS Code extender; update table state + const newData: Table.ViewOpts = response.data; + if (Object.keys(newData.actions).length > 1 || newData.actions.all?.length > 0) { + // Add an extra column to the end of each row if row actions are present + const rows = newData.rows?.map((row: Table.RowData) => { + return { ...row, actions: "" }; + }); + const columns = [ + ...(newData.columns ?? []), + { + ...(newData.columns.find((col) => col.field === "actions") ?? {}), + // Prevent cells from being selectable + cellStyle: { border: "none", outline: "none" }, + field: "actions", + minWidth: 360, + sortable: false, + suppressSizeToFit: true, + // Support a custom cell renderer for row actions + cellRenderer: + actionsCellRenderer ?? + ((params: any) => + // Render any actions for the given row and actions that apply to all rows + newData.actions[params.rowIndex] || newData.actions["all"] ? ( + + {[...(newData.actions[params.rowIndex] || []), ...(newData.actions["all"] || [])] + .filter((action) => { + if (action.condition == null) { + return true; + } - // Wrap function to properly handle named parameters - const cond = new Function(wrapFn(action.condition)); - // Invoke the wrapped function once to get the built function, then invoke it again with the parameters - return cond.call(null).call(null, params.data); - }) - .map((action, i) => ( - - vscodeApi.postMessage({ - command: action.command, - data: { - rowIndex: params.node.rowIndex, - row: { ...params.data, actions: undefined }, - field: params.colDef.field, - cell: params.colDef.valueFormatter - ? params.colDef.valueFormatter({ - value: params.data[params.colDef.field], - }) - : params.data[params.colDef.field], - }, - }) - } - style={{ marginRight: "0.25em", width: "fit-content" }} - > - {action.title} - - ))} - - ) : null), - }, - ]; - setTableData({ ...newData, rows, columns }); - } else { - setTableData(newData); - } - break; - default: - break; + // Wrap function to properly handle named parameters + const cond = new Function(wrapFn(action.condition)); + // Invoke the wrapped function once to get the built function, then invoke it again with the parameters + return cond()(null, params.data); + }) + .map((action, i) => ( + + vscodeApi.postMessage({ + command: action.command, + data: { + rowIndex: params.node.rowIndex, + row: { ...params.data, actions: undefined }, + field: params.colDef.field, + cell: params.colDef.valueFormatter + ? params.colDef.valueFormatter({ + value: params.data[params.colDef.field], + }) + : params.data[params.colDef.field], + }, + }) + } + style={{ marginRight: "0.25em", width: "fit-content" }} + > + {action.title} + + ))} + + ) : null), + }, + ]; + setTableData({ ...newData, rows, columns }); + } else { + setTableData(newData); + } } }); diff --git a/packages/zowe-explorer/src/webviews/src/table-view/types.ts b/packages/zowe-explorer/src/webviews/src/table-view/types.ts index 0ba060a14f..eda5fceab5 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/types.ts +++ b/packages/zowe-explorer/src/webviews/src/table-view/types.ts @@ -41,10 +41,10 @@ export const tableProps = ( rowData: tableData.rows, columnDefs: tableData.columns?.map((col) => ({ ...col, - comparator: col.comparator ? new Function(wrapFn(col.comparator)).call(null) : undefined, - colSpan: col.colSpan ? new Function(wrapFn(col.colSpan)).call(null) : undefined, - rowSpan: col.rowSpan ? new Function(wrapFn(col.rowSpan)).call(null) : undefined, - valueFormatter: col.valueFormatter ? new Function(wrapFn(col.valueFormatter)).call(null) : undefined, + comparator: col.comparator ? new Function(wrapFn(col.comparator))() : undefined, + colSpan: col.colSpan ? new Function(wrapFn(col.colSpan))() : undefined, + rowSpan: col.rowSpan ? new Function(wrapFn(col.rowSpan))() : undefined, + valueFormatter: col.valueFormatter ? new Function(wrapFn(col.valueFormatter))() : undefined, })), onCellContextMenu: contextMenu.callback, onCellValueChanged: tableData.columns?.some((col) => col.editable) From 337c20eac338bcbcbb58bbc75320e36088dd0676 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 5 Aug 2024 12:26:31 -0400 Subject: [PATCH 098/107] refactor: address SonarCloud issues (3/3) Signed-off-by: Trae Yelovich --- .../src/webviews/src/table-view/TableView.tsx | 69 +------------------ .../webviews/src/table-view/actionsColumn.tsx | 65 +++++++++++++++++ 2 files changed, 68 insertions(+), 66 deletions(-) create mode 100644 packages/zowe-explorer/src/webviews/src/table-view/actionsColumn.tsx diff --git a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx index ed68bcdfe9..8318cc15ba 100644 --- a/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx +++ b/packages/zowe-explorer/src/webviews/src/table-view/TableView.tsx @@ -2,16 +2,16 @@ import "ag-grid-community/styles/ag-grid.css"; // AG Grid Quartz Theme (used as base theme) import "ag-grid-community/styles/ag-theme-quartz.css"; -import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; import { AgGridReact } from "ag-grid-react"; import { useEffect, useRef, useState } from "preact/hooks"; import { getVsCodeTheme, isSecureOrigin, useMutableObserver } from "../utils"; import type { Table } from "@zowe/zowe-explorer-api"; -import { TableViewProps, tableProps, wrapFn } from "./types"; +import { TableViewProps, tableProps } from "./types"; import { useContextMenu } from "./ContextMenu"; // Custom styling (font family, VS Code color scheme, etc.) import "./style.css"; import { ActionsBar } from "./ActionsBar"; +import { actionsColumn } from "./actionsColumn"; const vscodeApi = acquireVsCodeApi(); @@ -77,70 +77,7 @@ export const TableView = ({ actionsCellRenderer, baseTheme, data }: TableViewPro const rows = newData.rows?.map((row: Table.RowData) => { return { ...row, actions: "" }; }); - const columns = [ - ...(newData.columns ?? []), - { - ...(newData.columns.find((col) => col.field === "actions") ?? {}), - // Prevent cells from being selectable - cellStyle: { border: "none", outline: "none" }, - field: "actions", - minWidth: 360, - sortable: false, - suppressSizeToFit: true, - // Support a custom cell renderer for row actions - cellRenderer: - actionsCellRenderer ?? - ((params: any) => - // Render any actions for the given row and actions that apply to all rows - newData.actions[params.rowIndex] || newData.actions["all"] ? ( - - {[...(newData.actions[params.rowIndex] || []), ...(newData.actions["all"] || [])] - .filter((action) => { - if (action.condition == null) { - return true; - } - - // Wrap function to properly handle named parameters - const cond = new Function(wrapFn(action.condition)); - // Invoke the wrapped function once to get the built function, then invoke it again with the parameters - return cond()(null, params.data); - }) - .map((action, i) => ( - - vscodeApi.postMessage({ - command: action.command, - data: { - rowIndex: params.node.rowIndex, - row: { ...params.data, actions: undefined }, - field: params.colDef.field, - cell: params.colDef.valueFormatter - ? params.colDef.valueFormatter({ - value: params.data[params.colDef.field], - }) - : params.data[params.colDef.field], - }, - }) - } - style={{ marginRight: "0.25em", width: "fit-content" }} - > - {action.title} - - ))} - - ) : null), - }, - ]; + const columns = [...(newData.columns ?? []), actionsColumn(newData, actionsCellRenderer, vscodeApi)]; setTableData({ ...newData, rows, columns }); } else { setTableData(newData); diff --git a/packages/zowe-explorer/src/webviews/src/table-view/actionsColumn.tsx b/packages/zowe-explorer/src/webviews/src/table-view/actionsColumn.tsx new file mode 100644 index 0000000000..8923d3198b --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/table-view/actionsColumn.tsx @@ -0,0 +1,65 @@ +import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; +import { TableViewProps, wrapFn } from "./types"; +import { Table } from "@zowe/zowe-explorer-api"; + +export const actionsColumn = (newData: Table.ViewOpts, actionsCellRenderer: TableViewProps["actionsCellRenderer"], vscodeApi: any) => ({ + ...(newData.columns.find((col) => col.field === "actions") ?? {}), + // Prevent cells from being selectable + cellStyle: { border: "none", outline: "none" }, + field: "actions", + minWidth: 360, + sortable: false, + suppressSizeToFit: true, + // Support a custom cell renderer for row actions + cellRenderer: + actionsCellRenderer ?? + ((params: any) => + // Render any actions for the given row and actions that apply to all rows + newData.actions[params.rowIndex] || newData.actions["all"] ? ( + + {[...(newData.actions[params.rowIndex] || []), ...(newData.actions["all"] || [])] + .filter((action) => { + if (action.condition == null) { + return true; + } + + // Wrap function to properly handle named parameters + const cond = new Function(wrapFn(action.condition)); + // Invoke the wrapped function once to get the built function, then invoke it again with the parameters + return cond()(null, params.data); + }) + .map((action, i) => ( + + vscodeApi.postMessage({ + command: action.command, + data: { + rowIndex: params.node.rowIndex, + row: { ...params.data, actions: undefined }, + field: params.colDef.field, + cell: params.colDef.valueFormatter + ? params.colDef.valueFormatter({ + value: params.data[params.colDef.field], + }) + : params.data[params.colDef.field], + }, + }) + } + style={{ marginRight: "0.25em", width: "fit-content" }} + > + {action.title} + + ))} + + ) : null), +}); From 0467ceb833e2668b3c372461db88811f02604c3d Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 5 Aug 2024 12:33:26 -0400 Subject: [PATCH 099/107] chore: add changelog entry to ZE Signed-off-by: Trae Yelovich --- packages/zowe-explorer/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index 10d46414b1..23843b4216 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -44,6 +44,7 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen - Implemented the `onVaultUpdate` VSCode events to notify extenders when credentials are updated on the OS vault by other applications. [#2994](https://github.com/zowe/zowe-explorer-vscode/pull/2994) - Changed default base profile naming scheme in newly generated configuration files to prevent name and property conflicts between Global and Project profiles [#2682](https://github.com/zowe/zowe-explorer-vscode/issues/2682) - Implemented the `onCredMgrUpdate` VSCode events to notify extenders when the local PC's credential manager has been updated by other applications. [#2994](https://github.com/zowe/zowe-explorer-vscode/pull/2994) +- Implemented support for building, exposing and displaying table views within Zowe Explorer. Tables can be customized and exposed using the helper facilities (`TableBuilder` and `TableMediator`) for an extender's specific use case. For more information on how to configure and show tables, please refer to the [wiki article on Table Views](https://github.com/zowe/zowe-explorer-vscode/wiki/Table-Views). [#2258](https://github.com/zowe/zowe-explorer-vscode/issues/2258) ### Bug fixes From 78e27ba89fc8abb6f07eae9e52c29efbcdfab4d1 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 5 Aug 2024 16:29:07 -0400 Subject: [PATCH 100/107] refactor(TableBuilder): use reset instead of initial value for 'data' Signed-off-by: Trae Yelovich --- .../src/vscode/ui/utils/TableBuilder.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index ec3b5dbca2..801ee1d7c8 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -34,20 +34,11 @@ import { TableMediator } from "./TableMediator"; */ export class TableBuilder { private context: ExtensionContext; - private data: Table.ViewOpts = { - actions: { - all: [], - }, - contextOpts: { - all: [], - }, - columns: [], - rows: [], - title: "", - }; + private data: Table.ViewOpts; private forWebviewView = false; public constructor(context: ExtensionContext) { + this.reset(); this.context = context; } From 51a91ef6829585becebf906257de84ff65ee9c39 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 6 Aug 2024 08:56:41 -0400 Subject: [PATCH 101/107] Use optional chaining for panel dispose Co-authored-by: Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com> Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/src/vscode/ui/WebView.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/WebView.ts b/packages/zowe-explorer-api/src/vscode/ui/WebView.ts index bed7a286b8..7af08ad6b5 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/WebView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/WebView.ts @@ -140,9 +140,7 @@ export class WebView { * Disposes of the webview instance */ protected dispose(): void { - if (this.panel) { - this.panel.dispose(); - } + this.panel?.dispose(); for (const disp of this.disposables) { disp.dispose(); From ffc40b081712f3614d952f21a6b832e7c4ed390d Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 6 Aug 2024 09:03:27 -0400 Subject: [PATCH 102/107] chore: update API changelog w/ WebView breaking change Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index 391ccda921..28dab4678b 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -40,6 +40,7 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t - Added the `onCredMgrsUpdate` VSCode event to notify extenders when the local PC's credential manager has been updated by other applications. [#2994](https://github.com/zowe/zowe-explorer-vscode/pull/2994) - **Breaking:** Updated most function signatures for exported programmatic interfaces. Changes make developing with the Zowe Explorer API more efficient for extenders by showing which properties they can expect when calling our APIs. [#2952](https://github.com/zowe/zowe-explorer-vscode/issues/2952) - Implemented support for building, exposing and displaying table views within Zowe Explorer. Tables can be customized and exposed using the helper facilities (`TableBuilder` and `TableMediator`) for an extender's specific use case. For more information on how to configure and show tables, please refer to the [wiki article on Table Views](https://github.com/zowe/zowe-explorer-vscode/wiki/Table-Views). [#2258](https://github.com/zowe/zowe-explorer-vscode/issues/2258) +- **Breaking:** Consolidated WebView API options into a single object (`WebViewOpts` type), both for developer convenience and to support future options. ### Bug fixes From f8c068de2b783a808e94d249a5c64e394f04f4a0 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 6 Aug 2024 09:10:40 -0400 Subject: [PATCH 103/107] refactor: add 'await' comment in cancelJobs, update useMutableObserver Signed-off-by: Trae Yelovich --- packages/zowe-explorer/src/trees/job/JobActions.ts | 4 ++++ packages/zowe-explorer/src/webviews/src/utils.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/zowe-explorer/src/trees/job/JobActions.ts b/packages/zowe-explorer/src/trees/job/JobActions.ts index b8602c8dd1..3cb12c8bf4 100644 --- a/packages/zowe-explorer/src/trees/job/JobActions.ts +++ b/packages/zowe-explorer/src/trees/job/JobActions.ts @@ -460,6 +460,10 @@ export class JobActions { jobsProvider.refreshElement(session); } + // `await`ing the following Gui methods causes unwanted side effects for other features (such as the jobs table view): + // * code execution stops before function returns (unexpected, undefined behavior) + // * before, we used `setImmediate` to delay updates to the jobs tree (to avoid desync), but removing the `await`s resolves the desync. + // * we do not expect the user to respond to these toasts, so we do not need to wait for their promises to be resolved. if (failedJobs.length > 0) { // Display any errors from the API Gui.warningMessage( diff --git a/packages/zowe-explorer/src/webviews/src/utils.ts b/packages/zowe-explorer/src/webviews/src/utils.ts index e6c79b0b12..aeac2aad57 100644 --- a/packages/zowe-explorer/src/webviews/src/utils.ts +++ b/packages/zowe-explorer/src/webviews/src/utils.ts @@ -15,7 +15,7 @@ export function getVsCodeTheme(): string | null { return document.body.getAttribute("data-vscode-theme-kind"); } -export const useMutableObserver = (target: Node, callback: MutationCallback, options: MutationObserverInit | undefined = undefined): void => { +export const useMutableObserver = (target: Node, callback: MutationCallback, options: MutationObserverInit | undefined): void => { useEffect(() => { const mutationObserver = new MutationObserver((mutations, observer) => callback(mutations, observer)); mutationObserver.observe(target, options); From 861bb2c3b96241de65b236f17396f6ebcc6f6e81 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 6 Aug 2024 09:20:29 -0400 Subject: [PATCH 104/107] refactor: move init for zowe-resources provider Signed-off-by: Trae Yelovich --- packages/zowe-explorer/src/extension.ts | 3 --- .../zowe-explorer/src/trees/shared/SharedInit.ts | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/zowe-explorer/src/extension.ts b/packages/zowe-explorer/src/extension.ts index 856d966c98..c4e92f08ae 100644 --- a/packages/zowe-explorer/src/extension.ts +++ b/packages/zowe-explorer/src/extension.ts @@ -23,7 +23,6 @@ import { SharedInit } from "./trees/shared/SharedInit"; import { SharedTreeProviders } from "./trees/shared/SharedTreeProviders"; import { USSInit } from "./trees/uss/USSInit"; import { ProfilesUtils } from "./utils/ProfilesUtils"; -import { TableViewProvider } from "@zowe/zowe-explorer-api"; /** * The function that runs when the extension is loaded @@ -44,8 +43,6 @@ export async function activate(context: vscode.ExtensionContext): Promise USSInit.initUSSProvider(context), job: () => JobInit.initJobsProvider(context), }); - - context.subscriptions.push(vscode.window.registerWebviewViewProvider("zowe-resources", TableViewProvider.getInstance())); SharedInit.registerCommonCommands(context, providers); SharedInit.registerRefreshCommand(context, activate, deactivate); ZoweExplorerExtender.createInstance(providers.ds, providers.uss, providers.job); diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index bd47500f7c..7c2307fabf 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -10,7 +10,17 @@ */ import * as vscode from "vscode"; -import { FileManagement, Gui, IZoweTree, IZoweTreeNode, Validation, ZosEncoding, ZoweScheme, imperative } from "@zowe/zowe-explorer-api"; +import { + FileManagement, + Gui, + IZoweTree, + IZoweTreeNode, + TableViewProvider, + Validation, + ZosEncoding, + ZoweScheme, + imperative, +} from "@zowe/zowe-explorer-api"; import { SharedActions } from "./SharedActions"; import { SharedHistoryView } from "./SharedHistoryView"; import { SharedTreeProviders } from "./SharedTreeProviders"; @@ -81,6 +91,9 @@ export class SharedInit { }) ); + // Contribute the "Zowe Resources" view as a WebviewView panel in Zowe Explorer. + context.subscriptions.push(vscode.window.registerWebviewViewProvider("zowe-resources", TableViewProvider.getInstance())); + // Webview for editing persistent items on Zowe Explorer context.subscriptions.push( vscode.commands.registerCommand("zowe.editHistory", () => { From c9a30617769a27a3b10e12893d0339e530d7a79d Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Fri, 9 Aug 2024 11:29:02 -0400 Subject: [PATCH 105/107] refactor: address feedback (1/2) Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/src/vscode/ui/TableView.ts | 2 +- .../zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index dfe247ddd1..cbca2bd19d 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -107,7 +107,7 @@ export namespace Table { headerName?: string; headerTooltip?: string; headerClass?: string | string[]; - weapHeaderText?: boolean; + wrapHeaderText?: boolean; autoHeaderHeight?: boolean; headerCheckboxSelection?: boolean; diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts index 801ee1d7c8..6a56f74fec 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableBuilder.ts @@ -42,6 +42,10 @@ export class TableBuilder { this.context = context; } + /** + * Prepares the table to be rendered within a WebviewViewProvider (such as the "Zowe Resources" panel) + * @returns The instance of the TableBuilder, with the private `forWebviewView` flag set to `true` + */ public isView(): this { this.forWebviewView = true; return this; From 4620206ac10f179349148b67d898a8dafc58e7d2 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Fri, 9 Aug 2024 13:20:34 -0400 Subject: [PATCH 106/107] refactor: address feedback (2/2) Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/package.json | 1 - packages/zowe-explorer-api/src/vscode/ui/TableView.ts | 6 ++---- .../zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts | 4 ---- packages/zowe-explorer/src/webviews/src/utils.ts | 6 +----- pnpm-lock.yaml | 3 --- 5 files changed, 3 insertions(+), 17 deletions(-) diff --git a/packages/zowe-explorer-api/package.json b/packages/zowe-explorer-api/package.json index 9d3d432c9c..fd8d8e96ea 100644 --- a/packages/zowe-explorer-api/package.json +++ b/packages/zowe-explorer-api/package.json @@ -39,7 +39,6 @@ "@zowe/zosmf-for-zowe-sdk": "8.0.0-next.202407232256", "deep-object-diff": "^1.1.9", "mustache": "^4.2.0", - "preact": "^10.16.0", "semver": "^7.6.0" }, "scripts": { diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index cbca2bd19d..35296cb4cc 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -409,9 +409,7 @@ export namespace Table { * @returns The unique ID for this table view */ public getId(): string { - if (this.uuid == null) { - this.uuid = randomUUID(); - } + this.uuid ??= randomUUID(); return `${this.data.title}-${this.uuid.substring(0, this.uuid.indexOf("-"))}##${this.context.extension.id}`; } @@ -472,7 +470,7 @@ export namespace Table { /** * Update an existing row in the table view. * @param index The AG GRID row index to update within the table - * @param row The new row content + * @param row The new row content. If `null`, the given row index will be deleted from the list of rows. * @returns Whether the webview successfully updated the new row */ public async updateRow(index: number, row: RowData | null): Promise { diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts index 2bd2514b1c..ea7728fe34 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/TableMediator.ts @@ -93,10 +93,6 @@ export class TableMediator { * @returns `true` if the table was deleted; `false` otherwise */ public removeTable(instance: Table.Instance): boolean { - if (Array.from(this.tables.values()).find((table) => table.getId() === instance.getId()) == null) { - return false; - } - return this.tables.delete(instance.getId()); } } diff --git a/packages/zowe-explorer/src/webviews/src/utils.ts b/packages/zowe-explorer/src/webviews/src/utils.ts index aeac2aad57..b08e1d1405 100644 --- a/packages/zowe-explorer/src/webviews/src/utils.ts +++ b/packages/zowe-explorer/src/webviews/src/utils.ts @@ -30,9 +30,5 @@ export function isSecureOrigin(origin: string): boolean { eventUrl.hostname.endsWith(".github.dev"); const isLocalVSCodeUser = eventUrl.protocol === "vscode-webview:"; - if (!isWebUser && !isLocalVSCodeUser) { - return false; - } - - return true; + return isWebUser || isLocalVSCodeUser; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59b6915198..a142928af1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -292,9 +292,6 @@ importers: mustache: specifier: ^4.2.0 version: 4.2.0 - preact: - specifier: ^10.16.0 - version: 10.22.0 semver: specifier: ^7.6.0 version: 7.6.2 From 67407c071ae76333114620540d2a2643426c4ec0 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Fri, 9 Aug 2024 14:12:26 -0400 Subject: [PATCH 107/107] refactor(WebView): unsafeEval option to enable fn evaluation Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/src/vscode/ui/TableView.ts | 1 + packages/zowe-explorer-api/src/vscode/ui/WebView.ts | 4 ++++ .../zowe-explorer-api/src/vscode/ui/utils/HTMLTemplate.ts | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts index 35296cb4cc..597c1963fa 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/TableView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/TableView.ts @@ -318,6 +318,7 @@ export namespace Table { super(data?.title ?? "Table view", "table-view", context, { onDidReceiveMessage: (message) => this.onMessageReceived(message), isView, + unsafeEval: true, }); if (data) { this.data = data; diff --git a/packages/zowe-explorer-api/src/vscode/ui/WebView.ts b/packages/zowe-explorer-api/src/vscode/ui/WebView.ts index 7af08ad6b5..6d461d1820 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/WebView.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/WebView.ts @@ -24,6 +24,8 @@ export type WebViewOpts = { retainContext?: boolean; /** Whether the webview should be prepared for a WebviewViewProvider. */ isView?: boolean; + /** Allow evaluation of functions within the webview script code. */ + unsafeEval?: boolean; }; export type UriPair = { @@ -96,6 +98,7 @@ export class WebView { }; const builtHtml = Mustache.render(HTMLTemplate, { + unsafeEval: this.webviewOpts?.unsafeEval, uris: this.uris, nonce: this.nonce, title: this.title, @@ -123,6 +126,7 @@ export class WebView { }; const builtHtml = Mustache.render(HTMLTemplate, { + unsafeEval: this.webviewOpts?.unsafeEval, uris: this.uris, nonce: this.nonce, title: this.title, diff --git a/packages/zowe-explorer-api/src/vscode/ui/utils/HTMLTemplate.ts b/packages/zowe-explorer-api/src/vscode/ui/utils/HTMLTemplate.ts index 10f511c06e..964734ab84 100644 --- a/packages/zowe-explorer-api/src/vscode/ui/utils/HTMLTemplate.ts +++ b/packages/zowe-explorer-api/src/vscode/ui/utils/HTMLTemplate.ts @@ -21,7 +21,7 @@ const HTMLTemplate: string = `