diff --git a/src/components/CredentialsSettings.jsx b/src/components/CredentialsSettings.jsx index cc7d424..b56584a 100644 --- a/src/components/CredentialsSettings.jsx +++ b/src/components/CredentialsSettings.jsx @@ -1,421 +1,3 @@ -import { useState, useEffect } from 'react'; -import { Button } from './ui/button'; -import { Input } from './ui/input'; -import { Key, Plus, Trash2, Eye, EyeOff, Copy, Check, Github, ExternalLink } from 'lucide-react'; -import { useVersionCheck } from '../hooks/useVersionCheck'; -import { version } from '../../package.json'; -import { authenticatedFetch } from '../utils/api'; -import { useTranslation } from 'react-i18next'; +import CredentialsSettingsTab from './settings/view/tabs/api-settings/CredentialsSettingsTab'; -function CredentialsSettings() { - const { t } = useTranslation('settings'); - const [apiKeys, setApiKeys] = useState([]); - const [githubCredentials, setGithubCredentials] = useState([]); - const [loading, setLoading] = useState(true); - const [showNewKeyForm, setShowNewKeyForm] = useState(false); - const [showNewGithubForm, setShowNewGithubForm] = useState(false); - const [newKeyName, setNewKeyName] = useState(''); - const [newGithubName, setNewGithubName] = useState(''); - const [newGithubToken, setNewGithubToken] = useState(''); - const [newGithubDescription, setNewGithubDescription] = useState(''); - const [showToken, setShowToken] = useState({}); - const [copiedKey, setCopiedKey] = useState(null); - const [newlyCreatedKey, setNewlyCreatedKey] = useState(null); - - // Version check hook - const { updateAvailable, latestVersion, releaseInfo } = useVersionCheck('siteboon', 'claudecodeui'); - - useEffect(() => { - fetchData(); - }, []); - - const fetchData = async () => { - try { - setLoading(true); - - // Fetch API keys - const apiKeysRes = await authenticatedFetch('/api/settings/api-keys'); - const apiKeysData = await apiKeysRes.json(); - setApiKeys(apiKeysData.apiKeys || []); - - // Fetch GitHub credentials only - const credentialsRes = await authenticatedFetch('/api/settings/credentials?type=github_token'); - const credentialsData = await credentialsRes.json(); - setGithubCredentials(credentialsData.credentials || []); - } catch (error) { - console.error('Error fetching settings:', error); - } finally { - setLoading(false); - } - }; - - const createApiKey = async () => { - if (!newKeyName.trim()) return; - - try { - const res = await authenticatedFetch('/api/settings/api-keys', { - method: 'POST', - body: JSON.stringify({ keyName: newKeyName }) - }); - - const data = await res.json(); - if (data.success) { - setNewlyCreatedKey(data.apiKey); - setNewKeyName(''); - setShowNewKeyForm(false); - fetchData(); - } - } catch (error) { - console.error('Error creating API key:', error); - } - }; - - const deleteApiKey = async (keyId) => { - if (!confirm(t('apiKeys.confirmDelete'))) return; - - try { - await authenticatedFetch(`/api/settings/api-keys/${keyId}`, { - method: 'DELETE' - }); - fetchData(); - } catch (error) { - console.error('Error deleting API key:', error); - } - }; - - const toggleApiKey = async (keyId, isActive) => { - try { - await authenticatedFetch(`/api/settings/api-keys/${keyId}/toggle`, { - method: 'PATCH', - body: JSON.stringify({ isActive: !isActive }) - }); - fetchData(); - } catch (error) { - console.error('Error toggling API key:', error); - } - }; - - const createGithubCredential = async () => { - if (!newGithubName.trim() || !newGithubToken.trim()) return; - - try { - const res = await authenticatedFetch('/api/settings/credentials', { - method: 'POST', - body: JSON.stringify({ - credentialName: newGithubName, - credentialType: 'github_token', - credentialValue: newGithubToken, - description: newGithubDescription - }) - }); - - const data = await res.json(); - if (data.success) { - setNewGithubName(''); - setNewGithubToken(''); - setNewGithubDescription(''); - setShowNewGithubForm(false); - fetchData(); - } - } catch (error) { - console.error('Error creating GitHub credential:', error); - } - }; - - const deleteGithubCredential = async (credentialId) => { - if (!confirm(t('apiKeys.github.confirmDelete'))) return; - - try { - await authenticatedFetch(`/api/settings/credentials/${credentialId}`, { - method: 'DELETE' - }); - fetchData(); - } catch (error) { - console.error('Error deleting GitHub credential:', error); - } - }; - - const toggleGithubCredential = async (credentialId, isActive) => { - try { - await authenticatedFetch(`/api/settings/credentials/${credentialId}/toggle`, { - method: 'PATCH', - body: JSON.stringify({ isActive: !isActive }) - }); - fetchData(); - } catch (error) { - console.error('Error toggling GitHub credential:', error); - } - }; - - const copyToClipboard = (text, id) => { - navigator.clipboard.writeText(text); - setCopiedKey(id); - setTimeout(() => setCopiedKey(null), 2000); - }; - - if (loading) { - return
{t('apiKeys.loading')}
; - } - - return ( -
- {/* New API Key Alert */} - {newlyCreatedKey && ( -
-

{t('apiKeys.newKey.alertTitle')}

-

- {t('apiKeys.newKey.alertMessage')} -

-
- - {newlyCreatedKey.apiKey} - - -
- -
- )} - - {/* API Keys Section */} -
-
-
- -

{t('apiKeys.title')}

-
- -
- -
-

- {t('apiKeys.description')} -

- - {t('apiKeys.apiDocsLink')} - - -
- - {showNewKeyForm && ( -
- setNewKeyName(e.target.value)} - className="mb-2" - /> -
- - -
-
- )} - -
- {apiKeys.length === 0 ? ( -

{t('apiKeys.empty')}

- ) : ( - apiKeys.map((key) => ( -
-
-
{key.key_name}
- {key.api_key} -
- {t('apiKeys.list.created')} {new Date(key.created_at).toLocaleDateString()} - {key.last_used && ` • ${t('apiKeys.list.lastUsed')} ${new Date(key.last_used).toLocaleDateString()}`} -
-
-
- - -
-
- )) - )} -
-
- - {/* GitHub Credentials Section */} -
-
-
- -

