diff --git a/.vscode/settings.json b/.vscode/settings.json index 65ef9279..aef762fd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,7 +19,7 @@ "editor.formatOnSave": true, "editor.tabSize": 4, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" diff --git a/CHANGELOG.md b/CHANGELOG.md index 88f9634f..85968f90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Change Log +## 1.7.0 (2024-04-08) + +### Changed + +- Reuse existing tabs when opening query grid windows. +- Fixed various small bugs. + +### Added + +- Favourite tables. + ## 1.6.0 (2024-02-07) ### Changed diff --git a/README.md b/README.md index bcf51d8d..cfd96d3e 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ This open source project is in active development. Our goal is to simplify the a - Launch query - Launch query on double click - Select tables form multiple databases + - Favourite tables (_new_) - Indexes - Fields - Filtering @@ -37,7 +38,7 @@ This open source project is in active development. Our goal is to simplify the a - Suggest field names - View record on double-click - Enable/disable filtering as you type - - Query grid table size management (_new_) + - Query grid table size management - Export - Formats - .D file diff --git a/docker/.env b/docker/.env index ca3b54e1..a304921d 100644 --- a/docker/.env +++ b/docker/.env @@ -3,7 +3,7 @@ DB_NAME=sports2020 DB_PORT=30000 DB_MIN_PORT=28020 # Port range must not include ports defined in /etc/services/ DB_MAX_PORT=28050 -IMAGE_NAME_126=gitlab.baltic-amadeus.lt:5005/progress/progress-docker/progress-build:12.6.0-centos +IMAGE_NAME_128=gitlab.baltic-amadeus.lt:5005/progress/progress-docker/progress-build:12.8.1-ubuntu DB_NAME2=sports20201 DB_PORT2=30001 @@ -11,4 +11,4 @@ DB_MIN_PORT2=28051 # Port range must not include ports defined in /etc/services/ DB_MAX_PORT2=28080 # Other Configuration Section -PROGRESS_BASE_IMAGE=gitlab.baltic-amadeus.lt:5005/progress/progress-docker/progress-build:12.6.0-centos \ No newline at end of file +PROGRESS_BASE_IMAGE=gitlab.baltic-amadeus.lt:5005/progress/progress-docker/progress-build:12.8.1-ubuntu \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 29612829..6084784c 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,11 +1,11 @@ version: '3.7' services: - sports-db-126: + sports-db-128: build: context: "./" dockerfile: "./Dockerfile" args: - - IMAGE_NAME=${IMAGE_NAME_126} + - IMAGE_NAME=${IMAGE_NAME_128} - DB_NAME=${DB_NAME} environment: - DB_NAME=${DB_NAME} @@ -15,12 +15,12 @@ services: ports: - ${DB_PORT}:${DB_PORT} - ${DB_MIN_PORT}-${DB_MAX_PORT}:${DB_MIN_PORT}-${DB_MAX_PORT} - sports-db-126-2: + sports-db-128-2: build: context: "./" dockerfile: "./Dockerfile" args: - - IMAGE_NAME=${IMAGE_NAME_126} + - IMAGE_NAME=${IMAGE_NAME_128} - DB_NAME=${DB_NAME2} environment: - DB_NAME=${DB_NAME2} diff --git a/docker/openedge-project.json b/docker/openedge-project.json index a3a79920..8db88284 100644 --- a/docker/openedge-project.json +++ b/docker/openedge-project.json @@ -1,7 +1,7 @@ { "name": "pro-bro", "version": "1.0", - "oeversion": "12.6", + "oeversion": "12.8", "graphicalMode": false, "charset": "utf-8", "extraParameters": "", diff --git a/package.json b/package.json index 790874ee..e0525cfb 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "DB", "Explorer" ], - "version": "1.6.0", + "version": "1.7.0", "repository": { "type": "git", "url": "https://github.com/BalticAmadeus/ProBro" @@ -87,6 +87,11 @@ "default": "true", "markdownDescription": "Use write triggers" }, + "pro-bro.useDeleteTriggers": { + "type": "boolean", + "default": "true", + "markdownDescription": "Use delete triggers" + }, "pro-bro.tempfiles": { "type": "string", "markdownDescription": "Specifies path where temporary files are created." @@ -177,6 +182,12 @@ "name": "Tables", "contextualTitle": "Tables", "title": "Tables" + }, + { + "id": "pro-bro-favorites", + "name": "Table favorites", + "contextualTitle": "Table favorites", + "title": "Table favorites" } ] }, @@ -234,6 +245,25 @@ { "command": "pro-bro.dblClickQuery", "title": "DblClickQuery" + }, + { + "command": "pro-bro.dblClickFavoriteQuery", + "title": "DblClickFavoriteQuery" + }, + { + "command": "pro-bro.addFavourite", + "title": "pro-bro: addFavourite", + "icon": "$(star-empty)" + }, + { + "command": "pro-bro.removeFavourite", + "title": "pro-bro: removeFavourite", + "icon": "$(star-full)" + }, + { + "command": "pro-bro.queryFavorite", + "title": "pro-bro: Query Favorite", + "icon": "$(play)" } ], "menus": { @@ -289,6 +319,21 @@ "command": "pro-bro.query", "when": "view == pro-bro-tables", "group": "inline" + }, + { + "command": "pro-bro.addFavourite", + "when": "view == pro-bro-tables", + "group": "inline" + }, + { + "command": "pro-bro.removeFavourite", + "when": "view == pro-bro-favorites", + "group": "inline" + }, + { + "command": "pro-bro.queryFavorite", + "when": "view == pro-bro-favorites", + "group": "inline" } ] } diff --git a/resources/images/DbConnection.png b/resources/images/DbConnection.png index b1386877..17f6dd7e 100644 Binary files a/resources/images/DbConnection.png and b/resources/images/DbConnection.png differ diff --git a/resources/images/createEditTab.png b/resources/images/createEditTab.png index 592d7b19..4f7484c7 100644 Binary files a/resources/images/createEditTab.png and b/resources/images/createEditTab.png differ diff --git a/resources/images/queryWindow.png b/resources/images/queryWindow.png index b6259452..2ca2946d 100644 Binary files a/resources/images/queryWindow.png and b/resources/images/queryWindow.png differ diff --git a/resources/markdown/manual.md b/resources/markdown/manual.md index e2ca4a65..91948551 100644 --- a/resources/markdown/manual.md +++ b/resources/markdown/manual.md @@ -1,36 +1,48 @@ -# **ProBro user manual** -This document provides guidelines how to use ProBro extension. +# **ProBro user manual** +This document provides guidelines how to use ProBro extension. ## **1. Opening ProBro extension:** + ![Db Connection](../images/DbConnection.png) - 1. ProBro extension image on sidebar. - 2. Create new db connection. - 3. Refresh db connection list. - 4. Delete existing connection. - 5. Edit existing connection. - 6. Group lists of databases. If no group is assigned to database, group is set to "Empty". +1. ProBro extension image on sidebar. +2. Create new db connection. +3. Refresh db connection list. +4. Delete existing connection. +5. Edit existing connection. +6. Launch data administration. +7. Launch data dictionary. +8. Launch procedure editor. +9. Refresh single connection. +10. Group lists of databases. If no group is assigned to database, group is set to "Empty". + +The icon indicates current status of the connection. Every db connection on the start of the extension or upon refreshing are tested to check if they are connected. Otherwise near the OpenEdge logo a red circle appears which indicates that the connection is unsuccessful (Disabled). - Every db connection on start of extension or refreshing are testes if they are connected. Otherwise near the OpenEdge logo appears red sign. +## **2 Creating new connection** - ## **2. Creating new connection** +### **2.1 Importing connections through openedge-project.json** - For creating and editing db connections is used same structure. For editing new tab with selected database information will be displayed. +Connections defined in **_openedge-project.json_** are automatically imported in the extension under seperate connections group. **_dbConnections_** Key must be defined for the connections to be imported. - ![Db Create](../images/createEditTab.png) +### **2.2 Creating new connection through the extension** - - **Connection name:** name of your database which will appear in group list. - - **Group:** name of group where database will be assigned. Default group name is "EMPTY" - - **Physical name:** location of you databases .db file. - - **Description:** optional description of db. - - **Host name:** host of database - - Port: port for database - - **User ID** and **password:** optional credentials if needed to connect to database. - - **Other parameters:** optional credentials if needed to connect database. +For creating and editing db connections is used same structure. For editing new tab with selected database information will be displayed. - ## **3. Table explorer** - Once selected database, table list is displayed. +![Db Create](../images/createEditTab.png) + +- **Connection name:** name of your database which will appear in group list. +- **Group:** name of group where database will be assigned. Default group name is "EMPTY" +- **Physical name:** location of you databases .db file. +- **Description:** optional description of db. +- **Host name:** host of database +- **Port:** port for database +- **User ID** and **password:** optional credentials if needed to connect to database. +- **Other parameters:** optional credentials if needed to connect database. + +## **3. Table explorer** + +Once the database is selected, table list is displayed. When clicked on the table list view, press CTRL + F for the ability to search for a certain table. ![table Explorer](../images/tableExplorer.png) @@ -40,16 +52,17 @@ This document provides guidelines how to use ProBro extension. ## 4. Fields and Indexes explorer - - Select table to see fields and indexes. Fields and Indexes are displayed in panel view. By default, they are on separate tabs, but can be merged by dragging. +- Select table to see fields and indexes. Fields and Indexes are displayed in the panel view. By default, they are on separate tabs, but can be merged by dragging. - - In fields explorer has multi-sorting and multi-filtering options. -Multi-sorting can be used by holding CTRL key and selecting preferred columns. +- Fields explorer has multi-sorting and multi-filtering options. + Multi-sorting can be used by holding CTRL key and selecting preferred columns. -- By selecting field rows, query columns can be shown/hidden. +- By selecting field rows, query columns can be shown/hidden. + - RECID, ROWID fields are hidden by default. They can be enabled through the fields explorer. ## **5. Query window** @@ -58,24 +71,26 @@ In first launch of query, all records are displayed. ProBro extension uses server-side multi-sorting and multi-filtering. ![query window](../images/queryWindow.png) + 1. **Custom query** request can be written to filter records. Query should be formatted like WHERE statement. 2. **Export of records:** - - formats available for exporting: - - JSON; - - Excel; + - formats available for exporting: + - JSON. + - Excel. - CSV. + - dumpFile. - The scope for exporting records can be selected: - table: export all records. ; - Filter: export only filtered records; - Selection: export only selected rows of records. - When selected table or filter scopes, custom queried data will be exported. - Record are exported in user sorted order. -3. **Record display formats:** - - RAW - Json formatted data; - - RAW - OE formatted data. +3. **Format:** + - JSON - JSON formatted data; + - PROGRESS - OE formatted data. 4. **CRUD operations:** - CREATE - create record; - UPDATE - update record. Available only when there is one selected record. - DELETE - deletes one or more selected records. - READ - double clicked on record, all details are shown in popup box. -5. **Data retrieval information:** ProBro extension is using lazy loading for better performance with large tables. Information shows current number of records, recent number of records loaded and record retrieval time in ms. \ No newline at end of file +5. **Data retrieval information:** ProBro extension is using lazy loading for better performance with large tables. Information shows current number of records, recent number of records loaded and record retrieval time in ms. diff --git a/resources/oe/src/oe.p b/resources/oe/src/oe.p index 82fa125b..8daaa8d6 100644 --- a/resources/oe/src/oe.p +++ b/resources/oe/src/oe.p @@ -12,7 +12,7 @@ FIELD cKey AS CHARACTER SERIALIZE-NAME "key" FIELD cLabel AS CHARACTER SERIALIZE-NAME "label" FIELD cType AS CHARACTER SERIALIZE-NAME "type" FIELD cFormat AS CHARACTER SERIALIZE-NAME "format" -FIELD iExtent AS INTEGER +FIELD iExtent AS INTEGER . message PROPATH. diff --git a/resources/oe/src/oeCore.i b/resources/oe/src/oeCore.i index 265a7dfa..af2d3080 100644 --- a/resources/oe/src/oeCore.i +++ b/resources/oe/src/oeCore.i @@ -169,8 +169,8 @@ PROCEDURE LOCAL_GET_TABLE_DETAILS: DO WHILE qhField:GET-NEXT(): IF bhField::_order > iFieldOrder - THEN ASSIGN - iFieldOrder = bhField::_order. + THEN ASSIGN + iFieldOrder = bhField::_order. jsonField = NEW Progress.Json.ObjectModel.JsonObject(). jsonField:Add("order", bhField::_order). @@ -403,8 +403,8 @@ FUNCTION GET_COLUMNS RETURNS Progress.Json.ObjectModel.JsonArray (lDumpFile AS L END. END. END. - // Add ROWID and RECID - DO iCount = 1 TO 2: + // Add ROWID and RECID + DO iCount = 1 TO 2: CREATE bttColumn. bttColumn.cName = ENTRY(iCount,"RECID,ROWID"). bttColumn.cKey = bttColumn.cName. @@ -433,7 +433,7 @@ FUNCTION GET_MODE RETURNS CHARACTER (): END FUNCTION. FUNCTION GET_FORMATTED_COLUMN_NAME RETURNS CHARACTER (cTableName AS CHARACTER, cColumnKey AS CHARACTER): - IF cColumnKey NE "ROWID" + IF cColumnKey NE "ROWID" AND cColumnKey NE "RECID" THEN DO: RETURN SUBSTITUTE("&1.&2", cTableName, cColumnKey). END. @@ -497,7 +497,7 @@ FUNCTION GET_ORDER_PHRASE RETURNS CHARACTER (): IF inputObject:GetJsonObject("params"):has("sortColumns") THEN DO: jsonSort = inputObject:GetJsonObject("params"):GetJsonArray("sortColumns"). - DO iChar = 1 TO jsonSort:Length: + DO iChar = 1 TO jsonSort:Length: cOrderPhrase = SUBSTITUTE("&1 BY &2 &3", cOrderPhrase, GET_FORMATTED_COLUMN_NAME(inputObject:GetJsonObject("params"):GetCharacter("tableName"), @@ -771,7 +771,8 @@ PROCEDURE LOCAL_SUBMIT_TABLE_DATA: DEFINE VARIABLE bh AS HANDLE NO-UNDO. DEFINE VARIABLE fhKey AS HANDLE NO-UNDO. DEFINE VARIABLE cMode AS CHARACTER NO-UNDO. - DEFINE VARIABLE lUseTriggers AS LOGICAL NO-UNDO. + DEFINE VARIABLE lUseWriteTriggers AS LOGICAL NO-UNDO. + DEFINE VARIABLE lUseDeleteTriggers AS LOGICAL NO-UNDO. DEFINE VARIABLE i AS INTEGER NO-UNDO. @@ -787,9 +788,15 @@ PROCEDURE LOCAL_SUBMIT_TABLE_DATA: cMode = inputObject:GetJsonObject("params"):GetCharacter("mode"). - lUseTriggers = inputObject:GetJsonObject("params"):GetLogical("useTriggers"). + lUseWriteTriggers = inputObject:GetJsonObject("params"):GetLogical("useWriteTriggers"). + lUseDeleteTriggers = inputObject:GetJsonObject("params"):GetLogical("useDeleteTriggers"). IF cMode = "DELETE" THEN DO: + IF lUseDeleteTriggers = false THEN DO: + bh:DISABLE-LOAD-TRIGGERS(FALSE). + bh:DISABLE-DUMP-TRIGGERS( ). + END. + DO i = 1 TO jsonCrud:Length: IF qh:REPOSITION-TO-ROWID(TO-ROWID(jsonCrud:GetCharacter(i))) THEN DO: IF qh:GET-NEXT(EXCLUSIVE-LOCK, NO-WAIT) THEN DO: @@ -810,7 +817,7 @@ PROCEDURE LOCAL_SUBMIT_TABLE_DATA: END. ELSE DO: IF cMode = "INSERT" OR cMode = "COPY" THEN DO: - IF lUseTriggers = false THEN DO: + IF lUseWriteTriggers = false THEN DO: bh:DISABLE-LOAD-TRIGGERS(FALSE). bh:DISABLE-DUMP-TRIGGERS( ). END. @@ -818,7 +825,7 @@ PROCEDURE LOCAL_SUBMIT_TABLE_DATA: bh:BUFFER-CREATE(). END. ELSE IF cMode = "UPDATE" THEN DO: - IF lUseTriggers = false THEN DO: + IF lUseWriteTriggers = false THEN DO: bh:DISABLE-LOAD-TRIGGERS(FALSE). bh:DISABLE-DUMP-TRIGGERS( ). END. @@ -850,4 +857,3 @@ PROCEDURE LOCAL_SUBMIT_TABLE_DATA: END. END. END PROCEDURE. - diff --git a/src/common/IExtensionSettings.ts b/src/common/IExtensionSettings.ts index 81d90b65..271af415 100644 --- a/src/common/IExtensionSettings.ts +++ b/src/common/IExtensionSettings.ts @@ -1,16 +1,17 @@ export interface ISettings { - batchSize: number; - batchMaxTimeout: number; - batchMinTimeout: number; - initialBatchSizeLoad: number; - logging: ILogging; - useWriteTriggers: boolean; - filterAsYouType: boolean; - gridTextSize: string; + batchSize: number; + batchMaxTimeout: number; + batchMinTimeout: number; + initialBatchSizeLoad: number; + logging: ILogging; + useWriteTriggers: boolean; + filterAsYouType: boolean; + useDeleteTriggers: boolean; + gridTextSize: string; } export interface ILogging { - react: boolean; - node: boolean; - openEdge: string; + react: boolean; + node: boolean; + openEdge: string; } diff --git a/src/common/commands/fieldsCommands.ts b/src/common/commands/fieldsCommands.ts new file mode 100644 index 00000000..1d5c37ed --- /dev/null +++ b/src/common/commands/fieldsCommands.ts @@ -0,0 +1,6 @@ +import { ICommand } from '../../view/app/model'; + +export interface HighlightFieldsCommand extends ICommand { + column: string; + tableName: string; +} diff --git a/src/db/DatabaseProcessor.ts b/src/db/DatabaseProcessor.ts index 39f49922..1a819a09 100644 --- a/src/db/DatabaseProcessor.ts +++ b/src/db/DatabaseProcessor.ts @@ -77,7 +77,7 @@ export class DatabaseProcessor implements IProcessor { } } - public getTableDetails( + public async getTableDetails( config: IConfig | undefined, tableName: string | undefined ): Promise { @@ -87,19 +87,27 @@ export class DatabaseProcessor implements IProcessor { command: 'get_table_details', params: tableName, }; - return this.execShell(params); + return { + ...(await this.execShell(params)), + tableName: tableName ?? '', + }; } else { - return Promise.resolve({ fields: [], indexes: [] }); + return Promise.resolve({ + fields: [], + indexes: [], + tableName: tableName ?? '', + }); } } private execShell(params: IOEParams): Promise { const cmd = `${Buffer.from(JSON.stringify(params)).toString('base64')}`; if ( - // if process is running and not timed out + // if process is running and not timed out DatabaseProcessor.processStartTime !== undefined && - DatabaseProcessor.processStartTime + DatabaseProcessor.processTimeout > - Date.now() + DatabaseProcessor.processStartTime + + DatabaseProcessor.processTimeout > + Date.now() ) { // then return error vscode.window.showInformationMessage('Processor is busy'); @@ -109,7 +117,9 @@ export class DatabaseProcessor implements IProcessor { if (DatabaseProcessor.errObj.isError) { // vscode.window.showErrorMessage(DatabaseProcessor.errObj.errMessage); - return Promise.resolve(new Error(DatabaseProcessor.errObj.errMessage)); + return Promise.resolve( + new Error(DatabaseProcessor.errObj.errMessage) + ); } DatabaseProcessor.processStartTime = Date.now(); diff --git a/src/extension.ts b/src/extension.ts index 16814858..7e617aa7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,6 +16,8 @@ import { readFile, parseOEFile } from './common/OpenEdgeJsonReaded'; import { VersionChecker } from './view/app/Welcome/VersionChecker'; import { WelcomePageProvider } from './webview/WelcomePageProvider'; import { AblHoverProvider } from './providers/AblHoverProvider'; +import { queryEditorCache } from './webview/queryEditor/queryEditorCache'; +import { FavoritesProvider } from './treeview/FavoritesProvider'; export function activate(context: vscode.ExtensionContext) { let extensionPort: number; @@ -223,7 +225,22 @@ export function activate(context: vscode.ExtensionContext) { const tablesListProvider = new TablesListProvider( fieldsProvider, - indexesProvider + indexesProvider, + context + ); + + const favoritesProvider = new FavoritesProvider( + fieldsProvider, + indexesProvider, + context + ); + + const favorites = vscode.window.createTreeView( + `${Constants.globalExtensionKey}-favorites`, + { treeDataProvider: favoritesProvider } + ); + favorites.onDidChangeSelection((e) => + favoritesProvider.onDidChangeSelection(e) ); const tables = vscode.window.createTreeView( @@ -249,7 +266,56 @@ export function activate(context: vscode.ExtensionContext) { ); groups.onDidChangeSelection((e) => - groupListProvider.onDidChangeSelection(e, tablesListProvider) + groupListProvider.onDidChangeSelection( + e, + tablesListProvider, + favoritesProvider + ) + ); + + /** + * Creates a new query editor or if already open, then reveals it from cache and refetch data + */ + const loadQueryEditor = (node: TableNode): void => { + const key = node.getFullName(true) ?? ''; + + const cachedQueryEditor = queryEditorCache.getQueryEditor(key); + + if (cachedQueryEditor) { + cachedQueryEditor.panel?.reveal(); + cachedQueryEditor.refetchData(); + return; + } + + const newQueryEditor = new QueryEditor( + context, + node, + tablesListProvider, + favoritesProvider, + fieldsProvider + ); + + queryEditorCache.setQueryEditor(key, newQueryEditor); + }; + + context.subscriptions.push( + vscode.commands.registerCommand( + `${Constants.globalExtensionKey}.addFavourite`, + (node: TableNode) => { + favoritesProvider.addTableToFavorites(node); + favoritesProvider.refresh(undefined); + } + ) + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + `${Constants.globalExtensionKey}.removeFavourite`, + (node: TableNode) => { + favoritesProvider.removeTableFromFavorites(node); + favoritesProvider.refresh(undefined); + } + ) ); context.subscriptions.push( @@ -277,12 +343,17 @@ export function activate(context: vscode.ExtensionContext) { `${Constants.globalExtensionKey}.query`, (node: TableNode) => { tablesListProvider.selectDbConfig(node); - new QueryEditor( - context, - node, - tablesListProvider, - fieldsProvider - ); + loadQueryEditor(node); + } + ) + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + `${Constants.globalExtensionKey}.queryFavorite`, + (node: TableNode) => { + favoritesProvider.selectDbConfig(node); + loadQueryEditor(node); } ) ); @@ -295,12 +366,7 @@ export function activate(context: vscode.ExtensionContext) { return; } - return new QueryEditor( - context, - tablesListProvider.node, - tablesListProvider, - fieldsProvider - ); + loadQueryEditor(tablesListProvider.node); } ) ); @@ -404,17 +470,30 @@ export function activate(context: vscode.ExtensionContext) { } ); + vscode.commands.registerCommand( + `${Constants.globalExtensionKey}.dblClickFavoriteQuery`, + () => { + if (favoritesProvider.node === undefined) { + return; + } + + favoritesProvider.countClick(); + if (favoritesProvider.tableClicked.count === 2) { + loadQueryEditor(favoritesProvider.node); + } + } + ); + vscode.commands.registerCommand( `${Constants.globalExtensionKey}.dblClickQuery`, - (_) => { + () => { + if (tablesListProvider.node === undefined) { + return; + } + tablesListProvider.countClick(); if (tablesListProvider.tableClicked.count === 2) { - new QueryEditor( - context, - tablesListProvider.node as TableNode, - tablesListProvider, - fieldsProvider - ); + loadQueryEditor(tablesListProvider.node); } } ); diff --git a/src/repo/processor/database/DbProcessor.ts b/src/repo/processor/database/DbProcessor.ts index 858e5eee..f66e2917 100644 --- a/src/repo/processor/database/DbProcessor.ts +++ b/src/repo/processor/database/DbProcessor.ts @@ -120,9 +120,16 @@ export class DbProcessor implements IProcessor { command: 'get_table_details', params: tableName, }; - return this.execRequest(config, params); + return { + ...(await this.execRequest(config, params)), + tableName: tableName ?? '', + }; } else { - return Promise.resolve({ fields: [], indexes: [] }); + return Promise.resolve({ + fields: [], + indexes: [], + tableName: tableName ?? '', + }); } } @@ -133,9 +140,9 @@ export class DbProcessor implements IProcessor { const cmd = `${Buffer.from(JSON.stringify(params)).toString('base64')}`; if ( - // if process is running and not timed out + // if process is running and not timed out this.processStartTime !== undefined && - this.processStartTime + this.processTimeout > Date.now() + this.processStartTime + this.processTimeout > Date.now() ) { // then return error vscode.window.showInformationMessage('Processor is busy'); diff --git a/src/treeview/FavoritesProvider.ts b/src/treeview/FavoritesProvider.ts new file mode 100644 index 00000000..0e28578a --- /dev/null +++ b/src/treeview/FavoritesProvider.ts @@ -0,0 +1,155 @@ +import * as vscode from 'vscode'; +import { TableNode, TableNodeSourceEnum } from './TableNode'; +import { TablesListProvider } from './TablesListProvider'; +import { PanelViewProvider } from '../webview/PanelViewProvider'; +import { IConfig } from '../view/app/model'; +import { Constants } from '../common/Constants'; + +export class FavoritesProvider extends TablesListProvider { + public _onDidChangeTreeData: vscode.EventEmitter< + TableNode | undefined | void + > = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = + this._onDidChangeTreeData.event; + public configs: IConfig[] | undefined; + + constructor( + fieldsProvider: PanelViewProvider, + indexesProvider: PanelViewProvider, + context: vscode.ExtensionContext + ) { + super(fieldsProvider, indexesProvider, context); + } + + /** + * Gets the tree item for a given table node. Adds a command to open the favorite table when double-clicked. + * @param element The table node to get the tree item for. + * @returns The tree item for the provided table node. + */ + public getTreeItem(element: TableNode): vscode.TreeItem { + const treeItem = element.getTreeItem(); + + treeItem.command = { + command: `${Constants.globalExtensionKey}.dblClickFavoriteQuery`, + title: 'Open Favorite Table', + arguments: [element], + }; + + return treeItem; + } + + /** + * Retrieves the list of favorite table nodes. + * @returns A promise that resolves to an array of TableNode objects representing the favorites. + */ + private async getFavorites(): Promise { + const favoritesData = this.context.globalState.get< + { + dbId: string; + tableName: string; + tableType: string; + connectionName: string; + connectionLabel: string; + }[] + >('favorites', []); + + const filteredFavorites = favoritesData.filter((favorite) => + this.configs?.some((config) => config.id === favorite.dbId) + ); + + const favorites = filteredFavorites.map( + (data) => + new TableNode( + this.context, + data.dbId, + data.tableName, + data.tableType, + data.connectionName, + data.connectionLabel, + TableNodeSourceEnum.Favorites + ) + ); + + return favorites; + } + + /** + * Adds a table to the list of favorites. + * @param node The table node to add to favorites. + */ + public addTableToFavorites(node: TableNode): void { + const favorites = this.context.globalState.get< + { + dbId: string; + tableName: string; + tableType: string; + connectionName: string; + connectionLabel: string; + }[] + >('favorites', []); + + const isAlreadyFavorite = favorites.some( + (fav) => fav.tableName === node.tableName && fav.dbId === node.dbId + ); + + if (isAlreadyFavorite) { + return; + } + favorites.push({ + dbId: node.dbId, + tableName: node.tableName, + tableType: node.tableType, + connectionName: node.connectionName, + connectionLabel: node.connectionLabel, + }); + + this.context.globalState.update('favorites', favorites); + vscode.window.showInformationMessage( + `Added ${node.tableName} to favorites.` + ); + } + + /** + * Removes a table from the list of favorites. + * @param node The table node to remove from favorites. + */ + public removeTableFromFavorites(node: TableNode): void { + const favorites = this.context.globalState.get< + { + dbId: string; + tableName: string; + tableType: string; + connectionName: string; + connectionLabel: string; + }[] + >('favorites', []); + + if (favorites.length === 0) { + return; + } + + const filteredFavorites = favorites.filter( + (fav) => + !(fav.tableName === node.tableName && fav.dbId === node.dbId) + ); + this.context.globalState.update('favorites', filteredFavorites); + vscode.window.showInformationMessage( + `Removed ${node.tableName} from favorites.` + ); + } + + public async getChildren(element?: TableNode): Promise { + if (!element) { + return this.getFavorites(); + } else { + return []; + } + } + + public refresh(configs: IConfig[] | undefined): void { + if (configs !== undefined) { + this.configs = configs; + } + this._onDidChangeTreeData.fire(); + } +} diff --git a/src/treeview/GroupListProvider.ts b/src/treeview/GroupListProvider.ts index fc34c727..0807dbfa 100644 --- a/src/treeview/GroupListProvider.ts +++ b/src/treeview/GroupListProvider.ts @@ -6,28 +6,32 @@ import { IConfig } from '../view/app/model'; import { TablesListProvider } from './TablesListProvider'; import { DbConnectionNode } from './DbConnectionNode'; import { IRefreshCallback } from './IRefreshCallback'; +import { FavoritesProvider } from './FavoritesProvider'; export class GroupListProvider -implements vscode.TreeDataProvider, IRefreshCallback + implements vscode.TreeDataProvider, IRefreshCallback { - private _onDidChangeTreeData: vscode.EventEmitter = - new vscode.EventEmitter(); + private _onDidChangeTreeData: vscode.EventEmitter< + INode | undefined | void + > = new vscode.EventEmitter(); readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; constructor( - private context: vscode.ExtensionContext, - private tables: vscode.TreeView + private context: vscode.ExtensionContext, + private tables: vscode.TreeView ) {} onDidChangeSelection( e: vscode.TreeViewSelectionChangeEvent, - tablesListProvider: vscode.TreeDataProvider + tablesListProvider: vscode.TreeDataProvider, + favoritesProvider: vscode.TreeDataProvider ): any { if (e.selection.length) { if ( e.selection[0] instanceof DbConnectionNode && - tablesListProvider instanceof TablesListProvider + tablesListProvider instanceof TablesListProvider && + favoritesProvider instanceof FavoritesProvider ) { const nodes = e.selection as DbConnectionNode[]; const configs: IConfig[] = []; @@ -38,10 +42,12 @@ implements vscode.TreeDataProvider, IRefreshCallback console.log('GroupList', configs); tablesListProvider.refresh(configs); + favoritesProvider.refresh(configs); return; } } (tablesListProvider as TablesListProvider).refresh(undefined); + (favoritesProvider as FavoritesProvider).refresh(undefined); } refresh(): void { @@ -63,12 +69,12 @@ implements vscode.TreeDataProvider, IRefreshCallback private async getGroupNodes(): Promise { const connections = this.context.globalState.get<{ - [key: string]: IConfig; - }>(`${Constants.globalExtensionKey}.dbconfig`); + [key: string]: IConfig; + }>(`${Constants.globalExtensionKey}.dbconfig`); const workspaceConnections = this.context.workspaceState.get<{ - [key: string]: IConfig; - }>(`${Constants.globalExtensionKey}.dbconfig`); + [key: string]: IConfig; + }>(`${Constants.globalExtensionKey}.dbconfig`); const groupNodes: groupNode.GroupNode[] = []; const groupNames: string[] = []; @@ -80,7 +86,9 @@ implements vscode.TreeDataProvider, IRefreshCallback } if (groupNames.indexOf(group) === -1) { groupNames.push(group); - groupNodes.push(new groupNode.GroupNode(this.context, group, this)); + groupNodes.push( + new groupNode.GroupNode(this.context, group, this) + ); } } } @@ -93,7 +101,9 @@ implements vscode.TreeDataProvider, IRefreshCallback } if (groupNames.indexOf(group) === -1) { groupNames.push(group); - groupNodes.push(new groupNode.GroupNode(this.context, group, this)); + groupNodes.push( + new groupNode.GroupNode(this.context, group, this) + ); } } } diff --git a/src/treeview/TableNode.ts b/src/treeview/TableNode.ts index c0052660..9a944f4a 100644 --- a/src/treeview/TableNode.ts +++ b/src/treeview/TableNode.ts @@ -2,24 +2,35 @@ import * as vscode from 'vscode'; import { TableDetails } from '../view/app/model'; import { INode } from './INode'; +export enum TableNodeSourceEnum { + Tables = 'tables', + Favorites = 'favorites', +} + export class TableNode implements INode { + public dbId: string; public tableName: string; public tableType: string; public connectionName: string; public connectionLabel: string; public cache: TableDetails | undefined; + public source: TableNodeSourceEnum; constructor( - private context: vscode.ExtensionContext, - tableName: string, - tableType: string, - connectionName: string, - connectionLabel: string + private context: vscode.ExtensionContext, + dbId: string, + tableName: string, + tableType: string, + connectionName: string, + connectionLabel: string, + source: TableNodeSourceEnum ) { + this.dbId = dbId; this.tableName = tableName; this.tableType = tableType; this.connectionName = connectionName; this.connectionLabel = connectionLabel; + this.source = source; } public getTreeItem(): vscode.TreeItem { @@ -35,7 +46,8 @@ export class TableNode implements INode { return []; } - public getFullName(): string { - return this.connectionLabel + '.' + this.tableName; + public getFullName(includeId = false): string { + const dbString = includeId ? `${this.dbId}_` : ''; + return `${dbString}${this.connectionLabel}.${this.tableName}`; } } diff --git a/src/treeview/TablesListProvider.ts b/src/treeview/TablesListProvider.ts index f76f1a96..45b3650f 100644 --- a/src/treeview/TablesListProvider.ts +++ b/src/treeview/TablesListProvider.ts @@ -3,7 +3,7 @@ import { INode } from './INode'; import * as tableNode from './TableNode'; import { IConfig, TableCount } from '../view/app/model'; import { ProcessorFactory } from '../repo/processor/ProcessorFactory'; -import { TableNode } from './TableNode'; +import { TableNode, TableNodeSourceEnum } from './TableNode'; import { PanelViewProvider } from '../webview/PanelViewProvider'; import { getAllColumnsCache, @@ -21,14 +21,16 @@ export class TablesListProvider implements vscode.TreeDataProvider { public tableClicked: TableCount = { tableName: undefined, count: 0 }; constructor( - private fieldsProvider: PanelViewProvider, - private indexesProvider: PanelViewProvider - ) {} + public fieldsProvider: PanelViewProvider, + public indexesProvider: PanelViewProvider, + public context: vscode.ExtensionContext + ) { + this.context = context; + } public displayData(node: TableNode, useCache = true) { this.fieldsProvider.tableNode = node; this.indexesProvider.tableNode = node; - console.log('displayData', node.tableName); if (useCache && node.cache) { this.fieldsProvider._view?.webview.postMessage({ id: '1', @@ -208,10 +210,12 @@ export class TablesListProvider implements vscode.TreeDataProvider { this.tableNodes?.push( new tableNode.TableNode( Constants.context, + config.id, table.name, table.tableType, connectionName, - connectionLabel + connectionLabel, + TableNodeSourceEnum.Tables ) ); } diff --git a/src/view/app/Components/Layout/Query/QueryDropdownMenu.tsx b/src/view/app/Components/Layout/Query/QueryDropdownMenu.tsx index a4cf7504..9b07cc9c 100644 --- a/src/view/app/Components/Layout/Query/QueryDropdownMenu.tsx +++ b/src/view/app/Components/Layout/Query/QueryDropdownMenu.tsx @@ -41,13 +41,36 @@ const QueryDropdownMenu: React.FC = ({ keepMounted open={Boolean(anchorEl)} onClose={() => setAnchorEl(null)} + sx={{ + '& .MuiPaper-root': { + backgroundColor: 'var(--vscode-input-background)', + size: 'small', + }, + }} > {Object.values(FormatType).map((format) => ( - handleFormat(format)}> - - {selectedOption === format && } + handleFormat(format)} + sx={{ + color: 'var(--vscode-input-foreground)', + }} + > + + {selectedOption === format && ( + + )} - + ))} diff --git a/src/view/app/Connection/connectionForm.tsx b/src/view/app/Connection/connectionForm.tsx index 72d59a5c..d2b7cf9e 100644 --- a/src/view/app/Connection/connectionForm.tsx +++ b/src/view/app/Connection/connectionForm.tsx @@ -5,23 +5,23 @@ import FileUploadRoundedIcon from '@mui/icons-material/FileUploadRounded'; import { PfParser } from '../utils/PfParser'; import { Logger } from '../../../common/Logger'; import { ISettings } from '../../../common/IExtensionSettings'; +import { getVSCodeAPI } from '@utils/vscode'; interface IConfigProps { - vscode: any; - initialData: IConfig; - configuration: ISettings; + initialData: IConfig; + configuration: ISettings; } interface IConfigState { - config: IConfig; + config: IConfig; } function ConnectionForm({ - vscode, initialData, configuration, ...props }: IConfigProps) { + const vscode = getVSCodeAPI(); const oldState = vscode.getState(); const initState = oldState ? oldState : { config: initialData }; const [vsState, _] = React.useState(initState); @@ -74,15 +74,18 @@ function ConnectionForm({ const messageEvent = (event) => { const message = event.data; switch (message.command) { - case 'group': - setGroupNames(message.columns); - if (groupNames.length === 0) { - getGroups(); - } else { - createListener(document.getElementById('input'), groupNames); - } + case 'group': + setGroupNames(message.columns); + if (groupNames.length === 0) { + getGroups(); + } else { + createListener( + document.getElementById('input'), + groupNames + ); + } - break; + break; } }; @@ -176,7 +179,8 @@ function ConnectionForm({ if (e.key === 'Enter' && selected !== null) { e.preventDefault(); - const selectedText = document.querySelector('.selected').textContent; + const selectedText = + document.querySelector('.selected').textContent; setGroup(selectedText); createListener(document.getElementById('input'), groupNames); @@ -195,7 +199,9 @@ function ConnectionForm({ item.classList.remove('selected'); }); if (selected.previousElementSibling === null) { - selected.parentElement.lastElementChild.classList.add('selected'); + selected.parentElement.lastElementChild.classList.add( + 'selected' + ); } else { selected.previousElementSibling.classList.add('selected'); } @@ -219,7 +225,9 @@ function ConnectionForm({ }); if (selected.nextElementSibling === null) { - selected.parentElement.firstElementChild.classList.add('selected'); + selected.parentElement.firstElementChild.classList.add( + 'selected' + ); } else { selected.nextElementSibling.classList.add('selected'); } @@ -244,23 +252,28 @@ function ConnectionForm({ } function mouseoverListener() { - document.querySelectorAll('.autocomplete-list li').forEach(function (item) { - item.addEventListener('mouseover', function () { - document - .querySelectorAll('.autocomplete-list li') - .forEach(function (item) { - item.classList.remove('selected'); - }); - this.classList.add('selected'); - }); - item.addEventListener('click', function () { - setGroup(this.innerHTML); - document.getElementById('input').focus(); - setTimeout(() => { - createListener(document.getElementById('input'), groupNames); - }, 301); + document + .querySelectorAll('.autocomplete-list li') + .forEach(function (item) { + item.addEventListener('mouseover', function () { + document + .querySelectorAll('.autocomplete-list li') + .forEach(function (item) { + item.classList.remove('selected'); + }); + this.classList.add('selected'); + }); + item.addEventListener('click', function () { + setGroup(this.innerHTML); + document.getElementById('input').focus(); + setTimeout(() => { + createListener( + document.getElementById('input'), + groupNames + ); + }, 301); + }); }); - }); } function hideSuggestions() { @@ -276,26 +289,26 @@ function ConnectionForm({ return ( -
-
-
Connect to server
+
+
+
Connect to server
{!workState && ( } > - Import .pf + Import .pf )}
-
-
-
-
+
+ +
+
{ setLabel(event.target.value); @@ -303,11 +316,11 @@ function ConnectionForm({ disabled={workState} />
-
+
{ getGroups(); @@ -323,12 +336,15 @@ function ConnectionForm({ disabled={workState} onKeyDown={handleKeyDown} /> -
    +
      -
      +
      { setName(event.target.value); @@ -337,11 +353,11 @@ function ConnectionForm({ />
      -
      -
      +
      +
      { setDescription(event.target.value); @@ -350,11 +366,11 @@ function ConnectionForm({ />
      -
      -
      +
      +
      { setHost(event.target.value); @@ -362,10 +378,10 @@ function ConnectionForm({ disabled={workState} />
      -
      +
      { setPort(event.target.value); @@ -373,10 +389,10 @@ function ConnectionForm({ disabled={workState} />
      -
      +
      { setUser(event.target.value); @@ -384,10 +400,10 @@ function ConnectionForm({ disabled={workState} />
      -
      +
      { setPassword(event.target.value); @@ -395,10 +411,10 @@ function ConnectionForm({ disabled={workState} />
      -
      +
      { setParams(event.target.value); @@ -407,15 +423,23 @@ function ConnectionForm({ />
      -
      +
      {!workState && ( -
      - +
      +
      )} {!workState && ( -
      - +
      +
      )}
      diff --git a/src/view/app/Connection/index.tsx b/src/view/app/Connection/index.tsx index 458a98d1..3cfbeb9b 100644 --- a/src/view/app/Connection/index.tsx +++ b/src/view/app/Connection/index.tsx @@ -13,13 +13,10 @@ declare global { } } -const vscode = window.acquireVsCodeApi(); - const root = createRoot(document.getElementById('root')); root.render( ); diff --git a/src/view/app/Fields/fields.tsx b/src/view/app/Fields/fields.tsx index 7fd05d19..6b8c7245 100644 --- a/src/view/app/Fields/fields.tsx +++ b/src/view/app/Fields/fields.tsx @@ -18,6 +18,7 @@ import { Logger } from '../../../common/Logger'; import * as columnName from './column.json'; import { OEDataTypePrimitive } from '@utils/oe/oeDataTypeEnum'; import { getVSCodeAPI, getVSCodeConfiguration } from '@utils/vscode'; +import { HighlightFieldsCommand } from '@src/common/commands/fieldsCommands'; interface FieldsExplorerEvent { id: string; @@ -25,10 +26,6 @@ interface FieldsExplorerEvent { data: TableDetails; } -interface IConfigProps { - tableDetails: TableDetails; -} - const filterCSS: React.CSSProperties = { inlineSize: '100%', padding: '4px', @@ -71,13 +68,14 @@ function rowKeyGetter(row: FieldRow) { return row.order; } -function Fields({ tableDetails }: Readonly) { - const [rows, setRows] = useState(tableDetails.fields); +function Fields() { + const [rows, setRows] = useState([]); const [dataLoaded, setDataLoaded] = useState(false); const [sortColumns, setSortColumns] = useState([]); const [selectedRows, setSelectedRows] = useState>(); const [windowHeight, setWindowHeight] = useState(window.innerHeight); const [filteredRows, setFilteredRows] = useState(rows); + const [tableName, setTableName] = useState(''); const vscode = getVSCodeAPI(); const configuration = getVSCodeConfiguration(); @@ -262,6 +260,7 @@ function Fields({ tableDetails }: Readonly) { : 'no'; } }); + setTableName(message.data.tableName); setRows(message.data.fields); setFilteredRows(message.data.fields); setDataLoaded(true); @@ -306,6 +305,7 @@ function Fields({ tableDetails }: Readonly) { ) ); } + break; } } ); @@ -332,6 +332,17 @@ function Fields({ tableDetails }: Readonly) { vscode.postMessage(obj); }; + const onRowDoubleClick = (row: FieldRow) => { + const obj: HighlightFieldsCommand = { + id: 'highlightColumn', + action: CommandAction.FieldsHighlightColumn, + column: row.name, + tableName: tableName, + }; + logger.log('highlight column', obj); + vscode.postMessage(obj); + }; + return (
      {!dataLoaded ? ( @@ -354,6 +365,7 @@ function Fields({ tableDetails }: Readonly) { sortColumns={sortColumns} onSortColumnsChange={setSortColumns} style={{ height: windowHeight }} + onRowDoubleClick={onRowDoubleClick} /> ) : null}
      diff --git a/src/view/app/Fields/index.tsx b/src/view/app/Fields/index.tsx index bee34421..882bcdfb 100644 --- a/src/view/app/Fields/index.tsx +++ b/src/view/app/Fields/index.tsx @@ -1,14 +1,12 @@ import { createRoot } from 'react-dom/client'; import './fields.css'; import Fields from './fields'; -import { TableDetails } from '@app/model'; declare global { interface Window { acquireVsCodeApi(): any; - tableDetails: TableDetails; } } const root = createRoot(document.getElementById('root')); -root.render(); +root.render(); diff --git a/src/view/app/Indexes/index.tsx b/src/view/app/Indexes/index.tsx index f69e6975..6f49d067 100644 --- a/src/view/app/Indexes/index.tsx +++ b/src/view/app/Indexes/index.tsx @@ -1,5 +1,4 @@ import { createRoot } from 'react-dom/client'; -import { TableDetails } from '@app/model'; import './indexes.css'; import Indexes from './indexes'; import { ISettings } from '@src/common/IExtensionSettings'; @@ -7,17 +6,9 @@ import { ISettings } from '@src/common/IExtensionSettings'; declare global { interface Window { acquireVsCodeApi(): any; - tableDetails: TableDetails; configuration: ISettings; } } -const vscode = window.acquireVsCodeApi(); const root = createRoot(document.getElementById('root')); -root.render( - -); +root.render(); diff --git a/src/view/app/Indexes/indexes.tsx b/src/view/app/Indexes/indexes.tsx index 8f6dd65f..0ce122b5 100644 --- a/src/view/app/Indexes/indexes.tsx +++ b/src/view/app/Indexes/indexes.tsx @@ -1,29 +1,23 @@ import * as React from 'react'; import { useState, useMemo } from 'react'; -import { CommandAction, IndexRow, TableDetails } from '../model'; +import { CommandAction, IndexRow } from '../model'; import DataGrid from 'react-data-grid'; import type { SortColumn } from 'react-data-grid'; import * as columnName from './column.json'; import { Logger } from '../../../common/Logger'; -import { ISettings } from '../../../common/IExtensionSettings'; - -interface IConfigProps { - vscode: any - tableDetails: TableDetails - configuration: ISettings; -} +import { getVSCodeAPI, getVSCodeConfiguration } from '@utils/vscode'; type Comparator = (a: IndexRow, b: IndexRow) => number; function getComparator(sortColumn: string): Comparator { switch (sortColumn) { - case 'cName': - case 'cFlags': - case 'cFields': - return (a, b) => { - return a[sortColumn].localeCompare(b[sortColumn]); - }; - default: - throw new Error(`unsupported sortColumn: "${sortColumn}"`); + case 'cName': + case 'cFlags': + case 'cFields': + return (a, b) => { + return a[sortColumn].localeCompare(b[sortColumn]); + }; + default: + throw new Error(`unsupported sortColumn: "${sortColumn}"`); } } @@ -31,23 +25,29 @@ function rowKeyGetter(row: IndexRow) { return row.cName; } -function Indexes({ tableDetails, configuration, vscode }: IConfigProps) { - const [rows, setRows] = useState(tableDetails.indexes); +function Indexes() { + const [rows, setRows] = useState([]); const [dataLoaded, setDataLoaded] = useState(false); const [sortColumns, setSortColumns] = useState([]); const [selectedRows, setSelectedRows] = useState>( () => new Set() ); const [windowHeight, setWindowHeight] = React.useState(window.innerHeight); + const vscode = getVSCodeAPI(); + const configuration = getVSCodeConfiguration(); const logger = new Logger(configuration.logging.react); const windowRezise = () => { setWindowHeight(window.innerHeight); }; - window.addEventListener('contextmenu', e => { - e.stopImmediatePropagation(); - }, true); + window.addEventListener( + 'contextmenu', + (e) => { + e.stopImmediatePropagation(); + }, + true + ); React.useEffect(() => { window.addEventListener('resize', windowRezise); @@ -79,15 +79,16 @@ function Indexes({ tableDetails, configuration, vscode }: IConfigProps) { const message = event.data; logger.log('indexes explorer data', message); switch (message.command) { - case 'data': - setRows(message.data.indexes); - setDataLoaded(true); + case 'data': + setRows(message.data.indexes); + setDataLoaded(true); } }); }); const refresh = () => { const obj = { + id: '2', action: CommandAction.RefreshTableData, }; logger.log('Refresh Table Data', obj); @@ -96,12 +97,10 @@ function Indexes({ tableDetails, configuration, vscode }: IConfigProps) { return (
      - {!dataLoaded ? ( - - ) : rows.length > 0 ? ( ; @@ -23,6 +30,28 @@ export interface UpdatePopupProps { isWindowSmall: boolean; } +interface UpdateCheckboxProps extends CheckboxProps { + children: ReactNode; +} + +const UpdateCheckbox: React.FC = ({ + children, + ...otherProps +}) => { + return ( + + + {children} + + } + control={} + > + + ); +}; + const UpdatePopup: React.FC = ({ selectedRows, tableName, @@ -37,9 +66,13 @@ const UpdatePopup: React.FC = ({ isWindowSmall, }) => { const configuration = getVSCodeConfiguration(); - const defaultTrigger = !!configuration.useWriteTriggers; + const defaultWriteTrigger = configuration.useWriteTriggers ?? true; + const defaultDeleteTrigger = configuration.useDeleteTriggers ?? true; - const [useTriggers, setUseTriggers] = useState(defaultTrigger); // !! fixes missing setting issue + const [useWriteTriggers, setUseWriteTriggers] = + useState(defaultWriteTrigger); + const [useDeleteTriggers, setUseDeleteTriggers] = + useState(defaultDeleteTrigger); const vscode = getVSCodeAPI(); const logger = new Logger(configuration.logging.react); const table = []; @@ -86,8 +119,7 @@ const UpdatePopup: React.FC = ({ processRecord(ProcessAction.Copy); }; - logger.log('crud action', action); - if (action !== ProcessAction.Delete) { + if (action >= 0 && action !== ProcessAction.Delete) { switch (action) { case ProcessAction.Update: case ProcessAction.Insert: @@ -201,24 +233,31 @@ const UpdatePopup: React.FC = ({ data: submitData, mode: ProcessAction[action], minTime: 0, - useTriggers: useTriggers, + useWriteTriggers: useWriteTriggers, + useDeleteTriggers: useDeleteTriggers, }, }; - setUseTriggers(defaultTrigger); + setUseWriteTriggers(defaultWriteTrigger); + setUseDeleteTriggers(defaultDeleteTrigger); logger.log('crud submit data', command); vscode.postMessage(command); }; - function listenForCheck() { - const checkbox = document.getElementById( - 'myCheckbox' - ) as HTMLInputElement; + const onTriggerCheckboxClick = (e: MouseEvent) => { + const target = e.target as HTMLInputElement; - checkbox.addEventListener('change', () => { - setUseTriggers(checkbox.checked); - }); - } + switch (action) { + case ProcessAction.Delete: + setUseDeleteTriggers(target.checked); + break; + case ProcessAction.Copy: + case ProcessAction.Insert: + case ProcessAction.Update: + setUseWriteTriggers(target.checked); + break; + } + }; return ( @@ -230,11 +269,19 @@ const UpdatePopup: React.FC = ({
      {action === ProcessAction.Delete ? ( -
      - Are You sure You want delete{' '} - {selectedRows.size} record - {selectedRows.size > 1 && 's'}? -
      + + + Are You sure You want delete{' '} + {selectedRows.size} record + {selectedRows.size > 1 && 's'}? + + + Use delete trigger + + ) : action === ProcessAction.Read ? ( <> @@ -242,20 +289,17 @@ const UpdatePopup: React.FC = ({
      ) : ( -
      + {table}
      - -
      + + )}
      @@ -266,11 +310,23 @@ const UpdatePopup: React.FC = ({ > {ProcessAction[action]} - ) : null} + ) : ( + } + onClick={() => { + setOpen(false); + updateRecord(); + }} + > + UPDATE + + )} { - setUseTriggers(defaultTrigger); + setUseWriteTriggers(defaultWriteTrigger); + setUseDeleteTriggers(defaultDeleteTrigger); setOpen(false); }} > diff --git a/src/view/app/Query/query.tsx b/src/view/app/Query/query.tsx index b28131da..6d599b07 100644 --- a/src/view/app/Query/query.tsx +++ b/src/view/app/Query/query.tsx @@ -7,7 +7,12 @@ import { useState, } from 'react'; -import DataGrid, { SortColumn, SelectColumn, CopyEvent } from 'react-data-grid'; +import DataGrid, { + SortColumn, + SelectColumn, + CopyEvent, + DataGridHandle, +} from 'react-data-grid'; import { IOETableData } from '@src/db/Oe'; import { CommandAction, ICommand, ProcessAction } from '../model'; @@ -21,6 +26,7 @@ import QueryFormHead from '@app/Components/Layout/Query/QueryFormHead'; import { IFilters } from '@app/common/types'; import { getVSCodeAPI, getVSCodeConfiguration } from '@utils/vscode'; import { green, red } from '@mui/material/colors'; +import { HighlightFieldsCommand } from '@src/common/commands/fieldsCommands'; const filterCSS: CSSProperties = { inlineSize: '100%', @@ -72,6 +78,7 @@ function QueryForm({ const [initialDataLoad, setInitialDataLoad] = useState(true); const [recordColor, setRecordColor] = useState('red'); const [selectedRows, setSelectedRows] = useState>(new Set()); + const queryGridRef = useRef(null); const configuration = getVSCodeConfiguration(); const logger = new Logger(configuration.logging.react); @@ -127,13 +134,34 @@ function QueryForm({ }; }, []); + const highlightColumn = (column: string) => { + // + 1 because columns start from index 1. Rows start from index 0. + const columnIdx: number = selectedColumns.indexOf(column) + 1; + + if (!columnIdx || columnIdx < 0) { + return; + } + + const cellHeight = getCellHeight(); + const rowIdx = Math.floor(scrollHeight / cellHeight); + + // scrollToColumn doesn't work, so a workaround is to use selectCell and rowIdx + queryGridRef.current?.selectCell({ idx: columnIdx, rowIdx: rowIdx }); + }; + const messageEvent = (event) => { const message = event.data; logger.log('got query data', message); switch (message.command) { + case 'highlightColumn': + highlightColumn((message as HighlightFieldsCommand).column); + break; case 'columns': setSelectedColumns([...message.columns]); break; + case 'refetch': + prepareQuery(); + break; case 'submit': if (message.data.error) { // should be displayed in UpdatePopup window @@ -435,28 +463,33 @@ function QueryForm({ minTime: minTime, }, }; + if (!isLoading) { + setIsLoading(true); + } logger.log('make query', command); vscode.postMessage(command); } - function isAtBottom({ currentTarget }: UIEvent): boolean { + const isAtBottom = ({ + currentTarget, + }: UIEvent): boolean => { return ( currentTarget.scrollTop + 10 >= currentTarget.scrollHeight - currentTarget.clientHeight ); - } + }; - function isHorizontalScroll({ + const isHorizontalScroll = ({ currentTarget, - }: UIEvent): boolean { + }: UIEvent): boolean => { return currentTarget.scrollTop === scrollHeight; - } + }; - async function handleScroll(event: UIEvent) { + const handleScroll = (event: UIEvent) => { + setScrollHeight(event.currentTarget.scrollTop); if (isLoading || !isAtBottom(event) || isHorizontalScroll(event)) { return; } - setScrollHeight(event.currentTarget.scrollTop); setIsLoading(true); makeQuery( loaded, @@ -467,7 +500,7 @@ function QueryForm({ configuration.batchMaxTimeout, configuration.batchMinTimeout ); - } + }; function onSortClick(inputSortColumns: SortColumn[]) { if (isLoading) { @@ -510,7 +543,10 @@ function QueryForm({ const [action, setAction] = useState(); const [readRow, setReadRow] = useState([]); - const readRecord = (row: string[]) => { + const readRecord = (row) => { + const selectedRowsSet = new Set(); + selectedRowsSet.add(row.ROWID); + setSelectedRows(selectedRowsSet); setAction(ProcessAction.Read); setReadRow(row); setOpen(true); @@ -541,18 +577,22 @@ function QueryForm({ } } - const calculateHeight = () => { - const rowCount = isFormatted ? formattedRows.length : rawRows.length; - let minHeight; + const getCellHeight = () => { if (configuration.gridTextSize === 'Large') { - minHeight = 40; + return 40; } else if (configuration.gridTextSize === 'Medium') { - minHeight = 30; + return 30; } else if (configuration.gridTextSize === 'Small') { - minHeight = 20; + return 20; } + return 30; + }; + + const calculateHeight = () => { + const rowCount = isFormatted ? formattedRows.length : rawRows.length; + const cellHeight = getCellHeight(); const startingHeight = 85; - const calculatedHeight = startingHeight + rowCount * minHeight; + const calculatedHeight = startingHeight + rowCount * cellHeight; return calculatedHeight; }; @@ -603,6 +643,7 @@ function QueryForm({ /> val.tableName === command.tableName + ); + + firstEditor?.panel?.reveal(); + firstEditor?.highlightColumn(command.column); + } + public resolveWebviewView( webviewView: vscode.WebviewView ): void | Thenable { @@ -56,6 +70,11 @@ export class FieldsViewProvider extends PanelViewProvider { ); this.notifyQueryEditors(); break; + case CommandAction.FieldsHighlightColumn: + this.highlightQueryEditorsColumn( + command as HighlightFieldsCommand + ); + break; } }); } diff --git a/src/webview/PanelViewProvider.ts b/src/webview/PanelViewProvider.ts index 6aa1d768..c7cfb2a2 100644 --- a/src/webview/PanelViewProvider.ts +++ b/src/webview/PanelViewProvider.ts @@ -1,15 +1,17 @@ import path = require('path'); import * as vscode from 'vscode'; import { Constants } from '../common/Constants'; -import { CommandAction, ICommand, TableDetails } from '../view/app/model'; +import { CommandAction, ICommand } from '../view/app/model'; import { TableNode } from '../treeview/TableNode'; import { TablesListProvider } from '../treeview/TablesListProvider'; +import { FavoritesProvider } from '../treeview/FavoritesProvider'; export class PanelViewProvider implements vscode.WebviewViewProvider { public static readonly viewType = `${Constants.globalExtensionKey}-panel`; public _view?: vscode.WebviewView; public tableNode?: TableNode; public tableListProvider?: TablesListProvider; + public favoritesProvider?: FavoritesProvider; public readonly configuration = vscode.workspace.getConfiguration( Constants.globalExtensionKey ); @@ -28,13 +30,9 @@ export class PanelViewProvider implements vscode.WebviewViewProvider { ), ], }; - this._view.webview.html = this.getWebviewContent({ - fields: [], - indexes: [], - selectedColumns: [], - }); + this._view.webview.html = this.getWebviewContent(); - this._view.onDidChangeVisibility((ev) => { + this._view.onDidChangeVisibility(() => { if (this._view?.visible) { if (this.tableNode) { this.tableListProvider?.displayData(this.tableNode); @@ -53,7 +51,7 @@ export class PanelViewProvider implements vscode.WebviewViewProvider { }); } - private getWebviewContent(data: TableDetails): string { + private getWebviewContent(): string { // Local path to main script run in the webview const reactAppPathOnDisk = vscode.Uri.file( path.join( @@ -83,7 +81,6 @@ export class PanelViewProvider implements vscode.WebviewViewProvider { diff --git a/src/webview/QueryEditor.ts b/src/webview/QueryEditor.ts index a5e1ed34..5dacb329 100644 --- a/src/webview/QueryEditor.ts +++ b/src/webview/QueryEditor.ts @@ -1,17 +1,19 @@ import path = require('path'); import * as vscode from 'vscode'; -import { ICommand, CommandAction } from '../view/app/model'; +import { ICommand, CommandAction, IConfig } from '../view/app/model'; import { IOETableData } from '../db/Oe'; -import { TableNode } from '../treeview/TableNode'; +import { TableNode, TableNodeSourceEnum } from '../treeview/TableNode'; import { TablesListProvider } from '../treeview/TablesListProvider'; import { FieldsViewProvider } from './FieldsViewProvider'; import { DumpFileFormatter } from './DumpFileFormatter'; import { Logger } from '../common/Logger'; import { ProcessorFactory } from '../repo/processor/ProcessorFactory'; import { Constants } from '../common/Constants'; +import { queryEditorCache } from './queryEditor/queryEditorCache'; +import { FavoritesProvider } from '../treeview/FavoritesProvider'; export class QueryEditor { - private readonly panel: vscode.WebviewPanel | undefined; + public readonly panel: vscode.WebviewPanel | undefined; private readonly extensionPath: string; private disposables: vscode.Disposable[] = []; public tableName: string; @@ -23,28 +25,43 @@ export class QueryEditor { private logger = new Logger(this.configuration.get('logging.node')!); constructor( - private context: vscode.ExtensionContext, - private tableNode: TableNode, - private tableListProvider: TablesListProvider, - private fieldProvider: FieldsViewProvider + private context: vscode.ExtensionContext, + private tableNode: TableNode, + private tableListProvider: TablesListProvider, + private favoritesProvider: FavoritesProvider, + private fieldProvider: FieldsViewProvider ) { this.extensionPath = context.asAbsolutePath(''); this.tableName = tableNode.tableName; this.fieldsProvider = fieldProvider; - if (tableListProvider.config) { - this.readOnly = tableListProvider.config?.isReadOnly; + let config: IConfig | undefined; + switch (this.tableNode.source) { + case TableNodeSourceEnum.Tables: + config = this.tableListProvider.config; + break; + case TableNodeSourceEnum.Favorites: + config = this.favoritesProvider.config; + break; + default: + return; + } + + if (config) { + this.readOnly = config?.isReadOnly; } this.panel = vscode.window.createWebviewPanel( 'queryOETable', // Identifies the type of the webview. Used internally - `${this.tableListProvider.config?.label}.${this.tableNode.tableName}`, // Title of the panel displayed to the user + `${config?.label}.${this.tableNode.tableName}`, // Title of the panel displayed to the user vscode.ViewColumn.One, // Editor column to show the new webview panel in. { enableScripts: true, retainContextWhenHidden: true, localResourceRoots: [ - vscode.Uri.file(path.join(context.asAbsolutePath(''), 'out')), + vscode.Uri.file( + path.join(context.asAbsolutePath(''), 'out') + ), ], } ); @@ -79,121 +96,129 @@ export class QueryEditor { (command: ICommand) => { this.logger.log('command:', command); switch (command.action) { - case CommandAction.Query: - if (this.tableListProvider.config) { - ProcessorFactory.getProcessorInstance() - .getTableData( - this.tableListProvider.config, - this.tableNode.tableName, - command.params - ) - .then((oe) => { - if (this.panel) { - const obj = { - id: command.id, - command: 'data', - columns: tableNode.cache?.selectedColumns, - data: oe, - }; - this.logger.log('data:', obj); - this.panel?.webview.postMessage(obj); - } - }); - } - break; - case CommandAction.CRUD: - if (this.tableListProvider.config) { - ProcessorFactory.getProcessorInstance() - .getTableData( - this.tableListProvider.config, - this.tableNode.tableName, - command.params - ) - .then((oe) => { - if (this.panel) { - const obj = { - id: command.id, - command: 'crud', - data: oe, - }; - this.logger.log('data:', obj); - this.panel?.webview.postMessage(obj); - } - }); - } - break; - case CommandAction.Submit: - if (this.tableListProvider.config) { - ProcessorFactory.getProcessorInstance() - .submitTableData( - this.tableListProvider.config, - this.tableNode.tableName, - command.params - ) - .then((oe) => { - if (this.panel) { - const obj = { - id: command.id, - command: 'submit', - data: oe, - }; - this.logger.log('data:', obj); - if ( - obj.data.description !== null && - obj.data.description !== undefined - ) { - if (obj.data.description === '') { - vscode.window.showErrorMessage( - 'Database Error: Trigger canceled action' - ); + case CommandAction.Query: + if (config) { + ProcessorFactory.getProcessorInstance() + .getTableData( + config, + this.tableNode.tableName, + command.params + ) + .then((oe) => { + if (this.panel) { + const obj = { + id: command.id, + command: 'data', + columns: + tableNode.cache + ?.selectedColumns, + data: oe, + }; + this.logger.log('data:', obj); + this.panel?.webview.postMessage(obj); + } + }); + } + break; + case CommandAction.CRUD: + if (config) { + ProcessorFactory.getProcessorInstance() + .getTableData( + config, + this.tableNode.tableName, + command.params + ) + .then((oe) => { + if (this.panel) { + const obj = { + id: command.id, + command: 'crud', + data: oe, + }; + this.logger.log('data:', obj); + this.panel?.webview.postMessage(obj); + } + }); + } + break; + case CommandAction.Submit: + if (config) { + ProcessorFactory.getProcessorInstance() + .submitTableData( + config, + this.tableNode.tableName, + command.params + ) + .then((oe) => { + if (this.panel) { + const obj = { + id: command.id, + command: 'submit', + data: oe, + }; + this.logger.log('data:', obj); + if ( + obj.data.description !== null && + obj.data.description !== undefined + ) { + if (obj.data.description === '') { + vscode.window.showErrorMessage( + 'Database Error: Trigger canceled action' + ); + } else { + vscode.window.showErrorMessage( + 'Database Error: ' + + obj.data.description + ); + } } else { - vscode.window.showErrorMessage( - 'Database Error: ' + obj.data.description + vscode.window.showInformationMessage( + 'Action was successful' ); } - } else { - vscode.window.showInformationMessage( - 'Action was successful' - ); + this.panel?.webview.postMessage(obj); } - this.panel?.webview.postMessage(obj); - } - }); - } - break; - case CommandAction.Export: - if (this.tableListProvider.config) { - ProcessorFactory.getProcessorInstance() - .getTableData( - this.tableListProvider.config, - this.tableNode.tableName, - command.params - ) - .then((oe) => { - if (this.panel) { - let exportData = oe; - if (command.params?.exportType === 'dumpFile') { - const dumpFileFormatter = new DumpFileFormatter(); - dumpFileFormatter.formatDumpFile( - oe, - this.tableNode.tableName, - this.tableListProvider.config!.label - ); - exportData = dumpFileFormatter.getDumpFile(); + }); + } + break; + case CommandAction.Export: + if (config) { + ProcessorFactory.getProcessorInstance() + .getTableData( + config, + this.tableNode.tableName, + command.params + ) + .then((oe) => { + if (this.panel) { + let exportData = oe; + if ( + command.params?.exportType === + 'dumpFile' + ) { + const dumpFileFormatter = + new DumpFileFormatter(); + dumpFileFormatter.formatDumpFile( + oe, + this.tableNode.tableName, + config!.label + ); + exportData = + dumpFileFormatter.getDumpFile(); + } + const obj = { + id: command.id, + command: 'export', + tableName: this.tableNode.tableName, + data: exportData, + format: command.params!.exportType, + }; + this.logger.log('data:', obj); + this.panel?.webview.postMessage(obj); } - const obj = { - id: command.id, - command: 'export', - tableName: this.tableNode.tableName, - data: exportData, - format: command.params!.exportType, - }; - this.logger.log('data:', obj); - this.panel?.webview.postMessage(obj); - } - }); - } - break; + }); + } + break; } }, undefined, @@ -204,6 +229,9 @@ export class QueryEditor { this.panel.onDidDispose( () => { + queryEditorCache.removeQueryEditor( + this.tableNode?.getFullName(true) + ); // When the panel is closed, cancel any future updates to the webview content this.fieldsProvider.removeQueryEditor(this); }, @@ -212,6 +240,14 @@ export class QueryEditor { ); } + public refetchData = (): void => { + const obj = { + command: 'refetch', + }; + this.logger.log('refetch:', obj); + this.panel?.webview.postMessage(obj); + }; + public updateFields() { const obj = { command: 'columns', @@ -221,17 +257,33 @@ export class QueryEditor { this.panel?.webview.postMessage(obj); } + /** + * Creates a request to the frontend to highlight a column + * @param {string} column column name to highlight for the table + */ + public highlightColumn(column: string) { + const obj = { + command: 'highlightColumn', + column: column, + }; + this.logger.log('highlighColumn:', obj); + this.panel?.webview.postMessage(obj); + } + private getWebviewContent(tableData: IOETableData): string { - // Local path to main script run in the webview + // Local path to main script run in the webview const reactAppPathOnDisk = vscode.Uri.file( path.join( vscode.Uri.file( - this.context.asAbsolutePath(path.join('out/view/app', 'query.js')) + this.context.asAbsolutePath( + path.join('out/view/app', 'query.js') + ) ).fsPath ) ); - const reactAppUri = this.panel?.webview.asWebviewUri(reactAppPathOnDisk); + const reactAppUri = + this.panel?.webview.asWebviewUri(reactAppPathOnDisk); const cspSource = this.panel?.webview.cspSource; return ` diff --git a/src/webview/queryEditor/queryEditorCache.ts b/src/webview/queryEditor/queryEditorCache.ts new file mode 100644 index 00000000..145f2805 --- /dev/null +++ b/src/webview/queryEditor/queryEditorCache.ts @@ -0,0 +1,39 @@ +import { QueryEditor } from '../QueryEditor'; + +const cache = new Map(); + +/** + * Gets the QueryEditor instance associated with a given table name. + * @param tableName The name of the table for which to retrieve the QueryEditor. + * @returns The QueryEditor instance if found; undefined otherwise. + */ +export const getQueryEditor = (tableName: string): QueryEditor | undefined => { + return cache.get(tableName); +}; + +/** + * Sets or updates the QueryEditor instance associated with a given table name. + * If a QueryEditor for the table already exists, it will be replaced. + * @param tableName The name of the table for which to set the QueryEditor. + * @param queryEditor The QueryEditor instance to associate with the table name. + */ +export const setQueryEditor = ( + tableName: string, + queryEditor: QueryEditor +): void => { + cache.set(tableName, queryEditor); +}; + +/** + * Removes the QueryEditor instance associated with a given table name from the cache. + * @param tableName The name of the table for which to remove the QueryEditor. + */ +export const removeQueryEditor = (tableName: string): void => { + cache.delete(tableName); +}; + +export const queryEditorCache = { + getQueryEditor, + setQueryEditor, + removeQueryEditor, +};