diff --git a/bridge/package.json b/bridge/package.json index 1820072..e93155d 100644 --- a/bridge/package.json +++ b/bridge/package.json @@ -1,6 +1,6 @@ { "name": "relwave-bridge", - "version": "0.9.0-rc-1", + "version": "0.9.0-rc-5", "type": "commonjs", "main": "dist/index.cjs", "scripts": { diff --git a/bridge/src/services/projectStore.ts b/bridge/src/services/projectStore.ts index 01af84a..5bc0f7c 100644 --- a/bridge/src/services/projectStore.ts +++ b/bridge/src/services/projectStore.ts @@ -1343,6 +1343,7 @@ export class ProjectStore { type?: string; ssl?: boolean; name?: string; + url?: string; } | null { const get = (...keys: string[]): string | undefined => { for (const k of keys) { @@ -1359,9 +1360,10 @@ export class ProjectStore { const type = get("DB_TYPE", "DATABASE_TYPE", "DB_ENGINE", "DATABASE_ENGINE"); const sslStr = get("DB_SSL", "DATABASE_SSL"); const name = get("DB_CONNECTION_NAME", "DATABASE_CONNECTION_NAME"); + const url = get("DATABASE_URL", "DB_URL"); - // Must have at least host or database to be useful - if (!host && !database) return null; + // Must have at least host, database, or url to be useful + if (!host && !database && !url) return null; const port = portStr ? parseInt(portStr, 10) : undefined; const ssl = sslStr ? !["false", "0", "no"].includes(sslStr.toLowerCase()) : undefined; @@ -1375,6 +1377,7 @@ export class ProjectStore { type: type?.toLowerCase(), ssl, name, + url, }; } @@ -1400,6 +1403,7 @@ export class ProjectStore { type?: string; ssl?: boolean; name?: string; + url?: string; } | null; }> { const sourceMetaPath = path.join(sourcePath, PROJECT_FILES.metadata); diff --git a/package.json b/package.json index c0ebc9d..8f69649 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "relwave", "private": true, - "version": "0.9.0-rc-1", + "version": "0.9.0-rc-5", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 065b871..eeac12c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3073,7 +3073,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "relwave" -version = "0.9.0-rc-1" +version = "0.9.0-rc-5" dependencies = [ "serde", "serde_json", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ac21854..83f27b6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "relwave" -version = "0.9.0-rc-1" +version = "0.9.0-rc-5" description = "A powerful, cross-platform desktop application for database management with native Git version control." authors = ["yashh56"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 8824bd1..155a779 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "RelWave", - "version": "0.9.0-rc-1", + "version": "0.9.0-rc-5", "identifier": "tech.relwave.app", "build": { "beforeDevCommand": "vite", diff --git a/src/features/database/components/TablesExplorerPanel.tsx b/src/features/database/components/TablesExplorerPanel.tsx index f7c1bda..5240437 100644 --- a/src/features/database/components/TablesExplorerPanel.tsx +++ b/src/features/database/components/TablesExplorerPanel.tsx @@ -6,6 +6,14 @@ import { ScrollArea } from '@/components/ui/scroll-area'; import { SelectedTable, TableInfo } from "@/features/database/types"; import { CreateTableDialog } from '@/features/schema-explorer/components'; import { useTableExplorerPanel } from '../hooks/useTableExplorerPanel'; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "@/components/ui/context-menu"; +import InsertDataDialog from '@/features/database/components/InsertDataDialog'; +import DropTableDialog from '@/features/schema-explorer/components/DropTableDialog'; interface TablesExplorerPanelProps { @@ -36,7 +44,11 @@ export default function TablesExplorerPanel({ setFilter, toggleFavorite, filteredTables, - isSelected + isSelected, + insertTableData, + setInsertTableData, + dropTableData, + setDropTableData } = useTableExplorerPanel({ dbId, tables, @@ -108,36 +120,47 @@ export default function TablesExplorerPanel({ ) : ( filteredTables.map((table) => ( - + {table.name} + - {table.name} - - + + + setInsertTableData(table)}> + Insert Data + + setDropTableData(table)} className="text-destructive"> + Delete Table + + + )) )} @@ -155,6 +178,26 @@ export default function TablesExplorerPanel({ onOpenChange={setCreateTableOpen} schemaName={selectedSchema} /> + + {insertTableData && ( + !open && setInsertTableData(null)} + dbId={dbId} + schemaName={insertTableData.schema || "public"} + tableName={insertTableData.name} + /> + )} + + {dropTableData && ( + !open && setDropTableData(null)} + dbId={dbId} + schemaName={dropTableData.schema || "public"} + tableName={dropTableData.name} + /> + )} ); diff --git a/src/features/database/hooks/useTableExplorerPanel.ts b/src/features/database/hooks/useTableExplorerPanel.ts index 8033066..60fdb04 100644 --- a/src/features/database/hooks/useTableExplorerPanel.ts +++ b/src/features/database/hooks/useTableExplorerPanel.ts @@ -17,6 +17,8 @@ export function useTableExplorerPanel({ }: TablesExplorerPanelProps) { const [searchQuery, setSearchQuery] = useState(''); const [createTableOpen, setCreateTableOpen] = useState(false); + const [insertTableData, setInsertTableData] = useState(null); + const [dropTableData, setDropTableData] = useState(null); const [favorites, setFavorites] = useState>( new Set(JSON.parse(localStorage.getItem('favoriteTables') || '[]')) @@ -58,6 +60,10 @@ export function useTableExplorerPanel({ setFilter, toggleFavorite, filteredTables, - isSelected + isSelected, + insertTableData, + setInsertTableData, + dropTableData, + setDropTableData }; } \ No newline at end of file diff --git a/src/features/home/components/AddConnectionDialog.tsx b/src/features/home/components/AddConnectionDialog.tsx index e63b0e1..d7e5c7f 100644 --- a/src/features/home/components/AddConnectionDialog.tsx +++ b/src/features/home/components/AddConnectionDialog.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { Database, Link as LinkIcon, FolderOpen, Shield, Server, User, Lock, Key, FileKey } from "lucide-react"; +import { Database, Link as LinkIcon, FolderOpen, Shield, Server, User, Lock, Key, FileKey, Settings } from "lucide-react"; import { open as openDialog } from "@tauri-apps/plugin-dialog"; import { Button } from "@/components/ui/button"; import { @@ -31,7 +31,7 @@ export function AddConnectionDialog({ isLoading, initialData, }: AddConnectionDialogProps) { - const [useUrl, setUseUrl] = useState(false); + const [useUrl, setUseUrl] = useState(true); const [connectionUrl, setConnectionUrl] = useState(""); const [formData, setFormData] = useState(INITIAL_FORM_DATA); @@ -44,7 +44,7 @@ export function AddConnectionDialog({ // Reset to empty form when opening without initial data setFormData(INITIAL_FORM_DATA); setConnectionUrl(""); - setUseUrl(false); + setUseUrl(true); } } }, [open, initialData]); @@ -79,7 +79,7 @@ export function AddConnectionDialog({ if (!newOpen) { setFormData(INITIAL_FORM_DATA); setConnectionUrl(""); - setUseUrl(false); + setUseUrl(true); } onOpenChange(newOpen); }; @@ -89,7 +89,18 @@ export function AddConnectionDialog({ return ( - + { + if (e.key === "Enter") { + const target = e.target as HTMLElement; + if (target.tagName !== "BUTTON" && target.tagName !== "A") { + e.preventDefault(); + if (!isLoading) handleSubmit(); + } + } + }} + > @@ -104,11 +115,14 @@ export function AddConnectionDialog({ {!isSQLite && ( setUseUrl(v === "url")}> - Parameters URL + + + Parameters + )} @@ -135,270 +149,274 @@ export function AddConnectionDialog({ /> -
- - -
- - {isSQLite ? ( + {!useUrl && (
- -
- handleInputChange("database", e.target.value)} - className="h-9 text-sm font-mono flex-1" - /> - -
+ +
- ) : ( - <> -
-
- - handleInputChange("host", e.target.value)} - className="h-9 text-sm font-mono" - /> -
-
- + )} + + {!useUrl && ( + isSQLite ? ( +
+ +
handleInputChange("port", e.target.value)} - className="h-9 text-sm font-mono" + placeholder="/path/to/database.db" + value={formData.database} + onChange={(e) => handleInputChange("database", e.target.value)} + className="h-9 text-sm font-mono flex-1" /> +
+ ) : ( + <> +
+
+ + handleInputChange("host", e.target.value)} + className="h-9 text-sm font-mono" + /> +
+
+ + handleInputChange("port", e.target.value)} + className="h-9 text-sm font-mono" + /> +
+
-
-
- - handleInputChange("user", e.target.value)} - className="h-9 text-sm font-mono" - /> +
+
+ + handleInputChange("user", e.target.value)} + className="h-9 text-sm font-mono" + /> +
+
+ + handleInputChange("password", e.target.value)} + className="h-9 text-sm" + /> +
+
- + handleInputChange("password", e.target.value)} - className="h-9 text-sm" + placeholder="myapp" + value={formData.database} + onChange={(e) => handleInputChange("database", e.target.value)} + className="h-9 text-sm font-mono" />
-
-
- - handleInputChange("database", e.target.value)} - className="h-9 text-sm font-mono" - /> -
- - {showSslOption && ( -
- - setFormData((prev) => ({ ...prev, ssl: checked as boolean })) - } - /> - -
- )} + {showSslOption && ( +
+ + setFormData((prev) => ({ ...prev, ssl: checked as boolean })) + } + /> + +
+ )} - {/* SSH Tunnel Section */} -
-
-
- - + {/* SSH Tunnel Section */} +
+
+
+ + +
+ handleInputChange("useSsh", checked)} + />
- handleInputChange("useSsh", checked)} - /> -
- {formData.useSsh && ( -
-
-
- - handleInputChange("sshHost", e.target.value)} - className="h-8 text-xs font-mono" - /> + {formData.useSsh && ( +
+
+
+ + handleInputChange("sshHost", e.target.value)} + className="h-8 text-xs font-mono" + /> +
+
+ + handleInputChange("sshPort", e.target.value)} + className="h-8 text-xs font-mono" + /> +
+
- + handleInputChange("sshPort", e.target.value)} + placeholder="ubuntu" + value={formData.sshUser} + onChange={(e) => handleInputChange("sshUser", e.target.value)} className="h-8 text-xs font-mono" />
-
- -
- - handleInputChange("sshUser", e.target.value)} - className="h-8 text-xs font-mono" - /> -
-
- - -
- - {formData.sshAuthMethod === "password" ? (
- - handleInputChange("sshPassword", e.target.value)} - className="h-8 text-xs" - /> + +
- ) : ( -
-
- -
- handleInputChange("sshPrivateKeyPath", e.target.value)} - className="h-8 text-xs font-mono flex-1" - /> - -
-
+ + {formData.sshAuthMethod === "password" ? (
handleInputChange("sshPassphrase", e.target.value)} + value={formData.sshPassword} + onChange={(e) => handleInputChange("sshPassword", e.target.value)} className="h-8 text-xs" />
-
- )} -
- )} -
- + ) : ( +
+
+ +
+ handleInputChange("sshPrivateKeyPath", e.target.value)} + className="h-8 text-xs font-mono flex-1" + /> + +
+
+
+ + handleInputChange("sshPassphrase", e.target.value)} + className="h-8 text-xs" + /> +
+
+ )} +
+ )} +
+ + ) )}
diff --git a/src/features/home/components/DeleteConnectionDialog.tsx b/src/features/home/components/DeleteConnectionDialog.tsx index 5dbdd16..3e33ac7 100644 --- a/src/features/home/components/DeleteConnectionDialog.tsx +++ b/src/features/home/components/DeleteConnectionDialog.tsx @@ -62,7 +62,21 @@ export function DeleteConnectionDialog({ return ( - + { + if (e.key === "Enter") { + const target = e.target as HTMLElement; + if (target.tagName !== "BUTTON" && target.tagName !== "A") { + e.preventDefault(); + if (!isDeleting) { + if (step === 1) handleNext(); + else handleConfirmDelete(); + } + } + } + }} + > {step === 1 ? ( <> diff --git a/src/features/home/hooks/useDeleteConnection.ts b/src/features/home/hooks/useDeleteConnection.ts index c5f93c5..6ac080d 100644 --- a/src/features/home/hooks/useDeleteConnection.ts +++ b/src/features/home/hooks/useDeleteConnection.ts @@ -29,6 +29,8 @@ export function useDeleteConnection(onSuccess?: () => void) { mutationFn: databaseService.deleteDatabase, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["databases"] }); + queryClient.invalidateQueries({ queryKey: ["totalStats"] }); + queryClient.invalidateQueries({ queryKey: ["connectionStatus"] }); onSuccess?.(); }, }); diff --git a/src/features/project/components/ImportProjectDialog.tsx b/src/features/project/components/ImportProjectDialog.tsx index cc2e222..c663504 100644 --- a/src/features/project/components/ImportProjectDialog.tsx +++ b/src/features/project/components/ImportProjectDialog.tsx @@ -1,6 +1,6 @@ -import { useState } from "react"; +import { useState, useEffect, useCallback } from "react"; import { open } from "@tauri-apps/plugin-dialog"; -import { FolderOpen, Database, Check, AlertCircle, Loader2, FileSearch } from "lucide-react"; +import { FolderOpen, Database, Check, AlertCircle, Loader2, FileSearch, LinkIcon, Settings } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -19,9 +19,11 @@ import { SelectValue, } from "@/components/ui/select"; import { Checkbox } from "@/components/ui/checkbox"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import type { ScanImportResult } from "@/features/project/types"; import { projectService } from "@/services/bridge/project"; import { databaseService } from "@/services/bridge/database"; +import { parseConnectionUrl } from "@/lib/parseConnectionUrl"; // ========================================== // Types @@ -73,15 +75,25 @@ export function ImportProjectDialog({ const [dbForm, setDbForm] = useState(INITIAL_DB_FORM); const [error, setError] = useState(null); const [importedName, setImportedName] = useState(""); + const [useUrl, setUseUrl] = useState(true); + const [connectionUrl, setConnectionUrl] = useState(""); - const reset = () => { + const reset = useCallback(() => { setStep("pick-folder"); setSelectedPath(""); setScanResult(null); setDbForm(INITIAL_DB_FORM); setError(null); setImportedName(""); - }; + setUseUrl(true); + setConnectionUrl(""); + }, []); + + useEffect(() => { + if (isOpen) { + reset(); + } + }, [isOpen, reset]); const handleOpenChange = (newOpen: boolean) => { if (!newOpen) reset(); @@ -129,6 +141,28 @@ export function ImportProjectDialog({ ssl: env?.ssl ?? false, }); + if (env?.url) { + setConnectionUrl(env.url); + setUseUrl(true); + const parsed = parseConnectionUrl(env.url); + if (parsed) { + setDbForm(prev => ({ + ...prev, + type: parsed.type, + host: parsed.host, + port: parsed.port, + user: parsed.user, + password: parsed.password, + database: parsed.database, + ssl: parsed.ssl, + })); + } + } else if (!env?.host && !env?.database) { + setUseUrl(true); + } else { + setUseUrl(false); + } + setStep("preview"); } catch (err: any) { setError(err.message || "Failed to scan folder"); @@ -311,6 +345,49 @@ export function ImportProjectDialog({ {/* Connection Form */}
+ {dbForm.type !== "sqlite" && ( + setUseUrl(v === "url")}> + + + + URL + + + + Parameters + + + + )} + + {useUrl && dbForm.type !== "sqlite" && ( +
+ + { + const url = e.target.value; + setConnectionUrl(url); + const parsed = parseConnectionUrl(url); + if (parsed) { + setDbForm(prev => ({ + ...prev, + type: parsed.type, + host: parsed.host, + port: parsed.port, + user: parsed.user, + password: parsed.password, + database: parsed.database, + ssl: parsed.ssl, + })); + } + }} + className="font-mono text-xs h-9" + /> +
+ )} +
-
- - -
- - {dbForm.type === "sqlite" ? ( + {!useUrl && (
- -
- handleDbInputChange("database", e.target.value)} - className="h-9 text-sm font-mono flex-1" - /> - -
+ +
- ) : ( - <> -
-
- - handleDbInputChange("host", e.target.value)} - className="h-9 text-sm font-mono" - /> -
-
- + )} + + {!useUrl && ( + dbForm.type === "sqlite" ? ( +
+ +
handleDbInputChange("port", e.target.value)} - className="h-9 text-sm font-mono" + placeholder="/path/to/database.db" + value={dbForm.database} + onChange={(e) => handleDbInputChange("database", e.target.value)} + className="h-9 text-sm font-mono flex-1" /> +
+ ) : ( + <> +
+
+ + handleDbInputChange("host", e.target.value)} + className="h-9 text-sm font-mono" + /> +
+
+ + handleDbInputChange("port", e.target.value)} + className="h-9 text-sm font-mono" + /> +
+
+ +
+
+ + handleDbInputChange("user", e.target.value)} + className="h-9 text-sm font-mono" + /> +
+
+ + handleDbInputChange("password", e.target.value)} + className="h-9 text-sm" + /> +
+
-
- + handleDbInputChange("user", e.target.value)} + placeholder="myapp" + value={dbForm.database} + onChange={(e) => handleDbInputChange("database", e.target.value)} className="h-9 text-sm font-mono" />
-
- - handleDbInputChange("password", e.target.value)} - className="h-9 text-sm" + +
+ handleDbInputChange("ssl", checked as boolean)} /> +
-
- -
- - handleDbInputChange("database", e.target.value)} - className="h-9 text-sm font-mono" - /> -
- -
- handleDbInputChange("ssl", checked as boolean)} - /> - -
- + + ) )}
diff --git a/src/features/project/hooks/useDbQueries.ts b/src/features/project/hooks/useDbQueries.ts index 13168b3..e70244b 100644 --- a/src/features/project/hooks/useDbQueries.ts +++ b/src/features/project/hooks/useDbQueries.ts @@ -228,6 +228,8 @@ export function useAddDatabase() { onSuccess: () => { // Invalidate and refetch database list queryClient.invalidateQueries({ queryKey: queryKeys.databases }); + queryClient.invalidateQueries({ queryKey: ["totalStats"] }); + queryClient.invalidateQueries({ queryKey: ["connectionStatus"] }); }, }); } @@ -259,6 +261,8 @@ export function useDeleteDatabase() { mutationFn: (id: string) => databaseService.deleteDatabase(id), onSuccess: (_, id) => { queryClient.invalidateQueries({ queryKey: queryKeys.databases }); + queryClient.invalidateQueries({ queryKey: ["totalStats"] }); + queryClient.invalidateQueries({ queryKey: ["connectionStatus"] }); // Remove all cached data for this database queryClient.removeQueries({ queryKey: queryKeys.tables(id) }); queryClient.removeQueries({ queryKey: queryKeys.stats(id) }); diff --git a/src/features/project/types.ts b/src/features/project/types.ts index 02efbb4..ad38032 100644 --- a/src/features/project/types.ts +++ b/src/features/project/types.ts @@ -137,6 +137,7 @@ export interface ScanImportResult { type?: string; ssl?: boolean; name?: string; + url?:string } | null; }