fix(ServerHealthSection): add debounce to linesCount input to prevent rapid API calls

- Introduce linesCountRaw state to capture raw input values
- Add handleLinesCountChange callback with 300ms debounce delay
- Reuse existing filterDebounceRef pattern with linesCountDebounceRef
- Guard against zero/negative values by enforcing minimum of 100 lines
- Update Select component to use debounced value and new handler
- Add comprehensive test coverage for debounce behavior and input validation

Fixes TASK-BUG-09: Typing '500' in the Lines field now fires single API
request instead of three (one per keystroke). This mirrors the existing
debounce pattern used for the filter input.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-23 08:24:58 +02:00
parent 1510dfc851
commit f0caa24d91
2 changed files with 128 additions and 3 deletions

View File

@@ -54,6 +54,9 @@ const AUTO_REFRESH_INTERVALS: { label: string; value: number }[] = [
/** Debounce delay for the filter input in milliseconds. */
const FILTER_DEBOUNCE_MS = 300;
/** Debounce delay for the lines count input in milliseconds. */
const LINES_COUNT_DEBOUNCE_MS = 300;
/** Log targets that are not file paths — file-based viewing is unavailable. */
const NON_FILE_TARGETS = new Set(["STDOUT", "STDERR", "SYSLOG", "SYSTEMD-JOURNAL"]);
@@ -179,6 +182,7 @@ export function ServerHealthSection(): React.JSX.Element {
const [isRefreshing, setIsRefreshing] = useState(false);
// ---- toolbar state -------------------------------------------------------
const [linesCountRaw, setLinesCountRaw] = useState<string>("200");
const [linesCount, setLinesCount] = useState<number>(200);
const [filterRaw, setFilterRaw] = useState<string>("");
const [filterValue, setFilterValue] = useState<string>("");
@@ -189,6 +193,7 @@ export function ServerHealthSection(): React.JSX.Element {
// ---- refs ----------------------------------------------------------------
const logContainerRef = useRef<HTMLDivElement>(null);
const filterDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const linesCountDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const autoRefreshTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
// ---- scroll helper -------------------------------------------------------
@@ -259,6 +264,18 @@ export function ServerHealthSection(): React.JSX.Element {
}, FILTER_DEBOUNCE_MS);
}, []);
// ---- lines count debounce ------------------------------------------------
const handleLinesCountChange = useCallback((value: string): void => {
setLinesCountRaw(value);
if (linesCountDebounceRef.current) clearTimeout(linesCountDebounceRef.current);
linesCountDebounceRef.current = setTimeout(() => {
const parsed = Number(value);
// Guard against zero or negative values; use minimum of 100
const validated = Math.max(parsed || 100, 100);
setLinesCount(validated);
}, LINES_COUNT_DEBOUNCE_MS);
}, []);
// ---- render helpers ------------------------------------------------------
const renderLogLine = (line: string, idx: number): React.JSX.Element => {
const severity = detectSeverity(line);
@@ -406,8 +423,8 @@ export function ServerHealthSection(): React.JSX.Element {
{/* Lines count selector */}
<Field label="Lines">
<Select
value={String(linesCount)}
onChange={(_e, d) => { setLinesCount(Number(d.value)); }}
value={linesCountRaw}
onChange={(_e, d) => { handleLinesCountChange(d.value); }}
>
{LINE_COUNT_OPTIONS.map((n) => (
<option key={n} value={String(n)}>