{t('apiKeys.github.title')}

-
- -
- -

- {t('apiKeys.github.descriptionAlt')} -

- - {showNewGithubForm && ( -
- setNewGithubName(e.target.value)} - /> - -
- setNewGithubToken(e.target.value)} - className="pr-10" - /> - -
- - setNewGithubDescription(e.target.value)} - /> - -
- - -
- - - {t('apiKeys.github.form.howToCreate')} - -
- )} - -
- {githubCredentials.length === 0 ? ( -

{t('apiKeys.github.empty')}

- ) : ( - githubCredentials.map((credential) => ( -
-
-
{credential.credential_name}
- {credential.description && ( -
{credential.description}
- )} -
- {t('apiKeys.github.added')} {new Date(credential.created_at).toLocaleDateString()} -
-
-
- - -
-
- )) - )} -
-
- - {/* Version Information */} -
-
- - v{version} - - {updateAvailable && latestVersion && ( - - {t('apiKeys.version.updateAvailable', { version: latestVersion })} - - - )} -
-
-
- ); -} - -export default CredentialsSettings; +export default CredentialsSettingsTab; diff --git a/src/components/GitSettings.jsx b/src/components/GitSettings.jsx index cdce8e4..eaa6598 100644 --- a/src/components/GitSettings.jsx +++ b/src/components/GitSettings.jsx @@ -1,131 +1,3 @@ -import { useState, useEffect } from 'react'; -import { Button } from './ui/button'; -import { Input } from './ui/input'; -import { GitBranch, Check } from 'lucide-react'; -import { authenticatedFetch } from '../utils/api'; -import { useTranslation } from 'react-i18next'; +import GitSettingsTab from './settings/view/tabs/git-settings/GitSettingsTab'; -function GitSettings() { - const { t } = useTranslation('settings'); - const [gitName, setGitName] = useState(''); - const [gitEmail, setGitEmail] = useState(''); - const [gitConfigLoading, setGitConfigLoading] = useState(false); - const [gitConfigSaving, setGitConfigSaving] = useState(false); - const [saveStatus, setSaveStatus] = useState(null); - - useEffect(() => { - loadGitConfig(); - }, []); - - const loadGitConfig = async () => { - try { - setGitConfigLoading(true); - const response = await authenticatedFetch('/api/user/git-config'); - if (response.ok) { - const data = await response.json(); - setGitName(data.gitName || ''); - setGitEmail(data.gitEmail || ''); - } - } catch (error) { - console.error('Error loading git config:', error); - } finally { - setGitConfigLoading(false); - } - }; - - const saveGitConfig = async () => { - try { - setGitConfigSaving(true); - const response = await authenticatedFetch('/api/user/git-config', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ gitName, gitEmail }) - }); - - if (response.ok) { - setSaveStatus('success'); - setTimeout(() => setSaveStatus(null), 3000); - } else { - const data = await response.json(); - setSaveStatus('error'); - console.error('Failed to save git config:', data.error); - } - } catch (error) { - console.error('Error saving git config:', error); - setSaveStatus('error'); - } finally { - setGitConfigSaving(false); - } - }; - - return ( -
-
-
- -

{t('git.title')}

-
- -

- {t('git.description')} -

- -
-
- - setGitName(e.target.value)} - placeholder="John Doe" - disabled={gitConfigLoading} - className="w-full" - /> -

