From c3015f8cb67836396d8f62713a6a83df524fbbb6 Mon Sep 17 00:00:00 2001 From: Caio Ricciuti Date: Fri, 4 Oct 2024 12:30:29 +0200 Subject: [PATCH 01/19] create new version --- src/App.jsx | 64 -- src/App.tsx | 43 + src/TabContents/HomeTabContent.jsx | 80 -- src/TabContents/QueryTabContent.jsx | 343 ------- src/components/AppInit.tsx | 37 + src/components/ConfirmationDialog.tsx | 103 +++ src/components/DbController.jsx | 227 ----- src/components/Sidebar.jsx | 303 ------ src/components/Sidebar.tsx | 332 +++++++ src/components/Tabs.jsx | 119 --- src/components/explorer/CreateDatabase.tsx | 447 +++++++++ src/components/explorer/CreateTable.tsx | 861 ++++++++++++++++++ src/components/explorer/DataExplorer.tsx | 126 +++ src/components/explorer/FieldManagement.tsx | 279 ++++++ src/components/explorer/FileUploadForm.tsx | 392 ++++++++ .../explorer/ManualCreationForm.tsx | 300 ++++++ src/components/explorer/TreeNode.tsx | 356 ++++++++ .../metrics/MetricItemComponent.tsx | 416 +++++++++ .../metrics/MetricsNavigationMenu.tsx | 138 +++ src/components/misc/DownloadDialog.tsx | 147 +++ src/components/misc/FilterComponent.tsx | 83 ++ src/components/privateRoute.tsx | 11 + src/components/table/CHUItable.tsx | 589 ++++++++++++ src/components/tabs/HomeTab.tsx | 130 +++ src/components/tabs/InformationTab.tsx | 282 ++++++ src/components/tabs/SqlTab.tsx | 142 +++ .../tabs/TableTabContent.tsx} | 71 +- src/components/tabs/WorkspaceTabs.tsx | 270 ++++++ src/components/tabs/editor/SqlEditor.tsx | 95 ++ src/components/theme-provider.tsx | 73 ++ src/components/ui/accordion.tsx | 56 ++ src/components/ui/alert-dialog.jsx | 97 -- src/components/ui/alert.jsx | 47 - src/components/ui/alert.tsx | 80 ++ src/components/ui/badge.jsx | 34 - src/components/ui/breadcrumb.tsx | 115 +++ src/components/ui/{button.jsx => button.tsx} | 29 +- src/components/ui/card-hover-effect.jsx | 101 -- src/components/ui/card.jsx | 50 - src/components/ui/card.tsx | 79 ++ src/components/ui/chart.tsx | 363 ++++++++ src/components/ui/checkbox.tsx | 28 + .../ui/{collapsible.jsx => collapsible.tsx} | 0 src/components/ui/command.jsx | 117 --- src/components/ui/command.tsx | 153 ++++ .../ui/{context-menu.jsx => context-menu.tsx} | 84 +- src/components/ui/{dialog.jsx => dialog.tsx} | 60 +- src/components/ui/{drawer.jsx => drawer.tsx} | 56 +- .../{dropdown-menu.jsx => dropdown-menu.tsx} | 85 +- src/components/ui/form.tsx | 176 ++++ src/components/ui/input.jsx | 19 - src/components/ui/input.tsx | 25 + src/components/ui/label.jsx | 16 - src/components/ui/label.tsx | 24 + src/components/ui/menubar.tsx | 234 +++++ src/components/ui/navigation-menu.tsx | 128 +++ src/components/ui/pagination.tsx | 117 +++ .../ui/{popover.jsx => popover.tsx} | 8 +- .../ui/{progress.jsx => progress.tsx} | 16 +- src/components/ui/radio-group.tsx | 42 + .../ui/{resizable.jsx => resizable.tsx} | 13 +- .../ui/{scroll-area.jsx => scroll-area.tsx} | 16 +- src/components/ui/{select.jsx => select.tsx} | 74 +- src/components/ui/separator.jsx | 23 - src/components/ui/separator.tsx | 29 + src/components/ui/{sheet.jsx => sheet.tsx} | 66 +- src/components/ui/skeleton.jsx | 10 - src/components/ui/skeleton.tsx | 15 + src/components/ui/slider.tsx | 26 + src/components/ui/{sonner.jsx => sonner.tsx} | 18 +- src/components/ui/sparkles.jsx | 421 --------- src/components/ui/table.jsx | 83 -- src/components/ui/table.tsx | 117 +++ src/components/ui/tabs.jsx | 44 - src/components/ui/tabs.tsx | 53 ++ src/components/ui/textarea.jsx | 18 - src/components/ui/textarea.tsx | 24 + .../ui/{tooltip.jsx => tooltip.tsx} | 8 +- src/helpers/appQueries.ts | 35 + src/helpers/donwloadCsv.js | 44 - src/helpers/echartsThemes.js | 37 - src/helpers/instanceAnalyticsQueries.js | 198 ---- src/helpers/metricsConfig.ts | 781 ++++++++++++++++ src/helpers/monacoConfig.ts | 435 +++++++++ src/helpers/transformRows.js | 36 - src/index.css | 226 +++-- src/lib/indexDB.ts | 66 ++ src/lib/tab.index.db.js | 95 -- src/lib/tablesIndexedDB.js | 98 -- src/lib/utils.js | 6 - src/lib/utils.ts | 6 + src/main.jsx | 13 - src/main.tsx | 12 + src/pages/Home.tsx | 29 + src/pages/HomePage.jsx | 44 - src/pages/InstanceMetricsPage.jsx | 264 ------ src/pages/Metrics.tsx | 130 +++ src/pages/NotFound.tsx | 38 + src/pages/Settings.tsx | 262 ++++++ src/pages/SettingsPage.jsx | 305 ------- src/providers/AutoCompleteMonaco.js | 228 ----- src/providers/ClickHouseContext.jsx | 146 --- src/providers/DatabasesTablesContext.jsx | 212 ----- src/providers/TabsStateContext.jsx | 324 ------- src/providers/theme.jsx | 55 -- src/store/appStore.ts | 357 ++++++++ src/types.ts | 84 ++ src/vite-env.d.ts | 1 + 108 files changed, 10360 insertions(+), 4563 deletions(-) delete mode 100644 src/App.jsx create mode 100644 src/App.tsx delete mode 100644 src/TabContents/HomeTabContent.jsx delete mode 100644 src/TabContents/QueryTabContent.jsx create mode 100644 src/components/AppInit.tsx create mode 100644 src/components/ConfirmationDialog.tsx delete mode 100644 src/components/DbController.jsx delete mode 100644 src/components/Sidebar.jsx create mode 100644 src/components/Sidebar.tsx delete mode 100644 src/components/Tabs.jsx create mode 100644 src/components/explorer/CreateDatabase.tsx create mode 100644 src/components/explorer/CreateTable.tsx create mode 100644 src/components/explorer/DataExplorer.tsx create mode 100644 src/components/explorer/FieldManagement.tsx create mode 100644 src/components/explorer/FileUploadForm.tsx create mode 100644 src/components/explorer/ManualCreationForm.tsx create mode 100644 src/components/explorer/TreeNode.tsx create mode 100644 src/components/metrics/MetricItemComponent.tsx create mode 100644 src/components/metrics/MetricsNavigationMenu.tsx create mode 100644 src/components/misc/DownloadDialog.tsx create mode 100644 src/components/misc/FilterComponent.tsx create mode 100644 src/components/privateRoute.tsx create mode 100644 src/components/table/CHUItable.tsx create mode 100644 src/components/tabs/HomeTab.tsx create mode 100644 src/components/tabs/InformationTab.tsx create mode 100644 src/components/tabs/SqlTab.tsx rename src/{TabContents/TableTabContent.jsx => components/tabs/TableTabContent.tsx} (86%) create mode 100644 src/components/tabs/WorkspaceTabs.tsx create mode 100644 src/components/tabs/editor/SqlEditor.tsx create mode 100644 src/components/theme-provider.tsx create mode 100644 src/components/ui/accordion.tsx delete mode 100644 src/components/ui/alert-dialog.jsx delete mode 100644 src/components/ui/alert.jsx create mode 100644 src/components/ui/alert.tsx delete mode 100644 src/components/ui/badge.jsx create mode 100644 src/components/ui/breadcrumb.tsx rename src/components/ui/{button.jsx => button.tsx} (70%) delete mode 100644 src/components/ui/card-hover-effect.jsx delete mode 100644 src/components/ui/card.jsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/chart.tsx create mode 100644 src/components/ui/checkbox.tsx rename src/components/ui/{collapsible.jsx => collapsible.tsx} (100%) delete mode 100644 src/components/ui/command.jsx create mode 100644 src/components/ui/command.tsx rename src/components/ui/{context-menu.jsx => context-menu.tsx} (69%) rename src/components/ui/{dialog.jsx => dialog.tsx} (57%) rename src/components/ui/{drawer.jsx => drawer.tsx} (55%) rename src/components/ui/{dropdown-menu.jsx => dropdown-menu.tsx} (69%) create mode 100644 src/components/ui/form.tsx delete mode 100644 src/components/ui/input.jsx create mode 100644 src/components/ui/input.tsx delete mode 100644 src/components/ui/label.jsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/components/ui/menubar.tsx create mode 100644 src/components/ui/navigation-menu.tsx create mode 100644 src/components/ui/pagination.tsx rename src/components/ui/{popover.jsx => popover.tsx} (79%) rename src/components/ui/{progress.jsx => progress.tsx} (56%) create mode 100644 src/components/ui/radio-group.tsx rename src/components/ui/{resizable.jsx => resizable.tsx} (82%) rename src/components/ui/{scroll-area.jsx => scroll-area.tsx} (70%) rename src/components/ui/{select.jsx => select.tsx} (64%) delete mode 100644 src/components/ui/separator.jsx create mode 100644 src/components/ui/separator.tsx rename src/components/ui/{sheet.jsx => sheet.tsx} (57%) delete mode 100644 src/components/ui/skeleton.jsx create mode 100644 src/components/ui/skeleton.tsx create mode 100644 src/components/ui/slider.tsx rename src/components/ui/{sonner.jsx => sonner.tsx} (65%) delete mode 100644 src/components/ui/sparkles.jsx delete mode 100644 src/components/ui/table.jsx create mode 100644 src/components/ui/table.tsx delete mode 100644 src/components/ui/tabs.jsx create mode 100644 src/components/ui/tabs.tsx delete mode 100644 src/components/ui/textarea.jsx create mode 100644 src/components/ui/textarea.tsx rename src/components/ui/{tooltip.jsx => tooltip.tsx} (79%) create mode 100644 src/helpers/appQueries.ts delete mode 100644 src/helpers/donwloadCsv.js delete mode 100644 src/helpers/echartsThemes.js delete mode 100644 src/helpers/instanceAnalyticsQueries.js create mode 100644 src/helpers/metricsConfig.ts create mode 100644 src/helpers/monacoConfig.ts delete mode 100644 src/helpers/transformRows.js create mode 100644 src/lib/indexDB.ts delete mode 100644 src/lib/tab.index.db.js delete mode 100644 src/lib/tablesIndexedDB.js delete mode 100644 src/lib/utils.js create mode 100644 src/lib/utils.ts delete mode 100644 src/main.jsx create mode 100644 src/main.tsx create mode 100644 src/pages/Home.tsx delete mode 100644 src/pages/HomePage.jsx delete mode 100644 src/pages/InstanceMetricsPage.jsx create mode 100644 src/pages/Metrics.tsx create mode 100644 src/pages/NotFound.tsx create mode 100644 src/pages/Settings.tsx delete mode 100644 src/pages/SettingsPage.jsx delete mode 100644 src/providers/AutoCompleteMonaco.js delete mode 100644 src/providers/ClickHouseContext.jsx delete mode 100644 src/providers/DatabasesTablesContext.jsx delete mode 100644 src/providers/TabsStateContext.jsx delete mode 100644 src/providers/theme.jsx create mode 100644 src/store/appStore.ts create mode 100644 src/types.ts create mode 100644 src/vite-env.d.ts diff --git a/src/App.jsx b/src/App.jsx deleted file mode 100644 index 7130812..0000000 --- a/src/App.jsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useEffect } from "react"; -import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom"; -import Sidebar from "./components/Sidebar"; -import { Toaster } from "@/components/ui/sonner"; -import { ClickHouseProvider } from "./providers/ClickHouseContext"; -import HomePage from "./pages/HomePage"; -import InstanceMetricsPage from "./pages/InstanceMetricsPage"; -import SettingsPage from "./pages/SettingsPage"; - -export default function App() { - useEffect(() => { - window.onbeforeunload = (e) => { - e.preventDefault(); - e.returnValue = null; - return null; - }; - }, []); - - return ( - -
- - - - - - - } /> - } - /> - } /> - -
-

