From 77c8cb68d81bed855b605369180ce6f8eb12819d Mon Sep 17 00:00:00 2001 From: Nacho Cordon Date: Wed, 23 Jul 2025 15:28:43 +0100 Subject: [PATCH] Adds ability to delete all connections --- packages/vscode-extension/package.json | 11 ++ .../src/commandHandlers/connection.ts | 28 +++ packages/vscode-extension/src/constants.ts | 2 + .../src/registrationService.ts | 5 + packages/vscode-extension/src/uiUtils.ts | 10 ++ .../vscode-extension/syntaxes/cypher.json | 2 +- .../tests/specs/unit/executeCommands.spec.ts | 166 +++++++++++++++++- 7 files changed, 221 insertions(+), 3 deletions(-) diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json index 7a9263c65..b4fdd3d3f 100644 --- a/packages/vscode-extension/package.json +++ b/packages/vscode-extension/package.json @@ -70,6 +70,12 @@ "category": "Neo4j", "icon": "$(trash)" }, + { + "command": "neo4j.deleteAllConnections", + "title": "Delete all connections", + "category": "Neo4j", + "icon": "$(trash)" + }, { "command": "neo4j.refreshConnections", "title": "Refresh connections", @@ -206,6 +212,11 @@ "when": "view == neo4jConnections", "group": "navigation" }, + { + "command": "neo4j.deleteAllConnections", + "when": "view == neo4jConnections", + "group": "navigation" + }, { "command": "neo4j.clearParameters", "when": "view == neo4jParameters", diff --git a/packages/vscode-extension/src/commandHandlers/connection.ts b/packages/vscode-extension/src/commandHandlers/connection.ts index 688cb8ecd..ea674f827 100644 --- a/packages/vscode-extension/src/commandHandlers/connection.ts +++ b/packages/vscode-extension/src/commandHandlers/connection.ts @@ -4,6 +4,7 @@ import { Connection, deleteConnectionAndUpdateDatabaseConnection, getActiveConnection, + getAllConnections, getConnectionByKey, getConnections, getPasswordForConnection, @@ -15,6 +16,7 @@ import { CONSTANTS } from '../constants'; import { getExtensionContext, getQueryRunner } from '../contextService'; import { ConnectionItem } from '../treeviews/connectionTreeDataProvider'; import { + displayConfirmAllConnectionsDeletionPrompt, displayConfirmConnectionDeletionPrompt, displayMessageForConnectionResult, displayMessageForSwitchDatabaseResult, @@ -119,6 +121,32 @@ export async function promptUserToDeleteConnectionAndDisplayConnectionResult( } } +/** + * Handler for DELETE_ALL_CONNECTION_COMMAND (neo4j.deleteAllConnections) + * This can be triggered from the Connection tree view and from the Command Palette + * Deletes all connections + * @param connectionItem The ConnectionItem to delete. + * @returns A promise that resolves when the handler has completed. + */ +export async function promptUserToDeleteAllConnectionsAndDisplayConnectionResult(): Promise { + const result = await displayConfirmAllConnectionsDeletionPrompt(); + + if (result === 'Yes') { + const connections = getAllConnections(); + await Promise.allSettled( + connections.map((connection) => + deleteConnectionAndUpdateDatabaseConnection(connection.key), + ), + ); + void commands.executeCommand( + CONSTANTS.COMMANDS.REFRESH_CONNECTIONS_COMMAND, + ); + void window.showInformationMessage( + CONSTANTS.MESSAGES.ALL_CONNECTIONS_DELETED, + ); + } +} + /** * Handler for CONNECT_COMMAND and DISCONNECT_COMMAND (neo4j.connect and neo4j.disconnect) * This may only be triggered from the Connection tree view. diff --git a/packages/vscode-extension/src/constants.ts b/packages/vscode-extension/src/constants.ts index e7e54dcdc..87522bb8e 100644 --- a/packages/vscode-extension/src/constants.ts +++ b/packages/vscode-extension/src/constants.ts @@ -6,6 +6,7 @@ export const CONSTANTS = { EDIT_CONNECTION_COMMAND: 'neo4j.editConnection', DELETE_CONNECTION_COMMAND: 'neo4j.deleteConnection', REFRESH_CONNECTIONS_COMMAND: 'neo4j.refreshConnections', + DELETE_ALL_CONNECTIONS_COMMAND: 'neo4j.deleteAllConnections', CONNECT_COMMAND: 'neo4j.connect', DISCONNECT_COMMAND: 'neo4j.disconnect', SWITCH_DATABASE_COMMAND: 'neo4j.switchDatabase', @@ -32,6 +33,7 @@ export const CONSTANTS = { RECONNECTED_MESSAGE: 'Reconnected to Neo4j.', CONNECTION_SAVED: 'Neo4j connection saved.', CONNECTION_DELETED: 'Neo4j connection deleted.', + ALL_CONNECTIONS_DELETED: 'All Neo4j connections have been deleted.', CONNECTION_VALIDATION_MESSAGE: 'Please fill in all required fields.', SUCCESSFULLY_SWITCHED_DATABASE_MESSAGE: 'Switched to database', ERROR_SWITCHING_DATABASE_MESSAGE: 'Error switching to database', diff --git a/packages/vscode-extension/src/registrationService.ts b/packages/vscode-extension/src/registrationService.ts index 6ebfcef5a..18003975a 100644 --- a/packages/vscode-extension/src/registrationService.ts +++ b/packages/vscode-extension/src/registrationService.ts @@ -4,6 +4,7 @@ import { cypherFileFromSelection, forceConnect, forceDisconnect, + promptUserToDeleteAllConnectionsAndDisplayConnectionResult, promptUserToDeleteConnectionAndDisplayConnectionResult, runCypher, saveConnectionAndDisplayConnectionResult, @@ -98,6 +99,10 @@ export function registerDisposables(): Disposable[] { CONSTANTS.COMMANDS.DELETE_CONNECTION_COMMAND, promptUserToDeleteConnectionAndDisplayConnectionResult, ), + commands.registerCommand( + CONSTANTS.COMMANDS.DELETE_ALL_CONNECTIONS_COMMAND, + promptUserToDeleteAllConnectionsAndDisplayConnectionResult, + ), commands.registerCommand( CONSTANTS.COMMANDS.CONNECT_COMMAND, (connectionItem: ConnectionItem) => diff --git a/packages/vscode-extension/src/uiUtils.ts b/packages/vscode-extension/src/uiUtils.ts index 18a71fda2..525e4ebc2 100644 --- a/packages/vscode-extension/src/uiUtils.ts +++ b/packages/vscode-extension/src/uiUtils.ts @@ -64,6 +64,16 @@ export async function displayConfirmConnectionDeletionPrompt( ); } +export async function displayConfirmAllConnectionsDeletionPrompt(): Promise< + string | null +> { + return await window.showWarningMessage( + `Are you sure you want to delete all connections?`, + { modal: true }, + 'Yes', + ); +} + /** * Utility function to display a message to the user based on the result of switching databases. * @param database The database that was switched to. diff --git a/packages/vscode-extension/syntaxes/cypher.json b/packages/vscode-extension/syntaxes/cypher.json index 301982a54..17b77cf5b 100644 --- a/packages/vscode-extension/syntaxes/cypher.json +++ b/packages/vscode-extension/syntaxes/cypher.json @@ -38,7 +38,7 @@ "name": "keyword.operator" }, { - "match": "(?i)\\b(ACCESS|ACTIVE|ADD|ADMIN|ADMINISTRATOR|ALIAS|ALIASES|ALL|ALLREDUCE|allShortestPaths|ALTER|AND|ANY|ARRAY|AS|ASC|ASCENDING|ASSIGN|AT|AUTH|BINDINGS|BOOL|BOOLEAN|BOOSTED|BOTH|BREAK|BUILT|BY|CALL|CASCADE|CASE|CIDR|CHANGE|COLLECT|COMMAND|COMMANDS|COMPOSITE|CONSTRAINT|CONSTRAINTS|CONTAINS|CONTINUE|COPY|COUNT|CREATE|CSV|CONCURRENT|CURRENT|DATA|DATABASE|DATABASES|DATE|DATETIME|DBMS|DEALLOCATE|DEFAULT|DEFINED|DELETE|DENY|DESC|DESCENDING|DESTROY|DETACH|DIFFERENT|DISTINCT|DRIVER|DROP|DRYRUN|DUMP|DURATION|EACH|EDGE|ELEMENT|ELEMENTS|ELSE|ENABLE|ENCRYPTED|END|ENDS|ERROR|EXECUTABLE|EXECUTE|EXIST|EXISTENCE|EXISTS|EXTENDED_IDENTIFIER|FAIL|FALSE|FIELDTERMINATOR|FILTER|FINISH|FLOAT|FLOAT32|FLOAT64|FOR|FOREACH|FROM|FULLTEXT|FUNCTION|FUNCTIONS|GRANT|GRAPH|GRAPHS|GROUP|GROUPS|HEADERS|HOME|ID|IF|IMMUTABLE|IMPERSONATE|IMPLIES|IN|INDEX|INDEXES|INF|INFINITY|INSERT|INT|INT8|INT16|INT32|INT64|INTEGER|INTEGER8|INTEGER16|INTEGER32|INTEGER64|IS|JOIN|KEY|LABEL|LABELS|LANGUAGE|LEADING|LET|LIMIT|LIST|LOAD|LOCAL|LOOKUP|MANAGEMENT|MAP|MATCH|MERGE|NAME|NAMES|NAN|NEW|NEXT|NFC|NFD|NFKC|NFKD|NODE|NODETACH|NODES|NONE|NORMALIZE|NORMALIZED|NOT|NOTHING|NOWAIT|NULL|OF|OFFSET|ON|ONLY|OPTION|OPTIONAL|OPTIONS|OR|ORDER|PASSWORD|PASSWORDS|PATH|PATHS|PLAINTEXT|POINT|POPULATED|PRIMARY|PRIMARIES|PRIVILEGE|PRIVILEGES|PROCEDURE|PROCEDURES|PROPERTIES|PROPERTY|PROVIDER|PROVIDERS|RANGE|READ|REALLOCATE|REDUCE|REL|RELATIONSHIP|RELATIONSHIPS|REMOVE|RENAME|REPEATABLE|REPLACE|REPLICA|REPLICAS|REPORT|REQUIRE|REQUIRED|RESTRICT|RETRY|RETURN|REVOKE|ROLE|ROLES|ROW|ROWS|SCAN|SECONDARY|SECONDARIES|SEC|SECOND|SECONDS|SEEK|SERVER|SERVERS|SET|SETTING|SETTINGS|SHARD|SHARDS|SHORTEST|shortestPath|SHOW|SIGNED|SINGLE|SKIP|START|STARTS|STATUS|STOP|VARCHAR|STRING|SUPPORTED|SUSPENDED|TARGET|TERMINATE|TEXT|THEN|TIME|TIMESTAMP|TIMEZONE|TO|TOPOLOGY|TRAILING|TRANSACTION|TRANSACTIONS|TRAVERSE|TRIM|TRUE|TYPE|TYPED|TYPES|UNION|UNIQUE|UNIQUENESS|UNWIND|URL|USE|USER|USERS|USING|VALUE|VECTOR|VERTEX|WAIT|WHEN|WHERE|WITH|WITHOUT|WRITE|XOR|YIELD|ZONE|ZONED|EXPLAIN|PROFILE|CYPHER)\\b", + "match": "(?i)\\b(ACCESS|ACTIVE|ADD|ADMIN|ADMINISTRATOR|ALIAS|ALIASES|ALL|ALLREDUCE|allShortestPaths|ALTER|AND|ANY|ARRAY|AS|ASC|ASCENDING|ASSIGN|AT|AUTH|BINDINGS|BOOL|BOOLEAN|BOOSTED|BOTH|BREAK|BUILT|BY|CALL|CASCADE|CASE|CIDR|CHANGE|COLLECT|COMMAND|COMMANDS|COMPOSITE|CONSTRAINT|CONSTRAINTS|CONTAINS|CONTINUE|COPY|COSINE|COUNT|CREATE|CSV|CONCURRENT|CURRENT|DATA|DATABASE|DATABASES|DATE|DATETIME|DBMS|DEALLOCATE|DEFAULT|DEFINED|DELETE|DENY|DESC|DESCENDING|DESTROY|DETACH|DIFFERENT|DISTINCT|DOT|DRIVER|DROP|DRYRUN|DUMP|DURATION|EACH|EDGE|ELEMENT|ELEMENTS|ELSE|ENABLE|ENCRYPTED|END|ENDS|ERROR|EUCLIDEAN|EUCLIDEAN_SQUARED|EXECUTABLE|EXECUTE|EXIST|EXISTENCE|EXISTS|EXTENDED_IDENTIFIER|FAIL|FALSE|FIELDTERMINATOR|FILTER|FINISH|FLOAT|FLOAT32|FLOAT64|FOR|FOREACH|FROM|FULLTEXT|FUNCTION|FUNCTIONS|GRANT|GRAPH|GRAPHS|GROUP|GROUPS|HAMMING|HEADERS|HOME|ID|IF|IMMUTABLE|IMPERSONATE|IMPLIES|IN|INDEX|INDEXES|INF|INFINITY|INSERT|INT|INT8|INT16|INT32|INT64|INTEGER|INTEGER8|INTEGER16|INTEGER32|INTEGER64|IS|JOIN|KEY|LABEL|LABELS|LANGUAGE|LEADING|LET|LIMIT|LIST|LOAD|LOCAL|LOOKUP|MANAGEMENT|MANHATTAN|MAP|MATCH|MERGE|NAME|NAMES|NAN|NEW|NEXT|NFC|NFD|NFKC|NFKD|NODE|NODETACH|NODES|NONE|NORMALIZE|NORMALIZED|NOT|NOTHING|NOWAIT|NULL|OF|OFFSET|ON|ONLY|OPTION|OPTIONAL|OPTIONS|OR|ORDER|PASSWORD|PASSWORDS|PATH|PATHS|PLAINTEXT|POINT|POPULATED|PRIMARY|PRIMARIES|PRIVILEGE|PRIVILEGES|PROCEDURE|PROCEDURES|PROPERTIES|PROPERTY|PROVIDER|PROVIDERS|RANGE|READ|REALLOCATE|REDUCE|REL|RELATIONSHIP|RELATIONSHIPS|REMOVE|RENAME|REPEATABLE|REPLACE|REPLICA|REPLICAS|REPORT|REQUIRE|REQUIRED|RESTRICT|RETRY|RETURN|REVOKE|ROLE|ROLES|ROW|ROWS|SCAN|SECONDARY|SECONDARIES|SEC|SECOND|SECONDS|SEEK|SERVER|SERVERS|SET|SETTING|SETTINGS|SHARD|SHARDS|SHORTEST|shortestPath|SHOW|SIGNED|SINGLE|SKIP|START|STARTS|STATUS|STOP|VARCHAR|STRING|SUPPORTED|SUSPENDED|TARGET|TERMINATE|TEXT|THEN|TIME|TIMESTAMP|TIMEZONE|TO|TOPOLOGY|TRAILING|TRANSACTION|TRANSACTIONS|TRAVERSE|TRIM|TRUE|TYPE|TYPED|TYPES|UNION|UNIQUE|UNIQUENESS|UNWIND|URL|USE|USER|USERS|USING|VALUE|VECTOR|VECTOR_DISTANCE|VECTOR_NORM|VERTEX|WAIT|WHEN|WHERE|WITH|WITHOUT|WRITE|XOR|YIELD|ZONE|ZONED|EXPLAIN|PROFILE|CYPHER)\\b", "name": "keyword" } ] diff --git a/packages/vscode-extension/tests/specs/unit/executeCommands.spec.ts b/packages/vscode-extension/tests/specs/unit/executeCommands.spec.ts index 4220d64b6..7f4eddd5a 100644 --- a/packages/vscode-extension/tests/specs/unit/executeCommands.spec.ts +++ b/packages/vscode-extension/tests/specs/unit/executeCommands.spec.ts @@ -3,14 +3,17 @@ import { after, afterEach, beforeEach } from 'mocha'; import * as sinon from 'sinon'; import { commands, MessageOptions, window } from 'vscode'; import { CONSTANTS } from '../../../src/constants'; -import { getNeo4j2025Configuration } from '../../helpers'; +import { + getNeo4j2025Configuration, + getNeo4j5Configuration, +} from '../../helpers'; import { connectDefault, neo4j2025ConnectionKey, saveDefaultConnection, } from '../../suiteSetup'; -suite('Execute commands spec', () => { +suite.only('Execute commands spec', () => { let sandbox: sinon.SinonSandbox; let showInformationMessageStub: sinon.SinonStub; let showErrorMessageStub: sinon.SinonStub; @@ -459,4 +462,163 @@ suite('Execute commands spec', () => { sandbox.assert.notCalled(showErrorMessageStub); }); }); + + suite('deleteAllConnectionsCommand', () => { + const connection2025 = { + name: 'deleteAllConnectionsCommand 2025', + key: 'deleteAllConnectionsCommand 2025', + scheme: getNeo4j2025Configuration().scheme, + host: getNeo4j2025Configuration().host, + port: getNeo4j2025Configuration().port, + user: getNeo4j2025Configuration().user, + database: getNeo4j2025Configuration().database, + state: 'inactive', + }; + const connection2025Password = getNeo4j2025Configuration().password; + + const connection5 = { + name: 'deleteAllConnectionsCommand 5', + key: 'deleteAllConnectionsCommand 5', + scheme: getNeo4j5Configuration().scheme, + host: getNeo4j5Configuration().host, + port: getNeo4j5Configuration().port, + user: getNeo4j5Configuration().user, + database: getNeo4j5Configuration().database, + state: 'inactive', + }; + + const connection5Password = getNeo4j5Configuration().password; + + test('Deleting all connections should show a success message', async () => { + await commands.executeCommand( + CONSTANTS.COMMANDS.SAVE_CONNECTION_COMMAND, + connection2025, + connection2025Password, + ); + + await commands.executeCommand( + CONSTANTS.COMMANDS.SAVE_CONNECTION_COMMAND, + connection5, + connection5Password, + ); + + const stub = sandbox.stub( + window, + 'showWarningMessage', + ) as unknown as sinon.SinonStub< + [string, MessageOptions, ...string[]], + Thenable + >; + + stub + .withArgs(sinon.match.string, sinon.match.object, sinon.match.string) + .resolves('Yes'); + + await commands.executeCommand( + CONSTANTS.COMMANDS.DELETE_ALL_CONNECTIONS_COMMAND, + ); + + sandbox.assert.calledWith( + showInformationMessageStub, + CONSTANTS.MESSAGES.ALL_CONNECTIONS_DELETED, + ); + }); + + test('Deleting all connections should disconnect active connections', async () => { + await commands.executeCommand( + CONSTANTS.COMMANDS.SAVE_CONNECTION_COMMAND, + connection2025, + connection2025Password, + ); + + await commands.executeCommand( + CONSTANTS.COMMANDS.SAVE_CONNECTION_COMMAND, + connection5, + connection5Password, + ); + + const stub = sandbox.stub( + window, + 'showWarningMessage', + ) as unknown as sinon.SinonStub< + [string, MessageOptions, ...string[]], + Thenable + >; + + stub + .withArgs(sinon.match.string, sinon.match.object, sinon.match.string) + .resolves('Yes'); + + await commands.executeCommand( + CONSTANTS.COMMANDS.DELETE_ALL_CONNECTIONS_COMMAND, + ); + + sandbox.assert.calledWith( + showInformationMessageStub, + CONSTANTS.MESSAGES.ALL_CONNECTIONS_DELETED, + ); + + sandbox.assert.calledWith( + showInformationMessageStub, + CONSTANTS.MESSAGES.DISCONNECTED_MESSAGE, + ); + }); + + test('Dismissing delete all connections prompt should not show any messages', async () => { + await commands.executeCommand( + CONSTANTS.COMMANDS.SAVE_CONNECTION_COMMAND, + connection2025, + connection2025Password, + ); + + await commands.executeCommand( + CONSTANTS.COMMANDS.SAVE_CONNECTION_COMMAND, + connection5, + connection5Password, + ); + + sandbox.stub(window, 'showWarningMessage').resolves(undefined); + showInformationMessageStub.reset(); + + await commands.executeCommand( + CONSTANTS.COMMANDS.DELETE_ALL_CONNECTIONS_COMMAND, + ); + + sandbox.assert.notCalled(showInformationMessageStub); + }); + + test('Any other response from delete all connections prompt should not show any messages', async () => { + await commands.executeCommand( + CONSTANTS.COMMANDS.SAVE_CONNECTION_COMMAND, + connection2025, + connection2025Password, + ); + + await commands.executeCommand( + CONSTANTS.COMMANDS.SAVE_CONNECTION_COMMAND, + connection5, + connection5Password, + ); + + const stub = sandbox.stub( + window, + 'showWarningMessage', + ) as unknown as sinon.SinonStub< + [string, MessageOptions, ...string[]], + Thenable + >; + + stub + .withArgs(sinon.match.string, sinon.match.object, sinon.match.string) + .resolves('No'); + + showInformationMessageStub.reset(); + + await commands.executeCommand( + CONSTANTS.COMMANDS.DELETE_ALL_CONNECTIONS_COMMAND, + ); + + sandbox.assert.notCalled(showInformationMessageStub); + }); + }); });