From 7108f7a12d44c1fbe7731d81d16bddcd21fefc18 Mon Sep 17 00:00:00 2001 From: Caio Ricciuti Date: Sun, 20 Oct 2024 10:56:32 +0200 Subject: [PATCH] Create database, create table, delete database and delete table added --- package-lock.json | 13 +- package.json | 1 + src/components/explorer/CreateDatabase.tsx | 275 +++++--- src/components/explorer/CreateTable.tsx | 604 +++--------------- src/components/explorer/DataExplorer.tsx | 58 +- src/components/explorer/FieldManagement.tsx | 478 ++++++++------ src/components/explorer/FileUploadForm.tsx | 392 ------------ .../explorer/ManualCreationForm.tsx | 468 +++++++------- src/components/explorer/TreeNode.tsx | 91 +-- src/components/misc/InfoDialog.tsx | 68 ++ src/components/table/CHUItable.tsx | 11 +- src/components/tabs/InformationTab.tsx | 10 +- src/components/ui/sheet.tsx | 46 +- src/helpers/sqlUtils.ts | 24 +- src/store/appStore.ts | 33 +- src/types.ts | 3 +- 16 files changed, 1044 insertions(+), 1531 deletions(-) delete mode 100644 src/components/explorer/FileUploadForm.tsx create mode 100644 src/components/misc/InfoDialog.tsx diff --git a/package-lock.json b/package-lock.json index f36b15c..27708bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,6 +57,7 @@ "zustand": "^5.0.0-rc.2" }, "devDependencies": { + "@types/node": "^22.7.7", "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", "@vitejs/plugin-react": "^4.2.1", @@ -2583,12 +2584,10 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.7.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.6.tgz", - "integrity": "sha512-/d7Rnj0/ExXDMcioS78/kf1lMzYk4BZV8MZGTBKzTGZ6/406ukkbYlIsZmMPhcR5KlkunDHQLrtAVmSq7r+mSw==", + "version": "22.7.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.7.tgz", + "integrity": "sha512-SRxCrrg9CL/y54aiMCG3edPKdprgMVGDXjA3gB8UmmBW5TcXzRUYAh8EWzTnSJFAd1rgImPELza+A3bJ+qxz8Q==", "dev": true, - "optional": true, - "peer": true, "dependencies": { "undici-types": "~6.19.2" } @@ -7639,9 +7638,7 @@ "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/update-browserslist-db": { "version": "1.1.1", diff --git a/package.json b/package.json index 18a5f28..8144073 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "zustand": "^5.0.0-rc.2" }, "devDependencies": { + "@types/node": "^22.7.7", "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", "@vitejs/plugin-react": "^4.2.1", diff --git a/src/components/explorer/CreateDatabase.tsx b/src/components/explorer/CreateDatabase.tsx index ecc71d6..7254893 100644 --- a/src/components/explorer/CreateDatabase.tsx +++ b/src/components/explorer/CreateDatabase.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef } from "react"; import { z } from "zod"; -import { CopyIcon, CopyCheck } from "lucide-react"; +import { CopyIcon, CopyCheck, InfoIcon } from "lucide-react"; import { Sheet, SheetContent, @@ -30,17 +30,11 @@ import { } from "@/components/ui/dialog"; import { toast } from "sonner"; import useAppstore from "@/store/appStore"; +import InfoDialog from "@/components/misc/InfoDialog"; +import ConfirmationDialog from "@/components/ConfirmationDialog"; -const ENGINE_OPTIONS = [ - "Atomic", - "Lazy", - "MySQL", - "PostgreSQL", - "MaterializedMySQL", - "MaterializedPostgreSQL", - "Replicated", - "SQLite", -]; + +const ENGINE_OPTIONS = ["Atomic", "Lazy"]; const CreateDatabase = () => { const { @@ -53,12 +47,14 @@ const CreateDatabase = () => { } = useAppstore(); // State variables for database creation + const [isInfoDialogOpen, setIsInfoDialogOpen] = useState(false); 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 [expirationTimeInSeconds, setExpirationTimeInSeconds] = useState(""); const [sql, setSql] = useState(""); const [loading, setLoading] = useState(false); const [errors, setErrors] = useState>({}); @@ -81,6 +77,7 @@ const CreateDatabase = () => { setClusterName(""); setEngine("Atomic"); setComment(""); + setExpirationTimeInSeconds(""); setSql(""); setErrors({}); setCreateDatabaseError(""); @@ -103,6 +100,7 @@ const CreateDatabase = () => { databaseName: z .string() .min(1, "Database name is required") + .regex(/^[a-zA-Z0-9_]+$/, "Database name must contain only letters, numbers, and underscores") .refine((value) => !/\s/.test(value), { message: "Database name cannot contain spaces", }), @@ -111,6 +109,13 @@ const CreateDatabase = () => { clusterName: z.string().min(1, "Cluster name is required").optional(), engine: z.string(), comment: z.string().optional(), + expirationTimeInSeconds: z.preprocess((a) => { + if (typeof a === "string" && a.trim() !== "") { + const parsed = parseInt(a, 10); + return isNaN(parsed) ? undefined : parsed; + } + return undefined; + }, z.number().int().positive().optional()), }) .refine( (data) => { @@ -127,7 +132,20 @@ const CreateDatabase = () => { .refine((data) => validateDatabaseName(data.databaseName), { message: "Database name already exists", path: ["databaseName"], - }); + }) + .refine( + (data) => { + if (data.engine === "Lazy") { + return typeof data.expirationTimeInSeconds === "number"; + } + return true; + }, + { + message: + "Expiration time is required and must be a positive integer when engine is Lazy", + path: ["expirationTimeInSeconds"], + } + ); // Function to validate and generate SQL const validateAndGenerateSQL = () => { @@ -139,6 +157,8 @@ const CreateDatabase = () => { clusterName: onCluster ? clusterName : undefined, engine, comment, + expirationTimeInSeconds: + engine === "Lazy" ? expirationTimeInSeconds : undefined, }); let sqlStatement = `CREATE DATABASE `; @@ -151,7 +171,9 @@ const CreateDatabase = () => { sqlStatement += `ON CLUSTER ${clusterName} `; } - if (engine) { + if (engine === "Lazy") { + sqlStatement += `ENGINE = Lazy(${expirationTimeInSeconds}) `; + } else if (engine) { sqlStatement += `ENGINE = ${engine} `; } @@ -186,17 +208,24 @@ const CreateDatabase = () => { 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: "", - }); + const result = await runQuery(sqlStatement); + + if (result.error) { + setCreateDatabaseError(result.error); + setLoading(false); + return; + } else { + fetchDatabaseInfo(); + toast.success("Database created successfully!"); + + // Optionally, add a new tab or perform other actions + addTab({ + id: "database-" + databaseName, + title: databaseName, + type: "information", + content: { database: databaseName, table: "" }, + }); + } // Reset all fields setDatabaseName(""); @@ -205,6 +234,7 @@ const CreateDatabase = () => { setClusterName(""); setEngine("Atomic"); setComment(""); + setExpirationTimeInSeconds(""); setSql(""); setErrors({}); setStatementCopiedToClipboard(false); @@ -235,7 +265,8 @@ const CreateDatabase = () => { onCluster || clusterName || engine !== "Atomic" || - comment + comment || + expirationTimeInSeconds ); }; @@ -252,61 +283,97 @@ const CreateDatabase = () => { setIsConfirmDialogOpen(false); closeCreateDatabaseModal(); // Reset form fields here + setDatabaseName(""); + setIfNotExists(false); + setOnCluster(false); + setClusterName(""); + setEngine("Atomic"); + setComment(""); + setExpirationTimeInSeconds(""); + setSql(""); + setErrors({}); + setStatementCopiedToClipboard(false); }; return ( <> {/* Confirmation Dialog */} - - - - Confirm Close - - Are you sure you want to close? All your work will be lost. - - - - - - - - + setIsConfirmDialogOpen(false)} + onConfirm={confirmClose} + title={"Confirm Close"} + description={ + "Are you sure you want to close? All your work will be lost." + } + /> {/* Create Database Sheet */} - + { + e.preventDefault(); + // auto focus on the first input + const firstInput = (e.target as HTMLElement)?.querySelector("input"); + if (firstInput) { + firstInput.focus(); + } + }} + className="xl:w-[1000px] sm:w-full sm:max-w-full overflow-auto" + > - Create Database + + 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}

- )} +
+
+ + { + 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}

