import * as Dialog from '@radix-ui/react-dialog'; import { useState, useEffect } from 'react'; import { toast } from 'react-toastify'; import { motion } from 'framer-motion'; import { classNames } from '~/utils/classNames'; import { getLocalStorage } from '~/lib/persistence/localStorage'; import type { GitLabUserResponse, GitLabProjectInfo } from '~/types/GitLab'; import { logStore } from '~/lib/stores/logs'; import { chatId } from '~/lib/persistence/useChatHistory'; import { useStore } from '@nanostores/react'; import { GitLabApiService } from '~/lib/services/gitlabApiService'; import { SearchInput, EmptyState, StatusIndicator, Badge } from '~/components/ui'; import { formatSize } from '~/utils/formatSize'; import { GitLabAuthDialog } from '~/components/@settings/tabs/gitlab/components/GitLabAuthDialog'; interface GitLabDeploymentDialogProps { isOpen: boolean; onClose: () => void; projectName: string; files: Record; } export function GitLabDeploymentDialog({ isOpen, onClose, projectName, files }: GitLabDeploymentDialogProps) { const [repoName, setRepoName] = useState(''); const [isPrivate, setIsPrivate] = useState(false); const [isLoading, setIsLoading] = useState(false); const [user, setUser] = useState(null); const [recentRepos, setRecentRepos] = useState([]); const [filteredRepos, setFilteredRepos] = useState([]); const [repoSearchQuery, setRepoSearchQuery] = useState(''); const [isFetchingRepos, setIsFetchingRepos] = useState(false); const [showSuccessDialog, setShowSuccessDialog] = useState(false); const [createdRepoUrl, setCreatedRepoUrl] = useState(''); const [pushedFiles, setPushedFiles] = useState<{ path: string; size: number }[]>([]); const [showAuthDialog, setShowAuthDialog] = useState(false); const currentChatId = useStore(chatId); // Load GitLab connection on mount useEffect(() => { if (isOpen) { const connection = getLocalStorage('gitlab_connection'); // Set a default repository name based on the project name setRepoName(projectName.replace(/\s+/g, '-').toLowerCase()); if (connection?.user && connection?.token) { setUser(connection.user); // Only fetch if we have both user and token if (connection.token.trim()) { fetchRecentRepos(connection.token, connection.gitlabUrl || 'https://gitlab.com'); } } } }, [isOpen, projectName]); // Filter repositories based on search query useEffect(() => { if (recentRepos.length === 0) { setFilteredRepos([]); return; } if (!repoSearchQuery.trim()) { setFilteredRepos(recentRepos); return; } const query = repoSearchQuery.toLowerCase().trim(); const filtered = recentRepos.filter( (repo) => repo.name.toLowerCase().includes(query) || (repo.description && repo.description.toLowerCase().includes(query)), ); setFilteredRepos(filtered); }, [recentRepos, repoSearchQuery]); const fetchRecentRepos = async (token: string, gitlabUrl = 'https://gitlab.com') => { if (!token) { logStore.logError('No GitLab token available'); toast.error('GitLab authentication required'); return; } try { setIsFetchingRepos(true); const apiService = new GitLabApiService(token, gitlabUrl); const repos = await apiService.getProjects(); setRecentRepos(repos); } catch (error) { console.error('Failed to fetch GitLab repositories:', error); logStore.logError('Failed to fetch GitLab repositories', { error }); toast.error('Failed to fetch recent repositories'); } finally { setIsFetchingRepos(false); } }; // Function to create a new repository or push to an existing one const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const connection = getLocalStorage('gitlab_connection'); if (!connection?.token || !connection?.user) { toast.error('Please connect your GitLab account in Settings > Connections first'); return; } if (!repoName.trim()) { toast.error('Repository name is required'); return; } setIsLoading(true); // Sanitize repository name to match what the API will create const sanitizedRepoName = repoName .replace(/[^a-zA-Z0-9-_.]/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, '') .toLowerCase(); try { const gitlabUrl = connection.gitlabUrl || 'https://gitlab.com'; const apiService = new GitLabApiService(connection.token, gitlabUrl); // Warn user if repository name was changed if (sanitizedRepoName !== repoName && sanitizedRepoName !== repoName.toLowerCase()) { toast.info(`Repository name sanitized to "${sanitizedRepoName}" to meet GitLab requirements`); } // Check if project exists using the sanitized name const projectPath = `${connection.user.username}/${sanitizedRepoName}`; const existingProject = await apiService.getProjectByPath(projectPath); const projectExists = existingProject !== null; if (projectExists && existingProject) { // Confirm overwrite const visibilityChange = existingProject.visibility !== (isPrivate ? 'private' : 'public') ? `\n\nThis will also change the repository from ${existingProject.visibility} to ${isPrivate ? 'private' : 'public'}.` : ''; const confirmOverwrite = window.confirm( `Repository "${sanitizedRepoName}" already exists. Do you want to update it? This will add or modify files in the repository.${visibilityChange}`, ); if (!confirmOverwrite) { setIsLoading(false); return; } // Update visibility if needed if (existingProject.visibility !== (isPrivate ? 'private' : 'public')) { toast.info('Updating repository visibility...'); await apiService.updateProjectVisibility(existingProject.id, isPrivate ? 'private' : 'public'); } // Update project with files toast.info('Uploading files to existing repository...'); await apiService.updateProjectWithFiles(existingProject.id, files); setCreatedRepoUrl(existingProject.http_url_to_repo); toast.success('Repository updated successfully!'); } else { // Create new project with files toast.info('Creating new repository...'); const newProject = await apiService.createProjectWithFiles(sanitizedRepoName, isPrivate, files); setCreatedRepoUrl(newProject.http_url_to_repo); toast.success('Repository created successfully!'); } // Set pushed files for display const fileList = Object.entries(files).map(([filePath, content]) => ({ path: filePath, size: new TextEncoder().encode(content).length, })); setPushedFiles(fileList); setShowSuccessDialog(true); // Save repository info localStorage.setItem( `gitlab-repo-${currentChatId}`, JSON.stringify({ owner: connection.user.username, name: sanitizedRepoName, url: createdRepoUrl, }), ); logStore.logInfo('GitLab deployment completed successfully', { type: 'system', message: `Successfully deployed ${fileList.length} files to ${projectExists ? 'existing' : 'new'} GitLab repository: ${projectPath}`, repoName: sanitizedRepoName, projectPath, filesCount: fileList.length, isNewProject: !projectExists, }); } catch (error) { console.error('Error pushing to GitLab:', error); logStore.logError('GitLab deployment failed', { error, repoName: sanitizedRepoName, projectPath: `${connection.user.username}/${sanitizedRepoName}`, }); // Provide specific error messages based on error type let errorMessage = 'Failed to push to GitLab'; if (error instanceof Error) { const errorMsg = error.message.toLowerCase(); if (errorMsg.includes('404') || errorMsg.includes('not found')) { errorMessage = 'Repository or GitLab instance not found. Please check your GitLab URL and repository permissions.'; } else if (errorMsg.includes('401') || errorMsg.includes('unauthorized')) { errorMessage = 'GitLab authentication failed. Please check your access token and permissions.'; } else if (errorMsg.includes('403') || errorMsg.includes('forbidden')) { errorMessage = 'Access denied. Your GitLab token may not have sufficient permissions to create/modify repositories.'; } else if (errorMsg.includes('network') || errorMsg.includes('fetch')) { errorMessage = 'Network error. Please check your internet connection and try again.'; } else if (errorMsg.includes('timeout')) { errorMessage = 'Request timed out. Please try again or check your connection.'; } else if (errorMsg.includes('rate limit')) { errorMessage = 'GitLab API rate limit exceeded. Please wait a moment and try again.'; } else { errorMessage = `GitLab error: ${error.message}`; } } toast.error(errorMessage); } finally { setIsLoading(false); } }; const handleClose = () => { setRepoName(''); setIsPrivate(false); setShowSuccessDialog(false); setCreatedRepoUrl(''); onClose(); }; const handleAuthDialogClose = () => { setShowAuthDialog(false); // Refresh user data after auth const connection = getLocalStorage('gitlab_connection'); if (connection?.user && connection?.token) { setUser(connection.user); fetchRecentRepos(connection.token, connection.gitlabUrl || 'https://gitlab.com'); } }; // Success Dialog if (showSuccessDialog) { return ( !open && handleClose()}>
Successfully pushed to GitLab

Successfully pushed to GitLab

Your code is now available on GitLab

Repository URL

{createdRepoUrl} { navigator.clipboard.writeText(createdRepoUrl); toast.success('URL copied to clipboard'); }} className="p-2 text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary dark:text-bolt-elements-textSecondary-dark dark:hover:text-bolt-elements-textPrimary-dark bg-bolt-elements-background-depth-1 dark:bg-bolt-elements-background-depth-4 rounded-lg border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark" whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} >

