317 lines
17 KiB
TypeScript
317 lines
17 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>
|
|
</>
|
|
);
|
|
}
|