refactor: extract signals filter builder, add ESLint 10 config, fix low-hanging issues

This commit is contained in:
Sebastian Seedorf
2026-06-04 14:09:12 +02:00
parent cf9bb33ecb
commit 4b05f2968e
34 changed files with 2145 additions and 188 deletions

View File

@@ -12,12 +12,14 @@ export function Header({ title, onEdit, onDelete }: HeaderProps) {
<span className="text-sm font-medium text-gray-200 truncate">{title}</span>
<div className="flex gap-1 ml-2 shrink-0">
<button
aria-label="Edit chart"
onClick={onEdit}
className="text-xs text-gray-400 hover:text-white px-1.5 py-0.5 rounded hover:bg-gray-700"
>
</button>
<button
aria-label="Delete chart"
onClick={onDelete}
className="text-xs text-gray-400 hover:text-red-400 px-1.5 py-0.5 rounded hover:bg-gray-700"
>
@@ -38,7 +40,7 @@ interface CardShellProps extends HeaderProps {
empty: boolean;
children: React.ReactNode;
/** Ref to the div where the uPlot legend will be mounted */
legendContainerRef?: React.RefObject<HTMLDivElement>;
legendContainerRef?: React.RefObject<HTMLDivElement | null>;
}
export function CardShell({

View File

@@ -13,12 +13,14 @@ export default function DividerCard({ title, onEdit, onDelete }: Props) {
<div className="flex-1 h-px bg-gray-600" />
<div className="flex gap-1 shrink-0">
<button
aria-label="Edit divider"
onClick={onEdit}
className="text-xs text-gray-500 hover:text-white px-1.5 py-0.5 rounded hover:bg-gray-700"
>
</button>
<button
aria-label="Delete divider"
onClick={onDelete}
className="text-xs text-gray-500 hover:text-red-400 px-1.5 py-0.5 rounded hover:bg-gray-700"
>

View File

@@ -1,12 +1,9 @@
'use client';
import 'uplot/dist/uPlot.min.css';
import uPlot from 'uplot';
import { useState, useEffect } from 'react';
import { useApp } from '@/lib/context';
import { resolveName } from '@/lib/localization';
import { getColorMap } from '@/lib/colors';
import type { ColorMap } from '@/lib/colors';
import uPlot from 'uplot';
import { CardShell } from './CardShell';
import {
makeYScale,
@@ -17,8 +14,13 @@ import {
} from './plotHelpers';
import { buildSeriesData } from './seriesData';
import { usePlot } from './usePlot';
import type { AlertConfig, ChartConfig, SessionBoundary, SignalRow } from '@/lib/types';
import type { TimeMode } from '@/lib/types';
import type { ColorMap } from '@/lib/colors';
import type { AlertConfig, ChartConfig, SessionBoundary, SignalRow, TimeMode } from '@/lib/types';
import { getColorMap } from '@/lib/colors';
import { useApp } from '@/lib/context';
import { resolveName } from '@/lib/localization';
interface Props {
config: ChartConfig;
@@ -81,7 +83,17 @@ export default function SignalsChart({
el,
);
},
[rows, sessions, alerts, config, timeMode, localeMap],
[
rows,
sessions,
alerts,
config.signal_type,
config.y_min,
config.y_max,
config.y_scale,
timeMode,
localeMap,
],
);
return (

View File

@@ -1,12 +1,15 @@
import { useState, useEffect } from 'react';
import { useApp } from '@/lib/context';
import { resolveName } from '@/lib/localization';
import { formatSI } from '@/lib/formatNumber';
import { getColorMap, getItemColor } from '@/lib/colors';
import type { ColorMap } from '@/lib/colors';
import { CardShell } from './CardShell';
import type { ColorMap } from '@/lib/colors';
import type { ChartConfig, SignalRow } from '@/lib/types';
import { getColorMap, getItemColor } from '@/lib/colors';
import { useApp } from '@/lib/context';
import { formatSI } from '@/lib/formatNumber';
import { resolveName } from '@/lib/localization';
interface Props {
config: ChartConfig;
rows: SignalRow[];

View File

@@ -2,12 +2,14 @@
import 'uplot/dist/uPlot.min.css';
import uPlot from 'uplot';
import { CardShell } from './CardShell';
import { AXIS_BASE, CURSOR_NO_DRAG, makeYScale } from './plotHelpers';
import { formatSI } from '@/lib/formatNumber';
import { usePlot } from './usePlot';
import type { ChartConfig, UpsRow } from '@/lib/types';
import type { TimeMode } from '@/lib/types';
import type { ChartConfig, UpsRow, TimeMode } from '@/lib/types';
import { formatSI } from '@/lib/formatNumber';
interface Props {
config: ChartConfig;

View File

@@ -1,9 +1,16 @@
import type { AlertConfig, ChartConfig, SessionBoundary, SignalRow, UpsRow } from '@/lib/types';
import type { TimeMode } from '@/lib/types';
import UpsChart from './UpsChart';
import DividerCard from './DividerCard';
import SignalsChart from './SignalsChart';
import TableViz from './TableViz';
import DividerCard from './DividerCard';
import UpsChart from './UpsChart';
import type {
AlertConfig,
ChartConfig,
SessionBoundary,
SignalRow,
UpsRow,
TimeMode,
} from '@/lib/types';
export interface ChartCardProps {
config: ChartConfig;

View File

@@ -1,8 +1,9 @@
import uPlot from 'uplot';
import type { ChartConfig } from '@/lib/types';
import { formatSI } from '@/lib/formatNumber';
import type { ColorMap } from '@/lib/colors';
import type { ChartConfig } from '@/lib/types';
import type uPlot from 'uplot';
import { getItemColor } from '@/lib/colors';
import { formatSI } from '@/lib/formatNumber';
const SEMANTIC_GREEN = '#4ade80';
const SEMANTIC_RED = '#f87171';

View File

@@ -1,5 +1,4 @@
import type { SignalRow, ChartConfig } from '@/lib/types';
import type { TimeMode } from '@/lib/types';
import type { SignalRow, ChartConfig, TimeMode } from '@/lib/types';
const MAX_SERIES = 80;
@@ -29,20 +28,25 @@ export function buildSeriesData(
? parseInt(row.game_tick, 10)
: new Date(row.real_time).getTime() / 1000;
if (!seriesMap.has(key)) seriesMap.set(key, new Map());
seriesMap.get(key)!.set(x, val);
seriesMap.get(key)?.set(x, val);
}
}
if (seriesMap.size === 0) return null;
const keys = [...seriesMap.keys()].slice(0, MAX_SERIES);
const allXs = [...new Set(keys.flatMap((k) => [...seriesMap.get(k)!.keys()]))].sort(
(a, b) => a - b,
);
const allXs = [
...new Set(
keys.flatMap((k) => {
const m = seriesMap.get(k);
return m ? [...m.keys()] : [];
}),
),
].sort((a, b) => a - b);
const data = keys.map((k) => {
const m = seriesMap.get(k)!;
return allXs.map((x) => m.get(x)); // undefined = gap
const m = seriesMap.get(k);
return m ? allXs.map((x) => m.get(x)) : []; // undefined = gap
});
return { keys, allXs, data };

View File

@@ -1,11 +1,12 @@
import { useCallback, useEffect, useRef } from 'react';
import uPlot from 'uplot';
import { useCallback, useEffect, useRef, type DependencyList } from 'react';
import type uPlot from 'uplot';
export type BuildFn = (
el: HTMLDivElement,
w: number,
h: number,
legendRef: React.RefObject<HTMLDivElement>,
legendRef: React.RefObject<HTMLDivElement | null>,
) => uPlot | null;
/** Converts a data index to the pixel x position uPlot expects for setCursor */
@@ -24,14 +25,13 @@ function idxToPixel(plot: uPlot, idx: number): number {
*/
export function usePlot(
build: BuildFn,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
deps: any[],
deps: DependencyList,
): {
containerRef: React.RefObject<HTMLDivElement | null>;
legendRef: React.RefObject<HTMLDivElement>;
legendRef: React.RefObject<HTMLDivElement | null>;
} {
const containerRef = useRef<HTMLDivElement | null>(null);
const legendRef = useRef<HTMLDivElement>(null!);
const legendRef = useRef<HTMLDivElement>(null);
const plotRef = useRef<uPlot | null>(null);
const lastIdxRef = useRef<number>(0);
@@ -64,7 +64,8 @@ export function usePlot(
});
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
// deps is intentionally dynamic — passed by parent to allow external rebuild triggers
// eslint-disable-next-line react-x/exhaustive-deps
}, deps);
useEffect(() => {