225 lines
6.5 KiB
TypeScript
225 lines
6.5 KiB
TypeScript
|
|
'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>
|
|||
|
|
);
|
|||
|
|
}
|