first commit
This commit is contained in:
1 parent
abb09fbea8
commit
72f6d49818
355 files changed
+121239
-3
No files matched your search
@@ -0,0 +1,434 @@
|
||||
"use client";
|
||||
|
||||
import { Eye, EyeOff, Loader2, RocketIcon, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { servers as serversApi, Server as ServerType } from "@/lib/api";
|
||||
|
||||
function createServerFormState(server?: ServerType) {
|
||||
return {
|
||||
name: server?.name ?? "",
|
||||
ip: server?.ip ?? "",
|
||||
sshPort: server?.sshPort ?? 22,
|
||||
username: server?.username ?? "root",
|
||||
authType: (server?.authType ?? "PASSWORD") as "PASSWORD" | "SSH_KEY",
|
||||
password: "",
|
||||
sshKey: "",
|
||||
location: server?.location ?? "",
|
||||
tags: server?.tags.join(", ") ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
interface ServerFormModalProps {
|
||||
server?: ServerType;
|
||||
onClose: () => void;
|
||||
onSaved: (message: string) => void;
|
||||
}
|
||||
|
||||
export default function ServerFormModal({
|
||||
server,
|
||||
onClose,
|
||||
onSaved,
|
||||
}: ServerFormModalProps) {
|
||||
const isEdit = Boolean(server);
|
||||
const [form, setForm] = useState(() => createServerFormState(server));
|
||||
const [showPw, setShowPw] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const submit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setLoading(true);
|
||||
try {
|
||||
const payload = {
|
||||
name: form.name,
|
||||
ip: form.ip,
|
||||
sshPort: form.sshPort,
|
||||
username: form.username,
|
||||
authType: form.authType,
|
||||
password:
|
||||
form.authType === "PASSWORD" && form.password.trim()
|
||||
? form.password
|
||||
: undefined,
|
||||
sshKey:
|
||||
form.authType === "SSH_KEY" && form.sshKey.trim()
|
||||
? form.sshKey
|
||||
: undefined,
|
||||
location: form.location || undefined,
|
||||
tags: form.tags
|
||||
? form.tags
|
||||
.split(",")
|
||||
.map((t: string) => t.trim())
|
||||
.filter(Boolean)
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (!isEdit && form.authType === "PASSWORD" && !form.password.trim()) {
|
||||
throw new Error("Password is required for PASSWORD auth type");
|
||||
}
|
||||
|
||||
if (!isEdit && form.authType === "SSH_KEY" && !form.sshKey.trim()) {
|
||||
throw new Error("SSH key is required for SSH_KEY auth type");
|
||||
}
|
||||
|
||||
if (server) {
|
||||
await serversApi.update(server.id, payload);
|
||||
onSaved(`Server \"${form.name}\" updated successfully`);
|
||||
} else {
|
||||
await serversApi.create(payload);
|
||||
onSaved(`Server \"${form.name}\" added successfully`);
|
||||
}
|
||||
onClose();
|
||||
} catch (err: unknown) {
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: `Failed to ${isEdit ? "update" : "add"} server`,
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-overlay">
|
||||
<div
|
||||
className="modal animate-slide-in"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: 520,
|
||||
padding: 28,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<h3
|
||||
style={{
|
||||
color: "var(--text-primary)",
|
||||
fontWeight: 700,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{isEdit ? "Edit Server" : "Add Server"}
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
color: "var(--text-muted)",
|
||||
}}
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
{error && (
|
||||
<div
|
||||
style={{
|
||||
background: "rgba(239,68,68,0.1)",
|
||||
border: "1px solid rgba(239,68,68,0.3)",
|
||||
borderRadius: 8,
|
||||
padding: "10px 14px",
|
||||
marginBottom: 14,
|
||||
fontSize: 13,
|
||||
color: "#ef4444",
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<form
|
||||
onSubmit={submit}
|
||||
style={{ display: "flex", flexDirection: "column", gap: 14 }}
|
||||
>
|
||||
<div
|
||||
style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: "var(--text-muted)",
|
||||
display: "block",
|
||||
marginBottom: 5,
|
||||
}}
|
||||
>
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
className="input"
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
placeholder="prod-01"
|
||||
required
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: "var(--text-muted)",
|
||||
display: "block",
|
||||
marginBottom: 5,
|
||||
}}
|
||||
>
|
||||
IP Address *
|
||||
</label>
|
||||
<input
|
||||
className="input"
|
||||
value={form.ip}
|
||||
onChange={(e) => setForm({ ...form, ip: e.target.value })}
|
||||
placeholder="192.168.1.10"
|
||||
required
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: "var(--text-muted)",
|
||||
display: "block",
|
||||
marginBottom: 5,
|
||||
}}
|
||||
>
|
||||
SSH Port *
|
||||
</label>
|
||||
<input
|
||||
className="input"
|
||||
type="number"
|
||||
value={form.sshPort}
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, sshPort: parseInt(e.target.value) })
|
||||
}
|
||||
min={1}
|
||||
max={65535}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: "var(--text-muted)",
|
||||
display: "block",
|
||||
marginBottom: 5,
|
||||
}}
|
||||
>
|
||||
Username *
|
||||
</label>
|
||||
<input
|
||||
className="input"
|
||||
value={form.username}
|
||||
onChange={(e) => setForm({ ...form, username: e.target.value })}
|
||||
placeholder="root"
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: "var(--text-muted)",
|
||||
display: "block",
|
||||
marginBottom: 5,
|
||||
}}
|
||||
>
|
||||
Auth Type *
|
||||
</label>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
{(["PASSWORD", "SSH_KEY"] as const).map((t) => (
|
||||
<button
|
||||
type="button"
|
||||
key={t}
|
||||
onClick={() => setForm({ ...form, authType: t })}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "8px 0",
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${form.authType === t ? "var(--accent)" : "var(--border)"}`,
|
||||
background:
|
||||
form.authType === t
|
||||
? "rgba(59,130,246,0.1)"
|
||||
: "var(--bg-input)",
|
||||
color:
|
||||
form.authType === t
|
||||
? "var(--accent)"
|
||||
: "var(--text-muted)",
|
||||
cursor: "pointer",
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{t === "PASSWORD" ? "Password" : "SSH Key"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{form.authType === "PASSWORD" && (
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: "var(--text-muted)",
|
||||
display: "block",
|
||||
marginBottom: 5,
|
||||
}}
|
||||
>
|
||||
Password {isEdit ? "(leave blank to keep current)" : "*"}
|
||||
</label>
|
||||
<div style={{ position: "relative" }}>
|
||||
<input
|
||||
className="input"
|
||||
type={showPw ? "text" : "password"}
|
||||
value={form.password}
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, password: e.target.value })
|
||||
}
|
||||
required={!isEdit}
|
||||
style={{ width: "100%", paddingRight: 36 }}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPw(!showPw)}
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 10,
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
color: "var(--text-muted)",
|
||||
}}
|
||||
>
|
||||
{showPw ? <EyeOff size={14} /> : <Eye size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{form.authType === "SSH_KEY" && (
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: "var(--text-muted)",
|
||||
display: "block",
|
||||
marginBottom: 5,
|
||||
}}
|
||||
>
|
||||
SSH Private Key {isEdit ? "(leave blank to keep current)" : "*"}
|
||||
</label>
|
||||
<textarea
|
||||
className="input"
|
||||
value={form.sshKey}
|
||||
onChange={(e) => setForm({ ...form, sshKey: e.target.value })}
|
||||
required={!isEdit}
|
||||
rows={6}
|
||||
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----"
|
||||
style={{
|
||||
width: "100%",
|
||||
fontFamily: "monospace",
|
||||
fontSize: 11,
|
||||
resize: "vertical",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: "var(--text-muted)",
|
||||
display: "block",
|
||||
marginBottom: 5,
|
||||
}}
|
||||
>
|
||||
Location
|
||||
</label>
|
||||
<input
|
||||
className="input"
|
||||
value={form.location}
|
||||
onChange={(e) => setForm({ ...form, location: e.target.value })}
|
||||
placeholder="Singapore"
|
||||
list="location-suggestions"
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
<datalist id="location-suggestions">
|
||||
<option value="Indonesia" />
|
||||
<option value="Malaysia" />
|
||||
<option value="Singapore" />
|
||||
<option value="India" />
|
||||
<option value="Japan" />
|
||||
<option value="Germany" />
|
||||
<option value="United States" />
|
||||
</datalist>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: "var(--text-muted)",
|
||||
display: "block",
|
||||
marginBottom: 5,
|
||||
}}
|
||||
>
|
||||
Tags (comma-separated)
|
||||
</label>
|
||||
<input
|
||||
className="input"
|
||||
value={form.tags}
|
||||
onChange={(e) => setForm({ ...form, tags: e.target.value })}
|
||||
placeholder="production, web"
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 10, marginTop: 4 }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="btn"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={loading}
|
||||
style={{
|
||||
flex: 2,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
{loading && <Loader2 size={14} className="animate-spin" />}
|
||||
<RocketIcon size={14} />
|
||||
{isEdit ? "Save Changes" : "Add Server"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user