openclaw-dashboard/components/ConfigEditor.tsx

225 lines
6.5 KiB
TypeScript
Raw Normal View History

'use client';
import { useEffect, useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Save, Download, Upload, RefreshCw, Check, X } from 'lucide-react';
import CodeMirror from '@uiw/react-codemirror';
import { json } from '@codemirror/lang-json';
import { oneDark } from '@codemirror/theme-one-dark';
export function ConfigEditor() {
const [config, setConfig] = useState<any>(null);
const [editing, setEditing] = useState(false);
const [editValue, setEditValue] = useState('');
const [saving, setSaving] = useState(false);
const [loading, setLoading] = useState(true);
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const fetchConfig = async () => {
setLoading(true);
try {
const response = await fetch('/api/config');
const data = await response.json();
setConfig(data);
setEditValue(JSON.stringify(data, null, 2));
} catch (error) {
setMessage({ type: 'error', text: 'Failed to load config' });
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchConfig();
}, []);
const handleSave = async () => {
setSaving(true);
setMessage(null);
try {
// Validate JSON
const parsed = JSON.parse(editValue);
const response = await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(parsed),
});
const result = await response.json();
if (result.success) {
setMessage({ type: 'success', text: result.message || 'Config saved successfully!' });
setConfig(parsed);
setEditing(false);
} else {
setMessage({ type: 'error', text: result.message || 'Failed to save config' });
}
} catch (error) {
setMessage({ type: 'error', text: 'Invalid JSON format' });
} finally {
setSaving(false);
}
};
const handleCancel = () => {
setEditValue(JSON.stringify(config, null, 2));
setEditing(false);
setMessage(null);
};
const handleDownload = () => {
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'openclaw-config.json';
a.click();
URL.revokeObjectURL(url);
};
const handleUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
try {
const content = event.target?.result as string;
JSON.parse(content); // Validate
setEditValue(content);
setEditing(true);
setMessage(null);
} catch {
setMessage({ type: 'error', text: 'Invalid JSON file' });
}
};
reader.readAsText(file);
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<RefreshCw className="w-8 h-8 animate-spin text-orange-400" />
</div>
);
}
return (
<div className="space-y-4">
{/* Actions */}
<div className="flex flex-wrap gap-2 items-center">
<Button
onClick={() => setEditing(true)}
disabled={editing}
className="bg-orange-500 hover:bg-orange-600"
>
Edit Config
</Button>
<Button
variant="outline"
onClick={fetchConfig}
disabled={loading}
className="border-zinc-700 hover:bg-zinc-800"
>
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
Reload
</Button>
<Button
variant="outline"
onClick={handleDownload}
className="border-zinc-700 hover:bg-zinc-800"
>
<Download className="w-4 h-4 mr-2" />
Export
</Button>
<Button
variant="outline"
onClick={() => document.getElementById('config-upload')?.click()}
className="border-zinc-700 hover:bg-zinc-800"
>
<Upload className="w-4 h-4 mr-2" />
Import
</Button>
<input
id="config-upload"
type="file"
accept=".json"
onChange={handleUpload}
className="hidden"
/>
</div>
{/* Message */}
{message && (
<Alert variant={message.type === 'error' ? 'destructive' : 'default'} className={
message.type === 'success' ? 'bg-green-500/10 border-green-500/20 text-green-400' : ''
}>
{message.type === 'success' ? (
<Check className="h-4 w-4" />
) : (
<X className="h-4 w-4" />
)}
<AlertDescription>{message.text}</AlertDescription>
</Alert>
)}
{/* Editor */}
<Card className="bg-zinc-900/50 border-zinc-800">
<CardHeader>
<CardTitle>openclaw.json</CardTitle>
</CardHeader>
<CardContent>
<div className="rounded-lg overflow-hidden border border-zinc-700">
<CodeMirror
value={editValue}
height="600px"
theme={oneDark}
extensions={[json()]}
editable={editing}
onChange={setEditValue}
className="text-sm"
/>
</div>
{editing && (
<div className="flex justify-end gap-2 mt-4">
<Button
variant="outline"
onClick={handleCancel}
disabled={saving}
className="border-zinc-700 hover:bg-zinc-800"
>
Cancel
</Button>
<Button
onClick={handleSave}
disabled={saving}
className="bg-green-600 hover:bg-green-700"
>
{saving ? (
<>
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
Saving...
</>
) : (
<>
<Save className="w-4 h-4 mr-2" />
Save & Apply
</>
)}
</Button>
</div>
)}
</CardContent>
</Card>
<div className="text-sm text-zinc-500">
<p> Warning: Editing the config directly restarts the Gateway to apply changes.</p>
</div>
</div>
);
}