Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gitlab is also nice #1

Merged
merged 15 commits into from
Dec 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ README.md

# Ignore environment examples and sensitive info
.env
*.local
*.example

# Ignore node modules, logs and cache files
Expand Down
2 changes: 1 addition & 1 deletion app/commit.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{ "commit": "c7cdbdfc3dc2ddc79589776c7e53d9857421d09e" }
{ "commit": "511bce10df5c8ac84bac6c295024296993723a24" }
204 changes: 161 additions & 43 deletions app/components/settings/connections/ConnectionsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,172 @@
import React, { useState } from 'react';
import { toast } from 'react-toastify';
import Cookies from 'js-cookie';
import React, { useState, useEffect } from 'react';
import { logStore } from '~/lib/stores/logs';
import { lookupSavedPassword, saveGitAuth, ensureEncryption } from '~/lib/hooks/useCredentials';

export default function ConnectionsTab() {
const [githubUsername, setGithubUsername] = useState(Cookies.get('githubUsername') || '');
const [githubToken, setGithubToken] = useState(Cookies.get('githubToken') || '');

const handleSaveConnection = () => {
Cookies.set('githubUsername', githubUsername);
Cookies.set('githubToken', githubToken);
logStore.logSystem('GitHub connection settings updated', {
username: githubUsername,
hasToken: !!githubToken,
const [credentials, setCredentials] = useState({
github: { username: '', token: '' },
gitlab: { username: '', token: '' },
});
const [expandedProviders, setExpandedProviders] = useState<Record<string, boolean>>({});

useEffect(() => {
initializeEncryption();
}, []);

const initializeEncryption = async () => {
const success = await ensureEncryption();

if (success) {
loadSavedCredentials();
}
};

const loadSavedCredentials = async () => {
for (const [, config] of Object.entries(providers)) {
const auth = await lookupSavedPassword(config.url);

if (auth?.username && auth?.password) {
config.setCredentials(auth.username, auth.password);
}
}
};

const toggleProvider = (provider: string) => {
setExpandedProviders((prev) => ({
...prev,
[provider]: !prev[provider],
}));
};

const providers = {
github: {
url: 'github.com',
username: credentials.github.username,
token: credentials.github.token,
title: 'GitHub',
instructions: 'Enter your GitHub username and personal access token.',
tokenSetupSteps: [
'1. Go to GitHub.com → Settings → Developer settings → Personal access tokens → Tokens (classic)',
'2. Generate new token (classic) with these scopes:',
' • repo (Full control of private repositories)',
' • workflow (Optional: Update GitHub Action workflows)',
'3. Copy the generated token and paste it here',
],
setCredentials: (username: string, token: string) =>
setCredentials((prev) => ({
...prev,
github: { username, token },
})),
},
gitlab: {
url: 'gitlab.com',
username: credentials.gitlab.username,
token: credentials.gitlab.token,
title: 'GitLab',
instructions: 'To set up GitLab access:',
tokenSetupSteps: [
'1. Go to GitLab.com → Profile Settings → Access Tokens',
'2. Create a new token with these scopes:',
' • api (Full API access)',
' • write_repository (Read/write access)',
'3. Copy the generated token and paste it here',
],
setCredentials: (username: string, token: string) =>
setCredentials((prev) => ({
...prev,
gitlab: { username, token },
})),
},
};

const handleSaveConnection = async (provider: keyof typeof providers) => {
const { url, username, token, title } = providers[provider];

await saveGitAuth(url, {
username,
password: token,
});

logStore.logSystem(`${title} connection settings updated`, {
username,
hasToken: !!token,
});
toast.success('GitHub credentials saved successfully!');
Cookies.set('git:github.com', JSON.stringify({ username: githubToken, password: 'x-oauth-basic' }));
};

return (
<div className="p-4 mb-4 border border-bolt-elements-borderColor rounded-lg bg-bolt-elements-background-depth-3">
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">GitHub Connection</h3>
<div className="flex mb-4">
<div className="flex-1 mr-2">
<label className="block text-sm text-bolt-elements-textSecondary mb-1">GitHub Username:</label>
<input
type="text"
value={githubUsername}
onChange={(e) => setGithubUsername(e.target.value)}
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
/>
</div>
<div className="flex-1">
<label className="block text-sm text-bolt-elements-textSecondary mb-1">Personal Access Token:</label>
<input
type="password"
value={githubToken}
onChange={(e) => setGithubToken(e.target.value)}
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
/>
</div>
</div>
<div className="flex mb-4">
<button
onClick={handleSaveConnection}
className="bg-bolt-elements-button-primary-background rounded-lg px-4 py-2 mr-2 transition-colors duration-200 hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-button-primary-text"
<div className="space-y-4">
{/* Encryption status section remains the same */}

{Object.entries(providers).map(([key, provider]) => (
<div
key={key}
className="p-4 border border-bolt-elements-borderColor rounded-lg bg-bolt-elements-background-depth-3"
>
Save Connection
</button>
</div>
<div className="flex items-center justify-between cursor-pointer" onClick={() => toggleProvider(key)}>
<div className="flex items-center">
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">{provider.title} Connection</h3>
{provider.username && (
<span className="ml-2 text-sm text-bolt-elements-textSecondary">({provider.username})</span>
)}
</div>
<div className="flex items-center">
{provider.username && provider.token && (
<div className="flex items-center mr-3">
<div className="w-2 h-2 rounded-full bg-green-500 mr-2" />
<span className="text-sm text-bolt-elements-textSecondary">Connected</span>
</div>
)}
<div className={`transform transition-transform ${expandedProviders[key] ? 'rotate-180' : ''}`}>
<div className="i-ph:caret-down text-bolt-elements-textSecondary" />
</div>
</div>
</div>

{expandedProviders[key] && (
<div className="mt-4">
<div className="mb-4 p-3 bg-bolt-elements-background-depth-4 rounded border border-bolt-elements-borderColor">
<p className="text-sm text-bolt-elements-textSecondary mb-2">{provider.instructions}</p>
<ul className="text-sm text-bolt-elements-textSecondary space-y-1">
{provider.tokenSetupSteps.map((step, index) => (
<li key={index}>{step}</li>
))}
</ul>
</div>

<div className="flex mb-4">
<div className="flex-1 mr-2">
<label className="block text-sm text-bolt-elements-textSecondary mb-1">
{provider.title} Username:
</label>
<input
type="text"
value={provider.username}
onChange={(e) => provider.setCredentials(e.target.value, provider.token)}
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
/>
</div>
<div className="flex-1">
<label className="block text-sm text-bolt-elements-textSecondary mb-1">Personal Access Token:</label>
<input
type="password"
value={provider.token}
onChange={(e) => provider.setCredentials(provider.username, e.target.value)}
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
/>
</div>
</div>
<div className="flex">
<button
onClick={() => handleSaveConnection(key as keyof typeof providers)}
className="bg-bolt-elements-button-primary-background rounded-lg px-4 py-2 mr-2 transition-colors duration-200 hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-button-primary-text"
>
Save {provider.title} Connection
</button>
</div>
</div>
)}
</div>
))}
</div>
);
}
103 changes: 68 additions & 35 deletions app/components/workbench/Workbench.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@ import { renderLogger } from '~/utils/logger';
import { EditorPanel } from './EditorPanel';
import { Preview } from './Preview';
import useViewport from '~/lib/hooks';
import Cookies from 'js-cookie';
import { ensureEncryption, lookupSavedPassword } from '~/lib/hooks/useCredentials';

interface WorkspaceProps {
chatStarted?: boolean;
isStreaming?: boolean;
}

interface GitCredentials {
github: boolean;
gitlab: boolean;
}

const viewTransition = { ease: cubicEasingFn };

const sliderOptions: SliderOptions<WorkbenchViewType> = {
Expand Down Expand Up @@ -58,6 +63,10 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
renderLogger.trace('Workbench');

const [isSyncing, setIsSyncing] = useState(false);
const [hasCredentials, setHasCredentials] = useState<GitCredentials>({
github: false,
gitlab: false,
});

const hasPreview = useStore(computed(workbenchStore.previews, (previews) => previews.length > 0));
const showWorkbench = useStore(workbenchStore.showWorkbench);
Expand All @@ -83,6 +92,20 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
workbenchStore.setDocuments(files);
}, [files]);

useEffect(() => {
checkGitCredentials();
}, []);

const checkGitCredentials = async () => {
const githubAuth = await lookupSavedPassword('github.com');
const gitlabAuth = await lookupSavedPassword('gitlab.com');

setHasCredentials({
github: !!(githubAuth?.username && githubAuth?.password),
gitlab: !!(gitlabAuth?.username && gitlabAuth?.password),
});
};

const onEditorChange = useCallback<OnEditorChange>((update) => {
workbenchStore.setCurrentDocumentContent(update.content);
}, []);
Expand Down Expand Up @@ -120,6 +143,38 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
}
}, []);