404 Not Found

-

- The page you are looking for does not exist. -

-
- - Go to Homepage - -
-
-
- } - /> - - - - -
- ); -} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..1345c8b --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,43 @@ +import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; +import Sidebar from "./components/Sidebar"; +import HomePage from "@/pages/Home"; +import MetricsPage from "@/pages/Metrics"; +import SettingsPage from "@/pages/Settings"; +import { ThemeProvider } from "@/components/theme-provider"; +import AppInitializer from "@/components/AppInit"; +import NotFound from "./pages/NotFound"; +import { PrivateRoute } from "@/components/privateRoute"; // Import your PrivateRoute + +export default function App() { + return ( + + + +
+ + + + + + } + /> + + + + } + /> + } /> + } /> + +
+
+
+
+ ); +} diff --git a/src/TabContents/HomeTabContent.jsx b/src/TabContents/HomeTabContent.jsx deleted file mode 100644 index c75a931..0000000 --- a/src/TabContents/HomeTabContent.jsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Github, Database } from "lucide-react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import clickHouseSvg from "/ch_logo.svg"; -import { useTabState } from "@/providers/TabsStateContext"; - -export default function HomeTabContent() { - const { addQueryTab } = useTabState(); - - const projects = [ - { - title: "Star us on GitHub!", - description: - "Star us on GitHub if you like this project. It helps us a lot, and it also helps other people find this project.", - link: "https://github.com/caioricciuti/ch-ui", - icon: , - }, - { - description: - "ClickHouse is a fast open-source OLAP database management system, designed for big data analytics. Ah, also, it's open-source.", - link: "https://clickhouse.com/?utm_source=clickhouse-ui-app&utm_medium=home-tab-card", - icon: ClickHouse, - }, - { - title: "Start Querying", - description: - "Create and run queries on your ClickHouse instance. You can also save your queries for later use.", - icon: , - cta: "Let's go!", - action: () => addQueryTab(), - }, - ]; - return ( - <> -
-
-
- {projects.map((project, index) => ( - - -
- - - {project.title || "ClickHouse"} - - -
- {project.icon} -
-
-
- -
-
-

- {project.description} -

- {project.cta && ( - - )} -
-
-
-
- ))} -
-
-
- - ); -} diff --git a/src/TabContents/QueryTabContent.jsx b/src/TabContents/QueryTabContent.jsx deleted file mode 100644 index 1654d05..0000000 --- a/src/TabContents/QueryTabContent.jsx +++ /dev/null @@ -1,343 +0,0 @@ -import { useEffect, useState } from "react"; -import Editor from "@monaco-editor/react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { - DownloadIcon, - Loader2, - FileSpreadsheetIcon, - FileJson, -} from "lucide-react"; -import { - ResizableHandle, - ResizablePanel, - ResizablePanelGroup, -} from "@/components/ui/resizable"; -import { useTheme } from "@/providers/theme"; -import { useTabState } from "@/providers/TabsStateContext"; -import { AgGridReact } from "ag-grid-react"; -import "ag-grid-community/styles/ag-grid.css"; -import "ag-grid-community/styles/ag-theme-alpine.css"; -import { format } from "sql-formatter"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import downloadCsv from "/src/helpers/donwloadCsv.js"; -import transformRows from "/src/helpers/transformRows.js"; -import { suggestions } from "@/providers/AutoCompleteMonaco"; - -export default function QueryTabContent({ tab }) { - const { theme } = useTheme(); - const { saveTab, updateQueryTab, runQuery, isLoadingQuery } = useTabState(); - const [monacoEditorContent, setMonacoEditorContent] = useState(""); - const [editorInstance, setEditorInstance] = useState(null); - - useEffect(() => { - setMonacoEditorContent(tab.tab_content); - }, [tab.tab_content]); - - useEffect(() => { - const handleRunQueryShortCut = (event) => { - if (event.metaKey && event.key === "Enter") { - const query = getSelectedText(editorInstance) || tab.tab_content; - runQuery(tab.tab_id, query); - } - }; - document.addEventListener("keydown", handleRunQueryShortCut); - - const handleSaveQueryShortCut = (event) => { - if (event.metaKey && event.key === "s") { - event.preventDefault(); - saveTab(tab); - } - }; - document.addEventListener("keydown", handleSaveQueryShortCut); - - return () => { - document.removeEventListener("keydown", handleRunQueryShortCut); - document.removeEventListener("keydown", handleSaveQueryShortCut); - }; - }, [tab, editorInstance]); - - function formatBytes(bytes, decimals = 2) { - if (bytes === 0) return "0 Bytes"; - - const k = 1024; - const dm = decimals < 0 ? 0 : decimals; - const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; - - const i = Math.floor(Math.log(bytes) / Math.log(k)); - - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; - } - - window.sqlCompletionProviderRegistered = - window.sqlCompletionProviderRegistered || false; - - const handleEditorDidMount = (editor, monaco) => { - setEditorInstance(editor); - - editor.addAction({ - id: "format-sql", - label: "Format SQL", - contextMenuOrder: 1.5, - run: function (ed) { - ed.getAction("editor.action.formatDocument").run(); - }, - }); - - monaco.languages.registerDocumentFormattingEditProvider("sql", { - provideDocumentFormattingEdits: function (model) { - const formattedSql = format(model.getValue()); - return [ - { - range: model.getFullModelRange(), - text: formattedSql, - }, - ]; - }, - }); - - if (!window.sqlCompletionProviderRegistered) { - monaco.languages.registerCompletionItemProvider("sql", { - provideCompletionItems: (model, position) => { - const word = model.getWordUntilPosition(position); - const range = { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: word.startColumn, - endColumn: word.endColumn, - }; - - return { - suggestions: [...suggestions].map((suggestion) => ({ - ...suggestion, - range, - })), - }; - }, - }); - window.sqlCompletionProviderRegistered = true; - } - - editor.addAction({ - id: "run-query", - label: "Run Query", - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter], - contextMenuGroupId: "navigation", - contextMenuOrder: 1.5, - run: function (ed) { - const query = getSelectedText(ed) || ed.getValue(); - runQuery(tab.tab_id, query); - }, - }); - - editor.onDidChangeModelContent(() => { - const tab_content = editor.getValue(); - updateQueryTab(tab.tab_id, { tab_content }); - }); - }; - - const getSelectedText = (editor) => { - if (!editor) return ""; - const selection = editor.getSelection(); - return editor.getModel().getValueInRange(selection); - }; - - return ( - <> -
-
-
- { - updateQueryTab(tab.tab_id, { tab_title: e.target.value }); - }} - /> - -
-
- -
-
- - - - - - - {!tab.tab_results && !tab.tab_errors ? ( -
-

No results to display

-
- ) : tab.tab_errors ? ( -
-

- Error: {tab.tab_errors} -

-
- ) : ( -
-
- {isLoadingQuery ? ( -
- -
- ) : ( - 0 - ? Object.entries(tab.tab_results[0]).map((key, _) => { - if (Array.isArray(key[1])) { - return { - filter: true, - field: key[1], - headerName: key[0], - valueFormatter: (params) => params ? params.data[key[0]] - .map(d => d === null ? 'null' : d) - .join(', ') : '', - } - } - return { - headerName: key[0], - field: key[0], - filter: true - } - }) - : [] - } - defaultColDef={{ resizable: true }} - pagination={true} - paginationPageSize={20} - overlayLoadingTemplate={ - 'Please wait while your rows are loading' - } - overlayNoRowsTemplate={`This query returned no results`} - /> - )} -
- -
-

- Rows Read: - {isLoadingQuery ? ( - - ) : ( - tab.tab_results_statistics.rows_read - )} -

-

- Elapsed: - {isLoadingQuery ? ( - - ) : ( - tab.tab_results_statistics.elapsed - )} -

-

- Data Read: - {isLoadingQuery ? ( - - ) : ( - formatBytes(tab.tab_results_statistics.bytes_read) - )} -

-

- Last Run: - {isLoadingQuery ? ( - - ) : ( - `${new Date(tab.last_run).toUTCString()} (${Math.round( - (new Date() - new Date(tab.last_run)) / 60000 - )} minutes ago)` - )} -

- - - - - - - - - -
-
- )} -
-
-
- - ); -} diff --git a/src/components/AppInit.tsx b/src/components/AppInit.tsx new file mode 100644 index 0000000..08bd287 --- /dev/null +++ b/src/components/AppInit.tsx @@ -0,0 +1,37 @@ +import React, { useEffect, useState, ReactNode } from "react"; +import { useNavigate } from "react-router-dom"; +import useAppStore from "@/store/appStore"; +import { Loader2 } from "lucide-react"; +import { toast } from "sonner"; + +const AppInitializer = ({ children }: { children: ReactNode }) => { + const { initializeApp, isInitialized, error } = useAppStore(); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const init = async () => { + await initializeApp(); + setIsLoading(false); + }; + init(); + }, [initializeApp]); + + useEffect(() => { + if (error) { + toast.error(`Failed to initialize application: ${error}`); + } + }, [error]); + + if (isLoading) { + return ( +
+ + Initializing application... +
+ ); + } + + return <>{children}; +}; + +export default AppInitializer; diff --git a/src/components/ConfirmationDialog.tsx b/src/components/ConfirmationDialog.tsx new file mode 100644 index 0000000..6f4f79b --- /dev/null +++ b/src/components/ConfirmationDialog.tsx @@ -0,0 +1,103 @@ +import React from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { AlertCircle, AlertTriangle, CheckCircle, Info } from "lucide-react"; + +type Variant = "danger" | "warning" | "info" | "success"; + +interface ConfirmationDialogProps { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + title: string; + description: string; + confirmText?: string; + cancelText?: string; + variant?: Variant; + onConfirmAction?: () => void; +} + +const variantStyles: Record = + { + danger: { + icon: , + color: "text-red-500", + }, + warning: { + icon: , + color: "text-yellow-500", + }, + info: { + icon: , + color: "text-blue-500", + }, + success: { + icon: , + color: "text-green-500", + }, + }; + +const ConfirmationDialog: React.FC = ({ + isOpen, + onClose, + onConfirm, + title, + description, + confirmText = "Confirm", + cancelText = "Cancel", + variant = "danger", +}) => { + const { icon, color } = variantStyles[variant]; + + return ( + + + +
+ + {icon} + {title} + +
+ + {description} + +
+ + + + +
+
+ ); +}; + +export default ConfirmationDialog; diff --git a/src/components/DbController.jsx b/src/components/DbController.jsx deleted file mode 100644 index d435164..0000000 --- a/src/components/DbController.jsx +++ /dev/null @@ -1,227 +0,0 @@ -import { useState } from "react"; -import { useTabState } from "@/providers/TabsStateContext"; -import { useDatabaseTablesState } from "@/providers/DatabasesTablesContext"; -import { Progress } from "@/components/ui/progress"; - -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; - -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; - -import { - FileSpreadsheetIcon, - Loader2, - RefreshCcw, - SearchXIcon, - Table, - PlusIcon, - MoreVertical, -} from "lucide-react"; - -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; - -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; - -export default function DbController() { - const { addTableTab, addQueryTab } = useTabState(); - const { - selectedDatabase, - changeSelectedDatabase, - availableDatabases, - isLoading, - availableTables, - fetchDatabases, - getTablesFromDatabase, - loadingProgress, - deleteTablesStoreDB, - } = useDatabaseTablesState(); - - const [searchQuery, setSearchQuery] = useState(""); - - const handleDeleteCacheAndReload = async () => { - await deleteTablesStoreDB(); - fetchDatabases(true); - }; - - if (isLoading) { - return ( - <> -
-
- - - Loading tables for {selectedDatabase} - -
- - {loadingProgress > 0 && ( -
- {loadingProgress}% -
- )} -
- - ); - } - - return ( -
-
-
-

Databases

- - - - - - - Delete All Cache and Reload - - - -
-
- -
-
- {availableDatabases && availableTables.length > 0 ? ( -
- - Tables - ({availableTables.length}) - -
- setSearchQuery(e.target.value)} - /> -
- {availableTables.filter((table) => - table.name.toLowerCase().includes(searchQuery.toLowerCase()), - ).length > 0 ? ( - availableTables - .filter((table) => - table.name.toLowerCase().includes(searchQuery.toLowerCase()), - ) - .map((table) => ( - -
-
-
- {table.engine === "MergeTree" ? ( - - ) : ( - - )} - - - - - - - - - {table.schema?.length > 0 ? ( - table.schema.map((col) => ( -
- - {col.name} - - - {col.type} - -
- )) - ) : ( -
- No schema found -
- )} -
- - - )) - ) : ( -

- No tables found -

- )} - - ) : ( -

- No tables found in the selected database -

- )} - - ); -} \ No newline at end of file diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx deleted file mode 100644 index b1e928f..0000000 --- a/src/components/Sidebar.jsx +++ /dev/null @@ -1,303 +0,0 @@ -import React from "react"; -import { useClickHouseState } from "@/providers/ClickHouseContext"; -import { Link, useLocation, useNavigate } from "react-router-dom"; -import { - SquareTerminal, - Github, - Loader2, - CircleCheckIcon, - AlertCircleIcon, - Sun, - Moon, - LifeBuoy, - CornerDownLeft, - ServerCogIcon, - Settings2, - Search, -} from "lucide-react"; -import { - Sheet, - SheetContent, - SheetDescription, - SheetHeader, - SheetTitle, - SheetTrigger, -} from "@/components/ui/sheet"; - -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { useTheme } from "@/providers/theme"; -import { Button } from "@/components/ui/button"; - -import { - Command, - CommandDialog, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, - CommandShortcut, -} from "@/components/ui/command" - -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip" - -const commandsSheet = [ - { - action: "Save Query", - command: ["S/Ctrl+S"], - context: "Query Editor", - }, - { - action: "Run Query", - command: [ - "⌘", - "+", - , - "/Ctrl", - "+", - , - ], - context: "Query Editor", - }, -]; -export default function Sidebar() { - const { theme, setTheme } = useTheme(); // Use the theme context - const { isServerAvailable, isLoading, version } = useClickHouseState(); - const location = useLocation(); - const [open, setOpen] = React.useState(false) - - const navigate = useNavigate(); - - const toggleTheme = () => { - const newTheme = theme === "dark" ? "light" : "dark"; - setTheme(newTheme); - }; - React.useEffect(() => { - const down = (e) => { - if (e.key === "k" && (e.metaKey || e.ctrlKey)) { - e.preventDefault() - setOpen((open) => !open) - } - } - document.addEventListener("keydown", down) - return () => document.removeEventListener("keydown", down) - }, []) - - return ( -
- - - Action - Command - Context - - - - {commandsSheet.map((command) => ( - - {command.action} - - - {/* Map each part of the command array to correctly render text and icons */} - {command.command.map((part, i) => ( - - {part} - - ))} - - - {command.context} - - ))} - -
- - - - {/* theme changer */} - - - - - - - - - Cmd/Ctrl + K - - - - - - - - - {/* server status */} - - -
- {isLoading ? ( - - ) : isServerAvailable && !isLoading ? ( - - ) : ( - - )} -
-
- -
-

