All checks were successful
Build and Push Docker Images / docker (push) Successful in 28s
189 lines
7.7 KiB
JavaScript
189 lines
7.7 KiB
JavaScript
import { useState, useEffect } from 'react'
|
|
import { X, Upload } from 'lucide-react'
|
|
import TagInput from './TagInput'
|
|
|
|
const DEFAULTS = { id: '', name: '', host: '', port: '8001', api_key: '', description: '', tags: [] }
|
|
|
|
const FIELDS = [
|
|
{ key: 'name', label: 'Nom affiché', placeholder: 'Mon VPS 1', required: true, type: 'text' },
|
|
{ key: 'id', label: 'Identifiant unique', placeholder: 'vps-1', required: true, type: 'text' },
|
|
{ key: 'host', label: 'IP ou hostname', placeholder: '192.168.1.10', required: true, type: 'text' },
|
|
{ key: 'port', label: 'Port agent', placeholder: '8001', required: true, type: 'number' },
|
|
{ key: 'api_key', label: 'Clé API agent', placeholder: '••••••••', required: true, type: 'password' },
|
|
{ key: 'description', label: 'Description', placeholder: 'Optionnel', required: false, type: 'text' },
|
|
]
|
|
|
|
export default function AddVpsModal({ onSave, onClose }) {
|
|
const [mode, setMode] = useState('manual') // 'manual' | 'import'
|
|
const [form, setForm] = useState(DEFAULTS)
|
|
const [saving, setSaving] = useState(false)
|
|
const [error, setError] = useState('')
|
|
const [json, setJson] = useState('')
|
|
const [jsonError, setJsonError] = useState('')
|
|
|
|
useEffect(() => {
|
|
const handler = (e) => { if (e.key === 'Escape') onClose() }
|
|
window.addEventListener('keydown', handler)
|
|
return () => window.removeEventListener('keydown', handler)
|
|
}, [onClose])
|
|
|
|
const set = (key) => (e) => setForm(f => ({ ...f, [key]: e.target.value }))
|
|
|
|
const handleImportJson = () => {
|
|
setJsonError('')
|
|
try {
|
|
const parsed = JSON.parse(json.trim())
|
|
const required = ['id', 'name', 'host', 'port', 'api_key']
|
|
const missing = required.filter(k => !parsed[k])
|
|
if (missing.length > 0) {
|
|
setJsonError(`Champs manquants : ${missing.join(', ')}`)
|
|
return
|
|
}
|
|
setForm({
|
|
id: String(parsed.id),
|
|
name: String(parsed.name),
|
|
host: String(parsed.host),
|
|
port: String(parsed.port),
|
|
api_key: String(parsed.api_key),
|
|
description: String(parsed.description ?? ''),
|
|
tags: Array.isArray(parsed.tags) ? parsed.tags : [],
|
|
})
|
|
setMode('manual')
|
|
} catch {
|
|
setJsonError('JSON invalide. Assurez-vous d\'avoir collé la config exportée.')
|
|
}
|
|
}
|
|
|
|
const handleSubmit = async (e) => {
|
|
e.preventDefault()
|
|
setSaving(true)
|
|
setError('')
|
|
try {
|
|
await onSave({ ...form, port: parseInt(form.port, 10) })
|
|
} catch (err) {
|
|
setError(err.message)
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/75 backdrop-blur-sm"
|
|
onClick={(e) => { if (e.target === e.currentTarget) onClose() }}
|
|
>
|
|
<div className="w-full max-w-md bg-gray-900 border border-gray-700 rounded-xl shadow-2xl">
|
|
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700">
|
|
<div className="flex items-center gap-2">
|
|
<h3 className="font-semibold text-sm">Ajouter un VPS</h3>
|
|
<div className="flex rounded-lg overflow-hidden border border-gray-700 text-xs">
|
|
<button
|
|
type="button"
|
|
onClick={() => setMode('manual')}
|
|
className={`px-2.5 py-1 transition-colors ${mode === 'manual' ? 'bg-indigo-600 text-white' : 'text-gray-400 hover:bg-gray-800'}`}
|
|
>
|
|
Manuel
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setMode('import')}
|
|
className={`px-2.5 py-1 transition-colors flex items-center gap-1 ${mode === 'import' ? 'bg-indigo-600 text-white' : 'text-gray-400 hover:bg-gray-800'}`}
|
|
>
|
|
<Upload size={11} />
|
|
Importer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<button onClick={onClose} className="p-1.5 rounded-lg hover:bg-gray-800 text-gray-500 hover:text-gray-200 transition-colors">
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|
|
|
|
{mode === 'import' ? (
|
|
<div className="p-4 space-y-3">
|
|
<p className="text-xs text-gray-400">
|
|
Collez le JSON copié via le bouton <strong className="text-gray-300">Exporter</strong> d'une autre instance.
|
|
</p>
|
|
<textarea
|
|
value={json}
|
|
onChange={(e) => setJson(e.target.value)}
|
|
rows={10}
|
|
placeholder='{"id": "vps-1", "name": "Mon VPS", ...}'
|
|
className="w-full px-3 py-2 rounded-lg bg-gray-800 border border-gray-700 text-xs font-mono placeholder-gray-600 focus:outline-none focus:border-indigo-500 transition-colors resize-none"
|
|
/>
|
|
{jsonError && (
|
|
<p className="text-xs text-red-400 bg-red-950/30 border border-red-900/40 rounded-lg px-3 py-2">
|
|
{jsonError}
|
|
</p>
|
|
)}
|
|
<div className="flex gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="flex-1 py-2 rounded-lg border border-gray-700 hover:bg-gray-800 text-sm transition-colors"
|
|
>
|
|
Annuler
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleImportJson}
|
|
disabled={!json.trim()}
|
|
className="flex-1 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-sm transition-colors font-medium flex items-center justify-center gap-1.5"
|
|
>
|
|
<Upload size={13} />
|
|
Importer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<form onSubmit={handleSubmit} className="p-4 space-y-3">
|
|
{FIELDS.map(({ key, label, placeholder, required, type }) => (
|
|
<div key={key}>
|
|
<label className="block text-xs text-gray-400 mb-1">
|
|
{label} {required && <span className="text-red-400">*</span>}
|
|
</label>
|
|
<input
|
|
type={type}
|
|
value={form[key]}
|
|
onChange={set(key)}
|
|
placeholder={placeholder}
|
|
required={required}
|
|
className="w-full px-3 py-2 rounded-lg bg-gray-800 border border-gray-700 text-sm placeholder-gray-600 focus:outline-none focus:border-indigo-500 transition-colors"
|
|
/>
|
|
</div>
|
|
))}
|
|
|
|
{error && (
|
|
<p className="text-xs text-red-400 bg-red-950/30 border border-red-900/40 rounded-lg px-3 py-2">
|
|
{error}
|
|
</p>
|
|
)}
|
|
|
|
<div>
|
|
<label className="block text-xs text-gray-400 mb-1">Tags</label>
|
|
<TagInput tags={form.tags} onChange={tags => setForm(f => ({ ...f, tags }))} />
|
|
<p className="text-xs text-gray-600 mt-1">Entrée ou virgule pour valider</p>
|
|
</div>
|
|
|
|
<div className="flex gap-2 pt-1">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="flex-1 py-2 rounded-lg border border-gray-700 hover:bg-gray-800 text-sm transition-colors"
|
|
>
|
|
Annuler
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={saving}
|
|
className="flex-1 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-sm transition-colors font-medium"
|
|
>
|
|
{saving ? 'Enregistrement…' : 'Ajouter'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|