Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 34 additions & 16 deletions frontend/components/methods/MethodsMock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,23 @@ function delay(ms: number): Promise<void> {
/** Max methods shown in the sidebar list before paginating (keeps the nav scannable). */
const METHODS_PAGE_SIZE = 14;

function buildInitialValuesForMethod(
def: MethodDef,
prev: Record<string, string>,
): Record<string, string> {
const next: Record<string, string> = {};
for (const p of def.params) {
if (p.hidden) {
next[p.name] = prev[p.name] ?? "";
} else if (p.options?.length) {
Comment on lines +48 to +52
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve hidden report values across method switches

Rebuilding state in buildInitialValuesForMethod from only the newly selected method’s params drops keys that aren’t in that method. For report_module_state, this means hidden dashboard fields are lost as soon as the user switches to another method, and switching back restores them as empty strings instead of preserving prior input. This creates repeated data loss during normal navigation and undermines the hidden-field preservation this change is trying to provide.

Useful? React with 👍 / 👎.

next[p.name] = p.options[0]!.value;
} else {
next[p.name] = "";
}
}
return next;
}

const LIVE_SIGNED_METHOD_IDS = new Set<string>([
"get_protocol_version",
"get_protocol_methods",
Expand Down Expand Up @@ -142,11 +159,7 @@ export function MethodsMock() {

useEffect(() => {
if (!selected) return;
const next: Record<string, string> = {};
selected.params.forEach((p) => {
if (p.options?.length) next[p.name] = p.options[0]!.value;
});
setValues(next);
setValues((prev) => buildInitialValuesForMethod(selected, prev));
}, [selected]);

useEffect(() => {
Expand All @@ -164,18 +177,20 @@ export function MethodsMock() {
setError(null);
setResult(null);
const def = METHOD_CATALOG.find((m) => m.id === id);
const next: Record<string, string> = {};
def?.params.forEach((p) => {
if (p.options?.length) next[p.name] = p.options[0]!.value;
});
setValues(next);
if (def) {
setValues((prev) => buildInitialValuesForMethod(def, prev));
} else {
setValues({});
}
}, []);

const runExecute = useCallback(async () => {
setError(null);
setResult(null);
if (!selected) return;
const missing = selected.params.filter((p) => p.required !== false && !values[p.name]?.trim());
const missing = selected.params.filter(
(p) => p.required !== false && !p.hidden && !values[p.name]?.trim(),
);
if (missing.length > 0) {
setError(`Fill in: ${missing.map((m) => m.label).join(", ")}`);
return;
Expand Down Expand Up @@ -615,6 +630,8 @@ export function MethodsMock() {
afterParams={
selected.id === "report_module_state" ? (
<ReportModuleStateDashboardFields
values={values}
onValueChange={setParam}
dashboard={reportDashboard}
setDashboard={setReportDashboard}
/>
Expand Down Expand Up @@ -679,9 +696,10 @@ function MethodPanel({
[loading, onExecute],
);

const lastParam = method.params[method.params.length - 1];
const visibleParams = method.params.filter((p) => !p.hidden);
const lastParam = visibleParams[visibleParams.length - 1];
const lastFieldEnterTip =
method.params.length === 0
visibleParams.length === 0
? null
: lastParam?.options?.length
? "Last field: Enter runs Execute when the dropdown is closed."
Expand Down Expand Up @@ -724,12 +742,12 @@ function MethodPanel({
</button>
) : null}
</div>
{method.params.length === 0 ? (
{visibleParams.length === 0 ? (
<p className="modulr-text-muted mt-3 text-sm">No parameters for this method.</p>
) : (
<div className="mt-4 space-y-4">
{method.params.map((p, idx) => {
const isLastField = idx === method.params.length - 1;
{visibleParams.map((p, idx) => {
const isLastField = idx === visibleParams.length - 1;
return (
<div key={p.name}>
<label
Expand Down
192 changes: 172 additions & 20 deletions frontend/components/methods/ReportModuleStateDashboardFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,25 @@ import type {
ReportModuleDashboardState,
} from "@/lib/reportModuleStateDetail";
import {
MAX_DASHBOARD_CARDS,
MAX_DASHBOARD_PIES,
FIXED_STANDARD_METRIC_CARD_COUNT,
FIXED_STANDARD_METRIC_CARDS,
HEALTH_ACTIVITY_UI,
MAX_CARD_DESCRIPTION_CHARS,
MAX_CUSTOM_DASHBOARD_CARDS,
MAX_DASHBOARD_PIES,
MAX_PIE_DESCRIPTION_CHARS,
MAX_PIE_SLICES,
NOTES_UI,
VALIDATOR_STATUS_PIE_UI,
} from "@/lib/reportModuleStateDetail";

const inp =
"mt-1 w-full rounded-lg border border-[var(--modulr-glass-border)] bg-[var(--modulr-glass-fill)] px-3 py-2 text-sm text-[var(--modulr-text)] outline-none ring-[var(--modulr-accent)] placeholder:text-[var(--modulr-text-muted)] focus:ring-2";
const ta = `${inp} modulr-scrollbar resize-y font-mono text-xs leading-relaxed`;
const roTitle =
"select-none rounded-md border border-[var(--modulr-glass-border)]/70 bg-[var(--modulr-page-bg)]/35 px-3 py-2 text-sm font-medium text-[var(--modulr-text-muted)]";
const roBody =
"select-none rounded-md border border-[var(--modulr-glass-border)]/70 bg-[var(--modulr-page-bg)]/25 px-3 py-2 text-sm leading-relaxed text-[var(--modulr-text-muted)]";

const emptyCard = (): DashboardCardInput => ({ title: "", value: "", description: "" });
const emptySlice = () => ({ label: "", percent: "" });
Expand All @@ -29,20 +38,137 @@ const emptyPie = (): DashboardPieInput => ({
});

type Props = {
values: Record<string, string>;
onValueChange: (name: string, v: string) => void;
dashboard: ReportModuleDashboardState;
setDashboard: Dispatch<SetStateAction<ReportModuleDashboardState>>;
};

export function ReportModuleStateDashboardFields({ dashboard, setDashboard }: Props) {
export function ReportModuleStateDashboardFields({
values,
onValueChange,
dashboard,
setDashboard,
}: Props) {
return (
<div className="mt-6 space-y-6 border-t border-[var(--modulr-glass-border)] pt-6">
<div>
<div className="mt-6 space-y-8 border-t border-[var(--modulr-glass-border)] pt-6">
<section>
<p className="text-xs font-semibold uppercase tracking-wider text-[var(--modulr-text-muted)]">
Standard metric cards (1–{FIXED_STANDARD_METRIC_CARD_COUNT})
</p>
<p className="modulr-text-muted mt-1 text-[11px] leading-relaxed">
Fixed titles and descriptions match the wire <code className="text-[var(--modulr-text)]">metrics</code> object.
Only the value is editable.
</p>
<div className="mt-3 space-y-4">
{FIXED_STANDARD_METRIC_CARDS.map((row, i) => (
<div
key={row.valueKey}
className="rounded-lg border border-[var(--modulr-glass-border)] bg-[var(--modulr-page-bg)]/15 p-3 sm:p-4"
>
<span className="text-[11px] font-medium text-[var(--modulr-text-muted)]">
Card {i + 1}
</span>
<p className={roTitle}>{row.title}</p>
<p className={`${roBody} mt-2`}>{row.description}</p>
<label className="mt-3 block text-xs font-medium text-[var(--modulr-text-muted)]">
Value
</label>
<input
type="text"
inputMode="numeric"
value={values[row.valueKey] ?? ""}
onChange={(e) =>
onValueChange(row.valueKey, e.target.value.replace(/\D/g, ""))
}
className={inp}
placeholder="non-negative integer"
autoComplete="off"
/>
</div>
))}
</div>
</section>

<section>
<p className="text-xs font-semibold uppercase tracking-wider text-[var(--modulr-text-muted)]">
Validator status (standard pie)
</p>
<p className="modulr-text-muted mt-1 text-[11px] leading-relaxed">
Same data as <code className="text-[var(--modulr-text)]">validator_status_pct</code> on the wire. Total stays in
sync with Card 4 (Validators).
</p>
<div className="mt-3 rounded-lg border border-[var(--modulr-glass-border)] bg-[var(--modulr-page-bg)]/15 p-3 sm:p-4">
<span className="text-[11px] font-medium text-[var(--modulr-text-muted)]">Fixed layout</span>
<p className={roTitle}>{VALIDATOR_STATUS_PIE_UI.title}</p>
<p className={`${roBody} mt-2`}>{VALIDATOR_STATUS_PIE_UI.description}</p>
<label className="mt-3 block text-xs font-medium text-[var(--modulr-text-muted)]">
Total (integer)
</label>
<input
type="text"
inputMode="numeric"
value={values.metric_validators ?? ""}
onChange={(e) =>
onValueChange("metric_validators", e.target.value.replace(/\D/g, ""))
}
className={inp}
placeholder="validator population for this split"
autoComplete="off"
/>
<p className="mt-2 text-[11px] font-medium text-[var(--modulr-text-muted)]">Slices</p>
<div className="mt-2 space-y-2">
{VALIDATOR_STATUS_PIE_UI.slices.map((sl) => (
<div key={sl.pctKey} className="flex flex-wrap items-end gap-2">
<div className="min-w-0 flex-1">
<label className="text-[10px] text-[var(--modulr-text-muted)]">Label</label>
<div className={roTitle}>{sl.label}</div>
</div>
<div className="w-20 shrink-0">
<label className="text-[10px] text-[var(--modulr-text-muted)]">%</label>
<input
type="text"
inputMode="numeric"
value={values[sl.pctKey] ?? ""}
onChange={(e) =>
onValueChange(sl.pctKey, e.target.value.replace(/\D/g, "").slice(0, 3))
}
className={inp}
placeholder="0–100"
autoComplete="off"
/>
</div>
</div>
))}
</div>
</div>
</section>

<section>
<p className="text-xs font-semibold uppercase tracking-wider text-[var(--modulr-text-muted)]">
{NOTES_UI.title} (optional)
</p>
<div className="mt-3 rounded-lg border border-[var(--modulr-glass-border)] bg-[var(--modulr-page-bg)]/15 p-3 sm:p-4">
<p className={roBody}>{NOTES_UI.description}</p>
<textarea
value={values[NOTES_UI.valueKey] ?? ""}
onChange={(e) => onValueChange(NOTES_UI.valueKey, e.target.value)}
rows={3}
className={`${ta} mt-2`}
placeholder="Optional"
spellCheck
/>
</div>
</section>

<section>
<p className="text-xs font-semibold uppercase tracking-wider text-[var(--modulr-text-muted)]">
Dashboard cards
Additional dashboard cards (7–10)
</p>
<p className="modulr-text-muted mt-1 text-[11px] leading-relaxed">
Up to {MAX_DASHBOARD_CARDS} cards — title, integer value, short description (≤{MAX_CARD_DESCRIPTION_CHARS}{" "}
chars). Empty title rows are skipped.
Up to {MAX_CUSTOM_DASHBOARD_CARDS} extra cards for the wire{" "}
<code className="text-[var(--modulr-text)]">dashboard_cards</code> array (≥1 required). Empty title rows are
skipped.
</p>
<div className="mt-3 space-y-4">
{dashboard.cards.map((card, i) => (
Expand All @@ -51,7 +177,9 @@ export function ReportModuleStateDashboardFields({ dashboard, setDashboard }: Pr
className="rounded-lg border border-[var(--modulr-glass-border)] bg-[var(--modulr-page-bg)]/15 p-3 sm:p-4"
>
<div className="flex flex-wrap items-center justify-between gap-2">
<span className="text-[11px] font-medium text-[var(--modulr-text-muted)]">Card {i + 1}</span>
<span className="text-[11px] font-medium text-[var(--modulr-text-muted)]">
Card {FIXED_STANDARD_METRIC_CARD_COUNT + i + 1}
</span>
{dashboard.cards.length > 1 ? (
<button
type="button"
Expand Down Expand Up @@ -79,7 +207,7 @@ export function ReportModuleStateDashboardFields({ dashboard, setDashboard }: Pr
})
}
className={inp}
placeholder="e.g. Active jobs"
placeholder="e.g. Providers spotlight"
autoComplete="off"
/>
<label className="mt-2 block text-xs font-medium text-[var(--modulr-text-muted)]">Value</label>
Expand All @@ -104,7 +232,10 @@ export function ReportModuleStateDashboardFields({ dashboard, setDashboard }: Pr
onChange={(e) =>
setDashboard((d) => {
const cards = [...d.cards];
cards[i] = { ...cards[i]!, description: e.target.value.slice(0, MAX_CARD_DESCRIPTION_CHARS) };
cards[i] = {
...cards[i]!,
description: e.target.value.slice(0, MAX_CARD_DESCRIPTION_CHARS),
};
return { ...d, cards };
})
}
Expand All @@ -119,7 +250,7 @@ export function ReportModuleStateDashboardFields({ dashboard, setDashboard }: Pr
</div>
))}
</div>
{dashboard.cards.length < MAX_DASHBOARD_CARDS ? (
{dashboard.cards.length < MAX_CUSTOM_DASHBOARD_CARDS ? (
<button
type="button"
className="mt-3 text-xs font-medium text-[var(--modulr-accent)] hover:underline"
Expand All @@ -128,15 +259,14 @@ export function ReportModuleStateDashboardFields({ dashboard, setDashboard }: Pr
+ Add card
</button>
) : null}
</div>
</section>

<div>
<section>
<p className="text-xs font-semibold uppercase tracking-wider text-[var(--modulr-text-muted)]">
Dashboard pie charts
Additional pie charts
</p>
<p className="modulr-text-muted mt-1 text-[11px] leading-relaxed">
Up to {MAX_DASHBOARD_PIES} pies. Each has a total metric name, total count (integer), optional chart
description, and up to {MAX_PIE_SLICES} named slices whose % values sum to 100.
Up to {MAX_DASHBOARD_PIES} custom pies for <code className="text-[var(--modulr-text)]">dashboard_pies</code>.
</p>
<div className="mt-3 space-y-5">
{dashboard.pies.map((pie, pi) => (
Expand All @@ -145,7 +275,9 @@ export function ReportModuleStateDashboardFields({ dashboard, setDashboard }: Pr
className="rounded-lg border border-[var(--modulr-glass-border)] bg-[var(--modulr-page-bg)]/15 p-3 sm:p-4"
>
<div className="flex flex-wrap items-center justify-between gap-2">
<span className="text-[11px] font-medium text-[var(--modulr-text-muted)]">Pie {pi + 1}</span>
<span className="text-[11px] font-medium text-[var(--modulr-text-muted)]">
Custom pie {pi + 1}
</span>
<button
type="button"
className="text-[11px] font-medium text-[var(--modulr-accent)] hover:underline"
Expand Down Expand Up @@ -189,7 +321,7 @@ export function ReportModuleStateDashboardFields({ dashboard, setDashboard }: Pr
})
}
className={inp}
placeholder="e.g. population this pie summarizes"
placeholder="population this pie summarizes"
autoComplete="off"
/>
<label className="mt-2 block text-xs font-medium text-[var(--modulr-text-muted)]">
Expand Down Expand Up @@ -304,7 +436,27 @@ export function ReportModuleStateDashboardFields({ dashboard, setDashboard }: Pr
+ Add pie chart
</button>
) : null}
</div>
</section>

<section>
<p className="text-xs font-semibold uppercase tracking-wider text-[var(--modulr-text-muted)]">
{HEALTH_ACTIVITY_UI.title}
</p>
<div className="mt-3 rounded-lg border border-[var(--modulr-glass-border)] bg-[var(--modulr-page-bg)]/15 p-3 sm:p-4">
<p className={roBody}>{HEALTH_ACTIVITY_UI.description}</p>
<label className="mt-3 block text-xs font-medium text-[var(--modulr-text-muted)]">
24 hourly values (comma-separated)
</label>
<textarea
value={values[HEALTH_ACTIVITY_UI.valueKey] ?? ""}
onChange={(e) => onValueChange(HEALTH_ACTIVITY_UI.valueKey, e.target.value)}
rows={4}
className={ta}
placeholder="e.g. 0.95, 0.96, … (24 numbers)"
spellCheck={false}
/>
</div>
</section>
</div>
);
}
Loading
Loading