fix(ui): simplify config header and add explicit save button

- Reduce config.yaml header from 26 lines to 2 lines
- Replace auto-save on blur with explicit Save button in Auth section
- Add hasChanges detection to enable/disable Save button
- Remove unused saveApiKey and saveSecret functions
This commit is contained in:
kaitranntt committed 2025-12-25 17:02:14 -05:00
1 parent ed6776aadc
commit 7e031b5097
2 files changed
+35 -76

No files matched your search

+2 -26
View File
@@ -233,32 +233,8 @@ export function loadOrCreateUnifiedConfig(): UnifiedConfig {
* Generate YAML header with helpful comments. * Generate YAML header with helpful comments.
*/ */
function generateYamlHeader(): string { function generateYamlHeader(): string {
return `# ============================================================================ return `# CCS Unified Configuration
# CCS Unified Configuration (config.yaml) # Docs: https://github.com/kaitranntt/ccs
# ============================================================================
# Generated by: ccs migrate
# Documentation: https://github.com/kaitranntt/ccs
#
# This file references your settings - actual env vars are in *.settings.json
# files (matching Claude's ~/.claude/settings.json pattern).
#
# To customize a profile:
# 1. Edit the *.settings.json file directly (e.g., ~/.ccs/glm.settings.json)
# 2. The file format matches Claude's settings.json: { "env": { ... } }\n#
# Structure:
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ profiles - References to *.settings.json files for API providers │
# │ cliproxy - References to *.settings.json files for OAuth providers │
# │ accounts - Isolated Claude instances (managed by 'ccs auth') │
# │ preferences - User preferences (theme, telemetry, auto-update) │
# └─────────────────────────────────────────────────────────────────────────────┘
#
# Usage:
# ccs <profile> Switch to profile
# ccs api add <name> Add new API profile
# ccs cliproxy create Create CLIProxy variant
# ccs migrate --rollback Restore from backup
#
`; `;
} }
+33 -50
View File
@@ -20,6 +20,7 @@ import {
Check, Check,
KeyRound, KeyRound,
ShieldCheck, ShieldCheck,
Save,
} from 'lucide-react'; } from 'lucide-react';
import { useRawConfig } from '../hooks'; import { useRawConfig } from '../hooks';
@@ -92,54 +93,35 @@ export default function AuthSection() {
} }
}, [error]); }, [error]);
// Save API key // Save all changes
const saveApiKey = async () => { const saveChanges = async () => {
if (editedApiKey === null) return; const hasApiKeyChange = editedApiKey !== null && editedApiKey !== tokens?.apiKey.value;
const hasSecretChange =
editedSecret !== null && editedSecret !== tokens?.managementSecret.value;
if (!hasApiKeyChange && !hasSecretChange) return;
try { try {
setSaving(true); setSaving(true);
setError(null); setError(null);
const payload: { apiKey?: string; managementSecret?: string } = {};
if (hasApiKeyChange) payload.apiKey = editedApiKey;
if (hasSecretChange) payload.managementSecret = editedSecret;
const response = await fetch('/api/settings/auth/tokens', { const response = await fetch('/api/settings/auth/tokens', {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apiKey: editedApiKey }), body: JSON.stringify(payload),
}); });
if (!response.ok) { if (!response.ok) {
const data = await response.json(); const data = await response.json();
throw new Error(data.error || 'Failed to save API key'); throw new Error(data.error || 'Failed to save tokens');
} }
setSuccess('API key updated. Restart CLIProxy to apply.'); setSuccess('Tokens updated. Restart CLIProxy to apply.');
setEditedApiKey(null); setEditedApiKey(null);
await fetchTokens();
await fetchRawConfig();
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setSaving(false);
}
};
// Save management secret
const saveSecret = async () => {
if (editedSecret === null) return;
try {
setSaving(true);
setError(null);
const response = await fetch('/api/settings/auth/tokens', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ managementSecret: editedSecret }),
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Failed to save management secret');
}
setSuccess('Management secret updated. Restart CLIProxy to apply.');
setEditedSecret(null); setEditedSecret(null);
await fetchTokens(); await fetchTokens();
await fetchRawConfig(); await fetchRawConfig();
@@ -230,6 +212,11 @@ export default function AuthSection() {
const displayApiKey = editedApiKey ?? tokens.apiKey.value; const displayApiKey = editedApiKey ?? tokens.apiKey.value;
const displaySecret = editedSecret ?? tokens.managementSecret.value; const displaySecret = editedSecret ?? tokens.managementSecret.value;
// Check for unsaved changes
const hasChanges =
(editedApiKey !== null && editedApiKey !== tokens.apiKey.value) ||
(editedSecret !== null && editedSecret !== tokens.managementSecret.value);
return ( return (
<> <>
{/* Toast-style alerts */} {/* Toast-style alerts */}
@@ -281,13 +268,6 @@ export default function AuthSection() {
type={showApiKey ? 'text' : 'password'} type={showApiKey ? 'text' : 'password'}
value={displayApiKey} value={displayApiKey}
onChange={(e) => setEditedApiKey(e.target.value)} onChange={(e) => setEditedApiKey(e.target.value)}
onBlur={() => {
if (editedApiKey !== null && editedApiKey !== tokens.apiKey.value) {
saveApiKey();
} else {
setEditedApiKey(null);
}
}}
placeholder="API key" placeholder="API key"
disabled={saving} disabled={saving}
className="pr-20 font-mono text-sm" className="pr-20 font-mono text-sm"
@@ -339,13 +319,6 @@ export default function AuthSection() {
type={showSecret ? 'text' : 'password'} type={showSecret ? 'text' : 'password'}
value={displaySecret} value={displaySecret}
onChange={(e) => setEditedSecret(e.target.value)} onChange={(e) => setEditedSecret(e.target.value)}
onBlur={() => {
if (editedSecret !== null && editedSecret !== tokens.managementSecret.value) {
saveSecret();
} else {
setEditedSecret(null);
}
}}
placeholder="Management secret" placeholder="Management secret"
disabled={saving} disabled={saving}
className="pr-20 font-mono text-sm" className="pr-20 font-mono text-sm"
@@ -406,7 +379,7 @@ export default function AuthSection() {
</ScrollArea> </ScrollArea>
{/* Footer */} {/* Footer */}
<div className="p-4 border-t bg-background"> <div className="p-4 border-t bg-background flex gap-2">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@@ -415,11 +388,21 @@ export default function AuthSection() {
fetchRawConfig(); fetchRawConfig();
}} }}
disabled={loading || saving} disabled={loading || saving}
className="w-full" className="flex-1"
> >
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} /> <RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
Refresh Refresh
</Button> </Button>
<Button
variant="default"
size="sm"
onClick={saveChanges}
disabled={!hasChanges || saving}
className="flex-1"
>
<Save className={`w-4 h-4 mr-2 ${saving ? 'animate-pulse' : ''}`} />
{saving ? 'Saving...' : 'Save'}
</Button>
</div> </div>
</> </>
); );