Skip to content

Commit

Permalink
Merge pull request #1 from D-Byte/gitlabIsAlsoNice
Browse files Browse the repository at this point in the history
Gitlab is also nice
  • Loading branch information
D-Byte authored Dec 15, 2024
2 parents be1d835 + 12a351f commit 1380871
Show file tree
Hide file tree
Showing 11 changed files with 774 additions and 176 deletions.
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

0 comments on commit 1380871

Please sign in to comment.