diff --git a/electron/helpers/mysql.ts b/electron/helpers/mysql.ts index 508e3dd..1da104c 100644 --- a/electron/helpers/mysql.ts +++ b/electron/helpers/mysql.ts @@ -251,6 +251,50 @@ async function safeEndConnection(connection?: PoolConnection): Promise { } } +interface DatabaseRow extends RowDataPacket { + Database?: string; + database?: string; +} + +async function listDatabases( + config: AppConnection +): Promise<{ success: boolean; databases: string[]; message?: string }> { + let connection: PoolConnection; + + try { + connection = await createConnection(config); + + const [rows] = await connection.query('SHOW DATABASES'); + const databases = rows + .map((r: DatabaseRow) => r.Database || r.database) + .filter( + (db: string) => + ![ + 'information_schema', + 'performance_schema', + 'mysql', + 'sys' + ].includes(db) + ); + + return { success: true, databases }; + } catch (err: any) { + let msg = err.message; + if (err.code === 'ER_ACCESS_DENIED_ERROR') { + msg = 'Access denied with the provided credentials'; + } else if (err.code === 'ECONNREFUSED') { + msg = 'Connection refused - check host and port'; + } + return { + success: false, + message: msg, + databases: [] + }; + } finally { + await safeEndConnection(connection); + } +} + export { createConnection, testConnection, @@ -258,5 +302,6 @@ export { closeAllPools, releaseConnection, safeEndConnection, + listDatabases, ERROR_MESSAGES }; diff --git a/electron/main/index.ts b/electron/main/index.ts index e578a51..a70f3a3 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -18,6 +18,7 @@ import { registerSqlExecutorHandlers } from '../modules/sql-executor'; import { registerUpdaterHandlers, cleanup } from '../modules/updater'; import { registerSshHandlers } from '../modules/ssh'; import { registerGitHandlers } from '../modules/git'; +import { registerDockMenuHandlers } from '../modules/dock-menu'; import { closeAllPools } from '../helpers/mysql'; import { closeAllConnections, closeAllTunnels } from '../helpers/ssh'; @@ -431,6 +432,7 @@ function registerHandlers(win: BrowserWindow) { registerUpdaterHandlers(win); registerSshHandlers(); registerGitHandlers(); + registerDockMenuHandlers(); handlersRegistered = true; } diff --git a/electron/modules/dock-menu.ts b/electron/modules/dock-menu.ts new file mode 100644 index 0000000..356a3e1 --- /dev/null +++ b/electron/modules/dock-menu.ts @@ -0,0 +1,234 @@ +import { ipcMain, Menu, app, BrowserWindow } from 'electron'; +import { AppConnection } from '../../src/types/ssh-connection'; +import { listDatabases } from '../helpers/mysql'; +import fs from 'fs'; +import path from 'path'; + +let currentConnectionId: string | null = null; +let currentAppConnection: AppConnection | null = null; +let availableDatabases: string[] = []; +let currentDatabase: string | null = null; +let projectDatabase: string | null = null; + +function updateDockMenu() { + if (process.platform !== 'darwin') { + return; + } + + if ( + !currentConnectionId || + !currentAppConnection || + availableDatabases.length === 0 + ) { + try { + app.dock.setMenu(Menu.buildFromTemplate([])); + } catch (error) { + console.error('Error clearing dock menu:', error); + } + return; + } + + const menuItems = availableDatabases.map((database) => ({ + label: database === currentDatabase ? `${database} ✓` : database, + type: 'normal' as const, + click: () => handleDatabaseSwitch(database) + })); + + try { + const menu = Menu.buildFromTemplate([ + { + label: 'Switch Database', + type: 'submenu', + submenu: menuItems + } + ]); + + app.dock.setMenu(menu); + } catch (error) { + console.error('Error setting dock menu:', error); + } +} + +async function updateEnvDatabase(projectPath: string, database: string) { + try { + if (!projectPath || !database) { + return { + success: false, + message: 'Missing project path or database name' + }; + } + + const envPath = path.join(projectPath, '.env'); + + if (!fs.existsSync(envPath)) { + return { + success: false, + message: '.env file not found in project' + }; + } + + let envContent = fs.readFileSync(envPath, 'utf8'); + const lines = envContent.split('\n'); + let dbLineFound = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + if (line.trim().startsWith('#')) { + continue; + } + + if (line.trim().startsWith('DB_DATABASE=')) { + lines[i] = `DB_DATABASE=${database}`; + dbLineFound = true; + break; + } + } + + if (!dbLineFound) { + lines.push(`DB_DATABASE=${database}`); + } + + const updatedContent = lines.join('\n'); + fs.writeFileSync(envPath, updatedContent); + + return { + success: true, + message: `Updated database to ${database} in .env file` + }; + } catch (error: any) { + return { + success: false, + message: error.message || 'Failed to update database in .env file' + }; + } +} + +async function handleDatabaseSwitch(databaseName: string) { + if ( + !currentAppConnection || + !currentConnectionId || + databaseName === currentDatabase + ) { + return; + } + + try { + currentAppConnection.localDbConfig.database = databaseName; + currentDatabase = databaseName; + + let envUpdated = false; + if (projectDatabase) { + const envResult = await updateEnvDatabase( + projectDatabase, + databaseName + ); + if (envResult.success) { + envUpdated = true; + } else { + console.error('Failed to update .env file:', envResult.message); + } + } + + updateDockMenu(); + + const windows = BrowserWindow.getAllWindows(); + + windows.forEach((window) => { + if (window.webContents) { + window.webContents.send('database-switched', { + connectionId: currentConnectionId, + database: databaseName, + success: true, + envUpdated + }); + } + }); + } catch (error: any) { + console.error('Failed to switch database from dock menu:', error); + } +} + +export function registerDockMenuHandlers() { + ipcMain.handle( + 'dock-menu:set-connection', + async ( + _, + connectionId: string, + appConnection: AppConnection, + projectPath?: string + ) => { + try { + currentConnectionId = connectionId; + currentAppConnection = appConnection; + currentDatabase = appConnection.localDbConfig?.database || null; + projectDatabase = projectPath || null; + + const result = await listDatabases(appConnection); + if (result.success) { + availableDatabases = result.databases; + updateDockMenu(); + } + + return { success: true }; + } catch (error: any) { + console.error('Error setting dock menu connection:', error); + return { success: false, message: error.message }; + } + } + ); + + ipcMain.handle('dock-menu:clear-connection', async () => { + try { + currentConnectionId = null; + currentAppConnection = null; + availableDatabases = []; + currentDatabase = null; + projectDatabase = null; + updateDockMenu(); + return { success: true }; + } catch (error: any) { + console.error('Error clearing dock menu connection:', error); + return { success: false, message: error.message }; + } + }); + + ipcMain.handle( + 'dock-menu:update-databases', + async ( + _, + databases: string[], + currentDb: string, + projectDb?: string + ) => { + try { + availableDatabases = databases; + currentDatabase = currentDb; + projectDatabase = projectDb || null; + updateDockMenu(); + return { success: true }; + } catch (error: any) { + console.error('Error updating dock menu databases:', error); + return { success: false, message: error.message }; + } + } + ); + + ipcMain.handle('dock-menu:refresh', async () => { + if (!currentAppConnection) { + return { success: false, message: 'No active connection' }; + } + + try { + const result = await listDatabases(currentAppConnection); + if (result.success) { + availableDatabases = result.databases; + updateDockMenu(); + } + return result; + } catch (error: any) { + console.error('Error refreshing dock menu databases:', error); + return { success: false, message: error.message }; + } + }); +} diff --git a/electron/modules/mysql.ts b/electron/modules/mysql.ts index 8e1f521..23c280e 100644 --- a/electron/modules/mysql.ts +++ b/electron/modules/mysql.ts @@ -2,15 +2,11 @@ import { ipcMain } from 'electron'; import { testConnection, createConnection, - safeEndConnection + safeEndConnection, + listDatabases } from '../helpers/mysql'; import { AppConnection } from '../../src/types/ssh-connection'; -import { PoolConnection, RowDataPacket } from 'mysql2/promise'; - -interface DatabaseRow extends RowDataPacket { - Database?: string; - database?: string; -} +import { PoolConnection } from 'mysql2/promise'; function registerMysqlHandlers() { ipcMain.handle( @@ -59,41 +55,7 @@ function registerMysqlHandlers() { ); ipcMain.handle('list-databases', async (_, config: AppConnection) => { - let connection: PoolConnection; - - try { - connection = await createConnection(config); - - const [rows] = - await connection.query('SHOW DATABASES'); - const databases = rows - .map((r: DatabaseRow) => r.Database || r.database) - .filter( - (db: string) => - ![ - 'information_schema', - 'performance_schema', - 'mysql', - 'sys' - ].includes(db) - ); - - return { success: true, databases }; - } catch (err) { - let msg = err.message; - if (err.code === 'ER_ACCESS_DENIED_ERROR') { - msg = 'Access denied with the provided credentials'; - } else if (err.code === 'ECONNREFUSED') { - msg = 'Connection refused - check host and port'; - } - return { - success: false, - message: msg, - databases: [] - }; - } finally { - await safeEndConnection(connection); - } + return await listDatabases(config); }); ipcMain.handle( diff --git a/electron/preload/index.ts b/electron/preload/index.ts index bd4bd82..838dba5 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -323,6 +323,36 @@ contextBridge.exposeInMainWorld('ipcRenderer', { ipcRenderer.invoke('git-status', projectPath), initRepository: (projectPath: string) => ipcRenderer.invoke('git-init', projectPath) + }, + + /** + * Dock Menu Operations + */ + dockMenu: { + setConnection: ( + connectionId: string, + appConnection: AppConnection, + projectPath?: string + ) => + ipcRenderer.invoke( + 'dock-menu:set-connection', + connectionId, + appConnection, + projectPath + ), + clearConnection: () => ipcRenderer.invoke('dock-menu:clear-connection'), + updateDatabases: ( + databases: string[], + currentDb: string, + projectDb?: string + ) => + ipcRenderer.invoke( + 'dock-menu:update-databases', + databases, + currentDb, + projectDb + ), + refresh: () => ipcRenderer.invoke('dock-menu:refresh') } }); diff --git a/package.json b/package.json index 700e33a..34a19b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "larabase", - "version": "1.2.8", + "version": "1.3.0", "main": "dist-electron/main/index.js", "description": "An Opined Database GUI for Laravel Developers", "author": "Tiago Padilha ", diff --git a/src/components/database/DatabaseSwitcher.vue b/src/components/database/DatabaseSwitcher.vue index 1d60e2a..2325692 100644 --- a/src/components/database/DatabaseSwitcher.vue +++ b/src/components/database/DatabaseSwitcher.vue @@ -6,6 +6,7 @@ import { useSidebarStore } from '@/store/sidebar'; import { useTabsStore } from '@/store/tabs'; import { useProjectStore } from '@/store/project'; import { AppConnection } from '@/types/ssh-connection'; +import { useDockMenu } from '@/composables/useDockMenu'; const showAlert = inject<(message: string, type: string) => void>('showAlert')!; @@ -13,6 +14,7 @@ const connectionStore = useConnectionsStore(); const sidebarStore = useSidebarStore(); const tabStore = useTabsStore(); const projectStore = useProjectStore(); +const { updateDockMenuDatabases } = useDockMenu(); const emit = defineEmits(['close']); @@ -87,6 +89,12 @@ async function switchDatabase(databaseName: string, shouldUpdateEnv: boolean) { await tabStore.closeAllTabs(); await sidebarStore.forceReloadDatabase(project.value); + + await updateDockMenuDatabases( + availableDatabases.value, + databaseName, + projectDatabase.value || undefined + ); } } catch (error: any) { console.error(`Failed to switch database: ${error.message}`); @@ -163,6 +171,12 @@ async function loadAvailableDatabases() { if (project.value.projectPath) { await checkProjectDatabase(); } + + await updateDockMenuDatabases( + result.databases, + project.value.dbConfig?.database || '', + projectDatabase.value || undefined + ); } else { showAlert(`Failed to load databases: ${result.message}`, 'error'); if (!hasExistingData) { diff --git a/src/composables/useDockMenu.ts b/src/composables/useDockMenu.ts new file mode 100644 index 0000000..1fb9715 --- /dev/null +++ b/src/composables/useDockMenu.ts @@ -0,0 +1,101 @@ +import { computed, watch } from 'vue'; +import { useConnectionsStore } from '@/store/connections'; +import { toRaw } from 'vue'; +import { AppConnection } from '@/types/ssh-connection'; + +export function useDockMenu() { + const connectionStore = useConnectionsStore(); + + const project = computed(() => { + return connectionStore.getSelectedProject; + }); + + async function setDockMenuConnection() { + if (!project.value) { + await window.ipcRenderer.dockMenu.clearConnection(); + return; + } + + const appConnection = { + localDbConfig: toRaw(project.value.dbConfig), + remote: toRaw(project.value.sshConfig) + } as AppConnection; + + try { + await window.ipcRenderer.dockMenu.setConnection( + project.value.id as string, + appConnection, + project.value.projectPath + ); + } catch (error) { + console.error('Error setting dock menu connection:', error); + } + } + + async function updateDockMenuDatabases( + databases: string[], + currentDb: string, + projectDb?: string + ) { + try { + await window.ipcRenderer.dockMenu.updateDatabases( + databases, + currentDb, + projectDb + ); + } catch (error) { + console.error('Error updating dock menu databases:', error); + } + } + + async function clearDockMenuConnection() { + try { + await window.ipcRenderer.dockMenu.clearConnection(); + } catch (error) { + console.error('Error clearing dock menu connection:', error); + } + } + + watch( + () => project.value, + async (newProject) => { + if (newProject) { + await setDockMenuConnection(); + } else { + await clearDockMenuConnection(); + } + }, + { immediate: true } + ); + + window.ipcRenderer.on('database-switched', async (_event, data) => { + if ( + data.success && + project.value && + data.connectionId === project.value.id + ) { + try { + const updatedProject = { ...project.value }; + if (updatedProject.dbConfig) { + updatedProject.dbConfig.database = data.database; + } + + await connectionStore.updateConnection( + project.value.id as string, + updatedProject + ); + + window.location.reload(); + } catch (error) { + console.error( + 'Error handling database switch from dock menu:', + error + ); + } + } + }); + + return { + updateDockMenuDatabases + }; +} diff --git a/src/views/DatabaseView.vue b/src/views/DatabaseView.vue index 9972787..aa130e1 100644 --- a/src/views/DatabaseView.vue +++ b/src/views/DatabaseView.vue @@ -23,11 +23,14 @@ import { useTabsStore } from '@/store/tabs'; import { useSplitPane } from '@/composables/useSplitPane'; import { ConnectionType } from '@/types/connection-types'; import { AppConnection } from '@/types/ssh-connection'; +import { useDockMenu } from '@/composables/useDockMenu'; const route = useRoute(); const connectionsStore = useConnectionsStore(); const tabsStore = useTabsStore(); +useDockMenu(); + const isContentReady = ref(false); const pendingMigrationsCount = ref(0);