diff --git a/src/components/Studio.tsx b/src/components/Studio.tsx index 19fc413..635d961 100644 --- a/src/components/Studio.tsx +++ b/src/components/Studio.tsx @@ -1037,7 +1037,12 @@ export default function Studio() { useEffect(() => { const initializeConnections = async () => { const LOG_PREFIX = '[DemoDB]'; - const loadedConnections = storage.getConnections(); + + // Clean up any invalid connections (missing required fields) + storage.cleanupInvalidConnections(); + + // Reload connections after cleanup to ensure we have fresh data + let loadedConnections = storage.getConnections(); // Fetch demo connection from server try { @@ -1053,48 +1058,45 @@ export default function Studio() { createdAt: new Date(data.connection.createdAt), }; - // Check if demo connection already exists (by id or isDemo flag) - const existingDemo = loadedConnections.find( - c => c.id === demoConn.id || (c.isDemo && c.type === 'postgres') + // Remove any old demo connections (by isDemo flag or specific demo IDs) + // This prevents duplicates when demo config changes or old demos exist + const oldDemoConnections = loadedConnections.filter( + c => c.isDemo || c.id === 'demo-postgres-neon' || c.id === 'demo-mock' ); - - if (existingDemo) { - // Update existing demo connection (credentials may have changed) - console.log(`${LOG_PREFIX} Updating existing demo connection:`, { - id: existingDemo.id, - name: demoConn.name, + + if (oldDemoConnections.length > 0) { + console.log(`${LOG_PREFIX} Removing ${oldDemoConnections.length} old demo connection(s)`); + oldDemoConnections.forEach(oldDemo => { + storage.deleteConnection(oldDemo.id); }); - const updatedDemo = { ...demoConn, id: existingDemo.id }; - storage.saveConnection(updatedDemo); - const updatedConnections = storage.getConnections(); - setConnections(updatedConnections); - - // Restore persisted active connection, fallback to first - if (updatedConnections.length > 0) { - const savedId = storage.getActiveConnectionId(); - const saved = savedId ? updatedConnections.find(c => c.id === savedId) : null; - setActiveConnection(saved ?? updatedConnections[0]); - } + // Reload after deletion + loadedConnections = storage.getConnections(); + } + + // Add the new demo connection + console.log(`${LOG_PREFIX} Adding demo connection:`, { + id: demoConn.id, + name: demoConn.name, + database: demoConn.database, + }); + storage.saveConnection(demoConn); + const updatedConnections = storage.getConnections(); + setConnections(updatedConnections); + + // Restore persisted active connection, fallback to demo if no others + const savedId = storage.getActiveConnectionId(); + const saved = savedId ? updatedConnections.find(c => c.id === savedId) : null; + + // Only auto-select demo if it was previously active or no other connections exist + const wasActiveDemoRemoved = oldDemoConnections.some(d => d.id === savedId); + // Use loadedConnections (after demo deletion) to check for non-demo connections + const nonDemoConnections = loadedConnections.filter(c => !c.isDemo); + + if (wasActiveDemoRemoved || nonDemoConnections.length === 0) { + console.log(`${LOG_PREFIX} Auto-selecting demo as active connection`); + setActiveConnection(saved ?? demoConn); } else { - // Add new demo connection - console.log(`${LOG_PREFIX} Adding new demo connection:`, { - id: demoConn.id, - name: demoConn.name, - database: demoConn.database, - }); - storage.saveConnection(demoConn); - const updatedConnections = storage.getConnections(); - setConnections(updatedConnections); - - // Restore persisted active connection, fallback to demo if no others - const savedId = storage.getActiveConnectionId(); - const saved = savedId ? updatedConnections.find(c => c.id === savedId) : null; - if (loadedConnections.length === 0) { - console.log(`${LOG_PREFIX} Auto-selecting demo as active connection (no other connections)`); - setActiveConnection(saved ?? demoConn); - } else { - setActiveConnection(saved ?? updatedConnections[0]); - } + setActiveConnection(saved ?? updatedConnections[0]); } return; } else { @@ -1107,6 +1109,7 @@ export default function Studio() { console.error(`${LOG_PREFIX} Failed to fetch demo connection:`, error); } + // Fallback: no demo connection, use loaded connections setConnections(loadedConnections); if (loadedConnections.length > 0) { const savedId = storage.getActiveConnectionId(); diff --git a/src/lib/storage.ts b/src/lib/storage.ts index d0952cf..4138749 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -1,62 +1,68 @@ -import { DatabaseConnection, QueryHistoryItem, SavedQuery, SchemaSnapshot, SavedChartConfig } from './types'; - -const CONNECTIONS_KEY = 'orchids_db_connections'; -const HISTORY_KEY = 'orchids_db_history'; -const SAVED_QUERIES_KEY = 'orchids_db_saved'; -const SCHEMA_SNAPSHOTS_KEY = 'libredb_schema_snapshots'; -const SAVED_CHARTS_KEY = 'libredb_saved_charts'; -const ACTIVE_CONNECTION_KEY = 'libredb_active_connection_id'; +import { + DatabaseConnection, + QueryHistoryItem, + SavedQuery, + SchemaSnapshot, + SavedChartConfig, +} from "./types"; + +const CONNECTIONS_KEY = "orchids_db_connections"; +const HISTORY_KEY = "orchids_db_history"; +const SAVED_QUERIES_KEY = "orchids_db_saved"; +const SCHEMA_SNAPSHOTS_KEY = "libredb_schema_snapshots"; +const SAVED_CHARTS_KEY = "libredb_saved_charts"; +const ACTIVE_CONNECTION_KEY = "libredb_active_connection_id"; const MAX_HISTORY_ITEMS = 500; const MAX_SNAPSHOTS = 50; export const storage = { // Connections getConnections: (): DatabaseConnection[] => { - if (typeof window === 'undefined') return []; + if (typeof window === "undefined") return []; const stored = localStorage.getItem(CONNECTIONS_KEY); if (!stored) return []; try { return JSON.parse(stored).map((conn: DatabaseConnection) => ({ ...conn, - createdAt: new Date(conn.createdAt) + createdAt: new Date(conn.createdAt), })); } catch (e) { - console.error('Failed to parse connections', e); + console.error("Failed to parse connections", e); return []; } }, saveConnection: (connection: DatabaseConnection) => { const connections = storage.getConnections(); - const existingIndex = connections.findIndex(c => c.id === connection.id); - + const existingIndex = connections.findIndex((c) => c.id === connection.id); + if (existingIndex > -1) { connections[existingIndex] = connection; } else { connections.push(connection); } - + localStorage.setItem(CONNECTIONS_KEY, JSON.stringify(connections)); }, deleteConnection: (id: string) => { const connections = storage.getConnections(); - const filtered = connections.filter(c => c.id !== id); + const filtered = connections.filter((c) => c.id !== id); localStorage.setItem(CONNECTIONS_KEY, JSON.stringify(filtered)); }, // History getHistory: (): QueryHistoryItem[] => { - if (typeof window === 'undefined') return []; + if (typeof window === "undefined") return []; const stored = localStorage.getItem(HISTORY_KEY); if (!stored) return []; try { return JSON.parse(stored).map((item: QueryHistoryItem) => ({ ...item, - executedAt: new Date(item.executedAt) + executedAt: new Date(item.executedAt), })); } catch (e) { - console.error('Failed to parse history', e); + console.error("Failed to parse history", e); return []; } }, @@ -73,56 +79,58 @@ export const storage = { // Saved Queries getSavedQueries: (): SavedQuery[] => { - if (typeof window === 'undefined') return []; + if (typeof window === "undefined") return []; const stored = localStorage.getItem(SAVED_QUERIES_KEY); if (!stored) return []; try { return JSON.parse(stored).map((q: SavedQuery) => ({ ...q, createdAt: new Date(q.createdAt), - updatedAt: new Date(q.updatedAt) + updatedAt: new Date(q.updatedAt), })); } catch (e) { - console.error('Failed to parse saved queries', e); + console.error("Failed to parse saved queries", e); return []; } }, saveQuery: (query: SavedQuery) => { const queries = storage.getSavedQueries(); - const existingIndex = queries.findIndex(q => q.id === query.id); - + const existingIndex = queries.findIndex((q) => q.id === query.id); + if (existingIndex > -1) { queries[existingIndex] = { ...query, updatedAt: new Date() }; } else { queries.push({ ...query, createdAt: new Date(), updatedAt: new Date() }); } - + localStorage.setItem(SAVED_QUERIES_KEY, JSON.stringify(queries)); }, deleteSavedQuery: (id: string) => { const queries = storage.getSavedQueries(); - const filtered = queries.filter(q => q.id !== id); + const filtered = queries.filter((q) => q.id !== id); localStorage.setItem(SAVED_QUERIES_KEY, JSON.stringify(filtered)); }, // Schema Snapshots getSchemaSnapshots: (connectionId?: string): SchemaSnapshot[] => { - if (typeof window === 'undefined') return []; + if (typeof window === "undefined") return []; const stored = localStorage.getItem(SCHEMA_SNAPSHOTS_KEY); if (!stored) return []; try { - const snapshots: SchemaSnapshot[] = JSON.parse(stored).map((s: SchemaSnapshot) => ({ - ...s, - createdAt: new Date(s.createdAt), - })); + const snapshots: SchemaSnapshot[] = JSON.parse(stored).map( + (s: SchemaSnapshot) => ({ + ...s, + createdAt: new Date(s.createdAt), + }), + ); if (connectionId) { - return snapshots.filter(s => s.connectionId === connectionId); + return snapshots.filter((s) => s.connectionId === connectionId); } return snapshots; } catch (e) { - console.error('Failed to parse schema snapshots', e); + console.error("Failed to parse schema snapshots", e); return []; } }, @@ -137,13 +145,13 @@ export const storage = { deleteSchemaSnapshot: (id: string) => { const snapshots = storage.getSchemaSnapshots(); - const filtered = snapshots.filter(s => s.id !== id); + const filtered = snapshots.filter((s) => s.id !== id); localStorage.setItem(SCHEMA_SNAPSHOTS_KEY, JSON.stringify(filtered)); }, // Saved Charts getSavedCharts: (): SavedChartConfig[] => { - if (typeof window === 'undefined') return []; + if (typeof window === "undefined") return []; const stored = localStorage.getItem(SAVED_CHARTS_KEY); if (!stored) return []; try { @@ -152,14 +160,14 @@ export const storage = { createdAt: new Date(c.createdAt), })); } catch (e) { - console.error('Failed to parse saved charts', e); + console.error("Failed to parse saved charts", e); return []; } }, saveChart: (chart: SavedChartConfig) => { const charts = storage.getSavedCharts(); - const existingIndex = charts.findIndex(c => c.id === chart.id); + const existingIndex = charts.findIndex((c) => c.id === chart.id); if (existingIndex > -1) { charts[existingIndex] = chart; } else { @@ -170,22 +178,67 @@ export const storage = { deleteChart: (id: string) => { const charts = storage.getSavedCharts(); - const filtered = charts.filter(c => c.id !== id); + const filtered = charts.filter((c) => c.id !== id); localStorage.setItem(SAVED_CHARTS_KEY, JSON.stringify(filtered)); }, // Active Connection ID getActiveConnectionId: (): string | null => { - if (typeof window === 'undefined') return null; + if (typeof window === "undefined") return null; return localStorage.getItem(ACTIVE_CONNECTION_KEY); }, setActiveConnectionId: (id: string | null) => { - if (typeof window === 'undefined') return; + if (typeof window === "undefined") return; if (id) { localStorage.setItem(ACTIVE_CONNECTION_KEY, id); } else { localStorage.removeItem(ACTIVE_CONNECTION_KEY); } }, + + // Cleanup invalid connections + cleanupInvalidConnections: (): number => { + if (typeof window === "undefined") return 0; + + const connections = storage.getConnections(); + const validConnections = connections.filter((conn) => { + // Skip demo connections - they're always valid + if (conn.type === "demo" || conn.isDemo) return true; + + // For PostgreSQL and MySQL, database name is required (unless using connection string) + if ( + (conn.type === "postgres" || conn.type === "mysql") && + !conn.connectionString + ) { + if (!conn.database || conn.database.trim() === "") { + console.warn( + `[Storage] Removing invalid ${conn.type} connection "${conn.name}" - missing database name`, + ); + return false; + } + } + + // For MongoDB with connection string mode, ensure connection string exists + if (conn.type === "mongodb" && conn.connectionString) { + if (conn.connectionString.trim() === "") { + console.warn( + `[Storage] Removing invalid MongoDB connection "${conn.name}" - empty connection string`, + ); + return false; + } + } + + return true; + }); + + const removedCount = connections.length - validConnections.length; + + if (removedCount > 0) { + localStorage.setItem(CONNECTIONS_KEY, JSON.stringify(validConnections)); + console.log(`[Storage] Cleaned up ${removedCount} invalid connection(s)`); + } + + return removedCount; + }, };