Skip to content

Commit 1380871

Browse files
authored
Merge pull request #1 from D-Byte/gitlabIsAlsoNice
Gitlab is also nice
2 parents be1d835 + 12a351f commit 1380871

11 files changed

+774
-176
lines changed

.dockerignore

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ README.md
1212

1313
# Ignore environment examples and sensitive info
1414
.env
15-
*.local
1615
*.example
1716

1817
# Ignore node modules, logs and cache files

app/commit.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{ "commit": "c7cdbdfc3dc2ddc79589776c7e53d9857421d09e" }
1+
{ "commit": "511bce10df5c8ac84bac6c295024296993723a24" }
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,172 @@
1-
import React, { useState } from 'react';
2-
import { toast } from 'react-toastify';
3-
import Cookies from 'js-cookie';
1+
import React, { useState, useEffect } from 'react';
42
import { logStore } from '~/lib/stores/logs';
3+
import { lookupSavedPassword, saveGitAuth, ensureEncryption } from '~/lib/hooks/useCredentials';
54

65
export default function ConnectionsTab() {
7-
const [githubUsername, setGithubUsername] = useState(Cookies.get('githubUsername') || '');
8-
const [githubToken, setGithubToken] = useState(Cookies.get('githubToken') || '');
9-
10-
const handleSaveConnection = () => {
11-
Cookies.set('githubUsername', githubUsername);
12-
Cookies.set('githubToken', githubToken);
13-
logStore.logSystem('GitHub connection settings updated', {
14-
username: githubUsername,
15-
hasToken: !!githubToken,
6+
const [credentials, setCredentials] = useState({
7+
github: { username: '', token: '' },
8+
gitlab: { username: '', token: '' },
9+
});
10+
const [expandedProviders, setExpandedProviders] = useState<Record<string, boolean>>({});
11+
12+
useEffect(() => {
13+
initializeEncryption();
14+
}, []);
15+
16+
const initializeEncryption = async () => {
17+
const success = await ensureEncryption();
18+
19+
if (success) {
20+
loadSavedCredentials();
21+
}
22+
};
23+
24+
const loadSavedCredentials = async () => {
25+
for (const [, config] of Object.entries(providers)) {
26+
const auth = await lookupSavedPassword(config.url);
27+
28+
if (auth?.username && auth?.password) {
29+
config.setCredentials(auth.username, auth.password);
30+
}
31+
}
32+
};
33+
34+
const toggleProvider = (provider: string) => {
35+
setExpandedProviders((prev) => ({
36+
...prev,
37+
[provider]: !prev[provider],
38+
}));
39+
};
40+
41+
const providers = {
42+
github: {
43+
url: 'github.com',
44+
username: credentials.github.username,
45+
token: credentials.github.token,
46+
title: 'GitHub',
47+
instructions: 'Enter your GitHub username and personal access token.',
48+
tokenSetupSteps: [
49+
'1. Go to GitHub.com → Settings → Developer settings → Personal access tokens → Tokens (classic)',
50+
'2. Generate new token (classic) with these scopes:',
51+
' • repo (Full control of private repositories)',
52+
' • workflow (Optional: Update GitHub Action workflows)',
53+
'3. Copy the generated token and paste it here',
54+
],
55+
setCredentials: (username: string, token: string) =>
56+
setCredentials((prev) => ({
57+
...prev,
58+
github: { username, token },
59+
})),
60+
},
61+
gitlab: {
62+
url: 'gitlab.com',
63+
username: credentials.gitlab.username,
64+
token: credentials.gitlab.token,
65+
title: 'GitLab',
66+
instructions: 'To set up GitLab access:',
67+
tokenSetupSteps: [
68+
'1. Go to GitLab.com → Profile Settings → Access Tokens',
69+
'2. Create a new token with these scopes:',
70+
' • api (Full API access)',
71+
' • write_repository (Read/write access)',
72+
'3. Copy the generated token and paste it here',
73+
],
74+
setCredentials: (username: string, token: string) =>
75+
setCredentials((prev) => ({
76+
...prev,
77+
gitlab: { username, token },
78+
})),
79+
},
80+
};
81+
82+
const handleSaveConnection = async (provider: keyof typeof providers) => {
83+
const { url, username, token, title } = providers[provider];
84+
85+
await saveGitAuth(url, {
86+
username,
87+
password: token,
88+
});
89+
90+
logStore.logSystem(`${title} connection settings updated`, {
91+
username,
92+
hasToken: !!token,
1693
});
17-
toast.success('GitHub credentials saved successfully!');
18-
Cookies.set('git:github.com', JSON.stringify({ username: githubToken, password: 'x-oauth-basic' }));
1994
};
2095

2196
return (
22-
<div className="p-4 mb-4 border border-bolt-elements-borderColor rounded-lg bg-bolt-elements-background-depth-3">
23-
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">GitHub Connection</h3>
24-
<div className="flex mb-4">
25-
<div className="flex-1 mr-2">
26-
<label className="block text-sm text-bolt-elements-textSecondary mb-1">GitHub Username:</label>
27-
<input
28-
type="text"
29-
value={githubUsername}
30-
onChange={(e) => setGithubUsername(e.target.value)}
31-
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"
32-
/>
33-
</div>
34-
<div className="flex-1">
35-
<label className="block text-sm text-bolt-elements-textSecondary mb-1">Personal Access Token:</label>
36-
<input
37-
type="password"
38-
value={githubToken}
39-
onChange={(e) => setGithubToken(e.target.value)}
40-
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"
41-
/>
42-
</div>
43-
</div>
44-
<div className="flex mb-4">
45-
<button
46-
onClick={handleSaveConnection}
47-
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"
97+
<div className="space-y-4">
98+
{/* Encryption status section remains the same */}
99+
100+
{Object.entries(providers).map(([key, provider]) => (
101+
<div
102+
key={key}
103+
className="p-4 border border-bolt-elements-borderColor rounded-lg bg-bolt-elements-background-depth-3"
48104
>
49-
Save Connection
50-
</button>
51-
</div>
105+
<div className="flex items-center justify-between cursor-pointer" onClick={() => toggleProvider(key)}>
106+
<div className="flex items-center">
107+
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">{provider.title} Connection</h3>
108+
{provider.username && (
109+
<span className="ml-2 text-sm text-bolt-elements-textSecondary">({provider.username})</span>
110+
)}
111+
</div>
112+
<div className="flex items-center">
113+
{provider.username && provider.token && (
114+
<div className="flex items-center mr-3">
115+
<div className="w-2 h-2 rounded-full bg-green-500 mr-2" />
116+
<span className="text-sm text-bolt-elements-textSecondary">Connected</span>
117+
</div>
118+
)}
119+
<div className={`transform transition-transform ${expandedProviders[key] ? 'rotate-180' : ''}`}>
120+
<div className="i-ph:caret-down text-bolt-elements-textSecondary" />
121+
</div>
122+
</div>
123+
</div>
124+
125+
{expandedProviders[key] && (
126+
<div className="mt-4">
127+
<div className="mb-4 p-3 bg-bolt-elements-background-depth-4 rounded border border-bolt-elements-borderColor">
128+
<p className="text-sm text-bolt-elements-textSecondary mb-2">{provider.instructions}</p>
129+
<ul className="text-sm text-bolt-elements-textSecondary space-y-1">
130+
{provider.tokenSetupSteps.map((step, index) => (
131+
<li key={index}>{step}</li>
132+
))}
133+
</ul>
134+
</div>
135+
136+
<div className="flex mb-4">
137+
<div className="flex-1 mr-2">
138+
<label className="block text-sm text-bolt-elements-textSecondary mb-1">
139+
{provider.title} Username:
140+
</label>
141+
<input
142+
type="text"
143+
value={provider.username}
144+
onChange={(e) => provider.setCredentials(e.target.value, provider.token)}
145+
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"
146+
/>
147+
</div>
148+
<div className="flex-1">
149+
<label className="block text-sm text-bolt-elements-textSecondary mb-1">Personal Access Token:</label>
150+
<input
151+
type="password"
152+
value={provider.token}
153+
onChange={(e) => provider.setCredentials(provider.username, e.target.value)}
154+
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"
155+
/>
156+
</div>
157+
</div>
158+
<div className="flex">
159+
<button
160+
onClick={() => handleSaveConnection(key as keyof typeof providers)}
161+
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"
162+
>
163+
Save {provider.title} Connection
164+
</button>
165+
</div>
166+
</div>
167+
)}
168+
</div>
169+
))}
52170
</div>
53171
);
54172
}

app/components/workbench/Workbench.client.tsx

+68-35
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,18 @@ import { renderLogger } from '~/utils/logger';
1717
import { EditorPanel } from './EditorPanel';
1818
import { Preview } from './Preview';
1919
import useViewport from '~/lib/hooks';
20-
import Cookies from 'js-cookie';
20+
import { ensureEncryption, lookupSavedPassword } from '~/lib/hooks/useCredentials';
2121

2222
interface WorkspaceProps {
2323
chatStarted?: boolean;
2424
isStreaming?: boolean;
2525
}
2626

27+
interface GitCredentials {
28+
github: boolean;
29+
gitlab: boolean;
30+
}
31+
2732
const viewTransition = { ease: cubicEasingFn };
2833

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

6065
const [isSyncing, setIsSyncing] = useState(false);
66+
const [hasCredentials, setHasCredentials] = useState<GitCredentials>({
67+
github: false,
68+
gitlab: false,
69+
});
6170

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

95+
useEffect(() => {
96+
checkGitCredentials();
97+
}, []);
98+
99+
const checkGitCredentials = async () => {
100+
const githubAuth = await lookupSavedPassword('github.com');
101+
const gitlabAuth = await lookupSavedPassword('gitlab.com');
102+
103+
setHasCredentials({
104+
github: !!(githubAuth?.username && githubAuth?.password),
105+
gitlab: !!(gitlabAuth?.username && gitlabAuth?.password),
106+
});
107+
};
108+
86109
const onEditorChange = useCallback<OnEditorChange>((update) => {
87110
workbenchStore.setCurrentDocumentContent(update.content);
88111
}, []);
@@ -120,6 +143,38 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
120143
}
121144
}, []);
122145

