bolt.diy/app/components/@settings/tabs/connections/VercelConnection.tsx
Stijnus 552f08acea
Some checks are pending
Docker Publish / docker-build-publish (push) Waiting to run
Update Stable Branch / prepare-release (push) Waiting to run
feat: update connectiontab and datatab security fix (#1614)
* feat: update connectiontab and datatab security fix

# Connection Components and Diagnostics Updates

## GitHub Connection Component Changes
- Updated the disconnect button styling to match Vercel's design:
  - Changed from `<Button>` component to native `<button>` element
  - Added red background (`bg-red-500`) with hover effect (`hover:bg-red-600`)
  - Updated icon from `i-ph:sign-out` to `i-ph:plug`
  - Simplified text to just "Disconnect"
  - Added connection status indicator with check-circle icon and "Connected to GitHub" text

## ConnectionDiagnostics Tab Updates
### Added New Connection Diagnostics
- Implemented diagnostics for Vercel and Supabase connections
- Added new helper function `safeJsonParse` for safer JSON parsing operations

### Diagnostic Checks Added
- **Vercel Diagnostics:**
  - LocalStorage token verification
  - API endpoint connectivity test
  - Connection status validation
  - Reset functionality for Vercel connection

- **Supabase Diagnostics:**
  - LocalStorage credentials verification
  - API endpoint connectivity test
  - Connection status validation
  - Reset functionality for Supabase connection

### UI Enhancements
- Added new status cards for Vercel and Supabase
- Implemented reset buttons with consistent styling
- Added loading states during diagnostics
- Enhanced error handling and user feedback

### Function Updates
- Extended `runDiagnostics` function to include Vercel and Supabase checks
- Added new reset helper functions for each connection type
- Improved error handling and status reporting
- Enhanced toast notifications for better user feedback

### Visual Consistency
- Matched styling of new diagnostic cards with existing GitHub and Netlify cards
- Consistent use of icons and status indicators
- Uniform button styling across all connection types
- Maintained consistent spacing and layout patterns

### Code Structure
- Organized diagnostic checks into clear, separate sections
- Improved error handling and type safety
- Enhanced code readability and maintainability
- Added comprehensive status compilation for all connections

These changes ensure a consistent user experience across all connection types while providing robust diagnostic capabilities for troubleshooting connection issues.

# DataTab.tsx Changes

## Code Cleanup
- Removed unused variables from useDataOperations hook:
  - Removed `handleExportAPIKeys`
  - Removed `handleUndo`
  - Removed `lastOperation`

This change improves code quality by removing unused variables and resolves ESLint warnings without affecting any functionality.

* Test commit to verify pre-commit hook
2025-04-08 13:06:43 +02:00

291 lines
12 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { motion } from 'framer-motion';
import { toast } from 'react-toastify';
import { useStore } from '@nanostores/react';
import { logStore } from '~/lib/stores/logs';
import { classNames } from '~/utils/classNames';
import {
vercelConnection,
isConnecting,
isFetchingStats,
updateVercelConnection,
fetchVercelStats,
} from '~/lib/stores/vercel';
export default function VercelConnection() {
const connection = useStore(vercelConnection);
const connecting = useStore(isConnecting);
const fetchingStats = useStore(isFetchingStats);
const [isProjectsExpanded, setIsProjectsExpanded] = useState(false);
useEffect(() => {
const fetchProjects = async () => {
if (connection.user && connection.token) {
await fetchVercelStats(connection.token);
}
};
fetchProjects();
}, [connection.user, connection.token]);
const handleConnect = async (event: React.FormEvent) => {
event.preventDefault();
isConnecting.set(true);
try {
const response = await fetch('https://api.vercel.com/v2/user', {
headers: {
Authorization: `Bearer ${connection.token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Invalid token or unauthorized');
}
const userData = (await response.json()) as any;
updateVercelConnection({
user: userData.user || userData, // Handle both possible structures
token: connection.token,
});
await fetchVercelStats(connection.token);
toast.success('Successfully connected to Vercel');
} catch (error) {
console.error('Auth error:', error);
logStore.logError('Failed to authenticate with Vercel', { error });
toast.error('Failed to connect to Vercel');
updateVercelConnection({ user: null, token: '' });
} finally {
isConnecting.set(false);
}
};
const handleDisconnect = () => {
updateVercelConnection({ user: null, token: '' });
toast.success('Disconnected from Vercel');
};
console.log('connection', connection);
return (
<motion.div
className="bg-[#FFFFFF] dark:bg-[#0A0A0A] rounded-lg border border-[#E5E5E5] dark:border-[#1A1A1A]"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<img
className="w-5 h-5 dark:invert"
height="24"
width="24"
crossOrigin="anonymous"
src={`https://cdn.simpleicons.org/vercel/black`}
/>
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Vercel Connection</h3>
</div>
</div>
{!connection.user ? (
<div className="space-y-4">
<div>
<label className="block text-sm text-bolt-elements-textSecondary mb-2">Personal Access Token</label>
<input
type="password"
value={connection.token}
onChange={(e) => updateVercelConnection({ ...connection, token: e.target.value })}
disabled={connecting}
placeholder="Enter your Vercel personal 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-bolt-elements-borderColorActive',
'disabled:opacity-50',
)}
/>
<div className="mt-2 text-sm text-bolt-elements-textSecondary">
<a
href="https://vercel.com/account/tokens"
target="_blank"
rel="noopener noreferrer"
className="text-bolt-elements-borderColorActive 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>
<button
onClick={handleConnect}
disabled={connecting || !connection.token}
className={classNames(
'px-4 py-2 rounded-lg text-sm flex items-center gap-2',
'bg-[#303030] text-white',
'hover:bg-[#5E41D0] hover:text-white',
'disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200',
'transform active:scale-95',
)}
>
{connecting ? (
<>
<div className="i-ph:spinner-gap animate-spin" />
Connecting...
</>
) : (
<>
<div className="i-ph:plug-charging w-4 h-4" />
Connect
</>
)}
</button>
</div>
) : (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<button
onClick={handleDisconnect}
className={classNames(
'px-4 py-2 rounded-lg text-sm flex items-center gap-2',
'bg-red-500 text-white',
'hover:bg-red-600',
)}
>
<div className="i-ph:plug w-4 h-4" />
Disconnect
</button>
<span className="text-sm text-bolt-elements-textSecondary flex items-center gap-1">
<div className="i-ph:check-circle w-4 h-4 text-green-500" />
Connected to Vercel
</span>
</div>
</div>
<div className="flex items-center gap-4 p-4 bg-[#F8F8F8] dark:bg-[#1A1A1A] rounded-lg">
{/* Debug output */}
<pre className="hidden">{JSON.stringify(connection.user, null, 2)}</pre>
<img
src={`https://vercel.com/api/www/avatar?u=${connection.user?.username || connection.user?.user?.username}`}
referrerPolicy="no-referrer"
crossOrigin="anonymous"
alt="User Avatar"
className="w-12 h-12 rounded-full border-2 border-bolt-elements-borderColorActive"
/>
<div>
<h4 className="text-sm font-medium text-bolt-elements-textPrimary">
{connection.user?.username || connection.user?.user?.username || 'Vercel User'}
</h4>
<p className="text-sm text-bolt-elements-textSecondary">
{connection.user?.email || connection.user?.user?.email || 'No email available'}
</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 Vercel projects...
</div>
) : (
<div>
<button
onClick={() => setIsProjectsExpanded(!isProjectsExpanded)}
className="w-full bg-transparent text-left text-sm font-medium text-bolt-elements-textPrimary mb-3 flex items-center gap-2"
>
<div className="i-ph:buildings w-4 h-4" />
Your Projects ({connection.stats?.totalProjects || 0})
<div
className={classNames(
'i-ph:caret-down w-4 h-4 ml-auto transition-transform',
isProjectsExpanded ? 'rotate-180' : '',
)}
/>
</button>
{isProjectsExpanded && connection.stats?.projects?.length ? (
<div className="grid gap-3">
{connection.stats.projects.map((project) => (
<a
key={project.id}
href={`https://vercel.com/dashboard/${project.id}`}
target="_blank"
rel="noopener noreferrer"
className="block p-4 rounded-lg border border-bolt-elements-borderColor hover:border-bolt-elements-borderColorActive transition-colors"
>
<div className="flex items-center justify-between">
<div>
<h5 className="text-sm font-medium text-bolt-elements-textPrimary flex items-center gap-2">
<div className="i-ph:globe w-4 h-4 text-bolt-elements-borderColorActive" />
{project.name}
</h5>
<div className="flex items-center gap-2 mt-2 text-xs text-bolt-elements-textSecondary">
{project.targets?.production?.alias && project.targets.production.alias.length > 0 ? (
<>
<a
href={`https://${project.targets.production.alias.find((a: string) => a.endsWith('.vercel.app') && !a.includes('-projects.vercel.app')) || project.targets.production.alias[0]}`}
target="_blank"
rel="noopener noreferrer"
className="hover:text-bolt-elements-borderColorActive"
>
{project.targets.production.alias.find(
(a: string) => a.endsWith('.vercel.app') && !a.includes('-projects.vercel.app'),
) || project.targets.production.alias[0]}
</a>
<span></span>
<span className="flex items-center gap-1">
<div className="i-ph:clock w-3 h-3" />
{new Date(project.createdAt).toLocaleDateString()}
</span>
</>
) : project.latestDeployments && project.latestDeployments.length > 0 ? (
<>
<a
href={`https://${project.latestDeployments[0].url}`}
target="_blank"
rel="noopener noreferrer"
className="hover:text-bolt-elements-borderColorActive"
>
{project.latestDeployments[0].url}
</a>
<span></span>
<span className="flex items-center gap-1">
<div className="i-ph:clock w-3 h-3" />
{new Date(project.latestDeployments[0].created).toLocaleDateString()}
</span>
</>
) : null}
</div>
</div>
{project.framework && (
<div className="text-xs text-bolt-elements-textSecondary px-2 py-1 rounded-md bg-[#F0F0F0] dark:bg-[#252525]">
<span className="flex items-center gap-1">
<div className="i-ph:code w-3 h-3" />
{project.framework}
</span>
</div>
)}
</div>
</a>
))}
</div>
) : isProjectsExpanded ? (
<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 in your Vercel account
</div>
) : null}
</div>
)}
</div>
)}
</div>
</motion.div>
);
}