- {t('git.name.help')} -

-
- -
- - setGitEmail(e.target.value)} - placeholder="john@example.com" - disabled={gitConfigLoading} - className="w-full" - /> -

- {t('git.email.help')} -

-
- -
- - - {saveStatus === 'success' && ( -
- - {t('git.status.success')} -
- )} -
-
-
-
- ); -} - -export default GitSettings; +export default GitSettingsTab; diff --git a/src/components/TasksSettings.jsx b/src/components/TasksSettings.jsx index 99f35c3..e341115 100644 --- a/src/components/TasksSettings.jsx +++ b/src/components/TasksSettings.jsx @@ -1,109 +1,3 @@ -import { Zap } from 'lucide-react'; -import { useTasksSettings } from '../contexts/TasksSettingsContext'; -import { useTranslation } from 'react-i18next'; +import TasksSettingsTab from './settings/view/tabs/tasks-settings/TasksSettingsTab'; -function TasksSettings() { - const { t } = useTranslation('settings'); - const { - tasksEnabled, - setTasksEnabled, - isTaskMasterInstalled, - isCheckingInstallation - } = useTasksSettings(); - - return ( -
- {/* Installation Status Check */} - {isCheckingInstallation ? ( -
-
-
- {t('tasks.checking')} -
-
- ) : ( - <> - {/* TaskMaster Not Installed Warning */} - {!isTaskMasterInstalled && ( -
-
-
- - - -
-
-
- {t('tasks.notInstalled.title')} -
-
-

{t('tasks.notInstalled.description')}

- -
- {t('tasks.notInstalled.installCommand')} -
- - - -
-

{t('tasks.notInstalled.afterInstallation')}

-
    -
  1. {t('tasks.notInstalled.steps.restart')}
  2. -
  3. {t('tasks.notInstalled.steps.autoAvailable')}
  4. -
  5. {t('tasks.notInstalled.steps.initCommand')}
  6. -
-
-
-
-
-
- )} - - {/* TaskMaster Settings */} - {isTaskMasterInstalled && ( -
-
-
-
-
- {t('tasks.settings.enableLabel')} -
-
- {t('tasks.settings.enableDescription')} -
-
- -
-
-
- )} - - )} -
- ); -} - -export default TasksSettings; +export default TasksSettingsTab; diff --git a/src/components/settings/Settings.tsx b/src/components/settings/Settings.tsx index fb6735d..f596e03 100644 --- a/src/components/settings/Settings.tsx +++ b/src/components/settings/Settings.tsx @@ -1,15 +1,15 @@ import { Settings as SettingsIcon, X } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import CredentialsSettings from '../CredentialsSettings'; -import GitSettings from '../GitSettings'; import LoginModal from '../LoginModal'; -import TasksSettings from '../TasksSettings'; import { Button } from '../ui/button'; import ClaudeMcpFormModal from './view/modals/ClaudeMcpFormModal'; import CodexMcpFormModal from './view/modals/CodexMcpFormModal'; import SettingsMainTabs from './view/SettingsMainTabs'; import AgentsSettingsTab from './view/tabs/agents-settings/AgentsSettingsTab'; import AppearanceSettingsTab from './view/tabs/AppearanceSettingsTab'; +import CredentialsSettingsTab from './view/tabs/api-settings/CredentialsSettingsTab'; +import GitSettingsTab from './view/tabs/git-settings/GitSettingsTab'; +import TasksSettingsTab from './view/tabs/tasks-settings/TasksSettingsTab'; import { useSettingsController } from './hooks/useSettingsController'; import type { AgentProvider, SettingsProject, SettingsProps } from './types/types'; @@ -129,7 +129,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set /> )} - {activeTab === 'git' && } + {activeTab === 'git' && } {activeTab === 'agents' && ( - + )} {activeTab === 'api' && (
- +
)} diff --git a/src/components/settings/hooks/useCredentialsSettings.ts b/src/components/settings/hooks/useCredentialsSettings.ts new file mode 100644 index 0000000..941ea65 --- /dev/null +++ b/src/components/settings/hooks/useCredentialsSettings.ts @@ -0,0 +1,272 @@ +import { useCallback, useEffect, useState } from 'react'; +import { authenticatedFetch } from '../../../utils/api'; +import type { + ApiKeyItem, + ApiKeysResponse, + CreatedApiKey, + GithubCredentialItem, + GithubCredentialsResponse, +} from '../view/tabs/api-settings/types'; + +type UseCredentialsSettingsArgs = { + confirmDeleteApiKeyText: string; + confirmDeleteGithubCredentialText: string; +}; + +const getApiError = (payload: { error?: string } | undefined, fallback: string) => ( + payload?.error || fallback +); + +export function useCredentialsSettings({ + confirmDeleteApiKeyText, + confirmDeleteGithubCredentialText, +}: UseCredentialsSettingsArgs) { + const [apiKeys, setApiKeys] = useState([]); + const [githubCredentials, setGithubCredentials] = useState([]); + const [loading, setLoading] = useState(true); + + const [showNewKeyForm, setShowNewKeyForm] = useState(false); + const [newKeyName, setNewKeyName] = useState(''); + + const [showNewGithubForm, setShowNewGithubForm] = useState(false); + const [newGithubName, setNewGithubName] = useState(''); + const [newGithubToken, setNewGithubToken] = useState(''); + const [newGithubDescription, setNewGithubDescription] = useState(''); + + const [showToken, setShowToken] = useState>({}); + const [copiedKey, setCopiedKey] = useState(null); + const [newlyCreatedKey, setNewlyCreatedKey] = useState(null); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + + const [apiKeysResponse, credentialsResponse] = await Promise.all([ + authenticatedFetch('/api/settings/api-keys'), + authenticatedFetch('/api/settings/credentials?type=github_token'), + ]); + + const [apiKeysPayload, credentialsPayload] = await Promise.all([ + apiKeysResponse.json() as Promise, + credentialsResponse.json() as Promise, + ]); + + setApiKeys(apiKeysPayload.apiKeys || []); + setGithubCredentials(credentialsPayload.credentials || []); + } catch (error) { + console.error('Error fetching settings:', error); + } finally { + setLoading(false); + } + }, []); + + const createApiKey = useCallback(async () => { + if (!newKeyName.trim()) { + return; + } + + try { + const response = await authenticatedFetch('/api/settings/api-keys', { + method: 'POST', + body: JSON.stringify({ keyName: newKeyName.trim() }), + }); + + const payload = await response.json() as ApiKeysResponse; + if (!response.ok || !payload.success) { + console.error('Error creating API key:', getApiError(payload, 'Failed to create API key')); + return; + } + + if (payload.apiKey) { + setNewlyCreatedKey(payload.apiKey); + } + setNewKeyName(''); + setShowNewKeyForm(false); + await fetchData(); + } catch (error) { + console.error('Error creating API key:', error); + } + }, [fetchData, newKeyName]); + + const deleteApiKey = useCallback(async (keyId: string) => { + if (!window.confirm(confirmDeleteApiKeyText)) { + return; + } + + try { + const response = await authenticatedFetch(`/api/settings/api-keys/${keyId}`, { + method: 'DELETE', + }); + + if (!response.ok) { + const payload = await response.json() as ApiKeysResponse; + console.error('Error deleting API key:', getApiError(payload, 'Failed to delete API key')); + return; + } + + await fetchData(); + } catch (error) { + console.error('Error deleting API key:', error); + } + }, [confirmDeleteApiKeyText, fetchData]); + + const toggleApiKey = useCallback(async (keyId: string, isActive: boolean) => { + try { + const response = await authenticatedFetch(`/api/settings/api-keys/${keyId}/toggle`, { + method: 'PATCH', + body: JSON.stringify({ isActive: !isActive }), + }); + + if (!response.ok) { + const payload = await response.json() as ApiKeysResponse; + console.error('Error toggling API key:', getApiError(payload, 'Failed to toggle API key')); + return; + } + + await fetchData(); + } catch (error) { + console.error('Error toggling API key:', error); + } + }, [fetchData]); + + const createGithubCredential = useCallback(async () => { + if (!newGithubName.trim() || !newGithubToken.trim()) { + return; + } + + try { + const response = await authenticatedFetch('/api/settings/credentials', { + method: 'POST', + body: JSON.stringify({ + credentialName: newGithubName.trim(), + credentialType: 'github_token', + credentialValue: newGithubToken, + description: newGithubDescription.trim(), + }), + }); + + const payload = await response.json() as GithubCredentialsResponse; + if (!response.ok || !payload.success) { + console.error('Error creating GitHub credential:', getApiError(payload, 'Failed to create GitHub credential')); + return; + } + + setNewGithubName(''); + setNewGithubToken(''); + setNewGithubDescription(''); + setShowNewGithubForm(false); + setShowToken((prev) => ({ ...prev, new: false })); + await fetchData(); + } catch (error) { + console.error('Error creating GitHub credential:', error); + } + }, [fetchData, newGithubDescription, newGithubName, newGithubToken]); + + const deleteGithubCredential = useCallback(async (credentialId: string) => { + if (!window.confirm(confirmDeleteGithubCredentialText)) { + return; + } + + try { + const response = await authenticatedFetch(`/api/settings/credentials/${credentialId}`, { + method: 'DELETE', + }); + + if (!response.ok) { + const payload = await response.json() as GithubCredentialsResponse; + console.error('Error deleting GitHub credential:', getApiError(payload, 'Failed to delete GitHub credential')); + return; + } + + await fetchData(); + } catch (error) { + console.error('Error deleting GitHub credential:', error); + } + }, [confirmDeleteGithubCredentialText, fetchData]); + + const toggleGithubCredential = useCallback(async (credentialId: string, isActive: boolean) => { + try { + const response = await authenticatedFetch(`/api/settings/credentials/${credentialId}/toggle`, { + method: 'PATCH', + body: JSON.stringify({ isActive: !isActive }), + }); + + if (!response.ok) { + const payload = await response.json() as GithubCredentialsResponse; + console.error('Error toggling GitHub credential:', getApiError(payload, 'Failed to toggle GitHub credential')); + return; + } + + await fetchData(); + } catch (error) { + console.error('Error toggling GitHub credential:', error); + } + }, [fetchData]); + + const copyToClipboard = useCallback(async (text: string, id: string) => { + try { + await navigator.clipboard.writeText(text); + setCopiedKey(id); + window.setTimeout(() => setCopiedKey(null), 2000); + } catch (error) { + console.error('Failed to copy to clipboard:', error); + } + }, []); + + const dismissNewlyCreatedKey = useCallback(() => { + setNewlyCreatedKey(null); + }, []); + + const cancelNewApiKeyForm = useCallback(() => { + setShowNewKeyForm(false); + setNewKeyName(''); + }, []); + + const cancelNewGithubForm = useCallback(() => { + setShowNewGithubForm(false); + setNewGithubName(''); + setNewGithubToken(''); + setNewGithubDescription(''); + setShowToken((prev) => ({ ...prev, new: false })); + }, []); + + const toggleNewGithubTokenVisibility = useCallback(() => { + setShowToken((prev) => ({ ...prev, new: !prev.new })); + }, []); + + useEffect(() => { + void fetchData(); + }, [fetchData]); + + return { + apiKeys, + githubCredentials, + loading, + showNewKeyForm, + setShowNewKeyForm, + newKeyName, + setNewKeyName, + showNewGithubForm, + setShowNewGithubForm, + newGithubName, + setNewGithubName, + newGithubToken, + setNewGithubToken, + newGithubDescription, + setNewGithubDescription, + showToken, + copiedKey, + newlyCreatedKey, + createApiKey, + deleteApiKey, + toggleApiKey, + createGithubCredential, + deleteGithubCredential, + toggleGithubCredential, + copyToClipboard, + dismissNewlyCreatedKey, + cancelNewApiKeyForm, + cancelNewGithubForm, + toggleNewGithubTokenVisibility, + }; +} diff --git a/src/components/settings/hooks/useGitSettings.ts b/src/components/settings/hooks/useGitSettings.ts new file mode 100644 index 0000000..23392b0 --- /dev/null +++ b/src/components/settings/hooks/useGitSettings.ts @@ -0,0 +1,96 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { authenticatedFetch } from '../../../utils/api'; + +type GitConfigResponse = { + gitName?: string; + gitEmail?: string; + error?: string; +}; + +type SaveStatus = 'success' | 'error' | null; + +export function useGitSettings() { + const [gitName, setGitName] = useState(''); + const [gitEmail, setGitEmail] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const [saveStatus, setSaveStatus] = useState(null); + const clearStatusTimerRef = useRef(null); + + const clearSaveStatus = useCallback(() => { + if (clearStatusTimerRef.current !== null) { + window.clearTimeout(clearStatusTimerRef.current); + clearStatusTimerRef.current = null; + } + setSaveStatus(null); + }, []); + + const loadGitConfig = useCallback(async () => { + try { + setIsLoading(true); + const response = await authenticatedFetch('/api/user/git-config'); + if (!response.ok) { + return; + } + + const data = await response.json() as GitConfigResponse; + setGitName(data.gitName || ''); + setGitEmail(data.gitEmail || ''); + } catch (error) { + console.error('Error loading git config:', error); + } finally { + setIsLoading(false); + } + }, []); + + const saveGitConfig = useCallback(async () => { + try { + setIsSaving(true); + const response = await authenticatedFetch('/api/user/git-config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ gitName, gitEmail }), + }); + + if (response.ok) { + setSaveStatus('success'); + clearStatusTimerRef.current = window.setTimeout(() => { + setSaveStatus(null); + clearStatusTimerRef.current = null; + }, 3000); + return; + } + + const data = await response.json() as GitConfigResponse; + console.error('Failed to save git config:', data.error); + setSaveStatus('error'); + } catch (error) { + console.error('Error saving git config:', error); + setSaveStatus('error'); + } finally { + setIsSaving(false); + } + }, [gitEmail, gitName]); + + useEffect(() => { + void loadGitConfig(); + }, [loadGitConfig]); + + useEffect(() => () => { + if (clearStatusTimerRef.current !== null) { + window.clearTimeout(clearStatusTimerRef.current); + } + }, []); + + return { + gitName, + setGitName, + gitEmail, + setGitEmail, + isLoading, + isSaving, + saveStatus, + clearSaveStatus, + saveGitConfig, + }; +} diff --git a/src/components/settings/view/tabs/api-settings/CredentialsSettingsTab.tsx b/src/components/settings/view/tabs/api-settings/CredentialsSettingsTab.tsx new file mode 100644 index 0000000..0f47512 --- /dev/null +++ b/src/components/settings/view/tabs/api-settings/CredentialsSettingsTab.tsx @@ -0,0 +1,100 @@ +import { useTranslation } from 'react-i18next'; +import { useVersionCheck } from '../../../../../hooks/useVersionCheck'; +import { useCredentialsSettings } from '../../../hooks/useCredentialsSettings'; +import ApiKeysSection from './sections/ApiKeysSection'; +import GithubCredentialsSection from './sections/GithubCredentialsSection'; +import NewApiKeyAlert from './sections/NewApiKeyAlert'; +import VersionInfoSection from './sections/VersionInfoSection'; + +export default function CredentialsSettingsTab() { + const { t } = useTranslation('settings'); + const { updateAvailable, latestVersion, currentVersion, releaseInfo } = useVersionCheck('siteboon', 'claudecodeui'); + const { + apiKeys, + githubCredentials, + loading, + showNewKeyForm, + setShowNewKeyForm, + newKeyName, + setNewKeyName, + showNewGithubForm, + setShowNewGithubForm, + newGithubName, + setNewGithubName, + newGithubToken, + setNewGithubToken, + newGithubDescription, + setNewGithubDescription, + showToken, + copiedKey, + newlyCreatedKey, + createApiKey, + deleteApiKey, + toggleApiKey, + createGithubCredential, + deleteGithubCredential, + toggleGithubCredential, + copyToClipboard, + dismissNewlyCreatedKey, + cancelNewApiKeyForm, + cancelNewGithubForm, + toggleNewGithubTokenVisibility, + } = useCredentialsSettings({ + confirmDeleteApiKeyText: t('apiKeys.confirmDelete'), + confirmDeleteGithubCredentialText: t('apiKeys.github.confirmDelete'), + }); + + if (loading) { + return
{t('apiKeys.loading')}
; + } + + return ( +
+ {newlyCreatedKey && ( + + )} + + + + + + +
+ ); +} diff --git a/src/components/settings/view/tabs/api-settings/sections/ApiKeysSection.tsx b/src/components/settings/view/tabs/api-settings/sections/ApiKeysSection.tsx new file mode 100644 index 0000000..200d9c9 --- /dev/null +++ b/src/components/settings/view/tabs/api-settings/sections/ApiKeysSection.tsx @@ -0,0 +1,109 @@ +import { ExternalLink, Key, Plus, Trash2 } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { Button } from '../../../../../ui/button'; +import { Input } from '../../../../../ui/input'; +import type { ApiKeyItem } from '../types'; + +type ApiKeysSectionProps = { + apiKeys: ApiKeyItem[]; + showNewKeyForm: boolean; + newKeyName: string; + onShowNewKeyFormChange: (value: boolean) => void; + onNewKeyNameChange: (value: string) => void; + onCreateApiKey: () => void; + onCancelCreateApiKey: () => void; + onToggleApiKey: (keyId: string, isActive: boolean) => void; + onDeleteApiKey: (keyId: string) => void; +}; + +export default function ApiKeysSection({ + apiKeys, + showNewKeyForm, + newKeyName, + onShowNewKeyFormChange, + onNewKeyNameChange, + onCreateApiKey, + onCancelCreateApiKey, + onToggleApiKey, + onDeleteApiKey, +}: ApiKeysSectionProps) { + const { t } = useTranslation('settings'); + + return ( +
+
+
+ +

{t('apiKeys.title')}

+
+ +
+ +
+

{t('apiKeys.description')}

+ + {t('apiKeys.apiDocsLink')} + + +
+ + {showNewKeyForm && ( +
+ onNewKeyNameChange(event.target.value)} + className="mb-2" + /> +
+ + +
+
+ )} + +
+ {apiKeys.length === 0 ? ( +

{t('apiKeys.empty')}

+ ) : ( + apiKeys.map((key) => ( +
+
+
{key.key_name}
+ {key.api_key} +
+ {t('apiKeys.list.created')} {new Date(key.created_at).toLocaleDateString()} + {key.last_used + ? ` - ${t('apiKeys.list.lastUsed')} ${new Date(key.last_used).toLocaleDateString()}` + : ''} +
+
+
+ + +
+
+ )) + )} +
+
+ ); +} diff --git a/src/components/settings/view/tabs/api-settings/sections/GithubCredentialsSection.tsx b/src/components/settings/view/tabs/api-settings/sections/GithubCredentialsSection.tsx new file mode 100644 index 0000000..6fe1403 --- /dev/null +++ b/src/components/settings/view/tabs/api-settings/sections/GithubCredentialsSection.tsx @@ -0,0 +1,141 @@ +import { Eye, EyeOff, Github, Plus, Trash2 } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { Button } from '../../../../../ui/button'; +import { Input } from '../../../../../ui/input'; +import type { GithubCredentialItem } from '../types'; + +type GithubCredentialsSectionProps = { + githubCredentials: GithubCredentialItem[]; + showNewGithubForm: boolean; + showNewTokenPlainText: boolean; + newGithubName: string; + newGithubToken: string; + newGithubDescription: string; + onShowNewGithubFormChange: (value: boolean) => void; + onNewGithubNameChange: (value: string) => void; + onNewGithubTokenChange: (value: string) => void; + onNewGithubDescriptionChange: (value: string) => void; + onToggleNewTokenVisibility: () => void; + onCreateGithubCredential: () => void; + onCancelCreateGithubCredential: () => void; + onToggleGithubCredential: (credentialId: string, isActive: boolean) => void; + onDeleteGithubCredential: (credentialId: string) => void; +}; + +export default function GithubCredentialsSection({ + githubCredentials, + showNewGithubForm, + showNewTokenPlainText, + newGithubName, + newGithubToken, + newGithubDescription, + onShowNewGithubFormChange, + onNewGithubNameChange, + onNewGithubTokenChange, + onNewGithubDescriptionChange, + onToggleNewTokenVisibility, + onCreateGithubCredential, + onCancelCreateGithubCredential, + onToggleGithubCredential, + onDeleteGithubCredential, +}: GithubCredentialsSectionProps) { + const { t } = useTranslation('settings'); + + return ( +
+
+
+ +

{t('apiKeys.github.title')}

+
+ +
+ +

{t('apiKeys.github.descriptionAlt')}

+ + {showNewGithubForm && ( +
+ onNewGithubNameChange(event.target.value)} + /> + +
+ onNewGithubTokenChange(event.target.value)} + className="pr-10" + /> + +
+ + onNewGithubDescriptionChange(event.target.value)} + /> + +
+ + +
+ + + {t('apiKeys.github.form.howToCreate')} + +
+ )} + +
+ {githubCredentials.length === 0 ? ( +

{t('apiKeys.github.empty')}

+ ) : ( + githubCredentials.map((credential) => ( +
+
+
{credential.credential_name}
+ {credential.description && ( +
{credential.description}
+ )} +
+ {t('apiKeys.github.added')} {new Date(credential.created_at).toLocaleDateString()} +
+
+
+ + +
+
+ )) + )} +
+
+ ); +} diff --git a/src/components/settings/view/tabs/api-settings/sections/NewApiKeyAlert.tsx b/src/components/settings/view/tabs/api-settings/sections/NewApiKeyAlert.tsx new file mode 100644 index 0000000..9d78746 --- /dev/null +++ b/src/components/settings/view/tabs/api-settings/sections/NewApiKeyAlert.tsx @@ -0,0 +1,42 @@ +import { Check, Copy } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { Button } from '../../../../../ui/button'; +import type { CreatedApiKey } from '../types'; + +type NewApiKeyAlertProps = { + apiKey: CreatedApiKey; + copiedKey: string | null; + onCopy: (text: string, id: string) => void; + onDismiss: () => void; +}; + +export default function NewApiKeyAlert({ + apiKey, + copiedKey, + onCopy, + onDismiss, +}: NewApiKeyAlertProps) { + const { t } = useTranslation('settings'); + + return ( +
+

{t('apiKeys.newKey.alertTitle')}

+

{t('apiKeys.newKey.alertMessage')}

+
+ + {apiKey.apiKey} + + +
+ +
+ ); +} diff --git a/src/components/settings/view/tabs/api-settings/sections/VersionInfoSection.tsx b/src/components/settings/view/tabs/api-settings/sections/VersionInfoSection.tsx new file mode 100644 index 0000000..47ff62b --- /dev/null +++ b/src/components/settings/view/tabs/api-settings/sections/VersionInfoSection.tsx @@ -0,0 +1,46 @@ +import { ExternalLink } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import type { ReleaseInfo } from '../../../../../../types/sharedTypes'; + +type VersionInfoSectionProps = { + currentVersion: string; + updateAvailable: boolean; + latestVersion: string | null; + releaseInfo: ReleaseInfo | null; +}; + +export default function VersionInfoSection({ + currentVersion, + updateAvailable, + latestVersion, + releaseInfo, +}: VersionInfoSectionProps) { + const { t } = useTranslation('settings'); + const releasesUrl = releaseInfo?.htmlUrl || 'https://github.com/siteboon/claudecodeui/releases'; + + return ( + + ); +} diff --git a/src/components/settings/view/tabs/api-settings/types.ts b/src/components/settings/view/tabs/api-settings/types.ts new file mode 100644 index 0000000..b3d3ae1 --- /dev/null +++ b/src/components/settings/view/tabs/api-settings/types.ts @@ -0,0 +1,36 @@ +export type ApiKeyItem = { + id: string; + key_name: string; + api_key: string; + created_at: string; + last_used?: string | null; + is_active: boolean; +}; + +export type CreatedApiKey = { + id: string; + keyName: string; + apiKey: string; + createdAt?: string; +}; + +export type GithubCredentialItem = { + id: string; + credential_name: string; + description?: string | null; + created_at: string; + is_active: boolean; +}; + +export type ApiKeysResponse = { + apiKeys?: ApiKeyItem[]; + success?: boolean; + error?: string; + apiKey?: CreatedApiKey; +}; + +export type GithubCredentialsResponse = { + credentials?: GithubCredentialItem[]; + success?: boolean; + error?: string; +}; diff --git a/src/components/settings/view/tabs/git-settings/GitSettingsTab.tsx b/src/components/settings/view/tabs/git-settings/GitSettingsTab.tsx new file mode 100644 index 0000000..84f633a --- /dev/null +++ b/src/components/settings/view/tabs/git-settings/GitSettingsTab.tsx @@ -0,0 +1,82 @@ +import { Check, GitBranch } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { useGitSettings } from '../../../hooks/useGitSettings'; +import { Button } from '../../../../ui/button'; +import { Input } from '../../../../ui/input'; + +export default function GitSettingsTab() { + const { t } = useTranslation('settings'); + const { + gitName, + setGitName, + gitEmail, + setGitEmail, + isLoading, + isSaving, + saveStatus, + saveGitConfig, + } = useGitSettings(); + + return ( +
+
+
+ +

{t('git.title')}

+
+ +

{t('git.description')}

+ +
+
+ + setGitName(event.target.value)} + placeholder="John Doe" + disabled={isLoading} + className="w-full" + /> +

{t('git.name.help')}

+
+ +
+ + setGitEmail(event.target.value)} + placeholder="john@example.com" + disabled={isLoading} + className="w-full" + /> +

