feat: update connectiontab and datatab security fix (#1614)
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

# 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
This commit is contained in:
Stijnus
2025-04-08 13:06:43 +02:00
committed by GitHub
parent 8c70dd6123
commit 552f08acea
11 changed files with 757 additions and 363 deletions

View File

@@ -6,6 +6,20 @@ import { classNames } from '~/utils/classNames';
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '~/components/ui/Collapsible';
import { CodeBracketIcon, ChevronDownIcon } from '@heroicons/react/24/outline';
// Helper function to safely parse JSON
const safeJsonParse = (item: string | null) => {
if (!item) {
return null;
}
try {
return JSON.parse(item);
} catch (e) {
console.error('Failed to parse JSON from localStorage:', e);
return null;
}
};
/**
* A diagnostics component to help troubleshoot connection issues
*/
@@ -24,6 +38,8 @@ export default function ConnectionDiagnostics() {
const localStorageChecks = {
githubConnection: localStorage.getItem('github_connection'),
netlifyConnection: localStorage.getItem('netlify_connection'),
vercelConnection: localStorage.getItem('vercel_connection'),
supabaseConnection: localStorage.getItem('supabase_connection'),
};
// Get diagnostic data from server
@@ -35,36 +51,25 @@ export default function ConnectionDiagnostics() {
const serverDiagnostics = await response.json();
// Get GitHub token if available
const githubToken = localStorageChecks.githubConnection
? JSON.parse(localStorageChecks.githubConnection)?.token
: null;
const authHeaders = {
// === GitHub Checks ===
const githubConnectionParsed = safeJsonParse(localStorageChecks.githubConnection);
const githubToken = githubConnectionParsed?.token;
const githubAuthHeaders = {
...(githubToken ? { Authorization: `Bearer ${githubToken}` } : {}),
'Content-Type': 'application/json',
};
console.log('Testing GitHub endpoints with token:', githubToken ? 'present' : 'missing');
// Test GitHub API endpoints
const githubEndpoints = [
{ name: 'User', url: '/api/system/git-info?action=getUser' },
{ name: 'Repos', url: '/api/system/git-info?action=getRepos' },
{ name: 'Default', url: '/api/system/git-info' },
];
const githubResults = await Promise.all(
githubEndpoints.map(async (endpoint) => {
try {
const resp = await fetch(endpoint.url, {
headers: authHeaders,
});
return {
endpoint: endpoint.name,
status: resp.status,
ok: resp.ok,
};
const resp = await fetch(endpoint.url, { headers: githubAuthHeaders });
return { endpoint: endpoint.name, status: resp.status, ok: resp.ok };
} catch (error) {
return {
endpoint: endpoint.name,
@@ -75,23 +80,17 @@ export default function ConnectionDiagnostics() {
}),
);
// Check if Netlify token works
// === Netlify Checks ===
const netlifyConnectionParsed = safeJsonParse(localStorageChecks.netlifyConnection);
const netlifyToken = netlifyConnectionParsed?.token;
let netlifyUserCheck = null;
const netlifyToken = localStorageChecks.netlifyConnection
? JSON.parse(localStorageChecks.netlifyConnection || '{"token":""}').token
: '';
if (netlifyToken) {
try {
const netlifyResp = await fetch('https://api.netlify.com/api/v1/user', {
headers: {
Authorization: `Bearer ${netlifyToken}`,
},
headers: { Authorization: `Bearer ${netlifyToken}` },
});
netlifyUserCheck = {
status: netlifyResp.status,
ok: netlifyResp.ok,
};
netlifyUserCheck = { status: netlifyResp.status, ok: netlifyResp.ok };
} catch (error) {
netlifyUserCheck = {
error: error instanceof Error ? error.message : String(error),
@@ -100,22 +99,55 @@ export default function ConnectionDiagnostics() {
}
}
// === Vercel Checks ===
const vercelConnectionParsed = safeJsonParse(localStorageChecks.vercelConnection);
const vercelToken = vercelConnectionParsed?.token;
let vercelUserCheck = null;
if (vercelToken) {
try {
const vercelResp = await fetch('https://api.vercel.com/v2/user', {
headers: { Authorization: `Bearer ${vercelToken}` },
});
vercelUserCheck = { status: vercelResp.status, ok: vercelResp.ok };
} catch (error) {
vercelUserCheck = {
error: error instanceof Error ? error.message : String(error),
ok: false,
};
}
}
// === Supabase Checks ===
const supabaseConnectionParsed = safeJsonParse(localStorageChecks.supabaseConnection);
const supabaseUrl = supabaseConnectionParsed?.projectUrl;
const supabaseAnonKey = supabaseConnectionParsed?.anonKey;
let supabaseCheck = null;
if (supabaseUrl && supabaseAnonKey) {
supabaseCheck = { ok: true, status: 200, message: 'URL and Key present in localStorage' };
} else {
supabaseCheck = { ok: false, message: 'URL or Key missing in localStorage' };
}
// Compile results
const results = {
timestamp: new Date().toISOString(),
localStorage: {
hasGithubConnection: Boolean(localStorageChecks.githubConnection),
hasNetlifyConnection: Boolean(localStorageChecks.netlifyConnection),
githubConnectionParsed: localStorageChecks.githubConnection
? JSON.parse(localStorageChecks.githubConnection)
: null,
netlifyConnectionParsed: localStorageChecks.netlifyConnection
? JSON.parse(localStorageChecks.netlifyConnection)
: null,
hasVercelConnection: Boolean(localStorageChecks.vercelConnection),
hasSupabaseConnection: Boolean(localStorageChecks.supabaseConnection),
githubConnectionParsed,
netlifyConnectionParsed,
vercelConnectionParsed,
supabaseConnectionParsed,
},
apiEndpoints: {
github: githubResults,
netlify: netlifyUserCheck,
vercel: vercelUserCheck,
supabase: supabaseCheck,
},
serverDiagnostics,
};
@@ -131,7 +163,20 @@ export default function ConnectionDiagnostics() {
toast.error('Netlify API connection is failing. Try reconnecting.');
}
if (!results.localStorage.hasGithubConnection && !results.localStorage.hasNetlifyConnection) {
if (results.localStorage.hasVercelConnection && vercelUserCheck && !vercelUserCheck.ok) {
toast.error('Vercel API connection is failing. Try reconnecting.');
}
if (results.localStorage.hasSupabaseConnection && supabaseCheck && !supabaseCheck.ok) {
toast.warning('Supabase connection check failed or missing details. Verify settings.');
}
if (
!results.localStorage.hasGithubConnection &&
!results.localStorage.hasNetlifyConnection &&
!results.localStorage.hasVercelConnection &&
!results.localStorage.hasSupabaseConnection
) {
toast.info('No connection data found in browser storage.');
}
} catch (error) {
@@ -151,6 +196,7 @@ export default function ConnectionDiagnostics() {
document.cookie = 'githubUsername=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
document.cookie = 'git:github.com=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
toast.success('GitHub connection data cleared. Please refresh the page and reconnect.');
setDiagnosticResults(null);
} catch (error) {
console.error('Error clearing GitHub data:', error);
toast.error('Failed to clear GitHub connection data');
@@ -163,12 +209,37 @@ export default function ConnectionDiagnostics() {
localStorage.removeItem('netlify_connection');
document.cookie = 'netlifyToken=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
toast.success('Netlify connection data cleared. Please refresh the page and reconnect.');
setDiagnosticResults(null);
} catch (error) {
console.error('Error clearing Netlify data:', error);
toast.error('Failed to clear Netlify connection data');
}
};
// Helper to reset Vercel connection
const resetVercelConnection = () => {
try {
localStorage.removeItem('vercel_connection');
toast.success('Vercel connection data cleared. Please refresh the page and reconnect.');
setDiagnosticResults(null);
} catch (error) {
console.error('Error clearing Vercel data:', error);
toast.error('Failed to clear Vercel connection data');
}
};
// Helper to reset Supabase connection
const resetSupabaseConnection = () => {
try {
localStorage.removeItem('supabase_connection');
toast.success('Supabase connection data cleared. Please refresh the page and reconnect.');
setDiagnosticResults(null);
} catch (error) {
console.error('Error clearing Supabase data:', error);
toast.error('Failed to clear Supabase connection data');
}
};
return (
<div className="flex flex-col gap-6">
{/* Connection Status Cards */}
@@ -303,6 +374,133 @@ export default function ConnectionDiagnostics() {
</div>
)}
</div>
{/* Vercel Connection Card */}
<div className="p-4 rounded-lg bg-bolt-elements-background dark:bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor hover:border-bolt-elements-borderColorActive/70 dark:hover:border-bolt-elements-borderColorActive/70 transition-all duration-200 h-[180px] flex flex-col">
<div className="flex items-center gap-2">
<div className="i-si:vercel text-bolt-elements-item-contentAccent dark:text-bolt-elements-item-contentAccent w-4 h-4" />
<div className="text-sm font-medium text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary">
Vercel Connection
</div>
</div>
{diagnosticResults ? (
<>
<div className="flex items-center gap-2 mt-2">
<span
className={classNames(
'text-xl font-semibold',
diagnosticResults.localStorage.hasVercelConnection
? 'text-green-500 dark:text-green-400'
: 'text-red-500 dark:text-red-400',
)}
>
{diagnosticResults.localStorage.hasVercelConnection ? 'Connected' : 'Not Connected'}
</span>
</div>
{diagnosticResults.localStorage.hasVercelConnection && (
<>
<div className="text-xs text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5">
<div className="i-ph:user w-3.5 h-3.5 text-bolt-elements-item-contentAccent dark:text-bolt-elements-item-contentAccent" />
User:{' '}
{diagnosticResults.localStorage.vercelConnectionParsed?.user?.username ||
diagnosticResults.localStorage.vercelConnectionParsed?.user?.user?.username ||
'N/A'}
</div>
<div className="text-xs text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5">
<div className="i-ph:check-circle w-3.5 h-3.5 text-bolt-elements-item-contentAccent dark:text-bolt-elements-item-contentAccent" />
API Status:{' '}
<Badge
variant={diagnosticResults.apiEndpoints.vercel?.ok ? 'default' : 'destructive'}
className="ml-1"
>
{diagnosticResults.apiEndpoints.vercel?.ok ? 'OK' : 'Failed'}
</Badge>
</div>
</>
)}
{!diagnosticResults.localStorage.hasVercelConnection && (
<Button
onClick={() => window.location.reload()}
variant="outline"
size="sm"
className="mt-auto self-start hover:bg-bolt-elements-item-backgroundActive/10 hover:text-bolt-elements-textPrimary dark:hover:bg-bolt-elements-item-backgroundActive/10 dark:hover:text-bolt-elements-textPrimary transition-colors"
>
<div className="i-ph:plug w-3.5 h-3.5 mr-1" />
Connect Now
</Button>
)}
</>
) : (
<div className="flex items-center justify-center h-full">
<div className="text-sm text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary flex items-center gap-2">
<div className="i-ph:info w-4 h-4" />
Run diagnostics to check connection status
</div>
</div>
)}
</div>
{/* Supabase Connection Card */}
<div className="p-4 rounded-lg bg-bolt-elements-background dark:bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor hover:border-bolt-elements-borderColorActive/70 dark:hover:border-bolt-elements-borderColorActive/70 transition-all duration-200 h-[180px] flex flex-col">
<div className="flex items-center gap-2">
<div className="i-si:supabase text-bolt-elements-item-contentAccent dark:text-bolt-elements-item-contentAccent w-4 h-4" />
<div className="text-sm font-medium text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary">
Supabase Connection
</div>
</div>
{diagnosticResults ? (
<>
<div className="flex items-center gap-2 mt-2">
<span
className={classNames(
'text-xl font-semibold',
diagnosticResults.localStorage.hasSupabaseConnection
? 'text-green-500 dark:text-green-400'
: 'text-red-500 dark:text-red-400',
)}
>
{diagnosticResults.localStorage.hasSupabaseConnection ? 'Configured' : 'Not Configured'}
</span>
</div>
{diagnosticResults.localStorage.hasSupabaseConnection && (
<>
<div className="text-xs text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5 truncate">
<div className="i-ph:link w-3.5 h-3.5 text-bolt-elements-item-contentAccent dark:text-bolt-elements-item-contentAccent flex-shrink-0" />
Project URL: {diagnosticResults.localStorage.supabaseConnectionParsed?.projectUrl || 'N/A'}
</div>
<div className="text-xs text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5">
<div className="i-ph:check-circle w-3.5 h-3.5 text-bolt-elements-item-contentAccent dark:text-bolt-elements-item-contentAccent" />
Config Status:{' '}
<Badge
variant={diagnosticResults.apiEndpoints.supabase?.ok ? 'default' : 'destructive'}
className="ml-1"
>
{diagnosticResults.apiEndpoints.supabase?.ok ? 'OK' : 'Check Failed'}
</Badge>
</div>
</>
)}
{!diagnosticResults.localStorage.hasSupabaseConnection && (
<Button
onClick={() => window.location.reload()}
variant="outline"
size="sm"
className="mt-auto self-start hover:bg-bolt-elements-item-backgroundActive/10 hover:text-bolt-elements-textPrimary dark:hover:bg-bolt-elements-item-backgroundActive/10 dark:hover:text-bolt-elements-textPrimary transition-colors"
>
<div className="i-ph:plug w-3.5 h-3.5 mr-1" />
Configure Now
</Button>
)}
</>
) : (
<div className="flex items-center justify-center h-full">
<div className="text-sm text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary flex items-center gap-2">
<div className="i-ph:info w-4 h-4" />
Run diagnostics to check connection status
</div>
</div>
)}
</div>
</div>
{/* Action Buttons */}
@@ -323,22 +521,42 @@ export default function ConnectionDiagnostics() {
<Button
onClick={resetGitHubConnection}
disabled={isRunning}
disabled={isRunning || !diagnosticResults?.localStorage.hasGithubConnection}
variant="outline"
className="flex items-center gap-2 hover:bg-bolt-elements-item-backgroundActive/10 hover:text-bolt-elements-textPrimary dark:hover:bg-bolt-elements-item-backgroundActive/10 dark:hover:text-bolt-elements-textPrimary transition-colors"
className="flex items-center gap-2 hover:bg-bolt-elements-item-backgroundActive/10 hover:text-bolt-elements-textPrimary dark:hover:bg-bolt-elements-item-backgroundActive/10 dark:hover:text-bolt-elements-textPrimary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<div className="i-ph:github-logo w-4 h-4" />
Reset GitHub Connection
Reset GitHub
</Button>
<Button
onClick={resetNetlifyConnection}
disabled={isRunning}
disabled={isRunning || !diagnosticResults?.localStorage.hasNetlifyConnection}
variant="outline"
className="flex items-center gap-2 hover:bg-bolt-elements-item-backgroundActive/10 hover:text-bolt-elements-textPrimary dark:hover:bg-bolt-elements-item-backgroundActive/10 dark:hover:text-bolt-elements-textPrimary transition-colors"
className="flex items-center gap-2 hover:bg-bolt-elements-item-backgroundActive/10 hover:text-bolt-elements-textPrimary dark:hover:bg-bolt-elements-item-backgroundActive/10 dark:hover:text-bolt-elements-textPrimary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<div className="i-si:netlify w-4 h-4" />
Reset Netlify Connection
Reset Netlify
</Button>
<Button
onClick={resetVercelConnection}
disabled={isRunning || !diagnosticResults?.localStorage.hasVercelConnection}
variant="outline"
className="flex items-center gap-2 hover:bg-bolt-elements-item-backgroundActive/10 hover:text-bolt-elements-textPrimary dark:hover:bg-bolt-elements-item-backgroundActive/10 dark:hover:text-bolt-elements-textPrimary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<div className="i-si:vercel w-4 h-4" />
Reset Vercel
</Button>
<Button
onClick={resetSupabaseConnection}
disabled={isRunning || !diagnosticResults?.localStorage.hasSupabaseConnection}
variant="outline"
className="flex items-center gap-2 hover:bg-bolt-elements-item-backgroundActive/10 hover:text-bolt-elements-textPrimary dark:hover:bg-bolt-elements-item-backgroundActive/10 dark:hover:text-bolt-elements-textPrimary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<div className="i-si:supabase w-4 h-4" />
Reset Supabase
</Button>
</div>

View File

@@ -149,48 +149,6 @@ export default function ConnectionsTab() {
</div>
</motion.div>
{/* Cloudflare Deployment Note - Highly visible */}
<motion.div
className="bg-gradient-to-r from-blue-50 to-blue-100 dark:from-blue-950/40 dark:to-blue-900/30 border border-blue-200 dark:border-blue-800/50 rounded-lg shadow-sm p-4 mb-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<div className="flex items-center gap-2 mb-2 text-blue-700 dark:text-blue-400">
<div className="i-ph:cloud-bold w-5 h-5" />
<h3 className="text-base font-medium">Using Cloudflare Pages?</h3>
</div>
<p className="text-sm text-blue-700 dark:text-blue-300 mb-2">
If you're experiencing GitHub connection issues (500 errors) on Cloudflare Pages deployments, you need to
configure environment variables in your Cloudflare dashboard:
</p>
<div className="bg-white/80 dark:bg-slate-900/60 rounded-md p-3 text-sm border border-blue-200 dark:border-blue-800/50">
<ol className="list-decimal list-inside pl-2 text-blue-700 dark:text-blue-300 space-y-2">
<li>
Go to <strong>Cloudflare Pages dashboard → Your project → Settings → Environment variables</strong>
</li>
<li>
Add <strong>both</strong> of these secrets (Production environment):
<ul className="list-disc list-inside pl-4 mt-1 mb-1">
<li>
<code className="px-1 py-0.5 bg-blue-100 dark:bg-blue-800/40 rounded">GITHUB_ACCESS_TOKEN</code>{' '}
(server-side API calls)
</li>
<li>
<code className="px-1 py-0.5 bg-blue-100 dark:bg-blue-800/40 rounded">VITE_GITHUB_ACCESS_TOKEN</code>{' '}
(client-side access)
</li>
</ul>
</li>
<li>
Add <code className="px-1 py-0.5 bg-blue-100 dark:bg-blue-800/40 rounded">VITE_GITHUB_TOKEN_TYPE</code> if
using fine-grained tokens
</li>
<li>Deploy a fresh build after adding these variables</li>
</ol>
</div>
</motion.div>
<div className="grid grid-cols-1 gap-6">
<Suspense fallback={<LoadingFallback />}>
<GitHubConnection />

View File

@@ -609,10 +609,10 @@ export default function GitHubConnection() {
}`}
className={classNames(
'w-full px-3 py-2 rounded-lg text-sm',
'bg-bolt-elements-background-depth-1 dark:bg-bolt-elements-background-depth-1',
'border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor',
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary dark:placeholder-bolt-elements-textTertiary',
'focus:outline-none focus:ring-1 focus:ring-bolt-elements-item-contentAccent dark:focus:ring-bolt-elements-item-contentAccent',
'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',
)}
/>
@@ -621,11 +621,10 @@ export default function GitHubConnection() {
href={`https://github.com/settings/tokens${connection.tokenType === 'fine-grained' ? '/beta' : '/new'}`}
target="_blank"
rel="noopener noreferrer"
className="text-bolt-elements-link-text dark:text-bolt-elements-link-text hover:text-bolt-elements-link-textHover dark:hover:text-bolt-elements-link-textHover flex items-center gap-1"
className="text-bolt-elements-borderColorActive hover:underline inline-flex items-center gap-1"
>
<div className="i-ph:key w-4 h-4" />
Get your token
<div className="i-ph:arrow-square-out w-3 h-3" />
<div className="i-ph:arrow-square-out w-4 h-4" />
</a>
<span className="mx-2"></span>
<span>
@@ -640,58 +639,48 @@ export default function GitHubConnection() {
<div className="flex items-center justify-between">
{!connection.user ? (
<Button
<button
onClick={handleConnect}
disabled={isConnecting || !connection.token}
variant="default"
className="flex items-center gap-2"
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',
)}
>
{isConnecting ? (
<>
<div className="i-ph:spinner-gap animate-spin w-4 h-4" />
<div className="i-ph:spinner-gap animate-spin" />
Connecting...
</>
) : (
<>
<div className="i-ph:github-logo w-4 h-4" />
<div className="i-ph:plug-charging w-4 h-4" />
Connect
</>
)}
</Button>
</button>
) : (
<>
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-4">
<Button
<button
onClick={handleDisconnect}
variant="destructive"
size="sm"
className="flex items-center gap-2"
>
<div className="i-ph:sign-out w-4 h-4" />
Disconnect
</Button>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<div className="i-ph:check-circle w-4 h-4 text-bolt-elements-icon-success dark:text-bolt-elements-icon-success" />
<span className="text-sm text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary">
Connected to GitHub using{' '}
<span className="text-bolt-elements-item-contentAccent dark:text-bolt-elements-item-contentAccent font-medium">
{connection.tokenType === 'classic' ? 'PAT' : 'Fine-grained Token'}
</span>
</span>
</div>
{connection.rateLimit && (
<div className="flex items-center gap-2 text-xs text-bolt-elements-textSecondary">
<div className="i-ph:chart-line-up w-3.5 h-3.5 text-bolt-elements-icon-success" />
<span>
API Limit: {connection.rateLimit.remaining.toLocaleString()}/
{connection.rateLimit.limit.toLocaleString()} Resets in{' '}
{Math.max(0, Math.floor((connection.rateLimit.reset * 1000 - Date.now()) / 60000))} min
</span>
</div>
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>
>
<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 GitHub
</span>
</div>
<div className="flex items-center gap-2">
<Button

View File

@@ -659,92 +659,68 @@ export default function NetlifyConnection() {
placeholder="Enter your Netlify API token"
className={classNames(
'w-full px-3 py-2 rounded-lg text-sm',
'bg-bolt-elements-background-depth-1 dark:bg-bolt-elements-background-depth-1',
'border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor',
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary dark:placeholder-bolt-elements-textTertiary',
'focus:outline-none focus:ring-1 focus:ring-bolt-elements-item-contentAccent dark:focus:ring-bolt-elements-item-contentAccent',
'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 dark:text-bolt-elements-textSecondary">
<div className="mt-2 text-sm text-bolt-elements-textSecondary">
<a
href="https://app.netlify.com/user/applications#personal-access-tokens"
target="_blank"
rel="noopener noreferrer"
className="text-bolt-elements-link-text dark:text-bolt-elements-link-text hover:text-bolt-elements-link-textHover dark:hover:text-bolt-elements-link-textHover flex items-center gap-1"
className="text-bolt-elements-borderColorActive hover:underline inline-flex items-center gap-1"
>
<div className="i-ph:key w-4 h-4" />
Get your token
<div className="i-ph:arrow-square-out w-3 h-3" />
<div className="i-ph:arrow-square-out w-4 h-4" />
</a>
</div>
<div className="flex items-center justify-between mt-4">
<Button
<button
onClick={handleConnect}
disabled={isConnecting || !tokenInput}
variant="default"
className="flex items-center gap-2"
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',
)}
>
{isConnecting ? (
<>
<div className="i-ph:spinner-gap animate-spin w-4 h-4" />
<div className="i-ph:spinner-gap animate-spin" />
Connecting...
</>
) : (
<>
<CloudIcon className="w-4 h-4" />
<div className="i-ph:plug-charging w-4 h-4" />
Connect
</>
)}
</Button>
</button>
</div>
</div>
) : (
<div className="flex flex-col w-full gap-4 mt-4">
<div className="flex flex-wrap items-center gap-3">
<Button onClick={handleDisconnect} variant="destructive" size="sm" className="flex items-center gap-2">
<div className="i-ph:sign-out w-4 h-4" />
<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>
<div className="flex items-center gap-2">
<CheckCircleIcon className="h-4 w-4 text-green-500" />
<span className="text-sm text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary">
Connected to Netlify
</span>
</div>
<div className="flex items-center gap-2 ml-auto">
<Button
variant="outline"
onClick={() => window.open('https://app.netlify.com', '_blank', 'noopener,noreferrer')}
className="flex items-center gap-2 hover:bg-bolt-elements-item-backgroundActive/10 hover:text-bolt-elements-textPrimary dark:hover:text-bolt-elements-textPrimary transition-colors"
>
<div className="i-ph:layout-dashboard w-4 h-4" />
Dashboard
</Button>
<Button
onClick={() => fetchNetlifyStats(connection.token)}
disabled={fetchingStats}
variant="outline"
className="flex items-center gap-2 hover:bg-bolt-elements-item-backgroundActive/10 hover:text-bolt-elements-textPrimary dark:hover:text-bolt-elements-textPrimary transition-colors"
>
{fetchingStats ? (
<>
<div className="i-ph:spinner-gap w-4 h-4 animate-spin text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary" />
<span className="text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary">
Refreshing...
</span>
</>
) : (
<>
<ArrowPathIcon className="h-4 w-4 text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary" />
<span className="text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary">
Refresh Stats
</span>
</>
)}
</Button>
</div>
</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 Netlify
</span>
</div>
{renderStats()}
</div>

View File

@@ -126,9 +126,10 @@ export default function VercelConnection() {
disabled={connecting || !connection.token}
className={classNames(
'px-4 py-2 rounded-lg text-sm flex items-center gap-2',
'bg-bolt-elements-borderColor text-white',
'hover:bg-bolt-elements-borderColorActive',
'disabled:opacity-50 disabled:cursor-not-allowed',
'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 ? (

View File

@@ -116,9 +116,6 @@ export function DataTab() {
handleResetChats,
handleDownloadTemplate,
handleImportAPIKeys,
handleExportAPIKeys,
handleUndo,
lastOperation,
} = useDataOperations({
customDb: db || undefined, // Pass the boltHistory database, converting null to undefined
onReloadSettings: () => window.location.reload(),
@@ -634,43 +631,6 @@ export function DataTab() {
<div>
<h2 className="text-xl font-semibold mb-4 text-bolt-elements-textPrimary">API Keys</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Card>
<CardHeader>
<div className="flex items-center mb-2">
<motion.div className="text-accent-500 mr-2" whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
<div className="i-ph-download-duotone w-5 h-5" />
</motion.div>
<CardTitle className="text-lg group-hover:text-bolt-elements-item-contentAccent transition-colors">
Export API Keys
</CardTitle>
</div>
<CardDescription>Export your API keys to a JSON file.</CardDescription>
</CardHeader>
<CardFooter>
<motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} className="w-full">
<Button
onClick={handleExportAPIKeys}
disabled={isExporting}
variant="outline"
size="sm"
className={classNames(
'hover:text-bolt-elements-item-contentAccent hover:border-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-item-backgroundAccent transition-colors w-full justify-center',
isExporting ? 'cursor-not-allowed' : '',
)}
>
{isExporting ? (
<>
<div className="i-ph-spinner-gap-bold animate-spin w-4 h-4 mr-2" />
Exporting...
</>
) : (
'Export Keys'
)}
</Button>
</motion.div>
</CardFooter>
</Card>
<Card>
<CardHeader>
<div className="flex items-center mb-2">
@@ -756,23 +716,6 @@ export function DataTab() {
</CardContent>
</Card>
</div>
{/* Undo Last Operation */}
{lastOperation && (
<div className="fixed bottom-4 right-4 bg-bolt-elements-bg-depth-3 text-bolt-elements-textPrimary p-4 rounded-lg shadow-lg flex items-center gap-3 z-50">
<div className="text-sm">
<span className="font-medium">Last action:</span> {lastOperation.type}
</div>
<Button
onClick={handleUndo}
variant="outline"
size="sm"
className="border-bolt-elements-borderColor text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundAccent hover:text-bolt-elements-item-contentAccent"
>
Undo
</Button>
</div>
)}
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { memo } from 'react';
import { memo, Fragment } from 'react';
import { Markdown } from './Markdown';
import type { JSONValue } from 'ai';
import Popover from '~/components/ui/Popover';
@@ -78,7 +78,7 @@ export const AssistantMessage = memo(({ content, annotations }: AssistantMessage
{codeContext.map((x) => {
const normalized = normalizedFilePath(x);
return (
<>
<Fragment key={normalized}>
<code
className="bg-bolt-elements-artifacts-inlineCode-background text-bolt-elements-artifacts-inlineCode-text px-1.5 py-1 rounded-md text-bolt-elements-item-contentAccent hover:underline cursor-pointer"
onClick={(e) => {
@@ -89,7 +89,7 @@ export const AssistantMessage = memo(({ content, annotations }: AssistantMessage
>
{normalized}
</code>
</>
</Fragment>
);
})}
</div>

View File

@@ -81,6 +81,7 @@ export function Chat() {
position="bottom-right"
pauseOnFocusLoss
transition={toastAnimation}
autoClose={3000}
/>
</>
);

View File

@@ -593,7 +593,10 @@ export const Preview = memo(() => {
* Intentionally disabled - we want to maintain scale of 1
* No dynamic scaling to ensure device frame matches external window exactly
*/
return () => {};
// Intentionally empty cleanup function - no cleanup needed
return () => {
// No cleanup needed
};
}, [isDeviceModeOn, showDeviceFrameInPreview, getDeviceScale, isLandscape, selectedWindowSize]);
// Function to get the frame color based on dark mode
@@ -711,6 +714,37 @@ export const Preview = memo(() => {
title={isFullscreen ? 'Exit Full Screen' : 'Full Screen'}
/>
{/* Simple preview button */}
<IconButton
icon="i-ph:browser"
onClick={() => {
if (!activePreview?.baseUrl) {
console.warn('[Preview] No active preview available');
return;
}
const match = activePreview.baseUrl.match(
/^https?:\/\/([^.]+)\.local-credentialless\.webcontainer-api\.io/,
);
if (!match) {
console.warn('[Preview] Invalid WebContainer URL:', activePreview.baseUrl);
return;
}
const previewId = match[1];
const previewUrl = `/webcontainer/preview/${previewId}`;
// Open in a new window with simple parameters
window.open(
previewUrl,
`preview-${previewId}`,
'width=1280,height=720,menubar=no,toolbar=no,location=no,status=no,resizable=yes',
);
}}
title="Open Preview in New Window"
/>
<div className="flex items-center relative">
<IconButton
icon="i-ph:arrow-square-out"