From d68d6fedeafb94d66a85386d03e4c154bebd9fef Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Wed, 10 Apr 2024 20:27:23 +0700 Subject: [PATCH] add trigger and improve search --- src/components/schema-sidebar-list.tsx | 145 ++++++++++++++++++++----- src/components/tabs/trigger-tab.tsx | 74 +++++++++++++ src/drivers/base-driver.ts | 17 +++ src/drivers/sqlite-base-driver.ts | 41 +++++-- src/lib/sql-parse-trigger.ts | 19 +--- src/messages/open-tab.tsx | 15 ++- 6 files changed, 261 insertions(+), 50 deletions(-) create mode 100644 src/components/tabs/trigger-tab.tsx diff --git a/src/components/schema-sidebar-list.tsx b/src/components/schema-sidebar-list.tsx index 3fab7bd5..426706cf 100644 --- a/src/components/schema-sidebar-list.tsx +++ b/src/components/schema-sidebar-list.tsx @@ -1,34 +1,55 @@ import { ScrollArea } from "./ui/scroll-area"; import { buttonVariants } from "./ui/button"; import { cn } from "@/lib/utils"; -import { LucideIcon, Table2 } from "lucide-react"; +import { LucideCog, LucideIcon, LucideView, Table2 } from "lucide-react"; import { OpenContextMenuList, openContextMenuFromEvent, } from "@/messages/openContextMenu"; import { useSchema } from "@/context/SchemaProvider"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { openTab } from "@/messages/open-tab"; +import { DatabaseSchemaItem } from "@/drivers/base-driver"; interface SchemaListProps { search: string; } +type DatabaseSchemaTreeNode = { + node: DatabaseSchemaItem; + sub: DatabaseSchemaItem[]; +}; + interface SchemaViewItemProps { + item: DatabaseSchemaItem; + highlight?: string; icon: LucideIcon; + iconClassName?: string; title: string; selected: boolean; onClick: () => void; onContextMenu: React.MouseEventHandler; + indentation?: boolean; } function SchemaViewItem({ icon: Icon, + iconClassName, title, onClick, + highlight, selected, onContextMenu, + indentation, + item, }: Readonly) { + const regex = new RegExp( + "(" + (highlight ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", + "i" + ); + + const splitedText = title.split(regex); + return (
{ - openTab({ - type: "table", - tableName: title, - }); + if (item.type === "table" || item.type === "view") { + openTab({ + type: "table", + tableName: title, + }); + } else if (item.type === "trigger") { + openTab({ + type: "trigger", + name: item.name, + }); + } }} className={cn( buttonVariants({ @@ -51,8 +79,21 @@ function SchemaViewItem({ "cursor-pointer" )} > - - {title} + {indentation && ( +
+ )} + + + {splitedText.map((text, idx) => { + return text.toLowerCase() === (highlight ?? "").toLowerCase() ? ( + + {text} + + ) : ( + {text} + ); + })} +
); } @@ -66,13 +107,16 @@ export default function SchemaList({ search }: Readonly) { }, [setSelectedIndex, search]); const prepareContextMenu = useCallback( - (tableName?: string) => { + (item?: DatabaseSchemaItem) => { + const selectedName = item?.name; + const isTable = item?.type === "table"; + return [ { title: "Copy Name", - disabled: !tableName, + disabled: !selectedName, onClick: () => { - window.navigator.clipboard.writeText(tableName ?? ""); + window.navigator.clipboard.writeText(selectedName ?? ""); }, }, { separator: true }, @@ -84,26 +128,57 @@ export default function SchemaList({ search }: Readonly) { }); }, }, - { - title: "Edit Table", - disabled: !tableName, - onClick: () => { - openTab({ - tableName, - type: "schema", - }); - }, - }, + isTable + ? { + title: "Edit Table", + onClick: () => { + openTab({ + tableName: item?.name, + type: "schema", + }); + }, + } + : undefined, { separator: true }, { title: "Refresh", onClick: () => refresh() }, - ] as OpenContextMenuList; + ].filter(Boolean) as OpenContextMenuList; }, [refresh] ); - const filteredSchema = schema.filter((s) => { - return s.name.toLowerCase().indexOf(search.toLowerCase()) >= 0; - }); + const filteredSchema = useMemo(() => { + // Build the tree first then we can flat it + let tree: DatabaseSchemaTreeNode[] = []; + const treeHash: Record = {}; + + for (const item of schema) { + if (item.type === "table" || item.type === "view") { + const node = { node: item, sub: [] }; + treeHash[item.name] = node; + tree.push(node); + } + } + + for (const item of schema) { + if (item.type === "trigger" && item.tableName) { + if (treeHash[item.tableName]) { + treeHash[item.tableName].sub.push(item); + } + } + } + + tree = tree.filter((s) => { + const foundName = + s.node.name.toLowerCase().indexOf(search.toLowerCase()) >= 0; + const foundInChildren = + s.sub.filter( + (c) => c.name.toLowerCase().indexOf(search.toLowerCase()) >= 0 + ).length > 0; + return foundName || foundInChildren; + }); + + return tree.map((r) => [r.node, ...r.sub]).flat(); + }, [schema, search]); return ( ) { openContextMenuFromEvent( prepareContextMenu( selectedIndex && schema[selectedIndex] - ? schema[selectedIndex].name + ? schema[selectedIndex] : undefined ) )(e) @@ -120,15 +195,29 @@ export default function SchemaList({ search }: Readonly) { >
{filteredSchema.map((item, schemaIndex) => { + let icon = Table2; + let iconClassName = "text-blue-600 dark:text-blue-300"; + if (item.type === "trigger") { + icon = LucideCog; + iconClassName = "text-purple-500"; + } else if (item.type === "view") { + icon = LucideView; + iconClassName = "text-green-600 dark:text-green-300"; + } + return ( { - openContextMenuFromEvent(prepareContextMenu(item.name))(e); + openContextMenuFromEvent(prepareContextMenu(item))(e); e.stopPropagation(); }} key={item.name} title={item.name} - icon={Table2} + iconClassName={iconClassName} + icon={icon} + indentation={item.type === "trigger"} selected={schemaIndex === selectedIndex} onClick={() => setSelectedIndex(schemaIndex)} /> diff --git a/src/components/tabs/trigger-tab.tsx b/src/components/tabs/trigger-tab.tsx new file mode 100644 index 00000000..f701e71c --- /dev/null +++ b/src/components/tabs/trigger-tab.tsx @@ -0,0 +1,74 @@ +import { useDatabaseDriver } from "@/context/DatabaseDriverProvider"; +import { DatabaseTriggerSchema } from "@/drivers/base-driver"; +import { useEffect, useState } from "react"; +import { Input } from "../ui/input"; +import SqlEditor from "../sql-editor"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../ui/select"; +import TableCombobox from "../table-combobox/TableCombobox"; + +export default function TriggerTab({ name }: { name: string }) { + const { databaseDriver } = useDatabaseDriver(); + const [trigger, setTrigger] = useState(); + const [error, setError] = useState(); + + useEffect(() => { + databaseDriver + .trigger(name) + .then(setTrigger) + .catch((e: Error) => { + setError(e.message); + }); + }, [databaseDriver, name]); + + if (error) { + return
{error}
; + } + + return ( +
+
+
Trigger Name
+ + +
+
+ +
+
+ +
+ {}} /> +
+
+
+
+ +
+
+
+ ); +} diff --git a/src/drivers/base-driver.ts b/src/drivers/base-driver.ts index 3fb1f6da..b44c7605 100644 --- a/src/drivers/base-driver.ts +++ b/src/drivers/base-driver.ts @@ -38,7 +38,9 @@ export interface SelectFromTableOptions { export type DatabaseValue = T | undefined | null; export interface DatabaseSchemaItem { + type: "table" | "trigger" | "view"; name: string; + tableName?: string; } export interface DatabaseTableColumn { @@ -108,6 +110,20 @@ export interface DatabaseTableSchema { createScript?: string; } +export type TriggerWhen = "BEFORE" | "AFTER" | "INSTEAD_OF"; + +export type TriggerOperation = "INSERT" | "UPDATE" | "DELETE"; + +export interface DatabaseTriggerSchema { + name: string; + operation: TriggerOperation; + when: TriggerWhen; + tableName: string; + columnNames?: string[]; + whenExpression: string; + statement: string; +} + interface DatabaseTableOperationInsert { operation: "INSERT"; values: Record; @@ -147,6 +163,7 @@ export abstract class BaseDriver { abstract schemas(): Promise; abstract tableSchema(tableName: string): Promise; + abstract trigger(name: string): Promise; abstract selectTable( tableName: string, diff --git a/src/drivers/sqlite-base-driver.ts b/src/drivers/sqlite-base-driver.ts index b23fd770..a1c0804e 100644 --- a/src/drivers/sqlite-base-driver.ts +++ b/src/drivers/sqlite-base-driver.ts @@ -7,6 +7,7 @@ import { DatabaseTableOperation, DatabaseTableOperationReslt, DatabaseTableSchema, + DatabaseTriggerSchema, SelectFromTableOptions, } from "./base-driver"; import { @@ -18,6 +19,7 @@ import { } from "@/lib/sql-helper"; import { parseCreateTableScript } from "@/lib/sql-parse-table"; import { validateOperation } from "@/lib/validation"; +import { parseCreateTriggerScript } from "@/lib/sql-parse-trigger"; export default abstract class SqliteLikeBaseDriver extends BaseDriver { protected escapeId(id: string) { @@ -30,13 +32,38 @@ export default abstract class SqliteLikeBaseDriver extends BaseDriver { async schemas(): Promise { const result = await this.query("SELECT * FROM sqlite_schema;"); - return result.rows - .filter((row) => row.type === "table") - .map((row) => { - return { - name: row.name as string, - }; - }); + const tmp: DatabaseSchemaItem[] = []; + const rows = result.rows as { + type: string; + name: string; + tbl_name: string; + sql: string; + }[]; + + for (const row of rows) { + if (row.type === "table") { + tmp.push({ type: "table", name: row.name }); + } else if (row.type === "trigger") { + tmp.push({ type: "trigger", name: row.name, tableName: row.tbl_name }); + } else if (row.type === "view") { + tmp.push({ type: "view", name: row.name }); + } + } + + return tmp; + } + + async trigger(name: string): Promise { + const result = await this.query( + `SELECT * FROM sqlite_schema WHERE "type"='trigger' AND name=${escapeSqlValue( + name + )};` + ); + + const triggerRow = result.rows[0]; + if (!triggerRow) throw new Error("Trigger does not exist"); + + return parseCreateTriggerScript(triggerRow.sql as string); } close(): void { diff --git a/src/lib/sql-parse-trigger.ts b/src/lib/sql-parse-trigger.ts index e2998f06..0e74d3c6 100644 --- a/src/lib/sql-parse-trigger.ts +++ b/src/lib/sql-parse-trigger.ts @@ -1,19 +1,10 @@ import { SQLite } from "@codemirror/lang-sql"; import { Cursor, parseColumnList } from "./sql-parse-table"; - -type TriggerWhen = "BEFORE" | "AFTER" | "INSTEAD_OF"; - -type TriggerOperation = "INSERT" | "UPDATE" | "DELETE"; - -export interface DatabaseTriggerSchema { - name: string; - operation: TriggerOperation; - when: TriggerWhen; - tableName: string; - columnNames?: string[]; - whenExpression: string; - statement: string; -} +import { + DatabaseTriggerSchema, + TriggerWhen, + TriggerOperation, +} from "@/drivers/base-driver"; export function parseCreateTriggerScript(sql: string): DatabaseTriggerSchema { const tree = SQLite.language.parser.parse(sql); diff --git a/src/messages/open-tab.tsx b/src/messages/open-tab.tsx index 645871fe..1364efbb 100644 --- a/src/messages/open-tab.tsx +++ b/src/messages/open-tab.tsx @@ -7,11 +7,13 @@ import { LucideTable, LucideTableProperties, LucideUser, + LucideCog, } from "lucide-react"; import QueryWindow from "@/components/tabs/query-tab"; import SchemaEditorTab from "@/components/tabs/schema-editor-tab"; import TableDataWindow from "@/components/tabs/table-data-tab"; import UsersTab from "@/components/tabs/users-tabs"; +import TriggerTab from "@/components/tabs/trigger-tab"; interface OpenTableTab { type: "table"; @@ -32,11 +34,18 @@ interface OpenUserTab { type: "user"; } +interface OpenTriggerTab { + type: "trigger"; + tableName?: string; + name?: string; +} + export type OpenTabsProps = | OpenTableTab | OpenQueryTab | OpenTableSchemaTab - | OpenUserTab; + | OpenUserTab + | OpenTriggerTab; export function openTab(props: OpenTabsProps) { return window.internalPubSub.send(MessageChannelName.OPEN_NEW_TAB, props); @@ -48,6 +57,7 @@ function generateKeyFromTab(tab: OpenTabsProps) { if (tab.type === "schema") return !tab.tableName ? "create-schema" : "schema-" + tab.tableName; if (tab.type === "user") return "user"; + if (tab.type === "trigger") return "trigger-" + tab.name; return ""; } @@ -56,6 +66,7 @@ function generateIconFromTab(tab: OpenTabsProps) { if (tab.type === "table") return LucideTable; if (tab.type === "schema") return LucideTableProperties; if (tab.type === "user") return LucideUser; + if (tab.type === "trigger") return LucideCog; return LucideActivity; } @@ -65,6 +76,7 @@ function generateTitle(tab: OpenTabsProps) { if (tab.type === "table") return tab.tableName; if (tab.type === "schema") return tab.tableName ? tab.tableName : "New Table"; if (tab.type === "user") return "User & Permission"; + if (tab.type === "trigger") return tab.name ?? ""; return ""; } @@ -75,6 +87,7 @@ function generateComponent(tab: OpenTabsProps) { if (tab.type === "schema") return ; if (tab.type === "user") return ; + if (tab.type === "trigger") return ; return
; }