Skip to content

Commit

Permalink
feat: add trigger management functionality and UI components
Browse files Browse the repository at this point in the history
  • Loading branch information
sokphaladam committed Dec 25, 2024
1 parent e90f571 commit ed15510
Show file tree
Hide file tree
Showing 11 changed files with 485 additions and 88 deletions.
13 changes: 13 additions & 0 deletions src/components/gui/schema-sidebar-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export default function SchemaList({ search }: Readonly<SchemaListProps>) {
(item?: DatabaseSchemaItem) => {
const selectedName = item?.name;
const isTable = item?.type === "table";
const isTrigger = item?.type === "trigger";

return [
{
Expand Down Expand Up @@ -170,6 +171,18 @@ export default function SchemaList({ search }: Readonly<SchemaListProps>) {
},
}
: undefined,
{ separator: true },
{
title: isTrigger ? "Edit Trigger" : "Create New Trigger",
onClick: () => {
openTab({
type: "trigger",
schemaName: item?.schemaName ?? currentSchemaName,
name: isTrigger ? item.name : 'create',
tableName: item?.tableSchema?.tableName
});
}
},
databaseDriver.getFlags().supportCreateUpdateTable
? { separator: true }
: undefined,
Expand Down
106 changes: 23 additions & 83 deletions src/components/gui/tabs/trigger-tab.tsx
Original file line number Diff line number Diff line change
@@ -1,88 +1,28 @@
import { DatabaseTriggerSchema } from "@/drivers/base-driver";
import { useEffect, useState } from "react";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useDatabaseDriver } from "@/context/driver-provider";
import SqlEditor from "../sql-editor";
import TableCombobox from "../table-combobox/TableCombobox";
import { noop } from "@/lib/utils";
import { useSchema } from "@/context/schema-provider";
import TriggerEditor, { TriggerEditorProps } from "../trigger-editor";
import { useTabsContext } from "../windows-tab";
import { LucideTableProperties } from "lucide-react";

export default function TriggerTab({
schemaName,
name,
}: {
schemaName: string;
name: string;
}) {
const { databaseDriver } = useDatabaseDriver();
const [trigger, setTrigger] = useState<DatabaseTriggerSchema>();
const [error, setError] = useState<string>();
export default function TriggerTab(props: TriggerEditorProps) {
const { refresh: refreshSchema } = useSchema();
const { replaceCurrentTab } = useTabsContext();

useEffect(() => {
databaseDriver
.trigger(schemaName, name)
.then(setTrigger)
.catch((e: Error) => {
setError(e.message);
});
}, [databaseDriver, schemaName, name]);

if (error) {
return <div className="p-4">{error}</div>;
const onSave = (trigger: TriggerEditorProps) => {
refreshSchema();
replaceCurrentTab({
component: (
<TriggerTab
tableName={trigger.tableName}
schemaName={trigger.schemaName}
name={trigger.name ?? ''}
/>
),
key: 'trigger-' + trigger.name || '',
identifier: 'trigger-' + trigger.name || '',
title: trigger.name || '',
icon: LucideTableProperties,
});
}

return (
<div className="flex flex-col overflow-hidden w-full h-full">
<div className="p-4 flex flex-col gap-2">
<div className="text-xs">Trigger Name</div>
<Input value={trigger?.name ?? ""} readOnly />

<div className="flex gap-2">
<div className="w-[200px]">
<Select value={trigger?.when ?? "BEFORE"}>
<SelectTrigger>
<SelectValue placeholder="When" />
</SelectTrigger>
<SelectContent>
<SelectItem value="BEFORE">Before</SelectItem>
<SelectItem value="AFTER">After</SelectItem>
<SelectItem value="INSTEAD_OF">Instead Of</SelectItem>
</SelectContent>
</Select>
</div>
<div className="w-[200px]">
<Select value={trigger?.operation}>
<SelectTrigger>
<SelectValue placeholder="Operation" />
</SelectTrigger>
<SelectContent>
<SelectItem value="INSERT">Insert</SelectItem>
<SelectItem value="UPDATE">Update</SelectItem>
<SelectItem value="DELETE">Delete</SelectItem>
</SelectContent>
</Select>
</div>
<TableCombobox
schemaName={schemaName}
value={trigger?.tableName}
onChange={noop}
/>
</div>
</div>
<div className="grow overflow-hidden">
<div className="h-full">
<SqlEditor
value={trigger?.statement ?? ""}
dialect={databaseDriver.getFlags().dialect}
/>
</div>
</div>
</div>
);
return <TriggerEditor {...props} onSave={onSave} />
}
193 changes: 193 additions & 0 deletions src/components/gui/trigger-editor/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { useDatabaseDriver } from "@/context/driver-provider";
import { DatabaseTriggerSchemaChange, TriggerOperation, TriggerWhen } from "@/drivers/base-driver";
import { LucideAlertCircle } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import TableCombobox from "../table-combobox/TableCombobox";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import SqlEditor from "../sql-editor";
import { TriggerController } from "./trigger-controller";
import { TriggerSaveDialog } from "./trigger-save-dialog";

export interface TriggerEditorProps {
name: string;
tableName?: string;
schemaName: string;
}

interface Props extends TriggerEditorProps {
onSave: (trigger: TriggerEditorProps) => void;
}

const initailTrigger: DatabaseTriggerSchemaChange = {
name: {
new: ''
},
operation: "INSERT",
when: "BEFORE",
tableName: "",
whenExpression: "",
statement: "",
schemaName: ''
}

function triggerHasChange(defaultTrigger: DatabaseTriggerSchemaChange, trigger: DatabaseTriggerSchemaChange) {
const objects = Object.keys(defaultTrigger) as (keyof DatabaseTriggerSchemaChange)[];

for (const key of objects) {
if (defaultTrigger[key as keyof DatabaseTriggerSchemaChange] !== trigger[key as keyof DatabaseTriggerSchemaChange]) {
return true;
}
}

return false;
}

export default function TriggerEditor(props: Props) {
const { name, tableName, schemaName } = props;
const { databaseDriver } = useDatabaseDriver();
const [defaultTrigger, setDefaultTrigger] = useState<DatabaseTriggerSchemaChange>({
...initailTrigger
})
const [trigger, setTrigger] = useState<DatabaseTriggerSchemaChange>({
...initailTrigger
});
const [error, setError] = useState<string>();
const [isExecuting, setIsExecuting] = useState(false);

const previewScript = useMemo(() => {
return trigger ? databaseDriver.createUpdateTriggerSchema(trigger) : [''];
}, [trigger, databaseDriver]);

const triggerChanging = useMemo(() => triggerHasChange(defaultTrigger, trigger), [defaultTrigger, trigger])

const getDefaultTrigger = useCallback(() => {
if (name !== 'create') {
databaseDriver
.trigger(schemaName, name)
.then(res => {
const t = {
...res,
name: {
new: res.name || '',
old: res.name || ''
},
schemaName
}
setDefaultTrigger(t)
setTrigger(t)
})
.catch((e: Error) => {
setError(e.message);
});
}
else {
const t = {
...initailTrigger,
tableName: tableName ?? "",
schemaName
}
setDefaultTrigger(t);
setTrigger(t)
}
}, [databaseDriver, name, schemaName, tableName])

useEffect(() => {
getDefaultTrigger();
}, [getDefaultTrigger]);

const onDiscard = () => {
setTrigger({ ...defaultTrigger })
}

return (
<div className="flex flex-col overflow-hidden w-full h-full">
{
isExecuting && (
<TriggerSaveDialog
onSave={(value) => {
props.onSave(value);
setIsExecuting(false);
}}
onClose={() => setIsExecuting(false)}
previewScript={previewScript}
schemaName={schemaName}
trigger={trigger}
tableName={tableName}
/>
)
}
<TriggerController
onSave={() => setIsExecuting(true)}
onDiscard={onDiscard}
previewScript={previewScript.join('\n')}
disabled={!trigger.name?.new || !schemaName || !triggerChanging}
isExecuting={isExecuting}
/>
<div className="p-4 flex flex-row gap-2">
<div className="w-full">
<div className="text-xs mb-2">Trigger Name</div>
<Input value={trigger?.name.new ?? trigger?.name.old ?? ""} onChange={e => setTrigger({ ...trigger, name: { ...trigger.name, new: e.target.value } })} />
</div>
<div className="w-[200px]">
<div className="text-xs mb-2">On Table</div>
<TableCombobox
schemaName={schemaName}
value={trigger?.tableName}
onChange={value => {
setTrigger({
...trigger,
tableName: value
})
}}
/>
</div>
</div>
<div className="p-4 flex flex-col gap-2">
<div className="text-xs">Event</div>
<div className="flex gap-2">
<div className="w-[200px]">
<Select value={trigger?.when ?? "BEFORE"} onValueChange={value => setTrigger({ ...trigger, when: value as TriggerWhen })}>
<SelectTrigger>
<SelectValue placeholder="When" />
</SelectTrigger>
<SelectContent>
<SelectItem value="BEFORE">Before</SelectItem>
<SelectItem value="AFTER">After</SelectItem>
<SelectItem value="INSTEAD_OF">Instead Of</SelectItem>
</SelectContent>
</Select>
</div>
<div className="w-[200px]">
<Select value={trigger?.operation} onValueChange={value => setTrigger({ ...trigger, operation: value as TriggerOperation })}>
<SelectTrigger>
<SelectValue placeholder="Operation" />
</SelectTrigger>
<SelectContent>
<SelectItem value="INSERT">Insert</SelectItem>
<SelectItem value="UPDATE">Update</SelectItem>
<SelectItem value="DELETE">Delete</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
{error && (
<div className="text-sm text-red-500 font-mono flex gap-4 justify-end items-end">
<LucideAlertCircle className="w-12 h-12" />
<p>{error}</p>
</div>
)}
<div className="grow overflow-hidden">
<div className="h-full">
<div className="text-xs my-2 mx-4">Trigger statement: (eg: &quot;SET NEW.columnA = TRIM(OLD.columnA)&quot;)</div>
<SqlEditor
value={trigger?.statement ?? ""}
dialect={databaseDriver.getFlags().dialect}
onChange={value => setTrigger({ ...trigger, statement: value })}
/>
</div>
</div>
</div >
)
}
62 changes: 62 additions & 0 deletions src/components/gui/trigger-editor/trigger-controller.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Button, buttonVariants } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
import { LucideCode, LucideLoader, LucideSave } from "lucide-react";
import CodePreview from "../code-preview";

interface Props {
onSave: () => void;
onDiscard: () => void;
previewScript: string;
isExecuting?: boolean;
disabled?: boolean;
}

export function TriggerController(props: Props) {
const { onSave, onDiscard, isExecuting, disabled, previewScript } = props;
return (
<div className="p-1 flex gap-2">
<Button
variant="ghost"
onClick={onSave}
disabled={disabled}
size={"sm"}
>
{isExecuting ? (
<LucideLoader className="w-4 h-4 mr-2 animate-spin" />
) : (
<LucideSave className="w-4 h-4 mr-2" />
)}
Save
</Button>
<Button
size={"sm"}
variant="ghost"
onClick={onDiscard}
disabled={disabled}
className="text-red-500"
>
Discard Change
</Button>

<div>
<Separator orientation="vertical" />
</div>

<Popover>
<PopoverTrigger>
<div className={buttonVariants({ size: "sm", variant: "ghost" })}>
<LucideCode className="w-4 h-4 mr-1" />
SQL Preview
</div>
</PopoverTrigger>
<PopoverContent style={{ width: 500 }}>
<div className="text-xs font-semibold mb-1">SQL Preview</div>
<div style={{ maxHeight: 400 }} className="overflow-y-auto">
<CodePreview code={previewScript} />
</div>
</PopoverContent>
</Popover>
</div>
)
}
Loading

0 comments on commit ed15510

Please sign in to comment.