Step 6: Retune the sample rate
This step is two small files. SampleRateForm.jsx owns the input, validation, and RPC call. SampleRateDialog.jsx is a modal shell that wraps the form.
The form
Create dashboard/src/components/SampleRateForm.jsx. The highlighted lines are the RelayX integration. Everything else is validation, local state, and styling.
import { useEffect, useRef, useState } from "react";
import { app, DEVICE_IDENT } from "../relayx.js";
const MIN_MS = 200;
const MAX_MS = 10 * 60 * 1000;
const DEFAULT_MS = 10000;
export function SampleRateForm({ open = true, onCancel, onSuccess }) {
const [rate, setRate] = useState(String(DEFAULT_MS));
const [pending, setPending] = useState(false);
const [error, setError] = useState(null);
const inputRef = useRef(null);
useEffect(() => {
if (!open) return;
setError(null);
setPending(false);
setRate(String(DEFAULT_MS));
const id = setTimeout(() => inputRef.current?.focus(), 40);
return () => clearTimeout(id);
}, [open]);
async function handleSubmit() {
const parsed = parseInt(rate, 10);
if (!Number.isFinite(parsed)) {
setError("Rate must be a number");
return;
}
if (parsed < MIN_MS || parsed > MAX_MS) {
setError(`Rate must be between ${MIN_MS} and ${MAX_MS} ms`);
return;
}
setPending(true);
setError(null);
try {
const result = await app.rpc.call({
device_ident: DEVICE_IDENT,
name: "updateSampleRate",
timeout: 10,
data: { rate: parsed },
});
if (result?.status !== "ok") {
const msg =
result?.data?.message ||
result?.message ||
`Device returned status "${result?.status ?? "unknown"}"`;
throw new Error(msg);
}
onSuccess?.(parsed);
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setPending(false);
}
}
// Styled card with header, body (input + inline error), and footer (Cancel + Update).
// Full source including inline styles is in the reference repo.
return (
<div style={{ width: 420, background: "var(--bg-1)", borderRadius: 12 }}>
<div style={{ padding: "16px 20px", borderBottom: "1px solid var(--line)" }}>
Set sample rate
</div>
<div style={{ padding: 20 }}>
<input
ref={inputRef}
type="number"
min={MIN_MS}
max={MAX_MS}
value={rate}
onChange={(e) => setRate(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") handleSubmit();
if (e.key === "Escape") onCancel?.();
}}
/>
{error && <div style={{ color: "var(--red)", marginTop: 8 }}>{error}</div>}
</div>
<div style={{ padding: "14px 20px", borderTop: "1px solid var(--line)", display: "flex", justifyContent: "flex-end", gap: 8 }}>
{onCancel && <button onClick={onCancel} disabled={pending}>Cancel</button>}
<button onClick={handleSubmit} disabled={pending}>
{pending ? "Updating…" : "Update"}
</button>
</div>
</div>
);
}
The validation is local and does not involve the device. Out-of-range values show an inline error before the RPC is even attempted. RPC failures flow through the same error state so the user sees one feedback surface regardless of where the failure came from.
onSuccess is a callback so the parent can decide what to do next: close a modal, show a toast, or both.
The modal
Create dashboard/src/components/SampleRateDialog.jsx:
import { SampleRateForm } from "./SampleRateForm.jsx";
export function SampleRateDialog({ open, onClose, onSuccess }) {
if (!open) return null;
const handleSuccess = (rateMs) => {
onClose();
onSuccess?.(rateMs);
};
return (
<div
role="dialog"
aria-modal="true"
onClick={onClose}
style={{
position: "fixed",
inset: 0,
zIndex: 900,
background: "rgba(21,17,10,0.4)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<div onClick={(e) => e.stopPropagation()}>
<SampleRateForm open={open} onCancel={onClose} onSuccess={handleSuccess} />
</div>
</div>
);
}
The dialog is pure modal chrome: overlay, click-outside-to-close, and a stop-propagation wrapper around the form. All form behavior, validation, and the RPC call live in SampleRateForm.
Test it
Add a button somewhere that opens the dialog. A minimal check:
import { useState } from "react";
import { SampleRateDialog } from "./components/SampleRateDialog.jsx";
function Example() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Set sample rate</button>
<SampleRateDialog
open={open}
onClose={() => setOpen(false)}
onSuccess={(rate) => console.log(`Applied rate: ${rate}`)}
/>
</>
);
}
With the device running and the relay closed, open the dialog and set the rate to 250. Press Update. The charts start updating four times per second. The dialog closes. Set it back to 10000 and the cadence returns to once every 10 seconds. The device never restarted.