openclaw-dashboard/components/ConfigEditor.tsx
AI Assistant c7f037c58a feat: Complete OpenClaw Dashboard with all features
- Dashboard Overview with real-time status display
- Live Log Viewer (scrollable, filterable)
- Config Editor with JSON syntax highlighting
- Model Switcher for provider management
- Provider Manager for API key configuration
- Quick Actions for common tasks
- API Routes: status, logs, config, actions, models, providers

Tech Stack:
- Next.js 14 (App Router)
- TypeScript
- Tailwind CSS
- shadcn/ui components
- CodeMirror for JSON editing
- Docker support with docker-compose
2026-02-27 05:55:23 +00:00

225 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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>
);
}