diff --git a/README.md b/README.md index e2d4728..e53db9d 100644 --- a/README.md +++ b/README.md @@ -103,17 +103,19 @@ For a comprehensive breakdown of all features, see the [Feature Reference](FEATU - pnpm - Rust toolchain (for Tauri) +RelWave uses separate package manifests for the app and the bridge, so install dependencies in both locations. + #### Development ```bash git clone https://github.com/Relwave/relwave-app.git cd relwave-app -# Install frontend dependencies +# Install app dependencies pnpm install # Install bridge dependencies -cd bridge && pnpm install && cd .. +pnpm --dir bridge install # Start development mode pnpm tauri dev @@ -121,10 +123,17 @@ pnpm tauri dev #### Production Build +Install both dependency sets first: + +```bash +pnpm install +pnpm --dir bridge install +``` + **Windows:** ```bash -pnpm run package-bridge +pnpm run bridge:package pnpm tauri build ``` @@ -133,10 +142,12 @@ pnpm tauri build ```bash sudo apt install libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf -pnpm --dir bridge run build:pkg:linux +pnpm run bridge:package pnpm tauri build ``` +The root `bridge:package` script delegates to the platform-specific bridge packaging command in `bridge/package.json` and prepares the bundled SQLite native binary in `src-tauri/resources/`. + ## Documentation ### Architecture @@ -206,7 +217,7 @@ relwave-app/ └── src-tauri/ # Tauri backend (Rust) ├── src/ # Application entry point ├── capabilities/ # Permission definitions - └── resources/ # Bundled bridge executable + └── resources/ # Bundled bridge executable and SQLite native binding ``` ### Configuration diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/chart/ChartVisualization.tsx b/src/components/chart/ChartVisualization.tsx deleted file mode 100644 index 04b93a3..0000000 --- a/src/components/chart/ChartVisualization.tsx +++ /dev/null @@ -1,268 +0,0 @@ -import { useEffect, useState } from "react"; -import { Button } from "@/components/ui/button"; -import { Loader2, BarChart3, Download, ChevronDown } from "lucide-react"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { toPng, toSvg } from "html-to-image"; -import { toast } from "sonner"; -import { ChartConfigPanel } from "./ChartConfigPanel"; -import ChartRenderer from "./ChartRenderer"; -import { ColumnDetails, SelectedTable } from "@/types/database"; -import { bridgeApi } from "@/services/bridgeApi"; - -interface ChartVisualizationProps { - selectedTable: SelectedTable; - dbId?: string; -} - -interface QueryResultRow { - count: string; -} - -interface QueryResultColumn { - name: string -} - -export interface QueryResultEventDetail { - sessionId: string; - batchIndex: number; - rows: QueryResultRow[]; - columns: QueryResultColumn[]; - completed: boolean; -} - -export const ChartVisualization = ({ selectedTable, dbId }: ChartVisualizationProps) => { - - const [chartType, setChartType] = useState<"bar" | "line" | "pie" | "scatter">("bar"); - const [xAxis, setXAxis] = useState(""); - const [yAxis, setYAxis] = useState(""); - const [chartTitle, setChartTitle] = useState("Query Results Visualization"); - const [columnData, setColumnData] = useState([]); - const [schemaData, setSchemaData] = useState(null); - const [rowData, setRowData] = useState([]); - const [querySessionId, setQuerySessionId] = useState(null); - const [isExecuting, setIsExecuting] = useState(false); - const [queryProgress, setQueryProgress] = useState(null); - const [errorMessage, setErrorMessage] = useState(null); - - - const handleExport = async (format: "png" | "svg") => { - const chartElement = document.getElementById("chart-container"); - if (!chartElement) return; - - try { - // Determine background based on current theme (simple detection based on dark class presence) - const isDarkMode = chartElement.closest('.dark'); - const backgroundColor = isDarkMode ? "#050505" : "#FFFFFF"; // Use app background - - const dataUrl = format === "png" - ? await toPng(chartElement, { quality: 0.95, backgroundColor }) - : await toSvg(chartElement, { backgroundColor }); - - const link = document.createElement("a"); - link.download = `chart-${Date.now()}.${format}`; - link.href = dataUrl; - link.click(); - - toast.success(`Chart exported as ${format.toUpperCase()}`); - } catch (error) { - toast.error("Failed to export chart"); - setErrorMessage("Failed to export chart"); - } - }; - - useEffect(() => { - async function getTables() { - if (dbId) { - try { - const result = await bridgeApi.getSchema(dbId); - const schemas = result?.schemas - - schemas?.map((schema) => { - if (schema.name === selectedTable.schema) { - schema.tables.map((table) => { - if (table.name === selectedTable.name) { - setColumnData(table.columns); - } - }) - } - }); - } catch (error) { - toast.error("Failed to fetch table schema"); - setErrorMessage("Failed to fetch table schema"); - } - } - } - - - getTables(); - }, [selectedTable, dbId]); - - // Execute query when x or y axis changes - useEffect(() => { - if (!xAxis || !yAxis) return; - - const executeQuery = async () => { - // Clear old data immediately when config changes - setRowData([]); - setIsExecuting(true); - setErrorMessage(null); - - try { - const sessionId = `chart-${Date.now()}`; - setQuerySessionId(sessionId); - - // X-axis: grouping dimension (non-primary keys like address, name) - // Y-axis: what we're counting (primary keys like id) - const sql = ` - SELECT "${xAxis}" as name, COUNT("${yAxis}") as count - FROM "${selectedTable.schema}"."${selectedTable.name}" - GROUP BY "${xAxis}" - ORDER BY count DESC - LIMIT 50 - `; - - await bridgeApi.runQuery({ - sessionId, - dbId: dbId || "", - sql: sql.trim(), - batchSize: 50, - }); - - // Query execution started successfully - } catch (err: any) { - console.error("Query execution error:", err); - setErrorMessage(err.message || "Failed to execute query"); - setIsExecuting(false); - setRowData([]); // Clear data on error - } - }; - - executeQuery(); - }, [xAxis, yAxis, selectedTable, dbId]); - - useEffect(() => { - const handleResult = (event: CustomEvent) => { - if (event.detail.sessionId !== querySessionId) return; - setSchemaData(event.detail); - // Replace data instead of appending to prevent accumulation - setRowData(event.detail.rows); - setIsExecuting(false); - }; - - const handleError = (event: CustomEvent) => { - if (event.detail.sessionId !== querySessionId) return; - - setIsExecuting(false); - setQuerySessionId(null); - setQueryProgress(null); - toast.error("Query failed", { description: event.detail.error?.message || "An error occurred" }); - }; - - const eventListeners = [ - { name: 'bridge:query.result', handler: handleResult }, - { name: 'bridge:query.error', handler: handleError }, - ]; - - eventListeners.forEach(listener => { - window.addEventListener(listener.name, listener.handler as EventListener); - }); - - return () => { - eventListeners.forEach(listener => { - window.removeEventListener(listener.name, listener.handler as EventListener); - }); - }; - }, [querySessionId]); - - - return ( -
- {/* Config Panel */} -
-
-
-
- -
- Configure Chart -
- - - - - - - handleExport("png")} className="text-xs"> - Export as PNG - - handleExport("svg")} className="text-xs"> - Export as SVG - - - -
- - -
- - {/* Chart Container */} -
- {chartTitle && ( -

- {chartTitle} -

- )} - - {isExecuting ? ( -
- -

Processing data...

-
- ) : errorMessage ? ( -
-
- -
-

{errorMessage}

-
- ) : !rowData.length ? ( -
-
- -
-

Select axes to visualize data

-
- ) : ( - - )} -
-
- ); -}; \ No newline at end of file diff --git a/src/components/common/DeveloperContextMenu.tsx b/src/components/common/DeveloperContextMenu.tsx deleted file mode 100644 index 89f41b5..0000000 --- a/src/components/common/DeveloperContextMenu.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import { useEffect, useState, useCallback } from "react"; -import { invoke } from "@tauri-apps/api/core"; -import { - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuSeparator, - ContextMenuTrigger, - ContextMenuShortcut, -} from "@/components/ui/context-menu"; -import { - ArrowLeft, - ArrowRight, - RefreshCw, - Bug, - Copy, - ClipboardPaste, - Scissors, -} from "lucide-react"; -import { getDeveloperMode } from "@/hooks/useDeveloperMode"; - -interface DeveloperContextMenuProps { - children: React.ReactNode; -} - -export function DeveloperContextMenu({ children }: DeveloperContextMenuProps) { - const [devMode, setDevMode] = useState(getDeveloperMode); - - // Listen for developer mode changes - useEffect(() => { - const handleChange = (e: CustomEvent<{ enabled: boolean }>) => { - setDevMode(e.detail.enabled); - }; - - window.addEventListener("developer-mode-change", handleChange as EventListener); - return () => { - window.removeEventListener("developer-mode-change", handleChange as EventListener); - }; - }, []); - - // Block dev tools keyboard shortcuts when dev mode is disabled - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (devMode) return; // Allow shortcuts when dev mode is enabled - - // Block Ctrl+Shift+I, Ctrl+Shift+J, Ctrl+Shift+C, F12 - if ( - (e.ctrlKey && e.shiftKey && (e.key === 'I' || e.key === 'i' || e.key === 'J' || e.key === 'j' || e.key === 'C' || e.key === 'c')) || - e.key === 'F12' - ) { - e.preventDefault(); - e.stopPropagation(); - } - }; - - window.addEventListener('keydown', handleKeyDown, true); - return () => { - window.removeEventListener('keydown', handleKeyDown, true); - }; - }, [devMode]); - - const handleReload = useCallback(async () => { - try { - await invoke("reload_webview"); - } catch (e) { - console.error("Failed to reload:", e); - window.location.reload(); - } - }, []); - - const handleBack = useCallback(async () => { - try { - await invoke("navigate_back"); - } catch (e) { - console.error("Failed to navigate back:", e); - window.history.back(); - } - }, []); - - const handleForward = useCallback(async () => { - try { - await invoke("navigate_forward"); - } catch (e) { - console.error("Failed to navigate forward:", e); - window.history.forward(); - } - }, []); - - const handleInspect = useCallback(async () => { - try { - await invoke("open_devtools"); - } catch (e) { - console.error("Failed to open devtools:", e); - } - }, []); - - const handleCopy = useCallback(() => { - document.execCommand("copy"); - }, []); - - const handleCut = useCallback(() => { - document.execCommand("cut"); - }, []); - - const handlePaste = useCallback(async () => { - try { - const text = await navigator.clipboard.readText(); - document.execCommand("insertText", false, text); - } catch (e) { - document.execCommand("paste"); - } - }, []); - - // If developer mode is disabled, block all context menus - if (!devMode) { - return ( -
e.preventDefault()} - > - {children} -
- ); - } - - return ( - - -
{children}
-
- - - - Back - Alt+← - - - - Forward - Alt+→ - - - - Reload - Ctrl+R - - - - - Inspect Element - F12 - - -
- ); -} diff --git a/src/components/common/ModeToggle.tsx b/src/components/common/ModeToggle.tsx deleted file mode 100644 index 4312c7b..0000000 --- a/src/components/common/ModeToggle.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Moon, Sun } from "lucide-react" - -import { Button } from "@/components/ui/button" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { useTheme } from "@/components/common/ThemeProvider" - -export function ModeToggle() { - const { setTheme } = useTheme() - - return ( - - - - - - setTheme("light")}> - Light - - setTheme("dark")}> - Dark - - setTheme("system")}> - System - - - - ) -} \ No newline at end of file diff --git a/src/components/common/index.ts b/src/components/common/index.ts deleted file mode 100644 index 8972c75..0000000 --- a/src/components/common/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { DataTable } from './DataTable' -export { ModeToggle } from './ModeToggle' -export { ThemeProvider, useTheme } from './ThemeProvider' diff --git a/src/components/dev/DeveloperContextMenu.tsx b/src/components/dev/DeveloperContextMenu.tsx new file mode 100644 index 0000000..32a7d36 --- /dev/null +++ b/src/components/dev/DeveloperContextMenu.tsx @@ -0,0 +1,61 @@ +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, + ContextMenuShortcut, +} from "@/components/ui/context-menu"; +import { ArrowLeft, ArrowRight, RefreshCw, Bug } from "lucide-react"; +import { useDevMode } from "@/features/settings/hooks/useDevMode"; +import { useDevModeKeyboard } from "@/features/settings/hooks/useDevModeKeyboard"; +import { useWebviewActions } from "@/features/settings/hooks/useWebviewActions"; + +interface DeveloperContextMenuProps { + children: React.ReactNode; +} + +export const DeveloperContextMenu = ({ children }: DeveloperContextMenuProps) => { + const devMode = useDevMode(); + useDevModeKeyboard(devMode); + const { reload, goBack, goForward, openDevtools } = useWebviewActions(); + + if (!devMode) { + return ( +
e.preventDefault()}> + {children} +
+ ); + } + + return ( + + +
{children}
+
+ + + + Back + Alt+← + + + + Forward + Alt+→ + + + + Reload + Ctrl+R + + + + + Inspect Element + F12 + + +
+ ); +}; \ No newline at end of file diff --git a/src/components/common/RemoteConfigDialog.tsx b/src/components/dev/RemoteConfigDialog.tsx similarity index 98% rename from src/components/common/RemoteConfigDialog.tsx rename to src/components/dev/RemoteConfigDialog.tsx index 11da2f7..c504a10 100644 --- a/src/components/common/RemoteConfigDialog.tsx +++ b/src/components/dev/RemoteConfigDialog.tsx @@ -23,9 +23,9 @@ import { useGitRemoteAdd, useGitRemoteRemove, useGitRemoteSetUrl, -} from "@/hooks/useGitAdvanced"; +} from "@/features/git/hooks/useGitAdvanced"; import { toast } from "sonner"; -import type { GitRemoteInfo } from "@/types/git"; +import type { GitRemoteInfo } from "@/features/git/types"; interface RemoteConfigDialogProps { open: boolean; @@ -95,7 +95,7 @@ export default function RemoteConfigDialog({ -
+
{isLoading && (
diff --git a/src/components/feedback/index.ts b/src/components/feedback/index.ts deleted file mode 100644 index e361719..0000000 --- a/src/components/feedback/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as BridgeFailed } from './BridgeFailed' -export { default as BridgeLoader } from './BridgeLoader' diff --git a/src/components/common/SlideOutPanel.tsx b/src/components/layout/SlideOutPanel.tsx similarity index 100% rename from src/components/common/SlideOutPanel.tsx rename to src/components/layout/SlideOutPanel.tsx diff --git a/src/components/common/TitleBar.tsx b/src/components/layout/TitleBar.tsx similarity index 100% rename from src/components/common/TitleBar.tsx rename to src/components/layout/TitleBar.tsx diff --git a/src/components/common/VerticalIconBar.tsx b/src/components/layout/VerticalIconBar.tsx similarity index 98% rename from src/components/common/VerticalIconBar.tsx rename to src/components/layout/VerticalIconBar.tsx index 3441ea8..58c3aed 100644 --- a/src/components/common/VerticalIconBar.tsx +++ b/src/components/layout/VerticalIconBar.tsx @@ -42,7 +42,7 @@ export default function VerticalIconBar({ dbId, activePanel, onPanelChange }: Ve ] : []; return ( -