refactor: extract signals filter builder, add ESLint 10 config, fix low-hanging issues
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useApp } from '@/lib/context';
|
||||
import { fetchAlerts, createAlert, updateAlert, deleteAlert } from '@/lib/api';
|
||||
import { resolveName, resolveKey } from '@/lib/localization';
|
||||
|
||||
import type { AlertConfig } from '@/lib/types';
|
||||
|
||||
import { fetchAlerts, createAlert, updateAlert, deleteAlert } from '@/lib/api';
|
||||
import { useApp } from '@/lib/context';
|
||||
import { resolveName, resolveKey } from '@/lib/localization';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
@@ -121,6 +123,7 @@ function AlertForm({
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
aria-label={submitLabel}
|
||||
onClick={onSubmit}
|
||||
className="flex-1 bg-indigo-600 hover:bg-indigo-500 text-white text-sm rounded py-1.5"
|
||||
>
|
||||
@@ -128,6 +131,7 @@ function AlertForm({
|
||||
</button>
|
||||
{onCancel && (
|
||||
<button
|
||||
aria-label="Cancel"
|
||||
onClick={onCancel}
|
||||
className="px-3 py-1.5 text-sm text-gray-400 hover:text-white rounded hover:bg-gray-700"
|
||||
>
|
||||
@@ -224,7 +228,11 @@ export default function AlertPanel({ open, onClose }: Props) {
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700">
|
||||
<span className="font-semibold text-white">Alerts</span>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-white">
|
||||
<button
|
||||
aria-label="Close alerts panel"
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-white"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
@@ -288,12 +296,14 @@ export default function AlertPanel({ open, onClose }: Props) {
|
||||
</div>
|
||||
<div className="flex gap-1 ml-2 shrink-0">
|
||||
<button
|
||||
aria-label="Edit alert"
|
||||
onClick={() => startEdit(a)}
|
||||
className="text-gray-500 hover:text-indigo-400"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
aria-label="Delete alert"
|
||||
onClick={() => handleDelete(a.id)}
|
||||
className="text-gray-500 hover:text-red-400"
|
||||
>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { ChartConfig } from '@/lib/types';
|
||||
|
||||
import { useApp } from '@/lib/context';
|
||||
import { resolveKey } from '@/lib/localization';
|
||||
import type { ChartConfig } from '@/lib/types';
|
||||
|
||||
type DraftChart = Omit<ChartConfig, 'id'>;
|
||||
|
||||
|
||||
@@ -2,10 +2,15 @@
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import GridLayout from 'react-grid-layout';
|
||||
|
||||
import ChartCard from './ChartCard';
|
||||
import ChartEditor from './ChartEditor';
|
||||
|
||||
import type { ChartConfig, SignalRow, UpsRow, SessionBoundary, AlertConfig } from '@/lib/types';
|
||||
import type { Layout, LayoutItem } from 'react-grid-layout';
|
||||
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
import 'react-resizable/css/styles.css';
|
||||
import { useApp } from '@/lib/context';
|
||||
import {
|
||||
fetchCharts,
|
||||
createChart,
|
||||
@@ -15,9 +20,7 @@ import {
|
||||
fetchSessions,
|
||||
fetchUps,
|
||||
} from '@/lib/api';
|
||||
import type { ChartConfig, SignalRow, UpsRow, SessionBoundary, AlertConfig } from '@/lib/types';
|
||||
import ChartCard from './ChartCard';
|
||||
import ChartEditor from './ChartEditor';
|
||||
import { useApp } from '@/lib/context';
|
||||
|
||||
const COLS = 6;
|
||||
const ROW_HEIGHT = 80;
|
||||
@@ -211,6 +214,7 @@ export default function Dashboard({ alerts }: Props) {
|
||||
</GridLayout>
|
||||
|
||||
<button
|
||||
aria-label="Create chart"
|
||||
onClick={() => setCreatingChart(true)}
|
||||
className="fixed bottom-6 right-6 w-12 h-12 bg-indigo-600 hover:bg-indigo-500 text-white rounded-full text-2xl shadow-lg flex items-center justify-center z-30"
|
||||
>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useApp } from '@/lib/context';
|
||||
import type { TimeRange, TimeMode } from '@/lib/types';
|
||||
|
||||
import { useApp } from '@/lib/context';
|
||||
|
||||
const RANGES: TimeRange[] = ['30m', '1h', '6h', '24h', '7d', '30d'];
|
||||
|
||||
export default function TimeRangeSelector() {
|
||||
|
||||
Reference in New Issue
Block a user