Pushed Files ({pushedFiles.length})

{pushedFiles.slice(0, 100).map((file) => (
{file.path} {formatSize(file.size)}
))} {pushedFiles.length > 100 && (
+{pushedFiles.length - 100} more files
)}
View Repository { navigator.clipboard.writeText(createdRepoUrl); toast.success('URL copied to clipboard'); }} className="px-4 py-2 rounded-lg bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3 text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary-dark hover:bg-bolt-elements-background-depth-3 dark:hover:bg-bolt-elements-background-depth-4 text-sm inline-flex items-center gap-2 border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark" whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} >
Copy URL Close
); } if (!user) { return ( !open && handleClose()}>
GitLab Connection Required

GitLab Connection Required

To deploy your code to GitLab, you need to connect your GitLab account first.

Close setShowAuthDialog(true)} className="px-4 py-2 rounded-lg bg-orange-500 text-white text-sm hover:bg-orange-600 inline-flex items-center gap-2" whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} >
Connect GitLab Account
{/* GitLab Auth Dialog */} ); } return ( !open && handleClose()}>
Deploy to GitLab

Deploy your code to a new or existing GitLab repository

{user.avatar_url && user.avatar_url !== 'null' && user.avatar_url !== '' ? ( {user.username} { // Handle CORS/COEP errors by hiding the image and showing fallback const target = e.target as HTMLImageElement; target.style.display = 'none'; const fallback = target.parentElement?.querySelector('.avatar-fallback') as HTMLElement; if (fallback) { fallback.style.display = 'flex'; } }} onLoad={(e) => { // Ensure fallback is hidden when image loads successfully const target = e.target as HTMLImageElement; const fallback = target.parentElement?.querySelector('.avatar-fallback') as HTMLElement; if (fallback) { fallback.style.display = 'none'; } }} /> ) : null}
{user.name ? ( user.name.charAt(0).toUpperCase() ) : user.username ? ( user.username.charAt(0).toUpperCase() ) : (
)}

{user.name || user.username}

@{user.username}

setRepoName(e.target.value)} placeholder="my-awesome-project" className="w-full pl-10 px-4 py-2 rounded-lg bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary-dark placeholder-bolt-elements-textTertiary dark:placeholder-bolt-elements-textTertiary-dark focus:outline-none focus:ring-2 focus:ring-orange-500" required />
{filteredRepos.length} of {recentRepos.length}
setRepoSearchQuery(e.target.value)} onClear={() => setRepoSearchQuery('')} className="bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark text-sm" />
{recentRepos.length === 0 && !isFetchingRepos ? ( ) : (
{filteredRepos.length === 0 && repoSearchQuery.trim() !== '' ? ( ) : ( filteredRepos.map((repo) => ( setRepoName(repo.name)} className="w-full p-3 text-left rounded-lg bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3 hover:bg-bolt-elements-background-depth-3 dark:hover:bg-bolt-elements-background-depth-4 transition-colors group border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark hover:border-orange-500/30" whileHover={{ scale: 1.01 }} whileTap={{ scale: 0.99 }} >
{repo.name}
{repo.visibility === 'private' && ( Private )}
{repo.description && (

{repo.description}

)}
{repo.star_count.toLocaleString()} {repo.forks_count.toLocaleString()} {new Date(repo.updated_at).toLocaleDateString()}
)) )}
)}
{isFetchingRepos && (
)}
setIsPrivate(e.target.checked)} className="rounded border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark text-orange-500 focus:ring-orange-500 dark:bg-bolt-elements-background-depth-3" />

Private repositories are only visible to you and people you share them with

Cancel {isLoading ? ( <>
Deploying... ) : ( <>
Deploy to GitLab )}
{/* GitLab Auth Dialog */} ); }