Gates / frontend (push) Successful in 1m22s
Gates / test (push) Successful in 1m54s
Gates / test-aarch64 (push) Successful in 7m57s
Gates / package (push) Successful in 5m29s
Gates / container (push) Successful in 10s
CI / gates (push) Successful in 32m51s
162 lines
4.2 KiB
TypeScript
162 lines
4.2 KiB
TypeScript
import { useState, type FormEvent } from "react";
|
|
import * as stylex from "@stylexjs/stylex";
|
|
import InlineError from "@/lib/InlineError";
|
|
import type { Upstream, UpstreamInput } from "@/lib/types";
|
|
import { styles as shared } from "@/ui/styles";
|
|
import { colors } from "@/ui/tokens.stylex";
|
|
|
|
const DEFAULT_PRIORITY = "100";
|
|
|
|
interface UpstreamFormProps {
|
|
initial?: Upstream;
|
|
busy: boolean;
|
|
error: Error | null;
|
|
onSubmit: (input: UpstreamInput) => Promise<void>;
|
|
onCancel?: () => void;
|
|
}
|
|
|
|
const styles = stylex.create({
|
|
form: {
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: "0.75rem",
|
|
marginTop: "1rem",
|
|
maxWidth: "36rem",
|
|
},
|
|
heading: {
|
|
fontSize: "1.125rem",
|
|
lineHeight: "1.75rem",
|
|
fontWeight: 500,
|
|
},
|
|
fieldLabel: {
|
|
display: "block",
|
|
fontSize: "0.875rem",
|
|
lineHeight: "1.25rem",
|
|
fontWeight: 500,
|
|
},
|
|
hint: {
|
|
marginTop: "0.25rem",
|
|
fontSize: "0.75rem",
|
|
lineHeight: "1rem",
|
|
color: colors.textMuted,
|
|
},
|
|
checkboxLabel: {
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "0.5rem",
|
|
fontSize: "0.875rem",
|
|
lineHeight: "1.25rem",
|
|
fontWeight: 500,
|
|
},
|
|
actions: {
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "0.5rem",
|
|
},
|
|
cancelButton: {
|
|
fontWeight: 500,
|
|
},
|
|
});
|
|
|
|
export default function UpstreamForm({ initial, busy, error, onSubmit, onCancel }: UpstreamFormProps) {
|
|
const [url, setUrl] = useState(initial?.url ?? "");
|
|
const [priority, setPriority] = useState(initial === undefined ? DEFAULT_PRIORITY : String(initial.priority));
|
|
const [enabled, setEnabled] = useState(initial?.enabled ?? true);
|
|
const [tlsName, setTlsName] = useState(initial?.tls_name ?? "");
|
|
|
|
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
|
event.preventDefault();
|
|
const parsed = Number(priority);
|
|
try {
|
|
// PUT replaces the row, so every field goes on every submit.
|
|
await onSubmit({
|
|
url: url.trim(),
|
|
priority: Number.isFinite(parsed) ? parsed : 0,
|
|
enabled,
|
|
tls_name: tlsName.trim(),
|
|
});
|
|
if (initial === undefined) {
|
|
setUrl("");
|
|
setPriority(DEFAULT_PRIORITY);
|
|
setEnabled(true);
|
|
setTlsName("");
|
|
}
|
|
} catch {
|
|
// The page renders the mutation error inline below the form.
|
|
}
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} {...stylex.props(styles.form)}>
|
|
<h2 {...stylex.props(styles.heading)}>{initial === undefined ? "Add upstream" : `Edit ${initial.url}`}</h2>
|
|
<div>
|
|
<label htmlFor="upstream-url" {...stylex.props(styles.fieldLabel)}>
|
|
URL
|
|
</label>
|
|
<input
|
|
id="upstream-url"
|
|
type="text"
|
|
required
|
|
value={url}
|
|
onChange={(event) => setUrl(event.target.value)}
|
|
placeholder="udp://1.1.1.1:53"
|
|
{...stylex.props(shared.input, shared.focusRing)}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="upstream-priority" {...stylex.props(styles.fieldLabel)}>
|
|
Priority
|
|
</label>
|
|
<input
|
|
id="upstream-priority"
|
|
type="number"
|
|
min={0}
|
|
value={priority}
|
|
onChange={(event) => setPriority(event.target.value)}
|
|
{...stylex.props(shared.input, shared.focusRing)}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="upstream-tls-name" {...stylex.props(styles.fieldLabel)}>
|
|
TLS name
|
|
</label>
|
|
<input
|
|
id="upstream-tls-name"
|
|
type="text"
|
|
value={tlsName}
|
|
onChange={(event) => setTlsName(event.target.value)}
|
|
placeholder="one.one.one.one"
|
|
{...stylex.props(shared.input, shared.focusRing)}
|
|
/>
|
|
<p {...stylex.props(styles.hint)}>
|
|
The SNI and certificate name for a <code>tls://</code> upstream. Leave empty for every other scheme.
|
|
</p>
|
|
</div>
|
|
<label {...stylex.props(styles.checkboxLabel)}>
|
|
<input
|
|
type="checkbox"
|
|
checked={enabled}
|
|
onChange={(event) => setEnabled(event.target.checked)}
|
|
{...stylex.props(shared.focusRing)}
|
|
/>
|
|
Enabled
|
|
</label>
|
|
<div {...stylex.props(styles.actions)}>
|
|
<button type="submit" disabled={busy} {...stylex.props(shared.primaryButton, shared.focusRing)}>
|
|
{initial === undefined ? "Add upstream" : "Save changes"}
|
|
</button>
|
|
{onCancel !== undefined && (
|
|
<button
|
|
type="button"
|
|
onClick={onCancel}
|
|
{...stylex.props(shared.button, styles.cancelButton, shared.focusRing)}
|
|
>
|
|
Cancel
|
|
</button>
|
|
)}
|
|
</div>
|
|
<InlineError error={error} />
|
|
</form>
|
|
);
|
|
}
|