{t('git.email.help')}

+
+ +
+ + + {saveStatus === 'success' && ( +
+ + {t('git.status.success')} +
+ )} +
+
+
+
+ ); +} diff --git a/src/components/settings/view/tabs/tasks-settings/TasksSettingsTab.tsx b/src/components/settings/view/tabs/tasks-settings/TasksSettingsTab.tsx new file mode 100644 index 0000000..55423e1 --- /dev/null +++ b/src/components/settings/view/tabs/tasks-settings/TasksSettingsTab.tsx @@ -0,0 +1,106 @@ +import { useTranslation } from 'react-i18next'; +import { useTasksSettings } from '../../../../../contexts/TasksSettingsContext'; + +type TasksSettingsContextValue = { + tasksEnabled: boolean; + setTasksEnabled: (enabled: boolean) => void; + isTaskMasterInstalled: boolean | null; + isCheckingInstallation: boolean; +}; + +export default function TasksSettingsTab() { + const { t } = useTranslation('settings'); + const { + tasksEnabled, + setTasksEnabled, + isTaskMasterInstalled, + isCheckingInstallation, + } = useTasksSettings() as TasksSettingsContextValue; + + return ( +
+ {isCheckingInstallation ? ( +
+
+
+ {t('tasks.checking')} +
+
+ ) : ( + <> + {!isTaskMasterInstalled && ( +
+
+
+ + + +
+
+
+ {t('tasks.notInstalled.title')} +
+
+

{t('tasks.notInstalled.description')}

+ +
+ {t('tasks.notInstalled.installCommand')} +
+ + + +
+

{t('tasks.notInstalled.afterInstallation')}

+
    +
  1. {t('tasks.notInstalled.steps.restart')}
  2. +
  3. {t('tasks.notInstalled.steps.autoAvailable')}
  4. +
  5. {t('tasks.notInstalled.steps.initCommand')}
  6. +
+
+
+
+
+
+ )} + + {isTaskMasterInstalled && ( +
+
+
+
+
{t('tasks.settings.enableLabel')}
+
{t('tasks.settings.enableDescription')}
+
+
+
+ )} + + )} +
+ ); +}