Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
83 changes: 43 additions & 40 deletions src/components/Studio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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();
Expand Down
131 changes: 92 additions & 39 deletions src/lib/storage.ts
Original file line number Diff line number Diff line change
@@ -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 [];
}
},
Expand All @@ -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 [];
}
},
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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;
},
};
Loading