Skip to content

Commit 2767998

Browse files
bchapuisclaude
andcommitted
Add inline database creation to database workflow nodes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f015060 commit 2767998

3 files changed

Lines changed: 244 additions & 0 deletions

File tree

apps/app/src/components/workflow/widgets/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { blobInputWidget } from "./input/blob-input";
1212
import { booleanInputWidget } from "./input/boolean-input";
1313
import { canvasInputWidget } from "./input/canvas-input";
1414
import { cronInputWidget } from "./input/cron-input";
15+
import { databaseTriggerInputWidget } from "./input/database-trigger-input";
1516
import { dateInputWidget } from "./input/date-input";
1617
import { discordTriggerInputWidget } from "./input/discord-trigger-input";
1718
import { documentInputWidget } from "./input/document-input";
@@ -77,6 +78,7 @@ const widgets = [
7778
emailTriggerInputWidget,
7879
httpRequestEndpointWidget,
7980
httpWebhookEndpointWidget,
81+
databaseTriggerInputWidget,
8082
queueTriggerInputWidget,
8183
telegramTriggerInputWidget,
8284
whatsappTriggerInputWidget,
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { useState } from "react";
2+
3+
import { useAuth } from "@/components/auth-context";
4+
import { Button } from "@/components/ui/button";
5+
import {
6+
Dialog,
7+
DialogContent,
8+
DialogDescription,
9+
DialogTitle,
10+
} from "@/components/ui/dialog";
11+
import { Input } from "@/components/ui/input";
12+
import { Label } from "@/components/ui/label";
13+
import { Spinner } from "@/components/ui/spinner";
14+
import { createDatabase } from "@/services/database-service";
15+
16+
interface DatabaseCreateDialogProps {
17+
isOpen: boolean;
18+
onClose: () => void;
19+
onCreated: (databaseId: string) => void;
20+
}
21+
22+
export function DatabaseCreateDialog({
23+
isOpen,
24+
onClose,
25+
onCreated,
26+
}: DatabaseCreateDialogProps) {
27+
const { organization } = useAuth();
28+
const [name, setName] = useState("");
29+
const [isSubmitting, setIsSubmitting] = useState(false);
30+
const [error, setError] = useState<string | null>(null);
31+
32+
const resetForm = () => {
33+
setName("");
34+
setError(null);
35+
};
36+
37+
const handleClose = () => {
38+
resetForm();
39+
onClose();
40+
};
41+
42+
const handleSubmit = async () => {
43+
if (!organization?.id) return;
44+
45+
setIsSubmitting(true);
46+
setError(null);
47+
48+
try {
49+
const response = await createDatabase({ name }, organization.id);
50+
onCreated(response.id);
51+
handleClose();
52+
} catch (err) {
53+
setError(
54+
err instanceof Error ? err.message : "Failed to create database"
55+
);
56+
} finally {
57+
setIsSubmitting(false);
58+
}
59+
};
60+
61+
return (
62+
<Dialog open={isOpen} onOpenChange={(open) => !open && handleClose()}>
63+
<DialogContent className="max-w-[450px]">
64+
<div>
65+
<DialogTitle className="text-base font-semibold">
66+
Create a Database
67+
</DialogTitle>
68+
<DialogDescription className="text-sm text-muted-foreground mt-1">
69+
Give your database a name.
70+
</DialogDescription>
71+
</div>
72+
73+
<div className="space-y-3">
74+
<div className="space-y-1.5">
75+
<Label htmlFor="database-name">Name</Label>
76+
<Input
77+
id="database-name"
78+
value={name}
79+
onChange={(e) => setName(e.target.value)}
80+
placeholder="My Database"
81+
/>
82+
<p className="text-xs text-muted-foreground">
83+
A display name for this database in Dafthunk.
84+
</p>
85+
</div>
86+
87+
{error && (
88+
<p className="text-sm text-destructive bg-destructive/10 px-3 py-2 rounded-md">
89+
{error}
90+
</p>
91+
)}
92+
93+
<div className="flex justify-end gap-2 pt-1">
94+
<Button
95+
type="button"
96+
variant="outline"
97+
onClick={handleClose}
98+
disabled={isSubmitting}
99+
>
100+
Cancel
101+
</Button>
102+
<Button
103+
onClick={handleSubmit}
104+
disabled={isSubmitting || name.trim() === ""}
105+
>
106+
{isSubmitting ? (
107+
<>
108+
<Spinner className="h-4 w-4 mr-1" />
109+
Creating...
110+
</>
111+
) : (
112+
"Create Database"
113+
)}
114+
</Button>
115+
</div>
116+
</div>
117+
</DialogContent>
118+
</Dialog>
119+
);
120+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { useState } from "react";
2+
3+
import {
4+
Select,
5+
SelectContent,
6+
SelectItem,
7+
SelectSeparator,
8+
SelectTrigger,
9+
SelectValue,
10+
} from "@/components/ui/select";
11+
import { useDatabases } from "@/services/database-service";
12+
import { cn } from "@/utils/utils";
13+
import { updateNodeInput, useWorkflow } from "../../workflow-context";
14+
import type { WorkflowParameter } from "../../workflow-types";
15+
import type { BaseWidgetProps } from "../widget";
16+
import { createWidget, getInputValue } from "../widget";
17+
import { DatabaseCreateDialog } from "./database-create-dialog";
18+
19+
const CREATE_NEW_SENTINEL = "__create_new__";
20+
21+
interface DatabaseTriggerInputProps extends BaseWidgetProps {
22+
nodeId: string;
23+
databaseId: string;
24+
inputs: WorkflowParameter[];
25+
}
26+
27+
function DatabaseTriggerInputWidget({
28+
nodeId,
29+
databaseId,
30+
inputs,
31+
className,
32+
disabled = false,
33+
}: DatabaseTriggerInputProps) {
34+
const { databases, isDatabasesLoading, mutateDatabases } = useDatabases();
35+
const { updateNodeData, edges, deleteEdge } = useWorkflow();
36+
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
37+
38+
const handleDatabaseChange = (value: string) => {
39+
if (value === CREATE_NEW_SENTINEL) {
40+
setIsCreateDialogOpen(true);
41+
return;
42+
}
43+
updateNodeInput(
44+
nodeId,
45+
"databaseId",
46+
value,
47+
inputs,
48+
updateNodeData,
49+
edges,
50+
deleteEdge
51+
);
52+
};
53+
54+
const handleDatabaseCreated = async (newDatabaseId: string) => {
55+
await mutateDatabases();
56+
updateNodeInput(
57+
nodeId,
58+
"databaseId",
59+
newDatabaseId,
60+
inputs,
61+
updateNodeData,
62+
edges,
63+
deleteEdge
64+
);
65+
setIsCreateDialogOpen(false);
66+
};
67+
68+
return (
69+
<div className={cn("p-2", className)}>
70+
<Select
71+
value={databaseId || ""}
72+
onValueChange={handleDatabaseChange}
73+
disabled={disabled || isDatabasesLoading}
74+
>
75+
<SelectTrigger className="h-6 text-xs">
76+
<SelectValue
77+
placeholder={
78+
isDatabasesLoading ? "Loading..." : "Select a database"
79+
}
80+
/>
81+
</SelectTrigger>
82+
<SelectContent>
83+
{databases.map((database) => (
84+
<SelectItem key={database.id} value={database.id}>
85+
{database.name}
86+
</SelectItem>
87+
))}
88+
<SelectSeparator />
89+
<SelectItem value={CREATE_NEW_SENTINEL}>+ New Database</SelectItem>
90+
</SelectContent>
91+
</Select>
92+
<DatabaseCreateDialog
93+
isOpen={isCreateDialogOpen}
94+
onClose={() => setIsCreateDialogOpen(false)}
95+
onCreated={handleDatabaseCreated}
96+
/>
97+
</div>
98+
);
99+
}
100+
101+
export const databaseTriggerInputWidget = createWidget({
102+
component: DatabaseTriggerInputWidget,
103+
nodeTypes: [
104+
"database-query",
105+
"database-execute",
106+
"database-import-table",
107+
"database-export-table",
108+
"database-describe-table",
109+
"database-list-tables",
110+
"database-get-row-count",
111+
"database-drop-table",
112+
"database-truncate-table",
113+
"database-table-exists",
114+
],
115+
inputField: "databaseId",
116+
managedFields: [],
117+
extractConfig: (nodeId, inputs) => ({
118+
nodeId,
119+
databaseId: getInputValue(inputs, "databaseId", ""),
120+
inputs,
121+
}),
122+
});

0 commit comments

Comments
 (0)