EnvEditor.tsx
132 lines1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import { useState, useCallback } from 'react';
interface EnvPair {
key: string;
value: string;
}
interface EnvEditorProps {
env: string; // JSON string like '{"KEY": "VALUE"}'
actionUrl: string; // POST URL for saving
}
function parseEnv(env: string): EnvPair[] {
try {
const obj = JSON.parse(env || '{}');
const pairs = Object.entries(obj).map(([key, value]) => ({
key,
value: String(value),
}));
return pairs.length > 0 ? pairs : [{ key: '', value: '' }];
} catch {
return [{ key: '', value: '' }];
}
}
function serializeEnv(pairs: EnvPair[]): string {
const obj: Record<string, string> = {};
for (const p of pairs) {
const k = p.key.trim();
if (k) obj[k] = p.value;
}
return JSON.stringify(obj, null, 2);
}
export function EnvEditor({ env, actionUrl }: EnvEditorProps) {
const [pairs, setPairs] = useState<EnvPair[]>(() => parseEnv(env));
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const update = useCallback((index: number, field: 'key' | 'value', val: string) => {
setPairs(prev => prev.map((p, i) => i === index ? { ...p, [field]: val } : p));
setSaved(false);
}, []);
const addRow = useCallback(() => {
setPairs(prev => [...prev, { key: '', value: '' }]);
}, []);
const removeRow = useCallback((index: number) => {
setPairs(prev => {
const next = prev.filter((_, i) => i !== index);
return next.length > 0 ? next : [{ key: '', value: '' }];
});
setSaved(false);
}, []);
const save = useCallback(async () => {
setSaving(true);
try {
const body = new FormData();
body.append('env', serializeEnv(pairs));
const res = await fetch(actionUrl, { method: 'POST', body });
if (res.ok) {
setSaved(true);
setTimeout(() => setSaved(false), 2000);
}
} finally {
setSaving(false);
}
}, [pairs, actionUrl]);
return (
<div>
<div className="space-y-1.5">
{pairs.map((pair, i) => (
<div key={i} className="flex items-center gap-1.5">
<input
type="text"
value={pair.key}
onChange={e => update(i, 'key', e.target.value)}
placeholder="KEY"
spellCheck={false}
className="flex-1 font-mono text-[12px] text-slate-600 bg-slate-50 border border-slate-200 rounded-lg px-2.5 py-1.5 focus:outline-none focus:border-indigo-300 focus:ring-1 focus:ring-indigo-100 transition-colors placeholder:text-slate-300"
/>
<input
type="text"
value={pair.value}
onChange={e => update(i, 'value', e.target.value)}
placeholder="value"
spellCheck={false}
className="flex-[1.5] font-mono text-[12px] text-slate-600 bg-slate-50 border border-slate-200 rounded-lg px-2.5 py-1.5 focus:outline-none focus:border-indigo-300 focus:ring-1 focus:ring-indigo-100 transition-colors placeholder:text-slate-300"
/>
<button
type="button"
onClick={() => removeRow(i)}
className="shrink-0 w-7 h-7 flex items-center justify-center rounded-lg text-slate-300 hover:text-red-400 hover:bg-red-50 transition-colors"
title="Remove"
>
<svg xmlns="http://www.w3.org/2000/svg" className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
))}
</div>
<div className="flex items-center justify-between mt-2.5">
<button
type="button"
onClick={addRow}
className="inline-flex items-center gap-1 text-[11px] font-medium text-slate-400 hover:text-indigo-500 transition-colors"
>
<svg xmlns="http://www.w3.org/2000/svg" className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
</svg>
Add variable
</button>
<div className="flex items-center gap-2">
{saved && <span className="text-[10px] text-emerald-500 font-medium">Saved</span>}
<button
type="button"
onClick={save}
disabled={saving}
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg text-[11px] font-medium text-white bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 transition-colors"
>
{saving ? 'Saving...' : 'Save'}
</button>
</div>
</div>
<p className="text-[10px] text-slate-400 mt-1.5">Restart required after changes.</p>
</div>
);
}