mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-05-03 11:51:36 +00:00
Remove console.log statement for debugging purposes in the API route and enhance the layout of buttons in the SupabaseConnection component by grouping them and adding a refresh button
340 lines
14 KiB
TypeScript
340 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 && !supabaseConn.credentials) {
|
|
fetchProjectApiKeys(supabaseConn.selectedProjectId).catch(console.error);
|
|
}
|
|
}, [isConnected, supabaseConn.selectedProjectId, supabaseConn.token, supabaseConn.credentials]);
|
|
|
|
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>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={() => fetchSupabaseStats(supabaseConn.token)}
|
|
className="px-2 py-1 rounded-md text-xs bg-[#F0F0F0] dark:bg-[#252525] text-bolt-elements-textSecondary hover:bg-[#E5E5E5] dark:hover:bg-[#333333] flex items-center gap-1"
|
|
title="Refresh projects list"
|
|
>
|
|
<div className="i-ph:arrows-clockwise w-3 h-3" />
|
|
Refresh
|
|
</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>
|
|
</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-backgroundDefault 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>
|
|
);
|
|
}
|