+ )} +
+ {/* ENGINE Selector */} +
+ + + +
{/* IF NOT EXISTS Checkbox */} @@ -358,22 +425,36 @@ const CreateDatabase = () => {
)} - {/* ENGINE Selector */} -
- - -
+ {/* Expiration Time Input (Conditional) */} + {engine === "Lazy" && ( +
+ + { + setExpirationTimeInSeconds(e.target.value); + setErrors((prev) => { + const newErrors = { ...prev }; + delete newErrors.expirationTimeInSeconds; + return newErrors; + }); + }} + placeholder="Enter expiration time in seconds" + className={ + errors.expirationTimeInSeconds ? "border-red-500" : "" + } + /> + {errors.expirationTimeInSeconds && ( +

+ {errors.expirationTimeInSeconds} +

+ )} +
+ )} {/* COMMENT Textarea */}
@@ -399,7 +480,10 @@ const CreateDatabase = () => {
+ + {/* Add the InfoDialog component here */} + setIsInfoDialogOpen(false)} + link="https://clickhouse.com/docs/en/engines/database-engines/?utm_source=ch-ui&utm_medium=create-database-info" + > + <> +

+ ClickHouse offers different database engines optimized for + various use cases. The choice of engine can affect performance, + data storage, and replication behavior. +

