Files
device-management/resources/js/pages/devices/form.tsx
T
Wian Drs 1e80be180e
tests / ci (push) Has been cancelled
Initial commit device-management
2026-08-25 09:42:00 +07:00

216 lines
13 KiB
TypeScript

import { Form, Head, Link } from '@inertiajs/react';
import { useState } from 'react';
import InputError from '@/components/input-error';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
type Item = { id: number; name: string; slug?: string };
type Model = Item & { device_vendor_id: number; device_type_id: number };
type Props = {
device: (Record<string, unknown> & { connection_ports?: Record<string, number> }) | null;
vendors: Item[];
types: Item[];
models: Model[];
};
export default function DeviceForm({ device, vendors, types, models }: Props) {
const editing = !!device;
const initialVendor = vendors.find((vendor) => vendor.id === Number(device?.device_vendor_id));
const [vendorSlug, setVendorSlug] = useState(initialVendor?.slug ?? '');
const fields = [
['name', 'Nama Perangkat'],
['management_address', 'IP / Domain Management'],
['location', 'Lokasi'],
];
const protocolProfiles: Record<string, Array<[string, string, number]>> = {
mikrotik: [['routeros_api', 'Port RouterOS API', 8728]],
zte: [['ssh', 'Port SSH', 22]],
hsgq: [['ssh', 'Port SSH', 22], ['telnet', 'Port Telnet', 23]],
hisfocus: [['telnet', 'Port Telnet', 23], ['web_http', 'Port WebGUI HTTP', 80]],
};
const flexibleProtocols: Array<[string, string, number]> = [
['ssh', 'Port SSH', 22], ['telnet', 'Port Telnet', 23],
['web_http', 'Port WebGUI HTTP', 80], ['snmp', 'Port SNMP', 161],
['vendor_api', 'Port Vendor API', 0],
];
const protocols = protocolProfiles[vendorSlug] ?? flexibleProtocols;
return (
<>
<Head title={editing ? 'Edit Perangkat' : 'Tambah Perangkat'} />
<div className="mx-auto flex w-full max-w-5xl flex-1 flex-col gap-6 p-4 md:p-6">
<div>
<h1 className="text-2xl font-semibold">
{editing ? 'Edit' : 'Tambah'} Perangkat
</h1>
<p className="text-sm text-muted-foreground">
Data teknis lainnya akan diambil otomatis saat perangkat
pertama kali terhubung.
</p>
</div>
<Form
action={editing ? `/devices/${device.id}` : '/devices'}
method={editing ? 'put' : 'post'}
>
{({ processing, errors }) => (
<Card>
<CardHeader>
<CardTitle>Informasi Perangkat</CardTitle>
</CardHeader>
<CardContent className="grid gap-5 md:grid-cols-2">
{fields.map(([name, label]) => (
<div key={name} className="grid gap-2">
<Label>{label}</Label>
<Input
name={name}
type="text"
defaultValue={String(
device?.[name] ?? '',
)}
required={[
'name',
'management_address',
].includes(name)}
/>
<InputError message={errors[name]} />
</div>
))}
<div className="grid gap-2">
<Label>Vendor</Label>
<select
name="device_vendor_id"
defaultValue={String(
device?.device_vendor_id ?? '',
)}
required
onChange={(event) => {
const selected = vendors.find((vendor) => vendor.id === Number(event.target.value));
setVendorSlug(selected?.slug ?? '');
}}
className="h-10 rounded-md border bg-background px-3"
>
<option value="">Pilih vendor</option>
{vendors.map((x) => (
<option key={x.id} value={x.id}>
{x.name}
</option>
))}
</select>
<InputError
message={errors.device_vendor_id}
/>
</div>
<div className="grid gap-2">
<Label>Tipe</Label>
<select
name="device_type_id"
defaultValue={String(
device?.device_type_id ?? '',
)}
required
className="h-10 rounded-md border bg-background px-3"
>
<option value="">Pilih tipe</option>
{types.map((x) => (
<option key={x.id} value={x.id}>
{x.name}
</option>
))}
</select>
<InputError
message={errors.device_type_id}
/>
</div>
<div className="grid gap-2">
<Label>Model</Label>
<select
name="device_model_id"
defaultValue={String(
device?.device_model_id ?? '',
)}
className="h-10 rounded-md border bg-background px-3"
>
<option value="">Tanpa model</option>
{models.map((x) => (
<option key={x.id} value={x.id}>
{x.name}
</option>
))}
</select>
</div>
<div className="rounded-lg border p-4 md:col-span-2">
<h3 className="font-medium">Koneksi Management</h3>
<p className="mb-4 text-sm text-muted-foreground">Kosongkan port jika protokol tersebut tidak tersedia pada perangkat.</p>
<InputError message={errors.connection_ports} />
<div className="grid gap-4 md:grid-cols-2">
{protocols.map(([protocol, label, defaultPort]) => (
<div key={`${vendorSlug}-${protocol}`} className="grid gap-2">
<Label>{label}</Label>
<Input
name={`connection_ports[${protocol}]`}
type="number"
min={1}
max={65535}
placeholder={defaultPort ? String(defaultPort) : 'Port custom'}
defaultValue={device?.connection_ports?.[protocol] ?? ''}
/>
<InputError message={errors[`connection_ports.${protocol}`]} />
</div>
))}
</div>
</div>
<div className="grid gap-2 md:col-span-2">
<Label>Catatan</Label>
<textarea
name="notes"
defaultValue={String(
device?.notes ?? '',
)}
className="min-h-24 rounded-md border bg-background p-3"
/>
</div>
{!editing && (
<>
<div className="grid gap-2">
<Label>Username Awal</Label>
<Input name="initial_username" required autoComplete="off" />
<InputError message={errors.initial_username} />
</div>
<div className="grid gap-2">
<Label>Password Awal</Label>
<Input name="initial_password" type="password" autoComplete="new-password" />
<p className="text-xs text-muted-foreground">Boleh kosong untuk perangkat MikroTik yang masih memakai password kosong.</p>
<InputError message={errors.initial_password} />
</div>
<div className="grid gap-2 md:col-span-2">
<Label>Enable Password (opsional)</Label>
<Input name="initial_enable_password" type="password" autoComplete="new-password" />
<p className="text-xs text-muted-foreground">Khusus perangkat CLI seperti ZTE C300/C320. Default pabrik tertentu adalah zxr10; isi sesuai kondisi perangkat.</p>
<InputError message={errors.initial_enable_password} />
</div>
<label className="flex items-start gap-3 rounded-lg border p-4 md:col-span-2">
<input type="checkbox" name="remove_legacy_users_on_activation" value="1" className="mt-1" />
<span>
<span className="block font-medium">Hapus user lama setelah koneksi pertama</span>
<span className="block text-sm text-muted-foreground">Dijalankan hanya setelah Base User RADIQ berhasil dibuat dan diuji login. User grup full tetap dipertahankan untuk mencegah perangkat terkunci.</span>
</span>
</label>
</>
)}
<div className="flex justify-end gap-3 md:col-span-2">
<Button variant="outline" asChild>
<Link href="/devices">Batal</Link>
</Button>
<Button disabled={processing}>
Simpan Perangkat
</Button>
</div>
</CardContent>
</Card>
)}
</Form>
</div>
</>
);
}