mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-05-05 20:54:40 +00:00
This commit introduces the ability to fetch and store Supabase API keys and URL credentials when a project is selected. This enables the application to dynamically configure the Supabase connection environment variables, improving the integration with Supabase services. The changes include updates to the Supabase connection logic, new API endpoints, and modifications to the chat and prompt components to utilize the new credentials.
330 lines
14 KiB
TypeScript
330 lines
14 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { useSupabaseConnection } from '~/lib/hooks/useSupabaseConnection';
|
|
import { classNames } from '~/utils/classNames';
|
|
import { useStore } from '@nanostores/react';
|
|
import { chatId } from '~/lib/persistence/useChatHistory';
|
|
import { fetchSupabaseStats } from '~/lib/stores/supabase';
|
|
import { Dialog, DialogRoot, DialogClose, DialogTitle, DialogButton } from '~/components/ui/Dialog';
|
|
|
|
export function SupabaseConnection() {
|
|
const {
|
|
connection: supabaseConn,
|
|
connecting,
|
|
fetchingStats,
|
|
isProjectsExpanded,
|
|
setIsProjectsExpanded,
|
|
isDropdownOpen: isDialogOpen,
|
|
setIsDropdownOpen: setIsDialogOpen,
|
|
handleConnect,
|
|
handleDisconnect,
|
|
selectProject,
|
|
handleCreateProject,
|
|
updateToken,
|
|
isConnected,
|
|
fetchProjectApiKeys,
|
|
} = useSupabaseConnection();
|
|
|
|
const currentChatId = useStore(chatId);
|
|
|
|
useEffect(() => {
|
|
const handleOpenConnectionDialog = () => {
|
|
setIsDialogOpen(true);
|
|
};
|
|
|
|
document.addEventListener('open-supabase-connection', handleOpenConnectionDialog);
|
|
|
|
return () => {
|
|
document.removeEventListener('open-supabase-connection', handleOpenConnectionDialog);
|
|
};
|
|
}, [setIsDialogOpen]);
|
|
|
|
useEffect(() => {
|
|
if (isConnected && currentChatId) {
|
|
const savedProjectId = localStorage.getItem(`supabase-project-${currentChatId}`);
|
|
|
|
/*
|
|
* If there's no saved project for this chat but there is a global selected project,
|
|
* use the global one instead of clearing it
|
|
*/
|
|
if (!savedProjectId && supabaseConn.selectedProjectId) {
|
|
// Save the current global project to this chat
|
|
localStorage.setItem(`supabase-project-${currentChatId}`, supabaseConn.selectedProjectId);
|
|
} else if (savedProjectId && savedProjectId !== supabaseConn.selectedProjectId) {
|
|
selectProject(savedProjectId);
|
|
}
|
|
}
|
|
}, [isConnected, currentChatId]);
|
|
|
|
useEffect(() => {
|
|
if (currentChatId && supabaseConn.selectedProjectId) {
|
|
localStorage.setItem(`supabase-project-${currentChatId}`, supabaseConn.selectedProjectId);
|
|
} else if (currentChatId && !supabaseConn.selectedProjectId) {
|
|
localStorage.removeItem(`supabase-project-${currentChatId}`);
|
|
}
|
|
}, [currentChatId, supabaseConn.selectedProjectId]);
|
|
|
|
useEffect(() => {
|
|
if (isConnected && supabaseConn.token) {
|
|
fetchSupabaseStats(supabaseConn.token).catch(console.error);
|
|
}
|
|
}, [isConnected, supabaseConn.token]);
|
|
|
|
useEffect(() => {
|
|
if (isConnected && supabaseConn.selectedProjectId && supabaseConn.token) {
|
|
fetchProjectApiKeys(supabaseConn.selectedProjectId).catch(console.error);
|
|
}
|
|
}, [isConnected, supabaseConn.selectedProjectId, supabaseConn.token]);
|
|
|
|
return (
|
|
<div className="relative">
|
|
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden mr-2 text-sm">
|
|
<Button
|
|
active
|
|
disabled={connecting}
|
|
onClick={() => setIsDialogOpen(!isDialogOpen)}
|
|
className="hover:bg-bolt-elements-item-backgroundActive !text-white flex items-center gap-2"
|
|
>
|
|
<img
|
|
className="w-4 h-4"
|
|
height="20"
|
|
width="20"
|
|
crossOrigin="anonymous"
|
|
src="https://cdn.simpleicons.org/supabase"
|
|
/>
|
|
{isConnected && supabaseConn.project && (
|
|
<span className="ml-1 text-xs max-w-[100px] truncate">{supabaseConn.project.name}</span>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
|
|
<DialogRoot open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
{isDialogOpen && (
|
|
<Dialog className="max-w-[520px] p-6">
|
|
{!isConnected ? (
|
|
<div className="space-y-4">
|
|
<DialogTitle>
|
|
<img
|
|
className="w-5 h-5"
|
|
height="24"
|
|
width="24"
|
|
crossOrigin="anonymous"
|
|
src="https://cdn.simpleicons.org/supabase"
|
|
/>
|
|
Connect to Supabase
|
|
</DialogTitle>
|
|
|
|
<div>
|
|
<label className="block text-sm text-bolt-elements-textSecondary mb-2">Access Token</label>
|
|
<input
|
|
type="password"
|
|
value={supabaseConn.token}
|
|
onChange={(e) => updateToken(e.target.value)}
|
|
disabled={connecting}
|
|
placeholder="Enter your Supabase access token"
|
|
className={classNames(
|
|
'w-full px-3 py-2 rounded-lg text-sm',
|
|
'bg-[#F8F8F8] dark:bg-[#1A1A1A]',
|
|
'border border-[#E5E5E5] dark:border-[#333333]',
|
|
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
|
'focus:outline-none focus:ring-1 focus:ring-[#3ECF8E]',
|
|
'disabled:opacity-50',
|
|
)}
|
|
/>
|
|
<div className="mt-2 text-sm text-bolt-elements-textSecondary">
|
|
<a
|
|
href="https://app.supabase.com/account/tokens"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-[#3ECF8E] hover:underline inline-flex items-center gap-1"
|
|
>
|
|
Get your token
|
|
<div className="i-ph:arrow-square-out w-4 h-4" />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2 mt-6">
|
|
<DialogClose asChild>
|
|
<DialogButton type="secondary">Cancel</DialogButton>
|
|
</DialogClose>
|
|
<button
|
|
onClick={handleConnect}
|
|
disabled={connecting || !supabaseConn.token}
|
|
className={classNames(
|
|
'px-4 py-2 rounded-lg text-sm flex items-center gap-2',
|
|
'bg-[#3ECF8E] text-white',
|
|
'hover:bg-[#3BBF84]',
|
|
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
)}
|
|
>
|
|
{connecting ? (
|
|
<>
|
|
<div className="i-ph:spinner-gap animate-spin" />
|
|
Connecting...
|
|
</>
|
|
) : (
|
|
<>
|
|
<div className="i-ph:plug-charging w-4 h-4" />
|
|
Connect
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<DialogTitle>
|
|
<img
|
|
className="w-5 h-5"
|
|
height="24"
|
|
width="24"
|
|
crossOrigin="anonymous"
|
|
src="https://cdn.simpleicons.org/supabase"
|
|
/>
|
|
Supabase Connection
|
|
</DialogTitle>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4 p-3 bg-[#F8F8F8] dark:bg-[#1A1A1A] rounded-lg">
|
|
<div>
|
|
<h4 className="text-sm font-medium text-bolt-elements-textPrimary">{supabaseConn.user?.email}</h4>
|
|
<p className="text-xs text-bolt-elements-textSecondary">Role: {supabaseConn.user?.role}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{fetchingStats ? (
|
|
<div className="flex items-center gap-2 text-sm text-bolt-elements-textSecondary">
|
|
<div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
|
|
Fetching projects...
|
|
</div>
|
|
) : (
|
|
<div>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<button
|
|
onClick={() => setIsProjectsExpanded(!isProjectsExpanded)}
|
|
className="bg-transparent text-left text-sm font-medium text-bolt-elements-textPrimary flex items-center gap-2"
|
|
>
|
|
<div className="i-ph:database w-4 h-4" />
|
|
Your Projects ({supabaseConn.stats?.totalProjects || 0})
|
|
<div
|
|
className={classNames(
|
|
'i-ph:caret-down w-4 h-4 transition-transform',
|
|
isProjectsExpanded ? 'rotate-180' : '',
|
|
)}
|
|
/>
|
|
</button>
|
|
<button
|
|
onClick={() => handleCreateProject()}
|
|
className="px-2 py-1 rounded-md text-xs bg-[#3ECF8E] text-white hover:bg-[#3BBF84] flex items-center gap-1"
|
|
>
|
|
<div className="i-ph:plus w-3 h-3" />
|
|
New Project
|
|
</button>
|
|
</div>
|
|
|
|
{isProjectsExpanded && (
|
|
<>
|
|
{!supabaseConn.selectedProjectId && (
|
|
<div className="mb-2 p-3 bg-[#F8F8F8] dark:bg-[#1A1A1A] rounded-lg text-sm text-bolt-elements-textSecondary">
|
|
Select a project or create a new one for this chat
|
|
</div>
|
|
)}
|
|
|
|
{supabaseConn.stats?.projects?.length ? (
|
|
<div className="grid gap-2 max-h-60 overflow-y-auto">
|
|
{supabaseConn.stats.projects.map((project) => (
|
|
<div
|
|
key={project.id}
|
|
className="block p-3 rounded-lg border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-[#3ECF8E] dark:hover:border-[#3ECF8E] transition-colors"
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h5 className="text-sm font-medium text-bolt-elements-textPrimary flex items-center gap-1">
|
|
<div className="i-ph:database w-3 h-3 text-[#3ECF8E]" />
|
|
{project.name}
|
|
</h5>
|
|
<div className="text-xs text-bolt-elements-textSecondary mt-1">
|
|
{project.region}
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => selectProject(project.id)}
|
|
className={classNames(
|
|
'px-3 py-1 rounded-md text-xs',
|
|
supabaseConn.selectedProjectId === project.id
|
|
? 'bg-[#3ECF8E] text-white'
|
|
: 'bg-[#F0F0F0] dark:bg-[#252525] text-bolt-elements-textSecondary hover:bg-[#3ECF8E] hover:text-white',
|
|
)}
|
|
>
|
|
{supabaseConn.selectedProjectId === project.id ? (
|
|
<span className="flex items-center gap-1">
|
|
<div className="i-ph:check w-3 h-3" />
|
|
Selected
|
|
</span>
|
|
) : (
|
|
'Select'
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-sm text-bolt-elements-textSecondary flex items-center gap-2">
|
|
<div className="i-ph:info w-4 h-4" />
|
|
No projects found
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex justify-end gap-2 mt-6">
|
|
<DialogClose asChild>
|
|
<DialogButton type="secondary">Close</DialogButton>
|
|
</DialogClose>
|
|
<DialogButton type="danger" onClick={handleDisconnect}>
|
|
<div className="i-ph:plug-x w-4 h-4" />
|
|
Disconnect
|
|
</DialogButton>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Dialog>
|
|
)}
|
|
</DialogRoot>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface ButtonProps {
|
|
active?: boolean;
|
|
disabled?: boolean;
|
|
children?: any;
|
|
onClick?: VoidFunction;
|
|
className?: string;
|
|
}
|
|
|
|
function Button({ active = false, disabled = false, children, onClick, className }: ButtonProps) {
|
|
return (
|
|
<button
|
|
className={classNames(
|
|
'flex items-center p-1.5',
|
|
{
|
|
'bg-bolt-elements-item-backgroundDefault hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary':
|
|
!active,
|
|
'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': active && !disabled,
|
|
'bg-bolt-elements-item-backgroundDefault text-alpha-gray-20 dark:text-alpha-white-20 cursor-not-allowed':
|
|
disabled,
|
|
},
|
|
className,
|
|
)}
|
|
onClick={onClick}
|
|
>
|
|
{children}
|
|
</button>
|
|
);
|
|
}
|