+

+ CH-UI supports only Atomic and Lazy engines. Using Atomic is + recommended for most use cases. +

+ +
diff --git a/src/components/explorer/CreateTable.tsx b/src/components/explorer/CreateTable.tsx index e6c98c1..b590b8f 100644 --- a/src/components/explorer/CreateTable.tsx +++ b/src/components/explorer/CreateTable.tsx @@ -8,17 +8,14 @@ import { SheetHeader, SheetTitle, } from "@/components/ui/sheet"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { toast } from "sonner"; import useAppStore from "@/store/appStore"; +import InfoDialog from "@/components/misc/InfoDialog"; import ConfirmationDialog from "@/components/ConfirmationDialog"; import ManualCreationForm from "@/components/explorer/ManualCreationForm"; -import FileUploadForm from "@/components/explorer/FileUploadForm"; -const TIME_FIELDS = ["Date", "DateTime"]; -const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB in bytes -const PREVIEW_ROW_COUNT = 10; // Number of rows to preview +const TIME_FIELDS = ["Date", "DateTime"]; // Interfaces interface Field { @@ -36,7 +33,7 @@ const CreateTable = () => { selectedDatabaseForCreateTable, closeCreateTableModal, fetchDatabaseInfo, - databaseData, + dataBaseExplorer, runQuery, addTab, } = useAppStore(); @@ -57,30 +54,8 @@ const CreateTable = () => { const [statementCopiedToClipBoard, setStatementCopiedToClipBoard] = useState(false); - // State variables for file upload - const [file, setFile] = useState(null); - const [fileType, setFileType] = useState<"csv" | "json">("csv"); - const [uploadedFileName, setUploadedFileName] = useState(""); - - // Advanced CSV options - const [csvDelimiter, setCsvDelimiter] = useState(","); - const [csvQuoteChar, setCsvQuoteChar] = useState('"'); - const [csvEscapeChar, setCsvEscapeChar] = useState("\\"); - const [csvHeaderRowsToSkip, setCsvHeaderRowsToSkip] = useState(0); - - // Nested JSON Handling - const [flattenJSON, setFlattenJSON] = useState(true); - const [jsonNestedPaths, setJsonNestedPaths] = useState([]); - - // State for confirmation dialog const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false); - // State for data preview - const [previewData, setPreviewData] = useState([]); - - // State for processing progress - const [isProcessing, setIsProcessing] = useState(false); - // Effect to set default database if selectedDatabaseForCreateTable is provided useEffect(() => { if (selectedDatabaseForCreateTable) { @@ -171,7 +146,7 @@ const CreateTable = () => { // Validate if the table name is unique in the selected database const validateTableName = (name: string) => { - const selectedDb = databaseData.find( + const selectedDb = dataBaseExplorer.find( (db: { name: string; children?: { name: string }[] }) => db.name === database ) as { name: string; children?: { name: string }[] } | undefined; @@ -215,126 +190,6 @@ const CreateTable = () => { .min(1, "At least one field is required"), }); - // Function to infer column types based on data - const inferColumnTypes = (headers: string[], data: any[]) => { - const types: string[] = headers.map(() => "String"); // Default type - - headers.forEach((header, colIndex) => { - let isNumber = true; - let isInteger = true; - let isDate = true; - - for (let row of data) { - const value = row[colIndex]; - if (value === null || value === undefined || value === "") continue; - - // Check for number - if (isNumber && isNaN(Number(value))) { - isNumber = false; - } - - // Check for integer - if (isInteger && !Number.isInteger(Number(value))) { - isInteger = false; - } - - // Check for date - if (isDate && isNaN(Date.parse(value))) { - isDate = false; - } - - // Early exit if all checks fail - if (!isNumber && !isInteger && !isDate) { - break; - } - } - - if (isInteger) { - types[colIndex] = "Int64"; - } else if (isNumber) { - types[colIndex] = "Float64"; - } else if (isDate) { - types[colIndex] = "DateTime"; - } else { - types[colIndex] = "String"; - } - }); - - return types; - }; - - // Function to flatten nested JSON objects - const flattenObject = ( - obj: any, - parentKey: string = "", - result: any = {} - ) => { - for (let key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - const newKey = parentKey ? `${parentKey}.${key}` : key; - if ( - typeof obj[key] === "object" && - obj[key] !== null && - !Array.isArray(obj[key]) - ) { - flattenObject(obj[key], newKey, result); - } else { - result[newKey] = obj[key]; - } - } - } - return result; - }; - - // Schema for file upload - const uploadSchema = z.object({ - database: z.string().min(1, "Database is required"), - tableName: z - .string() - .min(1, "Table name is required") - .refine(validateTableName, { - message: "Table name already exists in this database", - }), - file: z - .instanceof(File) - .refine( - (f) => f.size <= MAX_FILE_SIZE, - `File size should not exceed ${MAX_FILE_SIZE / (1024 * 1024)}MB` - ), - ...(fileType === "csv" - ? { - csvDelimiter: z - .string() - .min(1, "Delimiter is required") - .max(1, "Delimiter must be a single character"), - csvHeaderRowsToSkip: z.number().min(0, "Cannot skip negative rows"), - csvQuoteChar: z - .string() - .length(1, "Quote character must be a single character"), - csvEscapeChar: z - .string() - .length(1, "Escape character must be a single character"), - } - : {}), - ...(fileType === "json" - ? { - flattenJSON: z.boolean(), - jsonNestedPaths: z - .array(z.string()) - .optional() - .refine( - (paths) => { - if (!paths) return true; - return paths.every( - (path) => typeof path === "string" && path.length > 0 - ); - }, - { message: "Each path must be a non-empty string" } - ), - } - : {}), - }); - // Function to validate and generate SQL for manual creation const validateAndGenerateSQL = () => { try { @@ -390,283 +245,6 @@ const CreateTable = () => { } }; - // Function to handle file change - const handleFileChange = (selectedFile: File | null) => { - if (selectedFile) { - if (selectedFile.size > MAX_FILE_SIZE) { - setErrors((prev) => ({ - ...prev, - file: "File size exceeds 100MB limit", - })); - return; - } - setFile(selectedFile); - setUploadedFileName(selectedFile.name); - setErrors((prev) => ({ ...prev, file: "" })); - } - }; - - // Function to handle file type change - const handleFileTypeChange = (value: "csv" | "json") => { - setFileType(value); - setFile(null); - setUploadedFileName(""); - setErrors((prev) => ({ ...prev, file: "" })); - - // Reset advanced options when switching file types - if (value !== "csv") { - setCsvDelimiter(","); - setCsvQuoteChar('"'); - setCsvEscapeChar("\\"); - setCsvHeaderRowsToSkip(0); - } - if (value !== "json") { - setFlattenJSON(true); - setJsonNestedPaths([]); - } - }; - - // Function to generate preview data and infer fields - const generatePreviewData = (headers: string[], data: any[]) => { - const inferredTypes = inferColumnTypes(headers, data); - setPreviewData(data.slice(0, PREVIEW_ROW_COUNT)); - // Update field types based on inference - const updatedFields = headers.map((header, index) => ({ - name: header, - type: inferredTypes[index], - nullable: true, - isPrimaryKey: false, - isOrderBy: false, - isPartitionBy: false, - })); - setFields(updatedFields); - }; - - // Function to handle file-based table creation - const handleCreateFromFile = async () => { - if (!file) { - setErrors((prev) => ({ ...prev, file: "File is required" })); - return; - } - - const uploadData: any = { - database, - tableName, - file, - }; - - if (fileType === "csv") { - uploadData.csvDelimiter = csvDelimiter; - uploadData.csvHeaderRowsToSkip = csvHeaderRowsToSkip; - uploadData.csvQuoteChar = csvQuoteChar; - uploadData.csvEscapeChar = csvEscapeChar; - } - - if (fileType === "json") { - uploadData.flattenJSON = flattenJSON; - uploadData.jsonNestedPaths = jsonNestedPaths; - } - - try { - uploadSchema.parse(uploadData); - } 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); - } - return; - } - - setLoading(true); - setCreateTableError(""); - setIsProcessing(true); - - try { - const reader = new FileReader(); - reader.onload = async (e) => { - const content = e.target?.result; - if (!content || typeof content !== "string") { - setCreateTableError("Failed to read the file content."); - setLoading(false); - setIsProcessing(false); - return; - } - - let headers: string[] = []; - let data: any[] = []; - - if (fileType === "csv") { - const lines = content - .split("\n") - .filter((line) => line.trim() !== ""); - if (lines.length < 1) { - setCreateTableError( - "CSV file must have headers and at least one data row." - ); - setLoading(false); - setIsProcessing(false); - return; - } - - // Check if the CSV has headers - const useHeaders = true; // Assuming CSV has headers - if (useHeaders) { - headers = lines[0] - .split(csvDelimiter) - .map((header) => header.trim()); - data = lines - .slice(1 + csvHeaderRowsToSkip) - .map((line) => line.split(csvDelimiter)); - } else { - const columnCount = lines[0].split(csvDelimiter).length; - headers = Array.from( - { length: columnCount }, - (_, i) => `column_${i + 1}` - ); - data = lines.map((line) => line.split(csvDelimiter)); - } - - // Generate preview data and infer types - generatePreviewData(headers, data); - } else if (fileType === "json") { - try { - const jsonData = JSON.parse(content); - if (!Array.isArray(jsonData) || jsonData.length === 0) { - setCreateTableError( - "JSON file must contain an array of objects." - ); - setLoading(false); - setIsProcessing(false); - return; - } - - let processedData = jsonData; - if (flattenJSON) { - processedData = jsonData.map((item: any) => flattenObject(item)); - } else if (jsonNestedPaths.length > 0) { - // Implement logic to extract specific nested paths - processedData = jsonData.map((item: any) => { - const newItem: any = {}; - jsonNestedPaths.forEach((path) => { - const keys = path.split("."); - let value = item; - for (let key of keys) { - value = value ? value[key] : undefined; - } - newItem[path] = value; - }); - return newItem; - }); - } - - headers = Object.keys(processedData[0]); - data = processedData.map((obj: any) => Object.values(obj)); - - // Generate preview data and infer types - generatePreviewData(headers, data); - } catch (jsonError) { - setCreateTableError("Invalid JSON format."); - setLoading(false); - setIsProcessing(false); - return; - } - } - - // Validate fields before generating SQL - if (fields.length === 0 || fields.some((f) => !f.name || !f.type)) { - setCreateTableError("Failed to infer table schema from the file."); - setLoading(false); - setIsProcessing(false); - return; - } - - // Generate SQL statement - const fieldDefinitions = fields - .map( - (field) => - `${field.name} ${field.type}${ - field.nullable ? " NULL" : " NOT NULL" - }` - ) - .join(",\n "); - - let sqlStatement = `CREATE TABLE ${database}.${tableName}\n(\n ${fieldDefinitions}\n) ENGINE = ${engine}\n`; - - // For file uploads, set default ORDER BY and other clauses if not set - if (orderByFields.length === 0) { - sqlStatement += `ORDER BY tuple()\n`; - } else { - sqlStatement += `ORDER BY (${orderByFields.join(", ")})\n`; - } - - if (partitionByField) { - const partitionField = fields.find( - (f) => f.name === partitionByField - ); - if (partitionField && TIME_FIELDS.includes(partitionField.type)) { - sqlStatement += `PARTITION BY toYYYYMM(${partitionByField})\n`; - } else { - sqlStatement += `PARTITION BY ${partitionByField}\n`; - } - } - - if (primaryKeyFields.length > 0) { - sqlStatement += `PRIMARY KEY (${primaryKeyFields.join(", ")})\n`; - } - - if (comment) { - sqlStatement += `COMMENT '${comment}'`; - } - - setSql(sqlStatement.trim()); - - try { - // Execute CREATE TABLE - await runQuery(sqlStatement); - } catch (createError: any) { - setCreateTableError(createError.toString()); - setLoading(false); - setIsProcessing(false); - return; - } - - // Generate INSERT statements - let insertSQL = ""; - if (fileType === "csv") { - insertSQL = `INSERT INTO ${database}.${tableName} FORMAT CSVWithNames\n${content}`; - } else if (fileType === "json") { - insertSQL = `INSERT INTO ${database}.${tableName} FORMAT JSONEachRow\n${content}`; - } - - try { - await runQuery(insertSQL); - fetchDatabaseInfo(); - toast.success("Table created and data inserted successfully!"); - - // Reset all fields - resetForm(); - - closeCreateTableModal(); - } catch (insertError: any) { - setCreateTableError(insertError.toString()); - } finally { - setLoading(false); - setIsProcessing(false); - } - }; - - reader.readAsText(file); - } catch (error: any) { - setCreateTableError(error.toString()); - setLoading(false); - setIsProcessing(false); - } - }; - // Function to handle closing the sheet with confirmation const handleCloseSheet = () => { if ( @@ -681,8 +259,8 @@ const CreateTable = () => { field.isOrderBy || field.isPartitionBy ) || - comment || - file + comment + // Removed: || file ) { setIsConfirmDialogOpen(true); } else { @@ -708,15 +286,6 @@ const CreateTable = () => { setPartitionByField(null); setComment(""); setSql(""); - setFile(null); - setUploadedFileName(""); - setCsvDelimiter(","); - setCsvQuoteChar('"'); - setCsvEscapeChar("\\"); - setCsvHeaderRowsToSkip(0); - setFlattenJSON(true); - setJsonNestedPaths([]); - setPreviewData([]); setErrors({}); setCreateTableError(""); setStatementCopiedToClipBoard(false); @@ -730,14 +299,22 @@ const CreateTable = () => { setLoading(true); setCreateTableError(""); try { - await runQuery(sqlStatement); + const response = await runQuery(sqlStatement); + + if (response.error) { + setCreateTableError(response.error); + setLoading(false); + return; + } + fetchDatabaseInfo(); toast.success("Table created successfully!"); addTab({ + id: `${database}.${tableName}`, type: "information", title: `${database}.${tableName}`, - content: { database, table: tableName, query: "" }, + content: { database, table: tableName }, }); // Reset all fields @@ -757,101 +334,72 @@ const CreateTable = () => { setIsConfirmDialogOpen(false)} - onConfirm={confirmClose} title={""} description={""} /> + onConfirm={confirmClose} + title={"Confirm Close"} + description={ + "Are you sure you want to close? All your work will be lost." + } + /> {/* Create Table Sheet */} - + { + e.preventDefault(); + // auto focus on the first input + const firstInput = (e.target as HTMLElement)?.querySelector( + "input" + ); + if (firstInput) { + firstInput.focus(); + } + }} + className="xl:w-[1200px] sm:w-full sm:max-w-full overflow-auto" + > - Create Table + + Create Table{" "} + {database && tableName && ( + {`"${database}.${tableName}"`} + )} + - - - Manual Creation - File Upload - - - {/* Manual Table Creation Tab */} - - { - if (field === "database") setDatabase(value); - else if (field === "tableName") setTableName(value); - else if (field === "engine") setEngine(value); - else if (field === "comment") setComment(value); - }} - onAddField={addField} - onRemoveField={removeField} - onUpdateField={updateField} - onValidateAndGenerateSQL={validateAndGenerateSQL} - onCreateManual={handleCreateManual} - sql={sql} - onCopySQL={() => { - navigator.clipboard.writeText(sql); - toast.success("SQL statement copied to clipboard!"); - setStatementCopiedToClipBoard(true); - setTimeout(() => { - setStatementCopiedToClipBoard(false); - }, 4000); - }} - createTableError={createTableError} - statementCopiedToClipBoard={statementCopiedToClipBoard} - fieldTypes={[]} - databaseData={databaseData} - /> - - - {/* File Upload Tab */} - - { - if (field === "database") setDatabase(value); - else if (field === "tableName") setTableName(value); - }} - onFileChange={handleFileChange} - onFileTypeChange={handleFileTypeChange} - onCsvDelimiterChange={setCsvDelimiter} - onCsvQuoteCharChange={setCsvQuoteChar} - onCsvEscapeCharChange={setCsvEscapeChar} - onCsvHeaderRowsToSkipChange={setCsvHeaderRowsToSkip} - onFlattenJSONChange={setFlattenJSON} - onJsonNestedPathsChange={setJsonNestedPaths} - onRemoveFile={() => { - setFile(null); - setUploadedFileName(""); - setErrors((prev) => ({ ...prev, file: "" })); - }} - onCreateFromFile={handleCreateFromFile} - createTableError={createTableError} - isProcessing={isProcessing} - databaseData={databaseData} - /> - - + { + if (field === "database") setDatabase(value); + else if (field === "tableName") setTableName(value); + else if (field === "engine") setEngine(value); + else if (field === "comment") setComment(value); + }} + onAddField={addField} + onRemoveField={removeField} + onUpdateField={updateField} + onValidateAndGenerateSQL={validateAndGenerateSQL} + onCreateManual={handleCreateManual} + sql={sql} + onCopySQL={() => { + navigator.clipboard.writeText(sql); + toast.success("SQL statement copied to clipboard!"); + setStatementCopiedToClipBoard(true); + setTimeout(() => { + setStatementCopiedToClipBoard(false); + }, 4000); + }} + createTableError={createTableError} + statementCopiedToClipBoard={statementCopiedToClipBoard} + fieldTypes={[]} + databaseData={dataBaseExplorer} + /> diff --git a/src/components/explorer/DataExplorer.tsx b/src/components/explorer/DataExplorer.tsx index cb21ac4..728b2fa 100644 --- a/src/components/explorer/DataExplorer.tsx +++ b/src/components/explorer/DataExplorer.tsx @@ -2,14 +2,35 @@ import React, { useState, useCallback, useEffect, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { RefreshCcw, Search, SearchX } from "lucide-react"; +import { + RefreshCcw, + Search, + SearchX, + MoreVertical, + FolderPlus, + FilePlus, + TerminalIcon, +} from "lucide-react"; import useAppStore from "@/store/appStore"; import TreeNode, { TreeNodeData } from "@/components/explorer/TreeNode"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; const DatabaseExplorer: React.FC = () => { const [searchTerm, setSearchTerm] = useState(""); - const { dataBaseExplorer, tabError, fetchDatabaseInfo, isLoadingDatabase } = - useAppStore(); + const { + dataBaseExplorer, + tabError, + fetchDatabaseInfo, + isLoadingDatabase, + addTab, + openCreateDatabaseModal, + openCreateTableModal, + } = useAppStore(); const filteredData = useMemo(() => { if (!searchTerm) return dataBaseExplorer; @@ -37,7 +58,36 @@ const DatabaseExplorer: React.FC = () => {
-