const handleGitPush = async (provider: 'github' | 'gitlab') => {
const repoName = prompt(
`Please enter a name for your new ${provider === 'github' ? 'GitHub' : 'GitLab'} repository:`,
'bolt-generated-project',
);

// TODO store and load repoName from downloaded project
if (!repoName) {
toast.error('Repository name is required');
return;
}

if (!(await ensureEncryption())) {
toast.error('Failed to initialize secure storage');
return;
}

const auth = await lookupSavedPassword(`${provider}.com`);

if (auth?.username && auth?.password) {
if (provider === 'github') {
workbenchStore.pushToGitHub(repoName, auth.username, auth.password);
} else {
workbenchStore.pushToGitLab(repoName, auth.username, auth.password);
}
} else {
toast.info(
`Please set up your ${provider === 'github' ? 'GitHub' : 'GitLab'} credentials in the Connections tab`,
);
}
};

return (
chatStarted && (
<motion.div
Expand Down Expand Up @@ -168,40 +223,18 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
<div className="i-ph:terminal" />
Toggle Terminal
</PanelHeaderButton>
<PanelHeaderButton
className="mr-1 text-sm"
onClick={() => {
const repoName = prompt(
'Please enter a name for your new GitHub repository:',
'bolt-generated-project',
);

if (!repoName) {
alert('Repository name is required. Push to GitHub cancelled.');
return;
}

const githubUsername = Cookies.get('githubUsername');
const githubToken = Cookies.get('githubToken');

if (!githubUsername || !githubToken) {
const usernameInput = prompt('Please enter your GitHub username:');
const tokenInput = prompt('Please enter your GitHub personal access token:');

if (!usernameInput || !tokenInput) {
alert('GitHub username and token are required. Push to GitHub cancelled.');
return;
}

workbenchStore.pushToGitHub(repoName, usernameInput, tokenInput);
} else {
workbenchStore.pushToGitHub(repoName, githubUsername, githubToken);
}
}}
>
<div className="i-ph:github-logo" />
Push to GitHub
</PanelHeaderButton>
{hasCredentials.github && (
<PanelHeaderButton className="mr-1 text-sm" onClick={() => handleGitPush('github')}>
<div className="i-ph:github-logo" />
Push to GitHub
</PanelHeaderButton>
)}
{hasCredentials.gitlab && (
<PanelHeaderButton className="mr-1 text-sm" onClick={() => handleGitPush('gitlab')}>
<div className="i-ph:gitlab-logo" />
Push to GitLab
</PanelHeaderButton>
)}
</div>
)}
<IconButton
Expand Down
Loading
Loading