Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fcc7617
feat: implement optimized SSH service for improved file navigation an…
Tiagospem May 27, 2025
81ddb57
feat: implement optimized SSH service with caching and improved comma…
Tiagospem May 27, 2025
a119bd5
feat: update Vite configuration to include monaco-editor and define N…
Tiagospem May 27, 2025
b6b6732
feat: configure Monaco editor on SQL editor initialization
Tiagospem May 27, 2025
bb947c2
feat: enhance SSH service with optimized command execution and file o…
Tiagospem May 27, 2025
311a6fb
refactor: clean up SSH service code by removing unused interfaces and…
Tiagospem May 27, 2025
a406df9
feat: update permissions in settings to allow npm build and lint comm…
Tiagospem May 27, 2025
873198f
feat: add script to run Larabase application in terminal
Tiagospem May 27, 2025
24a6ff0
feat: configure Monaco editor on DotEnvEditor component initialization
Tiagospem May 27, 2025
5ad926c
feat: add optimized SSH methods for file operations and directory lis…
Tiagospem May 27, 2025
4f61020
feat: add optimized SSH methods for file operations
Tiagospem May 27, 2025
ff5dfe6
feat: refactor MySQL connection handling for improved clarity and eff…
Tiagospem May 27, 2025
52eebde
feat: implement optimized SSH connection manager with pooling and cle…
Tiagospem May 27, 2025
4452b10
feat: add optimized SSH service with caching and file operations
Tiagospem May 27, 2025
9a505b9
feat: add optimized SSH methods for listing, reading, and writing fil…
Tiagospem May 27, 2025
484b38f
feat: integrate optimized SSH service for file operations in RemoteFi…
Tiagospem May 27, 2025
49fb1ec
feat: integrate optimized SSH service for file operations in RemoteFi…
Tiagospem May 27, 2025
706f486
feat: add Monaco editor configuration with custom worker setup
Tiagospem May 27, 2025
4087306
feat: enhance remote file explorer with directory tree navigation and…
Tiagospem May 27, 2025
5071d6d
feat: update DirectoryTreeNode type to support files in the file expl…
Tiagospem May 27, 2025
e720907
feat: enhance Monaco editor configuration with custom worker implemen…
Tiagospem May 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{
"permissions": {
"allow": [
"Bash(grep:*)"
"Bash(grep:*)",
"Bash(npm run build:*)",
"Bash(npm run lint)"
],
"deny": []
}
Expand Down
54 changes: 54 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,60 @@ The application follows a communication pattern using Electron's IPC:
- Always use toRaw for reactive objects to avoid errors like "An object could not be cloned."
- Use separate loading states for initial loading vs. background refreshes to avoid UI flickering

## Remote File Explorer with Directory Tree

The application includes an enhanced remote file explorer with sidebar navigation:

### Features

- **Directory Tree Sidebar**: Collapsible tree structure for fast navigation through nested directories
- **State Persistence**: Remembers current directory when switching between file explorer and database views
- **File Integration**: Click files in tree to open directly in Monaco Editor
- **Connection-Aware State**: Different states maintained per SSH connection

### Implementation Details

- **Store**: `src/store/fileExplorer.ts` - Pinia store managing directory tree state and file operations
- **Components**:
- `DirectoryTreeSidebar.vue` - Main sidebar with expand/collapse functionality
- `DirectoryTreeNode.vue` - Individual tree nodes with file/directory icons
- `RemoteFileExplorer.vue` - Main file explorer with integrated sidebar
- **State Management**: Uses `expandedPaths` Set to track opened directories and `currentPath` for navigation
- **Performance**: Leverages existing optimized SSH connection pooling for instant navigation

### Path Construction Fixes

- **Breadcrumb Issues**: Fixed path calculation that was cutting off directory names
- **Tree Navigation**: Corrected file path construction to prevent "Is a directory" errors
- **Event Propagation**: Fixed Vue.js event handling using `(...args) => $emit('openFile', ...args)` pattern

## Monaco Editor Configuration

Monaco Editor requires special configuration for Vite/Electron environments:

### Setup

- **Central Configuration**: `src/utils/monaco-config.ts` provides centralized worker setup
- **Worker Configuration**: Proper import of workers using Vite's `?worker` suffix
- **Applied To**: RemoteFileEditor, SQLEditor (via useSQLEditor), DotEnvEditor

### Common Issues

- **`toUrl` Errors**: Fixed by configuring `MonacoEnvironment.getWorker` properly
- **Worker Loading**: Use `import worker from 'monaco-editor/esm/vs/.../worker?worker'` pattern
- **Multiple Components**: Always call `configureMonaco()` before using Monaco in any component

### Vite Configuration

```typescript
optimizeDeps: {
include: ['monaco-editor']
},
define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
}
```

## Git Integration

The application includes Git integration features:
Expand Down
12 changes: 6 additions & 6 deletions electron/helpers/mysql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ function getPoolKey(
)?.localPort
: config.localDbConfig.port;

// Para conexões remotas, use as informações do remoteDbConfig
const userPart = config.remote
? config.remote.remoteDbConfig.user
: config.localDbConfig.user;
Expand All @@ -142,14 +141,16 @@ async function getConnectionPool(
config: AppConnection,
{ useConnectionDb = true, targetDatabase = '' } = {}
): Promise<Pool> {
const connectionOptions = await getConnectionOptions(config, {
useConnectionDb,
targetDatabase
});

const poolKey = getPoolKey(config, useConnectionDb, targetDatabase);

if (!connectionPools.has(poolKey)) {
const poolConfig = {
...(await getConnectionOptions(config, {
useConnectionDb,
targetDatabase
})),
...connectionOptions,
connectionLimit: 10,
waitForConnections: true,
queueLimit: 0
Expand Down Expand Up @@ -188,7 +189,6 @@ async function ping(
const conn = await createConnection(config, options);

try {
// Define a interface para o resultado da consulta de teste
interface ConnectionTestRow extends RowDataPacket {
connection_test: number;
}
Expand Down
246 changes: 246 additions & 0 deletions electron/helpers/optimized-ssh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import { Client, ConnectConfig } from 'ssh2';
import * as fs from 'fs';
import { SshConnection } from '../../src/types/ssh-connection';

interface ConnectionPool {
client: Client;
lastUsed: number;
config: SshConnection;
}

class OptimizedSshManager {
private connections = new Map<string, ConnectionPool>();
private readonly CONNECTION_TIMEOUT = 300000; // 5 minutes
private readonly MAX_CONNECTIONS = 10;
private readonly cleanupInterval: NodeJS.Timeout;

constructor() {
this.cleanupInterval = setInterval(() => {
this.cleanupConnections();
}, 60000);
}

private getConnectionKey(config: SshConnection): string {
return `${config.user}@${config.host}:${config.port}:${config.remotePath}`;
}

private cleanupConnections(): void {
const now = Date.now();
const keysToDelete: string[] = [];

for (const [key, pool] of this.connections) {
if (now - pool.lastUsed > this.CONNECTION_TIMEOUT) {
try {
pool.client.end();
} catch (error) {
console.error('Error closing SSH connection:', error);
}
keysToDelete.push(key);
}
}

keysToDelete.forEach((key) => this.connections.delete(key));
console.log(
`SSH cleanup: removed ${keysToDelete.length} unused connections`
);
}

private async createOptimizedConnection(
config: SshConnection
): Promise<Client> {
return new Promise((resolve, reject) => {
const conn = new Client();

const connectConfig: ConnectConfig = {
host: config.host,
port: config.port,
username: config.user,
compress: true,
algorithms: {
kex: [
'diffie-hellman-group14-sha256',
'diffie-hellman-group16-sha512',
'diffie-hellman-group18-sha512',
'diffie-hellman-group-exchange-sha256'
],
cipher: [
'aes128-ctr',
'aes192-ctr',
'aes256-ctr',
'aes128-gcm',
'aes256-gcm'
],
hmac: ['hmac-sha2-256', 'hmac-sha2-512', 'hmac-sha1'],
compress: ['zlib@openssh.com', 'zlib', 'none']
},
readyTimeout: 10000,
keepaliveInterval: 30000, // Keep connection alive
keepaliveCountMax: 3
};

if (config.privateKey) {
try {
connectConfig.privateKey = fs.readFileSync(
config.privateKey,
'utf8'
);
if (config.passphrase) {
connectConfig.passphrase = config.passphrase;
}
} catch (error) {
reject(
new Error(
`Failed to read private key: ${error instanceof Error ? error.message : String(error)}`
)
);
return;
}
} else if (config.password) {
connectConfig.password = config.password;
} else {
reject(new Error('No authentication method provided'));
return;
}

conn.on('ready', () => {
console.log(
`Optimized SSH connection established to ${config.host} with compression`
);
resolve(conn);
});

conn.on('error', (err) => {
console.error(`SSH connection error to ${config.host}:`, err);
reject(err);
});

conn.on('close', () => {
console.log(`SSH connection closed to ${config.host}`);
});

conn.connect(connectConfig);
});
}

async getConnection(config: SshConnection): Promise<Client> {
const key = this.getConnectionKey(config);
const existing = this.connections.get(key);

if (existing) {
console.log('Reusing existing connection');
existing.lastUsed = Date.now();
return existing.client;
}

if (this.connections.size < this.MAX_CONNECTIONS) {
console.log('Creating new connection');
const client = await this.createOptimizedConnection(config);

this.connections.set(key, {
client,
lastUsed: Date.now(),
config
});

return client;
}

throw new Error(
'Maximum SSH connections reached. Please wait and try again.'
);
}

releaseConnection(config: SshConnection): void {
const key = this.getConnectionKey(config);
const pool = this.connections.get(key);

if (pool) {
pool.lastUsed = Date.now();
}
}

async executeCommand(
config: SshConnection,
command: string
): Promise<{ stdout: string; stderr: string; code: number | null }> {
const client = await this.getConnection(config);

try {
return new Promise((resolve, reject) => {
client.exec(command, (err, stream) => {
if (err) {
console.log(
'Command exec failed, removing connection from pool'
);
this.closeConnection(config);
reject(err);
return;
}

let stdout = '';
let stderr = '';

stream.on('data', (data: Buffer) => {
stdout += data.toString();
});

stream.stderr.on('data', (data: Buffer) => {
stderr += data.toString();
});

stream.on('close', (code: number | null) => {
this.releaseConnection(config);
resolve({ stdout, stderr, code });
});

stream.on('error', (err: Error) => {
console.log(
'Stream error, removing connection from pool'
);
this.closeConnection(config);
reject(err);
});
});
});
} catch (error) {
this.closeConnection(config);
throw error;
}
}

closeConnection(config: SshConnection): void {
const key = this.getConnectionKey(config);
const pool = this.connections.get(key);

if (pool) {
try {
pool.client.end();
} catch (error) {
console.error('Error closing SSH connection:', error);
}
this.connections.delete(key);
}
}

closeAllConnections(): void {
for (const pool of this.connections.values()) {
try {
pool.client.end();
} catch (error) {
console.error('Error closing SSH connection:', error);
}
}
this.connections.clear();
clearInterval(this.cleanupInterval);
}

getConnectionStats(): { total: number; available: number } {
return {
total: this.connections.size,
available: this.connections.size
};
}
}

export const optimizedSshManager = new OptimizedSshManager();
export default optimizedSshManager;
Loading