Explorer

+
+

Explorer

+ + + + + + openCreateDatabaseModal()}> + Create Database + + openCreateTableModal("")}> + Create Table + + + addTab({ + id: Math.random().toString(36).substr(2, 9), + type: "sql", + title: "Query", + content: "", + }) + } + > + New Query + + + +
- {fields.map((field, index) => ( -
- {/* Field Name */} -
- - - - - - - Enter the name of the column. No spaces allowed. - - - - onUpdateField(index, "name", e.target.value)} - className={errors[`fields.${index}.name`] ? "border-red-500" : ""} - /> - {errors[`fields.${index}.name`] && ( -

- {errors[`fields.${index}.name`]} -

- )} -
+ {/* Fields List */} +
+ {fields.map((field, index) => ( +
+ {/* Field Name */} +
+ + + + + + + Enter the name of the column. No spaces allowed. + + + + onUpdateField(index, "name", e.target.value)} + className={`h-10 ${ + errors[`fields.${index}.name`] ? "border-red-500" : "" + }`} + aria-invalid={!!errors[`fields.${index}.name`]} + aria-describedby={ + errors[`fields.${index}.name`] + ? `error-field-name-${index}` + : undefined + } + /> + {errors[`fields.${index}.name`] && ( +

+ {errors[`fields.${index}.name`]} +

+ )} +
- {/* Field Type */} -
- - - - - - - Select the data type of the column. - - - - -
+ {/* Field Type */} +
+ + + + + + + Select the data type of the column. + + + + +
- {/* Nullable Selector */} -
- - - - - - - Specify whether the column can contain NULL values. - - - - -
- - {/* Column Description */} + {/* Nullable Selector */} +
+ + + + + + + Specify whether the column can contain NULL values. + + + + +
-
- - - - - - - Enter a description for the column. - - - - - onUpdateField(index, "description", e.target.value) - } - /> -
+ {/* Column Description */} +
+ + + + + + + Enter a description for the column. + + + + + onUpdateField(index, "description", e.target.value) + } + className="h-10" + /> +
- {/* Field Options: PK, OB, PB */} -
- {/* Primary Key Checkbox */} - - - - - onUpdateField(index, "isPrimaryKey", !field.isPrimaryKey) - } - disabled={!field.name} - aria-label="Primary Key" - /> - - - Mark this column as part of the primary key. - - - - + {/* Field Options: PK, OB, PB */} +
+ {/* Primary Key Checkbox */} +
+ + + + + onUpdateField( + index, + "isPrimaryKey", + !field.isPrimaryKey + ) + } + disabled={!field.name} + aria-label={`Mark ${field.name} as Primary Key`} + /> + + + + Mark this column as part of the primary key. + + + + + +
- {/* Order By Checkbox */} - - - - - onUpdateField(index, "isOrderBy", !field.isOrderBy) - } - disabled={!field.name} - aria-label="Order By" - /> - - - Include this column in the ORDER BY clause. - - - - + {/* Order By Checkbox */} +
+ + + + + onUpdateField(index, "isOrderBy", !field.isOrderBy) + } + disabled={!field.name} + aria-label={`Include ${field.name} in ORDER BY`} + /> + + + + Include this column in the ORDER BY clause. + + + + + +
- {/* Partition By Checkbox */} - - - - - onUpdateField( - index, - "isPartitionBy", - !field.isPartitionBy - ) - } - disabled={ - !field.name || !["Date", "DateTime"].includes(field.type) - } - aria-label="Partition By" - /> - - - Use this column for table partitioning. - - - - + {/* Partition By Checkbox */} +
+ + + + + onUpdateField( + index, + "isPartitionBy", + !field.isPartitionBy + ) + } + disabled={ + !field.name || + !["Date", "DateTime"].includes(field.type) + } + aria-label={`Use ${field.name} for Partition By`} + /> + + + + Use this column for table partitioning.
+ Only available for Date and DateTime types. +
+
+
+
+ +
- {/* Remove Field Button */} - + {/* Remove Field Button */} + +
-
- ))} + ))} +
); }; diff --git a/src/components/explorer/FileUploadForm.tsx b/src/components/explorer/FileUploadForm.tsx deleted file mode 100644 index 95e7b47..0000000 --- a/src/components/explorer/FileUploadForm.tsx +++ /dev/null @@ -1,392 +0,0 @@ -// components/CreateTable/FileUploadForm.tsx -import React, { useState } from "react"; -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 { - Accordion, - AccordionItem, - AccordionTrigger, - AccordionContent, -} from "@/components/ui/accordion"; -import { Checkbox } from "@/components/ui/checkbox"; -import { Upload, FileText, X } from "lucide-react"; -import { Textarea } from "@/components/ui/textarea"; -import { Alert, AlertDescription } from "@/components/ui/alert"; - -import CHUITable from "@/components/table/CHUItable"; // Import your CHUITable component - -interface FileUploadFormProps { - database: string; - tableName: string; - fileType: "csv" | "json"; - file: File | null; - uploadedFileName: string; - csvDelimiter: string; - csvQuoteChar: string; - csvEscapeChar: string; - csvHeaderRowsToSkip: number; - flattenJSON: boolean; - jsonNestedPaths: string[]; - errors: Record; - previewData: any[]; - fields: any[]; // Pass fields to reconstruct meta - databaseData: any[]; // Add databaseData property - onChange: (field: string, value: any) => void; - onFileChange: (file: File | null) => void; - onFileTypeChange: (type: "csv" | "json") => void; - onCsvDelimiterChange: (value: string) => void; - onCsvQuoteCharChange: (value: string) => void; - onCsvEscapeCharChange: (value: string) => void; - onCsvHeaderRowsToSkipChange: (value: number) => void; - onFlattenJSONChange: (value: boolean) => void; - onJsonNestedPathsChange: (paths: string[]) => void; - onRemoveFile: () => void; - onCreateFromFile: () => void; - createTableError: string; - isProcessing: boolean; -} - -const FileUploadForm: React.FC = ({ - database, - tableName, - fileType, - file, - uploadedFileName, - csvDelimiter, - csvQuoteChar, - csvEscapeChar, - csvHeaderRowsToSkip, - flattenJSON, - jsonNestedPaths, - errors, - previewData, - fields = [], // Ensure fields has a default value - onChange, - onFileChange, - onFileTypeChange, - onCsvDelimiterChange, - onCsvQuoteCharChange, - onCsvEscapeCharChange, - onCsvHeaderRowsToSkipChange, - onFlattenJSONChange, - onJsonNestedPathsChange, - onRemoveFile, - onCreateFromFile, - createTableError, - isProcessing, - databaseData, -}) => { - const [showPreview, setShowPreview] = useState(false); - - // Function to construct meta for CHUITable from fields - const constructMeta = () => { - return fields.map((field, index) => ({ - name: field.name || `Column ${index + 1}`, - type: field.type || "String", - })); - }; - - // Prepare the result object for CHUITable - const chuITableResult = { - meta: constructMeta(), - data: previewData.map((row) => { - const rowObj: any = {}; - fields.forEach((field, index) => { - rowObj[field.name || `Column ${index + 1}`] = row[index]; - }); - return rowObj; - }), - statistics: { - elapsed: 0, - rows_read: previewData.length, - bytes_read: 0, - }, - message: undefined, - query_id: undefined, - }; - - return ( -
- {/* Database Selector */} -
- - - {errors.database && ( -

{errors.database}

- )} -
- - {/* Table Name Input */} -
- - onChange("tableName", e.target.value)} - placeholder="Enter table name" - className={errors.tableName ? "border-red-500" : ""} - /> - {errors.tableName && ( -

{errors.tableName}

- )} -
- - {/* File Type Selector */} -
- - -
- - {/* Advanced Options Accordion */} - - - Advanced Options - - {/* Advanced CSV Options */} - {fileType === "csv" && ( -
-
- - onCsvDelimiterChange(e.target.value)} - placeholder="e.g., , ; |" - maxLength={1} - /> - {errors.csvDelimiter && ( -

- {errors.csvDelimiter} -

- )} -
- -
- - onCsvQuoteCharChange(e.target.value)} - placeholder='"' - maxLength={1} - /> - {errors.csvQuoteChar && ( -

- {errors.csvQuoteChar} -

- )} -
- -
- - onCsvEscapeCharChange(e.target.value)} - placeholder="\" - maxLength={1} - /> - {errors.csvEscapeChar && ( -

- {errors.csvEscapeChar} -

- )} -
- -
- - - onCsvHeaderRowsToSkipChange(Number(e.target.value)) - } - placeholder="0" - /> - {errors.csvHeaderRowsToSkip && ( -

- {errors.csvHeaderRowsToSkip} -

- )} -
-
- )} - - {/* Advanced JSON Options */} - {fileType === "json" && ( -
-
- - onFlattenJSONChange(!!checked) - } - aria-label="Flatten JSON" - /> - -
- {!flattenJSON && ( -
- -