feat: comprehensive service integration refactor with enhanced tabs architecture (#1978)

* feat: add service tabs refactor with GitHub, GitLab, Supabase, Vercel, and Netlify integration

This commit introduces a comprehensive refactor of the connections system,
replacing the single connections tab with dedicated service integration tabs:

 New Service Tabs:
- GitHub Tab: Complete integration with repository management, stats, and API
- GitLab Tab: GitLab project integration and management
- Supabase Tab: Database project management with comprehensive analytics
- Vercel Tab: Project deployment management and monitoring
- Netlify Tab: Site deployment and build management

🔧 Supporting Infrastructure:
- Enhanced store management for each service with auto-connect via env vars
- API routes for secure server-side token handling and data fetching
- Updated TypeScript types with missing properties and interfaces
- Comprehensive hooks for service connections and state management
- Security utilities for API endpoint validation

🎨 UI/UX Improvements:
- Individual service tabs with tailored functionality
- Motion animations and improved loading states
- Connection testing and health monitoring
- Advanced analytics dashboards for each service
- Consistent design patterns across all service tabs

🛠️ Technical Changes:
- Removed legacy connection tab in favor of individual service tabs
- Updated tab configuration and routing system
- Added comprehensive error handling and loading states
- Enhanced type safety with extended interfaces
- Implemented environment variable auto-connection features

Note: Some TypeScript errors remain and will need to be resolved in follow-up commits.
The dev server runs successfully and the service tabs are functional.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: comprehensive service integration refactor with enhanced tabs architecture

Major architectural improvements to service integrations:

**Service Integration Refactor:**
- Complete restructure of service connection tabs (GitHub, GitLab, Vercel, Netlify, Supabase)
- Migrated from centralized ConnectionsTab to dedicated service-specific tabs
- Added shared service integration components for consistent UX
- Implemented auto-connection feature using environment variables

**New Components & Architecture:**
- ServiceIntegrationLayout for consistent service tab structure
- ConnectionStatus, ServiceCard components for reusable UI patterns
- BranchSelector component for repository branch management
- Enhanced authentication dialogs with improved error handling

**API & Backend Enhancements:**
- New API endpoints: github-branches, gitlab-branches, gitlab-projects, vercel-user
- Enhanced GitLab API service with comprehensive project management
- Improved connection testing hooks (useConnectionTest)
- Better error handling and rate limiting across all services

**Configuration & Environment:**
- Updated .env.example with comprehensive service integration guides
- Added auto-connection support for all major services
- Improved development and production environment configurations
- Enhanced tab management with proper service icons

**Code Quality & TypeScript:**
- Fixed all TypeScript errors across service integration components
- Enhanced type definitions for Vercel, Supabase, and other service integrations
- Improved type safety with proper optional chaining and type assertions
- Better separation of concerns between UI and business logic

**Removed Legacy Code:**
- Removed redundant connection components and consolidated into service tabs
- Cleaned up unused imports and deprecated connection patterns
- Streamlined authentication flows across all services

This refactor provides a more maintainable, scalable architecture for service integrations
while significantly improving the user experience for managing external connections.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: clean up dead code and consolidate utilities

- Remove legacy .eslintrc.json (replaced by flat config)
- Remove duplicate app/utils/types.ts (unused type definitions)
- Remove app/utils/cn.ts and consolidate with classNames utility
- Clean up unused ServiceErrorHandler class implementation
- Enhance classNames utility to support boolean values
- Update GlowingEffect.tsx to use consolidated classNames utility

Removes ~150+ lines of unused code while maintaining all functionality.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Simplify terminal health checks and improve project setup

Removed aggressive health checking and reconnection logic from TerminalManager to prevent issues with terminal responsiveness. Updated TerminalTabs to remove onReconnect handlers. Enhanced projectCommands utility to generate non-interactive setup commands and detect shadcn projects, improving automation and reliability of project setup.

* fix: resolve GitLab deployment issues and enhance GitHub deployment reliability

GitLab Deployment Fixes:
- Fix COEP header issue for avatar images by adding crossOrigin and referrerPolicy attributes
- Implement repository name sanitization to handle special characters and ensure GitLab compliance
- Enhance error handling with detailed validation error parsing and user-friendly messages
- Add explicit path field and description to project creation requests
- Improve URL encoding and project path resolution for proper API calls
- Add graceful file commit handling with timeout and error recovery

GitHub Deployment Enhancements:
- Add comprehensive repository name validation and sanitization
- Implement real-time feedback for invalid characters in repository name input
- Enhance error handling with specific error types and retry suggestions
- Improve user experience with better error messages and validation feedback
- Add repository name length limits and character restrictions
- Show sanitized name preview to users before submission

General Improvements:
- Add GitLabAuthDialog component for improved authentication flow
- Enhance logging and debugging capabilities for deployment operations
- Improve accessibility with proper dialog titles and descriptions
- Add better user notifications for name sanitization and validation issues

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Stijnus
2025-09-08 19:29:12 +02:00
committed by GitHub
parent 2fde6f8081
commit 4ca535b9d1
94 changed files with 12201 additions and 2986 deletions

View File

@@ -7,3 +7,9 @@ export { default } from './useViewport';
export { useFeatures } from './useFeatures';
export { useNotifications } from './useNotifications';
export { useConnectionStatus } from './useConnectionStatus';
export { useGitHubConnection } from './useGitHubConnection';
export { useGitHubStats } from './useGitHubStats';
export { useGitLabConnection } from './useGitLabConnection';
export { useGitLabAPI } from './useGitLabAPI';
export { useSupabaseConnection } from './useSupabaseConnection';
export { useConnectionTest } from './useConnectionTest';

View File

@@ -0,0 +1,63 @@
import { useState, useCallback } from 'react';
import type { ConnectionTestResult } from '~/components/@settings/shared/service-integration';
interface UseConnectionTestOptions {
testEndpoint: string;
serviceName: string;
getUserIdentifier?: (data: any) => string;
}
export function useConnectionTest({ testEndpoint, serviceName, getUserIdentifier }: UseConnectionTestOptions) {
const [testResult, setTestResult] = useState<ConnectionTestResult | null>(null);
const testConnection = useCallback(async () => {
setTestResult({
status: 'testing',
message: 'Testing connection...',
});
try {
const response = await fetch(testEndpoint, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (response.ok) {
const data = await response.json();
const userIdentifier = getUserIdentifier ? getUserIdentifier(data) : 'User';
setTestResult({
status: 'success',
message: `Connected successfully to ${serviceName} as ${userIdentifier}`,
timestamp: Date.now(),
});
} else {
const errorData = (await response.json().catch(() => ({}))) as { error?: string };
setTestResult({
status: 'error',
message: `Connection failed: ${errorData.error || `${response.status} ${response.statusText}`}`,
timestamp: Date.now(),
});
}
} catch (error) {
setTestResult({
status: 'error',
message: `Connection failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
timestamp: Date.now(),
});
}
}, [testEndpoint, serviceName, getUserIdentifier]);
const clearTestResult = useCallback(() => {
setTestResult(null);
}, []);
return {
testResult,
testConnection,
clearTestResult,
isTestingConnection: testResult?.status === 'testing',
};
}

View File

@@ -0,0 +1,6 @@
// Basic GitHub API hook placeholder
export const useGitHubAPI = () => {
return {
// Placeholder implementation
};
};

View File

@@ -0,0 +1,250 @@
import { useState, useEffect, useCallback } from 'react';
import { useStore } from '@nanostores/react';
import { toast } from 'react-toastify';
import Cookies from 'js-cookie';
import type { GitHubUserResponse, GitHubConnection } from '~/types/GitHub';
import { useGitHubAPI } from './useGitHubAPI';
import { githubConnection, isConnecting, updateGitHubConnection } from '~/lib/stores/github';
export interface ConnectionState {
isConnected: boolean;
isLoading: boolean;
isConnecting: boolean;
connection: GitHubConnection | null;
error: string | null;
isServerSide: boolean; // Indicates if this is a server-side connection
}
export interface UseGitHubConnectionReturn extends ConnectionState {
connect: (token: string, tokenType: 'classic' | 'fine-grained') => Promise<void>;
disconnect: () => void;
refreshConnection: () => Promise<void>;
testConnection: () => Promise<boolean>;
}
const STORAGE_KEY = 'github_connection';
export function useGitHubConnection(): UseGitHubConnectionReturn {
const connection = useStore(githubConnection);
const connecting = useStore(isConnecting);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Create API instance - will update when connection changes
useGitHubAPI();
// Load saved connection on mount
useEffect(() => {
loadSavedConnection();
}, []);
const loadSavedConnection = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
// Check if connection already exists in store (likely from initialization)
if (connection?.user) {
setIsLoading(false);
return;
}
// If we have a token but no user, or incomplete data, refresh
if (connection?.token && (!connection.user || !connection.stats)) {
await refreshConnectionData(connection);
}
setIsLoading(false);
} catch (error) {
console.error('Error loading saved connection:', error);
setError('Failed to load saved connection');
setIsLoading(false);
// Clean up corrupted data
localStorage.removeItem(STORAGE_KEY);
}
}, [connection]);
const refreshConnectionData = useCallback(async (connection: GitHubConnection) => {
if (!connection.token) {
return;
}
try {
// Make direct API call instead of using hook
const response = await fetch('https://api.github.com/user', {
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `${connection.tokenType === 'classic' ? 'token' : 'Bearer'} ${connection.token}`,
'User-Agent': 'Bolt.diy',
},
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const userData = (await response.json()) as GitHubUserResponse;
const updatedConnection: GitHubConnection = {
...connection,
user: userData,
};
updateGitHubConnection(updatedConnection);
} catch (error) {
console.error('Error refreshing connection data:', error);
}
}, []);
const connect = useCallback(async (token: string, tokenType: 'classic' | 'fine-grained') => {
console.log('useGitHubConnection.connect called with tokenType:', tokenType);
if (!token.trim()) {
console.log('Token validation failed - empty token');
setError('Token is required');
return;
}
console.log('Setting isConnecting to true');
isConnecting.set(true);
setError(null);
try {
console.log('Making API request to GitHub...');
// Test the token by fetching user info
const response = await fetch('https://api.github.com/user', {
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `${tokenType === 'classic' ? 'token' : 'Bearer'} ${token}`,
'User-Agent': 'Bolt.diy',
},
});
console.log('GitHub API response status:', response.status, response.statusText);
if (!response.ok) {
throw new Error(`Authentication failed: ${response.status} ${response.statusText}`);
}
const userData = (await response.json()) as GitHubUserResponse;
// Create connection object
const connectionData: GitHubConnection = {
user: userData,
token,
tokenType,
};
// Set cookies for API requests
Cookies.set('githubToken', token);
Cookies.set('githubUsername', userData.login);
Cookies.set(
'git:github.com',
JSON.stringify({
username: token,
password: 'x-oauth-basic',
}),
);
// Update the store
updateGitHubConnection(connectionData);
toast.success(`Connected to GitHub as ${userData.login}`);
} catch (error) {
console.error('Failed to connect to GitHub:', error);
const errorMessage = error instanceof Error ? error.message : 'Failed to connect to GitHub';
setError(errorMessage);
toast.error(`Failed to connect: ${errorMessage}`);
throw error;
} finally {
isConnecting.set(false);
}
}, []);
const disconnect = useCallback(() => {
// Clear localStorage
localStorage.removeItem(STORAGE_KEY);
// Clear all GitHub-related cookies
Cookies.remove('githubToken');
Cookies.remove('githubUsername');
Cookies.remove('git:github.com');
// Reset store
updateGitHubConnection({
user: null,
token: '',
tokenType: 'classic',
});
setError(null);
toast.success('Disconnected from GitHub');
}, []);
const refreshConnection = useCallback(async () => {
if (!connection?.token) {
throw new Error('No connection to refresh');
}
setIsLoading(true);
setError(null);
try {
await refreshConnectionData(connection);
} catch (error) {
console.error('Error refreshing connection:', error);
setError('Failed to refresh connection');
throw error;
} finally {
setIsLoading(false);
}
}, [connection, refreshConnectionData]);
const testConnection = useCallback(async (): Promise<boolean> => {
if (!connection) {
return false;
}
try {
// For server-side connections, test via our API
const isServerSide = !connection.token;
if (isServerSide) {
const response = await fetch('/api/github-user');
return response.ok;
}
// For client-side connections, test directly
const response = await fetch('https://api.github.com/user', {
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `${connection.tokenType === 'classic' ? 'token' : 'Bearer'} ${connection.token}`,
'User-Agent': 'Bolt.diy',
},
});
return response.ok;
} catch (error) {
console.error('Connection test failed:', error);
return false;
}
}, [connection]);
return {
isConnected: !!connection?.user,
isLoading,
isConnecting: connecting,
connection,
error,
isServerSide: !connection?.token, // Server-side if no token
connect,
disconnect,
refreshConnection,
testConnection,
};
}

View File

@@ -0,0 +1,321 @@
import { useState, useCallback, useEffect, useMemo } from 'react';
import { toast } from 'react-toastify';
import type { GitHubStats, GitHubConnection } from '~/types/GitHub';
import { gitHubApiService } from '~/lib/services/githubApiService';
export interface UseGitHubStatsState {
stats: GitHubStats | null;
isLoading: boolean;
isRefreshing: boolean;
error: string | null;
lastUpdated: Date | null;
}
export interface UseGitHubStatsOptions {
autoFetch?: boolean;
refreshInterval?: number; // in milliseconds
cacheTimeout?: number; // in milliseconds
}
export interface UseGitHubStatsReturn extends UseGitHubStatsState {
fetchStats: () => Promise<void>;
refreshStats: () => Promise<void>;
clearStats: () => void;
isStale: boolean;
}
const STATS_CACHE_KEY = 'github_stats_cache';
const DEFAULT_CACHE_TIMEOUT = 30 * 60 * 1000; // 30 minutes
export function useGitHubStats(
connection: GitHubConnection | null,
options: UseGitHubStatsOptions = {},
isServerSide: boolean = false,
): UseGitHubStatsReturn {
const { autoFetch = false, refreshInterval, cacheTimeout = DEFAULT_CACHE_TIMEOUT } = options;
const [state, setState] = useState<UseGitHubStatsState>({
stats: null,
isLoading: false,
isRefreshing: false,
error: null,
lastUpdated: null,
});
// Configure API service when connection is available
const apiService = useMemo(() => {
if (!connection?.token) {
return null;
}
// Configure the singleton instance with the current connection
gitHubApiService.configure({
token: connection.token,
tokenType: connection.tokenType,
});
return gitHubApiService;
}, [connection?.token, connection?.tokenType]);
// Check if stats are stale
const isStale = useMemo(() => {
if (!state.lastUpdated || !state.stats) {
return true;
}
return Date.now() - state.lastUpdated.getTime() > cacheTimeout;
}, [state.lastUpdated, state.stats, cacheTimeout]);
// Load cached stats on mount
useEffect(() => {
loadCachedStats();
}, []);
// Auto-fetch stats when connection changes - with better handling
useEffect(() => {
if (autoFetch && connection && (!state.stats || isStale)) {
/*
* For server-side connections, always try to fetch
* For client-side connections, only fetch if we have an API service
*/
if (isServerSide || apiService) {
// Use a timeout to prevent immediate fetching on mount
const timeoutId = setTimeout(() => {
fetchStats().catch((error) => {
console.warn('Failed to auto-fetch stats:', error);
// Don't throw error on auto-fetch to prevent crashes
});
}, 100);
return () => clearTimeout(timeoutId);
}
}
return undefined;
}, [autoFetch, connection, apiService, state.stats, isStale, isServerSide]);
// Set up refresh interval if provided
useEffect(() => {
if (!refreshInterval || !connection) {
return undefined;
}
const interval = setInterval(() => {
if (isStale) {
refreshStats();
}
}, refreshInterval);
return () => clearInterval(interval);
}, [refreshInterval, connection, isStale]);
const loadCachedStats = useCallback(() => {
try {
const cached = localStorage.getItem(STATS_CACHE_KEY);
if (cached) {
const { stats, timestamp, userLogin } = JSON.parse(cached);
// Only use cached data if it's for the current user
if (userLogin === connection?.user?.login) {
setState((prev) => ({
...prev,
stats,
lastUpdated: new Date(timestamp),
}));
}
}
} catch (error) {
console.error('Error loading cached stats:', error);
// Clear corrupted cache
localStorage.removeItem(STATS_CACHE_KEY);
}
}, [connection?.user?.login]);
const saveCachedStats = useCallback((stats: GitHubStats, userLogin: string) => {
try {
const cacheData = {
stats,
timestamp: Date.now(),
userLogin,
};
localStorage.setItem(STATS_CACHE_KEY, JSON.stringify(cacheData));
} catch (error) {
console.error('Error saving stats to cache:', error);
}
}, []);
const fetchStats = useCallback(async () => {
if (!connection?.user) {
setState((prev) => ({
...prev,
error: 'GitHub connection not available',
isLoading: false,
isRefreshing: false,
}));
return;
}
setState((prev) => ({
...prev,
isLoading: !prev.stats, // Show loading only if no stats yet
isRefreshing: !!prev.stats, // Show refreshing if stats exist
error: null,
}));
try {
let stats: GitHubStats;
if (isServerSide || !connection.token) {
// Use server-side API for stats
const response = await fetch('/api/github-stats');
if (!response.ok) {
if (response.status === 401) {
throw new Error('GitHub authentication required');
}
const errorData: any = await response.json();
throw new Error(errorData.error || 'Failed to fetch stats from server');
}
stats = await response.json();
} else {
// Use client-side API service for stats
if (!apiService) {
throw new Error('GitHub API service not available');
}
stats = await apiService.generateComprehensiveStats(connection.user);
}
const now = new Date();
setState((prev) => ({
...prev,
stats,
isLoading: false,
isRefreshing: false,
lastUpdated: now,
error: null,
}));
// Cache the stats
saveCachedStats(stats, connection.user.login);
// Update the connection object with stats if needed
if (connection.stats?.lastUpdated !== stats.lastUpdated) {
const updatedConnection = {
...connection,
stats,
};
localStorage.setItem('github_connection', JSON.stringify(updatedConnection));
}
// Only show success toast for manual refreshes, not auto-fetches
if (state.isRefreshing) {
toast.success('GitHub stats updated successfully');
}
} catch (error) {
console.error('Error fetching GitHub stats:', error);
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch GitHub stats';
setState((prev) => ({
...prev,
isLoading: false,
isRefreshing: false,
error: errorMessage,
}));
// Only show error toast for manual actions, not auto-fetches
if (state.isRefreshing) {
toast.error(`Failed to update GitHub stats: ${errorMessage}`);
}
throw error;
}
}, [apiService, connection, saveCachedStats, isServerSide]);
const refreshStats = useCallback(async () => {
if (state.isRefreshing || state.isLoading) {
return; // Prevent multiple simultaneous requests
}
await fetchStats();
}, [fetchStats, state.isRefreshing, state.isLoading]);
const clearStats = useCallback(() => {
setState({
stats: null,
isLoading: false,
isRefreshing: false,
error: null,
lastUpdated: null,
});
// Clear cache
localStorage.removeItem(STATS_CACHE_KEY);
}, []);
return {
...state,
fetchStats,
refreshStats,
clearStats,
isStale,
};
}
// Helper hook for lightweight stats fetching (just repositories)
export function useGitHubRepositories(connection: GitHubConnection | null) {
const [repositories, setRepositories] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const apiService = useMemo(() => {
if (!connection?.token) {
return null;
}
// Configure the singleton instance with the current connection
gitHubApiService.configure({
token: connection.token,
tokenType: connection.tokenType,
});
return gitHubApiService;
}, [connection?.token, connection?.tokenType]);
const fetchRepositories = useCallback(async () => {
if (!apiService) {
setError('GitHub connection not available');
return;
}
setIsLoading(true);
setError(null);
try {
const repos = await apiService.getAllUserRepositories();
setRepositories(repos);
} catch (error) {
console.error('Error fetching repositories:', error);
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch repositories';
setError(errorMessage);
throw error;
} finally {
setIsLoading(false);
}
}, [apiService]);
return {
repositories,
isLoading,
error,
fetchRepositories,
};
}

View File

@@ -0,0 +1,7 @@
// Basic GitLab API hook placeholder
export const useGitLabAPI = (config?: { token: string; baseUrl: string }) => {
return {
// Placeholder implementation - will be expanded as needed
config,
};
};

View File

@@ -0,0 +1,256 @@
import { useState, useEffect, useCallback } from 'react';
import { useStore } from '@nanostores/react';
import { toast } from 'react-toastify';
import Cookies from 'js-cookie';
import type { GitLabConnection } from '~/types/GitLab';
import { useGitLabAPI } from './useGitLabAPI';
import { gitlabConnectionStore, gitlabConnection, isGitLabConnected } from '~/lib/stores/gitlabConnection';
export interface ConnectionState {
isConnected: boolean;
isLoading: boolean;
isConnecting: boolean;
connection: GitLabConnection | null;
error: string | null;
}
export interface UseGitLabConnectionReturn extends ConnectionState {
connect: (token: string, gitlabUrl?: string) => Promise<void>;
disconnect: () => void;
refreshConnection: () => Promise<void>;
testConnection: () => Promise<boolean>;
refreshStats: () => Promise<void>;
}
const STORAGE_KEY = 'gitlab_connection';
export function useGitLabConnection(): UseGitLabConnectionReturn {
const connection = useStore(gitlabConnection);
const isConnected = useStore(isGitLabConnected);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isConnecting, setIsConnecting] = useState(false);
// Create API instance - will update when connection changes
useGitLabAPI(
connection?.token
? { token: connection.token, baseUrl: connection.gitlabUrl || 'https://gitlab.com' }
: { token: '', baseUrl: 'https://gitlab.com' },
);
// Load saved connection on mount
useEffect(() => {
loadSavedConnection();
}, []);
const loadSavedConnection = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
// Check if connection already exists in store (likely from initialization)
if (connection?.user) {
setIsLoading(false);
return;
}
// Load saved connection from localStorage
const savedConnection = localStorage.getItem(STORAGE_KEY);
if (savedConnection) {
const parsed = JSON.parse(savedConnection);
if (parsed.user && parsed.token) {
// Update the store with saved connection
gitlabConnectionStore.setGitLabUrl(parsed.gitlabUrl || 'https://gitlab.com');
gitlabConnectionStore.setToken(parsed.token);
// Test the connection to make sure it's still valid
await refreshConnectionData(parsed);
}
}
setIsLoading(false);
} catch (error) {
console.error('Error loading saved connection:', error);
setError('Failed to load saved connection');
setIsLoading(false);
// Clean up corrupted data
localStorage.removeItem(STORAGE_KEY);
}
}, [connection]);
const refreshConnectionData = useCallback(async (connection: GitLabConnection) => {
if (!connection.token) {
return;
}
try {
// Make direct API call instead of using hook
const baseUrl = connection.gitlabUrl || 'https://gitlab.com';
const response = await fetch(`${baseUrl}/api/v4/user`, {
headers: {
'Content-Type': 'application/json',
'PRIVATE-TOKEN': connection.token,
},
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
// const userData = (await response.json()) as GitLabUserResponse;
await response.json(); // Parse response but don't store - data handled by store
/*
* Update connection with user data - unused variable removed
* const updatedConnection: GitLabConnection = {
* ...connection,
* user: userData,
* };
*/
gitlabConnectionStore.setGitLabUrl(baseUrl);
gitlabConnectionStore.setToken(connection.token);
} catch (error) {
console.error('Error refreshing connection data:', error);
}
}, []);
const connect = useCallback(async (token: string, gitlabUrl = 'https://gitlab.com') => {
if (!token.trim()) {
setError('Token is required');
return;
}
setIsConnecting(true);
setError(null);
try {
console.log('Calling GitLab store connect method...');
// Use the store's connect method which handles everything properly
const result = await gitlabConnectionStore.connect(token, gitlabUrl);
if (!result.success) {
throw new Error(result.error || 'Connection failed');
}
console.log('GitLab connection successful, now fetching stats...');
// Fetch stats after successful connection
try {
const statsResult = await gitlabConnectionStore.fetchStats(true);
if (statsResult.success) {
console.log('GitLab stats fetched successfully:', statsResult.stats);
} else {
console.error('Failed to fetch GitLab stats:', statsResult.error);
}
} catch (statsError) {
console.error('Failed to fetch GitLab stats:', statsError);
// Don't fail the connection if stats fail
}
toast.success('Connected to GitLab successfully!');
} catch (error) {
console.error('Failed to connect to GitLab:', error);
const errorMessage = error instanceof Error ? error.message : 'Failed to connect to GitLab';
setError(errorMessage);
toast.error(`Failed to connect: ${errorMessage}`);
throw error;
} finally {
setIsConnecting(false);
}
}, []);
const disconnect = useCallback(() => {
// Clear localStorage
localStorage.removeItem(STORAGE_KEY);
// Clear all GitLab-related cookies
Cookies.remove('gitlabToken');
Cookies.remove('gitlabUsername');
Cookies.remove('gitlabUrl');
// Reset store
gitlabConnectionStore.disconnect();
setError(null);
toast.success('Disconnected from GitLab');
}, []);
const refreshConnection = useCallback(async () => {
if (!connection?.token) {
throw new Error('No connection to refresh');
}
setIsLoading(true);
setError(null);
try {
await refreshConnectionData(connection);
} catch (error) {
console.error('Error refreshing connection:', error);
setError('Failed to refresh connection');
throw error;
} finally {
setIsLoading(false);
}
}, [connection, refreshConnectionData]);
const testConnection = useCallback(async (): Promise<boolean> => {
if (!connection?.token) {
return false;
}
try {
const baseUrl = connection.gitlabUrl || 'https://gitlab.com';
const response = await fetch(`${baseUrl}/api/v4/user`, {
headers: {
'Content-Type': 'application/json',
'PRIVATE-TOKEN': connection.token,
},
});
return response.ok;
} catch (error) {
console.error('Connection test failed:', error);
return false;
}
}, [connection]);
const refreshStats = useCallback(async () => {
if (!connection?.token) {
throw new Error('No connection to refresh stats');
}
try {
const statsResult = await gitlabConnectionStore.fetchStats(true);
if (!statsResult.success) {
throw new Error(statsResult.error || 'Failed to refresh stats');
}
} catch (error) {
console.error('Error refreshing GitLab stats:', error);
throw error;
}
}, [connection]);
return {
isConnected,
isLoading,
isConnecting,
connection,
error,
connect,
disconnect,
refreshConnection,
testConnection,
refreshStats,
};
}

View File

@@ -9,6 +9,7 @@ import {
isFetchingApiKeys,
updateSupabaseConnection,
fetchProjectApiKeys,
initializeSupabaseConnection,
} from '~/lib/stores/supabase';
export function useSupabaseConnection() {
@@ -20,22 +21,44 @@ export function useSupabaseConnection() {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
useEffect(() => {
const savedConnection = localStorage.getItem('supabase_connection');
const savedCredentials = localStorage.getItem('supabaseCredentials');
const initConnection = async () => {
console.log('useSupabaseConnection: Initializing connection...');
if (savedConnection) {
const parsed = JSON.parse(savedConnection);
if (savedCredentials && !parsed.credentials) {
parsed.credentials = JSON.parse(savedCredentials);
// First, try to initialize from server-side token
try {
await initializeSupabaseConnection();
console.log('useSupabaseConnection: Server-side initialization completed');
} catch {
console.log('useSupabaseConnection: Server-side initialization failed, trying localStorage');
}
updateSupabaseConnection(parsed);
// Then check localStorage for additional data
const savedConnection = localStorage.getItem('supabase_connection');
const savedCredentials = localStorage.getItem('supabaseCredentials');
if (parsed.token && parsed.selectedProjectId && !parsed.credentials) {
fetchProjectApiKeys(parsed.selectedProjectId, parsed.token).catch(console.error);
if (savedConnection) {
console.log('useSupabaseConnection: Loading from localStorage');
const parsed = JSON.parse(savedConnection);
if (savedCredentials && !parsed.credentials) {
parsed.credentials = JSON.parse(savedCredentials);
}
// Only update if we don't already have a connection from server-side
const currentState = supabaseConnection.get();
if (!currentState.user) {
updateSupabaseConnection(parsed);
}
if (parsed.token && parsed.selectedProjectId && !parsed.credentials) {
fetchProjectApiKeys(parsed.selectedProjectId, parsed.token).catch(console.error);
}
}
}
};
initConnection();
}, []);
const handleConnect = async () => {