diff --git a/README.md b/README.md index e35118d..8574fbe 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,22 @@ For production npm run preview ``` +#### Configuration +The application will defaults to the connection setup on first access. + + +To preset connection details use the following ENV variables: +``` +VITE_CLICKHOUSE_URL +VITE_CLICKHOUSE_USER +VITE_CLICKHOUSE_PASS +``` + +For self-serving setups such as quackpipe +``` +VITE_SELFSERVICE +``` + ## 📄 License [ch-ui](https://github.com/caioricciuti/ch-ui) fork licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/src/components/common/AppInit.tsx b/src/components/common/AppInit.tsx index 7fa3da0..e4da575 100644 --- a/src/components/common/AppInit.tsx +++ b/src/components/common/AppInit.tsx @@ -9,6 +9,7 @@ declare global { VITE_CLICKHOUSE_PASS?: string; VITE_CLICKHOUSE_USE_ADVANCED?: boolean; VITE_CLICKHOUSE_CUSTOM_PATH?: string; + VITE_SELFSERVICE?: string; }; } } @@ -46,10 +47,19 @@ const AppInitializer = ({ children }: { children: ReactNode }) => { const envUrl = ( import.meta.env?.VITE_CLICKHOUSE_URL || window.env?.VITE_CLICKHOUSE_URL ); const envUser = ( import.meta.env?.VITE_CLICKHOUSE_USER || window.env?.VITE_CLICKHOUSE_USER ); const envPass = ( import.meta.env?.VITE_CLICKHOUSE_PASS || window.env?.VITE_CLICKHOUSE_PASS ); - const envUseAdvanced = window.env?.VITE_CLICKHOUSE_USE_ADVANCED; - const envCustomPath = window.env?.VITE_CLICKHOUSE_CUSTOM_PATH; + const envUseAdvanced = ( import.meta.env?.VITE_CLICKHOUSE_USE_ADVANCED || window.env?.VITE_CLICKHOUSE_USE_ADVANCED ); + const envCustomPath = ( import.meta.env?.VITE_CLICKHOUSE_CUSTOM_PATH || window.env?.VITE_CLICKHOUSE_CUSTOM_PATH ); - if (envUrl && envUser) { + if (import.meta.env?.VITE_SELFSERVICE || window.env?.VITE_SELFSERVICE) { + setCredential({ + url: window.location.origin, + username: 'default', + password: '', + useAdvanced: false, + customPath: "", + }); + setCredentialSource("env"); + } else if (envUrl) { setCredential({ url: envUrl, username: envUser, diff --git a/src/features/explorer/components/CreateDatabase.tsx b/src/features/explorer/components/CreateDatabase.tsx index 94b0dad..7c8eede 100644 --- a/src/features/explorer/components/CreateDatabase.tsx +++ b/src/features/explorer/components/CreateDatabase.tsx @@ -163,20 +163,6 @@ const CreateDatabase = () => { } sqlStatement += `${databaseName} `; - if (onCluster && clusterName) { - sqlStatement += `ON CLUSTER ${clusterName} `; - } - - if (engine === "Lazy") { - sqlStatement += `ENGINE = Lazy(${expirationTimeInSeconds}) `; - } else if (engine) { - sqlStatement += `ENGINE = ${engine} `; - } - - if (comment) { - sqlStatement += `COMMENT '${comment}'`; - } - setSql(sqlStatement.trim()); setErrors({}); return sqlStatement.trim(); diff --git a/src/features/explorer/components/CreateTable.tsx b/src/features/explorer/components/CreateTable.tsx index 1ce9094..377bbf1 100644 --- a/src/features/explorer/components/CreateTable.tsx +++ b/src/features/explorer/components/CreateTable.tsx @@ -171,7 +171,7 @@ const CreateTable = () => { return `${field.name} ${typeStr} ${nullableStr}${commentStr}`; }).join(",\n "); - let sql = `CREATE TABLE ${database}.${tableName}\n(\n ${fieldDefinitions}\n) ENGINE = ${engine}`; + let sql = `CREATE TABLE ${database}.${tableName}\n(\n ${fieldDefinitions}\n)`; if (primaryKeyFields.length > 0) { sql += `\nPRIMARY KEY (${primaryKeyFields.join(", ")})`; @@ -350,4 +350,4 @@ const CreateTable = () => { ); }; -export default CreateTable; \ No newline at end of file +export default CreateTable; diff --git a/src/features/metrics/config/metricsConfig.ts b/src/features/metrics/config/metricsConfig.ts index c6e9e38..d9176ee 100644 --- a/src/features/metrics/config/metricsConfig.ts +++ b/src/features/metrics/config/metricsConfig.ts @@ -1,6 +1,3 @@ -//ignore TS check -// @ts-nocheck - import { ChartConfig } from "@/components/ui/chart"; import { HomeIcon, @@ -13,6 +10,7 @@ import { CpuIcon, AlertTriangleIcon, } from "lucide-react"; +import { ReactNode, ComponentType } from "react"; export interface Metrics { title: string; @@ -28,29 +26,51 @@ export interface MetricItem { type: "card" | "table" | "chart"; chartType?: "bar" | "line" | "area" | "pie" | "radar" | "radial"; description: string; - chartConfig?: ChartConfig; + chartConfig?: CustomChartConfig; tiles?: number; } +export type ChartTheme = { + light: string; + dark: string; +} + +export type ChartDataConfig = { + label?: ReactNode; + icon?: ComponentType<{}>; +} & ({ color?: string; theme?: never } | { color?: never; theme: ChartTheme }); + +export type CustomChartConfig = { + indexBy: string; + [key: string]: ChartDataConfig | string | undefined; +} + export const metrics: Metrics[] = [ { title: "Overview", scope: "overview", - description: "Overview of ClickHouse metrics.", + description: "Overview of DuckDB metrics.", icon: HomeIcon, items: [ { title: "Server Uptime (days)", - query: `SELECT 1`, + query: ` + SELECT + ROUND(SUM(julianday('now') - julianday(start_time)), 2) AS uptime_days + FROM pragma_database_list + WHERE name = 'main' + `, type: "card", description: - "Total time the server has been running in seconds, minutes, hours, and days.", + "Total time the server has been running in days.", tiles: 1, }, { title: "Total Databases", query: ` - SELECT count(database) as total_databases FROM (SHOW ALL TABLES) + SELECT COUNT(*) AS total_databases + FROM pragma_database_list + WHERE name NOT IN ('main', 'temp') `, type: "card", description: "Total number of databases excluding system databases.", @@ -59,7 +79,9 @@ export const metrics: Metrics[] = [ { title: "Total Tables", query: ` - SELECT count(name) as total_tables FROM (SHOW ALL TABLES); + SELECT COUNT(*) AS total_tables + FROM information_schema.tables + WHERE table_schema NOT IN ('main', 'temp') `, type: "card", description: "Total number of user tables excluding temporary tables.", @@ -70,7 +92,7 @@ export const metrics: Metrics[] = [ query: `SELECT version() AS version`, type: "card", description: - "Version of the ClickHouse server running on the current instance.", + "Version of the DuckDB server running on the current instance.", tiles: 1, }, ], @@ -83,65 +105,92 @@ export const metrics: Metrics[] = [ items: [ { title: "Total Tables", - query: `SELECT count(name) as total_tables FROM (SHOW ALL TABLES);`, + query: ` + SELECT COUNT(*) AS total_tables + FROM information_schema.tables + WHERE table_schema NOT IN ('main', 'temp') + `, type: "card", description: "Total number of user-defined tables.", tiles: 1, }, { title: "Total System Tables", - query: `SELECT count(name) as total_tables FROM (SHOW ALL TABLES);`, + query: ` + SELECT COUNT(*) AS total_tables + FROM information_schema.tables + WHERE table_schema IN ('main', 'temp') + `, type: "card", description: "Total number of system tables.", tiles: 1, }, - ], - }, - { - title: "Queries", - scope: "queries", - description: "Comprehensive metrics related to queries in the system.", - icon: TerminalSquareIcon, - items: [ { - title: "Queries Per Second (QPS)", - query: `SELECT 0`, - type: "chart", - chartType: "area", - description: "Rate of queries per second over the last hour.", - chartConfig: { - indexBy: "minute", - qps: { - label: "QPS", - color: "hsl(var(--chart-3))", - }, - }, + title: "Total Temporary Tables", + query: ` + SELECT COUNT(*) AS total_tables + FROM information_schema.tables + WHERE table_schema LIKE 'temp%' + `, + type: "card", + description: "Total number of temporary tables.", + tiles: 1, + }, + { + title: "Biggest Table", + query: ` + SELECT table_name AS table + FROM information_schema.tables + WHERE table_schema NOT IN ('main', 'temp') + ORDER BY table_name DESC + LIMIT 1 + `, + type: "card", + description: "Largest table in the system.", + tiles: 1, + }, + { + title: "Table Cardinality", + query: ` + SELECT table_schema, table_name AS table, COUNT(*) AS total_rows + FROM information_schema.tables + WHERE table_schema NOT IN ('main', 'temp') + GROUP BY table_schema, table_name + ORDER BY total_rows DESC + LIMIT 10 + `, + type: "table", + description: "Number of rows in the top 10 tables.", + tiles: 2, + }, + { + title: "Table Row Counts", + query: ` + SELECT table_schema, table_name AS table, COUNT(*) AS total_rows + FROM information_schema.tables + WHERE table_schema NOT IN ('main', 'temp') + GROUP BY table_schema, table_name + ORDER BY total_rows DESC + LIMIT 10 + `, + type: "table", + description: "Number of rows in the top 10 tables.", tiles: 2, }, ], }, { - title: "Performance", - scope: "performance", - description: "Performance-related metrics.", - icon: CpuIcon, + title: "Settings & Config", + scope: "settings", + description: "Settings and configuration.", + icon: Settings2, items: [ { - title: "CPU Usage", - query: ` - SELECT 0 - `, - type: "chart", - chartType: "line", - description: "CPU usage over the last hour.", - chartConfig: { - indexBy: "minute", - cpu_usage: { - label: "CPU Usage", - color: "hsl(var(--chart-5))", - }, - }, - tiles: 2, + title: "Current Settings", + query: `SELECT * FROM pragma_settings`, + type: "table", + description: "Current DuckDB settings.", + tiles: 4, }, ], }, diff --git a/src/features/workspace/components/HomeTab.tsx b/src/features/workspace/components/HomeTab.tsx index a5aa823..2518521 100644 --- a/src/features/workspace/components/HomeTab.tsx +++ b/src/features/workspace/components/HomeTab.tsx @@ -93,27 +93,26 @@ const HomeTab = () => { setLoading(true); setError(null); try { - const recentQueries = await runQuery(` - SELECT DISTINCT - replaceAll(query, 'FORMAT JSON', '') AS cleaned_query, - max(event_time) AS latest_event_time, + const recentQueries = await runQuery(` + SELECT DISTINCT + REPLACE(query, 'FORMAT JSON', '') AS cleaned_query, + MAX(event_time) AS latest_event_time, query_kind, - length(replaceAll(query, 'FORMAT JSON', '')) AS query_length + LENGTH(REPLACE(query, 'FORMAT JSON', '')) AS query_length FROM - system.query_log + pragma_sql_log WHERE user = '${credential.username}' - AND event_time >= (current_timestamp() - INTERVAL 2 DAY) - AND arrayExists(db -> db NOT LIKE '%system%', databases) + AND event_time >= (CURRENT_TIMESTAMP - INTERVAL '2' DAY) AND query NOT LIKE 'SELECT DISTINCT%' GROUP BY cleaned_query, query_kind ORDER BY latest_event_time DESC - LIMIT - 6; - `); - setRecentItems(recentQueries.data); + LIMIT + 6; + `); + setRecentItems(recentQueries.data); } catch (err) { setError("Failed to load recent queries"); console.error(err); @@ -122,6 +121,7 @@ const HomeTab = () => { } }; + const truncateQuery = (query: string, length: number = 50) => { return query.length > length ? `${query.slice(0, length)}...` : query; }; diff --git a/src/features/workspace/editor/appQueries.ts b/src/features/workspace/editor/appQueries.ts index 3465836..5f8e873 100644 --- a/src/features/workspace/editor/appQueries.ts +++ b/src/features/workspace/editor/appQueries.ts @@ -6,30 +6,30 @@ export const appQueries: Record = { getIntellisense: { query: ` SELECT - database, - table, - name AS column_name, - type AS column_type - FROM system.columns - ORDER BY database, table, column_name; + table_catalog AS database, + table_schema AS schema, + table_name AS table, + column_name, + data_type AS column_type + FROM information_schema.columns + ORDER BY table_catalog, table_schema, table_name, column_name; `, }, getDatabasesTables: { query: ` SELECT - databases.name AS database_name, - tables.name AS table_name, - tables.engine AS table_type - FROM system.databases AS databases - LEFT JOIN system.tables AS tables - ON databases.name = tables.database - ORDER BY database_name, table_name; + table_catalog AS database_name, + table_schema AS schema_name, + table_name AS table_name, + table_type + FROM information_schema.tables + ORDER BY table_catalog, table_schema, table_name; `, }, getClickHouseFunctions: { - query: `SELECT name from system.functions`, + query: `SELECT function_name AS name FROM duckdb_functions`, }, getKeywords: { - query: `SELECT keyword FROM system.keywords`, + query: `SELECT keyword FROM duckdb_keywords`, }, }; diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index d474f94..18eae81 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -16,19 +16,15 @@ function HomePage() { }, []); return ( -
-{/* +
-*/} -{/* -*/} ()( try { const result = await clickHouseClient.query({ query: ` - SELECT if(grant_option = 1, true, false) AS is_admin - FROM system.grants - WHERE user_name = currentUser() - LIMIT 1 + SELECT false AS is_admin `, }); @@ -495,12 +492,16 @@ const useAppStore = create()( })); try { - const result = await runQuery(` - SELECT COUNT(*) as exists - FROM system.tables - WHERE database = 'CH_UI' - AND name = 'saved_queries' - `); + + + await runQuery("ATTACH '/tmp/chui.db' AS CH_UI"); + const result = await runQuery(` + SELECT COUNT(*) as exists + FROM information_schema.tables + WHERE table_catalog = 'CH_UI' + AND table_name = 'saved_queries' + `); + const response = result as SavedQueriesCheckResponse; const isActive = response.data[0]?.exists > 0; @@ -545,26 +546,24 @@ const useAppStore = create()( try { // Run queries in sequence with proper error handling - await runQuery("CREATE DATABASE IF NOT EXISTS CH_UI").then( - async () => { - await runQuery(` - CREATE TABLE IF NOT EXISTS CH_UI.saved_queries ( - id String, - name String, - query String, - created_at DateTime64(3), - updated_at DateTime64(3), - owner String, - is_public Boolean DEFAULT false, - tags Array(String) DEFAULT [], - description String DEFAULT '', - PRIMARY KEY (id) - ) ENGINE = MergeTree() - ORDER BY (id, created_at) - SETTINGS index_granularity = 8192 - `); - } - ); + await runQuery("ATTACH '/tmp/chui.db' AS CH_UI").then( + async () => { + await runQuery(` + CREATE TABLE IF NOT EXISTS CH_UI.saved_queries ( + id STRING, + name STRING, + query STRING, + created_at TIMESTAMP, + updated_at TIMESTAMP, + owner STRING, + is_public BOOLEAN DEFAULT false, + tags STRING[] DEFAULT [], + description STRING DEFAULT '', + PRIMARY KEY (id) + ) + `); + } + ); // Verify the table was created successfully const isActive = await get().checkSavedQueriesStatus(); @@ -605,7 +604,8 @@ const useAppStore = create()( })); try { - await runQuery("DROP TABLE IF EXISTS CH_UI.saved_queries"); + await runQuery("ATTACH '/tmp/chui.db' AS CH_UI"); + await runQuery("DROP TABLE IF EXISTS CH_UI.saved_queries"); // Verify the table was dropped successfully const isActive = await get().checkSavedQueriesStatus();