- Server Status:{" "} - {isLoading - ? "Loading..." - : isServerAvailable - ? "Online" - : "Offline"} -

-
-
-

Server Version: {version}

-
-
-
- - - ); -} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx new file mode 100644 index 0000000..85fd30a --- /dev/null +++ b/src/components/Sidebar.tsx @@ -0,0 +1,332 @@ +import { useState, useEffect, useRef } from "react"; +import { Link, useLocation, useNavigate } from "react-router-dom"; +import { useTheme } from "@/components/theme-provider"; +import { + SquareTerminal, + Github, + Loader2, + CircleCheckIcon, + AlertCircleIcon, + Sun, + Moon, + LifeBuoy, + CornerDownLeft, + Settings2, + Search, + ChevronRight, + ChevronLeft, + LineChart, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { toast } from "sonner"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandShortcut, +} from "@/components/ui/command"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import Logo from "/logo.png"; +import useAppStore from "@/store/appStore"; + +const commandsSheet = [ + { + action: "Save Query", + command: ["S/Ctrl+S"], + context: "Query Editor", + }, + { + action: "Run Query", + command: [ + "⌘", + "+", + , + "/Ctrl", + "+", + , + ], + context: "Query Editor", + }, +]; + +const Sidebar = () => { + const { theme, setTheme } = useTheme(); + const { isServerAvailable, version, isLoadingCredentials, clearCredentials } = + useAppStore(); + const location = useLocation(); + const navigate = useNavigate(); + const [isExpanded, setIsExpanded] = useState(false); + const [open, setOpen] = useState(false); + const sidebarRef = useRef(null); + + const toggleTheme = () => { + setTheme(theme === "dark" ? "light" : "dark"); + }; + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "k" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + setOpen((open) => !open); + } + }; + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, []); + + const navItems = [ + { to: "/", label: "Home", icon: SquareTerminal }, + { to: "/metrics", label: "Metrics", icon: LineChart }, + { to: "/settings", label: "Settings", icon: Settings2 }, + { + to: "https://github.com/caioricciuti/ch-ui?utm_source=ch-ui&utm_medium=sidebar", + label: "GitHub", + icon: Github, + isNewWindow: true, + }, + ]; + + return ( +
+
+ + Logo + {isExpanded && ( + CH-UI + )} + + {isExpanded && ( + + )} +
+ + + + + +
+
+ + + + + + + Search (Cmd/Ctrl + K) + + + + + + + + + + + + + Command Cheat Sheet + + Useful commands to enhance your experience. + + + + + + Action + Command + Context + + + + {commandsSheet.map((command) => ( + + {command.action} + + + {command.command.map((part, i) => ( + {part} + ))} + + + {command.context} + + ))} + +
+
+
+
+ + + + + + +
+

Server Status

+

+ {isLoadingCredentials + ? "Connecting..." + : isServerAvailable + ? "Connected" + : "Disconnected"} +

+

Version: {version}

+
+
+
+
+ + {!isExpanded && ( + + )} + + + + + No results found. + + {navItems.map((item) => ( + { + navigate(item.to); + setOpen(false); + }} + > + + {item.label} + + ))} + + + { + toast.success("Theme changed!"); + toggleTheme(); + setOpen(false); + }} + > + Toggle Theme + + { + clearCredentials(); + toast.success("Credentials cleared!"); + setOpen(false); + }} + > + Reset Credentials + + + + +
+ ); +}; + +export default Sidebar; diff --git a/src/components/Tabs.jsx b/src/components/Tabs.jsx deleted file mode 100644 index 4c2c602..0000000 --- a/src/components/Tabs.jsx +++ /dev/null @@ -1,119 +0,0 @@ -import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; -import { Button } from "@/components/ui/button"; -import { - TerminalSquareIcon, - FilePlus2, - HomeIcon, - Loader2, - SquareX, - Table, - X, -} from "lucide-react"; -import { Skeleton } from "./ui/skeleton"; -import { useTabState } from "@/providers/TabsStateContext"; -import HomeTabContent from "@/TabContents/HomeTabContent"; -import TableTabContent from "@/TabContents/TableTabContent"; -import QueryTabContent from "@/TabContents/QueryTabContent"; - -export default function TabsManager() { - const { - tabs, - activeTab, - setActiveTab, - isLoading, - addQueryTab, - deleteTab, - closeAllTabs - } = useTabState(); - - if (isLoading) { - return ( -
- - -
- ); - } - - return ( -
- { - setActiveTab(e); - }} - > - -
- - {tabs.length > 2 && ( - - - )} -
- {tabs.map((tab) => ( - - {tab.tab_type === "home" ? ( - - - - ) : tab.tab_type === "query" ? ( - - - {tab.tab_title} - - ) : tab.tab_type === "table" ? ( - - - {tab.tab_title} - - ) : ( - - {tab.tab_title} - - )} - {tab.tab_type !== "home" && ( - - )} - - ))} - - {tabs.map((tab) => ( - - {tab.tab_type === "query" && } - {tab.tab_type === "home" && } - {tab.tab_type === "table" && } - - ))} - - - ); -} \ No newline at end of file diff --git a/src/components/explorer/CreateDatabase.tsx b/src/components/explorer/CreateDatabase.tsx new file mode 100644 index 0000000..ecc71d6 --- /dev/null +++ b/src/components/explorer/CreateDatabase.tsx @@ -0,0 +1,447 @@ +import { useState, useEffect, useRef } from "react"; +import { z } from "zod"; +import { CopyIcon, CopyCheck } from "lucide-react"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Textarea } from "@/components/ui/textarea"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog"; +import { toast } from "sonner"; +import useAppstore from "@/store/appStore"; + +const ENGINE_OPTIONS = [ + "Atomic", + "Lazy", + "MySQL", + "PostgreSQL", + "MaterializedMySQL", + "MaterializedPostgreSQL", + "Replicated", + "SQLite", +]; + +const CreateDatabase = () => { + const { + isCreateDatabaseModalOpen, + closeCreateDatabaseModal, + fetchDatabaseInfo, + dataBaseExplorer, + runQuery, + addTab, + } = useAppstore(); + + // State variables for database creation + const [databaseName, setDatabaseName] = useState(""); + const [ifNotExists, setIfNotExists] = useState(false); + const [onCluster, setOnCluster] = useState(false); + const [clusterName, setClusterName] = useState(""); + const [engine, setEngine] = useState("Atomic"); + const [comment, setComment] = useState(""); + const [sql, setSql] = useState(""); + const [loading, setLoading] = useState(false); + const [errors, setErrors] = useState>({}); + const [createDatabaseError, setCreateDatabaseError] = useState(""); + const [statementCopiedToClipboard, setStatementCopiedToClipboard] = + useState(false); + + // State for confirmation dialog + const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false); + + // Ref for SQL copy button tooltip (optional) + const copyButtonRef = useRef(null); + + // Effect to reset fields when modal is opened + useEffect(() => { + if (isCreateDatabaseModalOpen) { + setDatabaseName(""); + setIfNotExists(false); + setOnCluster(false); + setClusterName(""); + setEngine("Atomic"); + setComment(""); + setSql(""); + setErrors({}); + setCreateDatabaseError(""); + setStatementCopiedToClipboard(false); + } + }, [isCreateDatabaseModalOpen]); + + // Validate if the database name is unique unless IF NOT EXISTS is checked + const validateDatabaseName = (name: string): boolean => { + if (ifNotExists) return true; + const existingDb = dataBaseExplorer.find( + (db: any) => db.name.toLowerCase() === name.toLowerCase() + ); + return !existingDb; + }; + + // Schema for database creation + const databaseSchema = z + .object({ + databaseName: z + .string() + .min(1, "Database name is required") + .refine((value) => !/\s/.test(value), { + message: "Database name cannot contain spaces", + }), + ifNotExists: z.boolean(), + onCluster: z.boolean(), + clusterName: z.string().min(1, "Cluster name is required").optional(), + engine: z.string(), + comment: z.string().optional(), + }) + .refine( + (data) => { + if (data.onCluster) { + return data.clusterName && data.clusterName.trim() !== ""; + } + return true; + }, + { + message: "Cluster name is required when ON CLUSTER is selected", + path: ["clusterName"], + } + ) + .refine((data) => validateDatabaseName(data.databaseName), { + message: "Database name already exists", + path: ["databaseName"], + }); + + // Function to validate and generate SQL + const validateAndGenerateSQL = () => { + try { + databaseSchema.parse({ + databaseName, + ifNotExists, + onCluster, + clusterName: onCluster ? clusterName : undefined, + engine, + comment, + }); + + let sqlStatement = `CREATE DATABASE `; + if (ifNotExists) { + sqlStatement += `IF NOT EXISTS `; + } + sqlStatement += `${databaseName} `; + + if (onCluster && clusterName) { + sqlStatement += `ON CLUSTER ${clusterName} `; + } + + if (engine) { + sqlStatement += `ENGINE = ${engine} `; + } + + if (comment) { + sqlStatement += `COMMENT '${comment}'`; + } + + setSql(sqlStatement.trim()); + setErrors({}); + return sqlStatement.trim(); + } catch (error) { + if (error instanceof z.ZodError) { + const newErrors: { [key: string]: string } = {}; + error.errors.forEach((err) => { + const path = err.path.join("."); + newErrors[path] = err.message; + }); + setErrors(newErrors); + } else { + toast.error("Unknown error occurred"); + } + return null; + } + }; + + // Function to handle creating the database + const handleCreateDatabase = async () => { + const sqlStatement = validateAndGenerateSQL(); + if (!sqlStatement) return; + + setLoading(true); + setCreateDatabaseError(""); + + try { + await runQuery(sqlStatement); + fetchDatabaseInfo(); + toast.success("Database created successfully!"); + + // Optionally, add a new tab or perform other actions + addTab({ + id: "database-" + databaseName, + title: databaseName, + type: "information", + content: "", + }); + + // Reset all fields + setDatabaseName(""); + setIfNotExists(false); + setOnCluster(false); + setClusterName(""); + setEngine("Atomic"); + setComment(""); + setSql(""); + setErrors({}); + setStatementCopiedToClipboard(false); + + closeCreateDatabaseModal(); + } catch (error: any) { + setCreateDatabaseError(error.toString()); + } finally { + setLoading(false); + } + }; + + // Function to handle copying SQL to clipboard + const handleCopySQL = () => { + navigator.clipboard.writeText(sql); + toast.success("SQL statement copied to clipboard!"); + setStatementCopiedToClipboard(true); + setTimeout(() => { + setStatementCopiedToClipboard(false); + }, 4000); + }; + + // Function to check if there are unsaved changes + const hasUnsavedChanges = () => { + return ( + databaseName || + ifNotExists || + onCluster || + clusterName || + engine !== "Atomic" || + comment + ); + }; + + // Function to handle closing the sheet with confirmation + const handleCloseSheet = () => { + if (hasUnsavedChanges()) { + setIsConfirmDialogOpen(true); + } else { + closeCreateDatabaseModal(); + } + }; + + const confirmClose = () => { + setIsConfirmDialogOpen(false); + closeCreateDatabaseModal(); + // Reset form fields here + }; + + return ( + <> + {/* Confirmation Dialog */} + + + + Confirm Close + + Are you sure you want to close? All your work will be lost. + + + + + + + + + + {/* Create Database Sheet */} + + + + Create Database + + +
+ {/* Database Name */} +
+ + { + setDatabaseName(e.target.value); + setErrors((prev) => { + const newErrors = { ...prev }; + delete newErrors.databaseName; + return newErrors; + }); + }} + placeholder="Enter database name" + className={errors.databaseName ? "border-red-500" : ""} + /> + {errors.databaseName && ( +

{errors.databaseName}

+ )} +
+ + {/* IF NOT EXISTS Checkbox */} +
+ + setIfNotExists(checked as boolean) + } + /> + +
+ + {/* ON CLUSTER Checkbox */} +
+ setOnCluster(checked as boolean)} + /> + +
+ + {/* Cluster Name Input */} + {onCluster && ( +
+ + { + setClusterName(e.target.value); + setErrors((prev) => ({ ...prev, clusterName: "" })); + }} + placeholder="Enter cluster name" + className={errors.clusterName ? "border-red-500" : ""} + /> + {errors.clusterName && ( +

{errors.clusterName}

+ )} +
+ )} + + {/* ENGINE Selector */} +
+ + +
+ + {/* COMMENT Textarea */} +
+ +