mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-05-03 11:51:36 +00:00
Add support for Supabase database operations, including migrations and queries. Implement new Supabase-related types, actions, and components to handle database interactions. Enhance the prompt system to include Supabase-specific instructions and constraints. Ensure data integrity and security by enforcing row-level security and proper migration practices.
200 lines
7.1 KiB
TypeScript
200 lines
7.1 KiB
TypeScript
import { AnimatePresence, motion } from 'framer-motion';
|
|
import type { SupabaseAlert } from '~/types/actions';
|
|
import { classNames } from '~/utils/classNames';
|
|
import { supabaseConnection } from '~/lib/stores/supabase';
|
|
import { useStore } from '@nanostores/react';
|
|
import { useState } from 'react';
|
|
|
|
interface Props {
|
|
alert: SupabaseAlert;
|
|
clearAlert: () => void;
|
|
postMessage: (message: string) => void;
|
|
}
|
|
|
|
export function SupabaseChatAlert({ alert, clearAlert, postMessage }: Props) {
|
|
const { content } = alert;
|
|
const connection = useStore(supabaseConnection);
|
|
const [isExecuting, setIsExecuting] = useState(false);
|
|
const [isCollapsed, setIsCollapsed] = useState(true);
|
|
|
|
// Determine connection state
|
|
const isConnected = !!(connection.token && connection.selectedProjectId);
|
|
|
|
// Set title and description based on connection state
|
|
const title = isConnected ? 'Supabase Query' : 'Supabase Connection Required';
|
|
const description = isConnected ? 'Execute database query' : 'Supabase connection required';
|
|
const message = isConnected
|
|
? 'Please review the proposed changes and apply them to your database.'
|
|
: 'Please connect to Supabase to continue with this operation.';
|
|
|
|
const handleConnectClick = () => {
|
|
// Dispatch an event to open the Supabase connection dialog
|
|
document.dispatchEvent(new CustomEvent('open-supabase-connection'));
|
|
};
|
|
|
|
// Determine if we should show the Connect button or Apply Changes button
|
|
const showConnectButton = !isConnected;
|
|
|
|
const executeSupabaseAction = async (sql: string) => {
|
|
if (!connection.token || !connection.selectedProjectId) {
|
|
console.error('No Supabase token or project selected');
|
|
return;
|
|
}
|
|
|
|
setIsExecuting(true);
|
|
|
|
try {
|
|
const response = await fetch('/api/supabase/query', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${connection.token}`,
|
|
},
|
|
body: JSON.stringify({
|
|
projectId: connection.selectedProjectId,
|
|
query: sql,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = (await response.json()) as any;
|
|
throw new Error(`Supabase query failed: ${errorData.error?.message || response.statusText}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
console.log('Supabase query executed successfully:', result);
|
|
clearAlert();
|
|
} catch (error) {
|
|
console.error('Failed to execute Supabase action:', error);
|
|
postMessage(
|
|
`*Error executing Supabase query please fix and return the query again*\n\`\`\`\n${error instanceof Error ? error.message : String(error)}\n\`\`\`\n`,
|
|
);
|
|
} finally {
|
|
setIsExecuting(false);
|
|
}
|
|
};
|
|
|
|
const cleanSqlContent = (content: string) => {
|
|
if (!content) {
|
|
return '';
|
|
}
|
|
|
|
let cleaned = content.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
|
|
cleaned = cleaned.replace(/(--).*$/gm, '').replace(/(#).*$/gm, '');
|
|
|
|
const statements = cleaned
|
|
.split(';')
|
|
.map((stmt) => stmt.trim())
|
|
.filter((stmt) => stmt.length > 0)
|
|
.join(';\n\n');
|
|
|
|
return statements;
|
|
};
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
<motion.div
|
|
initial={{ opacity: 0, y: -20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -20 }}
|
|
transition={{ duration: 0.3 }}
|
|
className="max-w-chat rounded-lg border-l-2 border-l-[#098F5F] border-bolt-elements-borderColor bg-bolt-elements-background-depth-2"
|
|
>
|
|
{/* Header */}
|
|
<div className="p-4 pb-2">
|
|
<div className="flex items-center gap-2">
|
|
<img height="10" width="18" crossOrigin="anonymous" src="https://cdn.simpleicons.org/supabase" />
|
|
<h3 className="text-sm font-medium text-[#3DCB8F]">{title}</h3>
|
|
</div>
|
|
</div>
|
|
|
|
{/* SQL Content */}
|
|
<div className="px-4">
|
|
{!isConnected ? (
|
|
<div className="p-3 rounded-md bg-bolt-elements-background-depth-3">
|
|
<span className="text-sm text-bolt-elements-textPrimary">
|
|
You must first connect to Supabase and select a project.
|
|
</span>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div
|
|
className="flex items-center p-2 rounded-md bg-bolt-elements-background-depth-3 cursor-pointer"
|
|
onClick={() => setIsCollapsed(!isCollapsed)}
|
|
>
|
|
<div className="i-ph:database text-bolt-elements-textPrimary mr-2"></div>
|
|
<span className="text-sm text-bolt-elements-textPrimary flex-grow">
|
|
{description || 'Create table and setup auth'}
|
|
</span>
|
|
<div
|
|
className={`i-ph:caret-up text-bolt-elements-textPrimary transition-transform ${isCollapsed ? 'rotate-180' : ''}`}
|
|
></div>
|
|
</div>
|
|
|
|
{!isCollapsed && content && (
|
|
<div className="mt-2 p-3 bg-bolt-elements-background-depth-4 rounded-md overflow-auto max-h-60 font-mono text-xs text-bolt-elements-textSecondary">
|
|
<pre>{cleanSqlContent(content)}</pre>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Message and Actions */}
|
|
<div className="p-4">
|
|
<p className="text-sm text-bolt-elements-textSecondary mb-4">{message}</p>
|
|
|
|
<div className="flex gap-2">
|
|
{showConnectButton ? (
|
|
<button
|
|
onClick={handleConnectClick}
|
|
className={classNames(
|
|
`px-3 py-2 rounded-md text-sm font-medium`,
|
|
'bg-[#098F5F]',
|
|
'hover:bg-[#0aa06c]',
|
|
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500',
|
|
'text-white',
|
|
'flex items-center gap-1.5',
|
|
)}
|
|
>
|
|
Connect to Supabase
|
|
</button>
|
|
) : (
|
|
<button
|
|
onClick={() => executeSupabaseAction(content)}
|
|
disabled={isExecuting}
|
|
className={classNames(
|
|
`px-3 py-2 rounded-md text-sm font-medium`,
|
|
'bg-[#098F5F]',
|
|
'hover:bg-[#0aa06c]',
|
|
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500',
|
|
'text-white',
|
|
'flex items-center gap-1.5',
|
|
isExecuting ? 'opacity-70 cursor-not-allowed' : '',
|
|
)}
|
|
>
|
|
{isExecuting ? 'Applying...' : 'Apply Changes'}
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={clearAlert}
|
|
disabled={isExecuting}
|
|
className={classNames(
|
|
`px-3 py-2 rounded-md text-sm font-medium`,
|
|
'bg-[#503B26]',
|
|
'hover:bg-[#774f28]',
|
|
'focus:outline-none',
|
|
'text-[#F79007]',
|
|
isExecuting ? 'opacity-70 cursor-not-allowed' : '',
|
|
)}
|
|
>
|
|
Dismiss
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
);
|
|
}
|