146+
const handleGitPush = async (provider: 'github' | 'gitlab') => {
147+
const repoName = prompt(
148+
`Please enter a name for your new ${provider === 'github' ? 'GitHub' : 'GitLab'} repository:`,
149+
'bolt-generated-project',
150+
);
151+
152+
// TODO store and load repoName from downloaded project
153+
if (!repoName) {
154+
toast.error('Repository name is required');
155+
return;
156+
}
157+
158+
if (!(await ensureEncryption())) {
159+
toast.error('Failed to initialize secure storage');
160+
return;
161+
}
162+
163+
const auth = await lookupSavedPassword(`${provider}.com`);
164+
165+
if (auth?.username && auth?.password) {
166+
if (provider === 'github') {
167+
workbenchStore.pushToGitHub(repoName, auth.username, auth.password);
168+
} else {
169+
workbenchStore.pushToGitLab(repoName, auth.username, auth.password);
170+
}
171+
} else {
172+
toast.info(
173+
`Please set up your ${provider === 'github' ? 'GitHub' : 'GitLab'} credentials in the Connections tab`,
174+
);
175+
}
176+
};
177+
123178
return (
124179
chatStarted && (
125180
<motion.div
@@ -168,40 +223,18 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
168223
<div className="i-ph:terminal" />
169224
Toggle Terminal
170225
</PanelHeaderButton>
171-
<PanelHeaderButton
172-
className="mr-1 text-sm"
173-
onClick={() => {
174-
const repoName = prompt(
175-
'Please enter a name for your new GitHub repository:',
176-
'bolt-generated-project',
177-
);
178-
179-
if (!repoName) {
180-
alert('Repository name is required. Push to GitHub cancelled.');
181-
return;
182-
}
183-
184-
const githubUsername = Cookies.get('githubUsername');
185-
const githubToken = Cookies.get('githubToken');
186-
187-
if (!githubUsername || !githubToken) {
188-
const usernameInput = prompt('Please enter your GitHub username:');
189-
const tokenInput = prompt('Please enter your GitHub personal access token:');
190-
191-
if (!usernameInput || !tokenInput) {
192-
alert('GitHub username and token are required. Push to GitHub cancelled.');
193-
return;
194-
}
195-
196-
workbenchStore.pushToGitHub(repoName, usernameInput, tokenInput);
197-
} else {
198-
workbenchStore.pushToGitHub(repoName, githubUsername, githubToken);
199-
}
200-
}}
201-
>
202-
<div className="i-ph:github-logo" />
203-
Push to GitHub
204-
</PanelHeaderButton>
226+
{hasCredentials.github && (
227+
<PanelHeaderButton className="mr-1 text-sm" onClick={() => handleGitPush('github')}>
228+
<div className="i-ph:github-logo" />
229+
Push to GitHub
230+
</PanelHeaderButton>
231+
)}
232+
{hasCredentials.gitlab && (
233+
<PanelHeaderButton className="mr-1 text-sm" onClick={() => handleGitPush('gitlab')}>
234+
<div className="i-ph:gitlab-logo" />
235+
Push to GitLab
236+
</PanelHeaderButton>
237+
)}
205238
</div>
206239
)}
207240
<IconButton

0 commit comments

Comments
 (0)