Repository navigation
[ENHANCEMENT] Upgrade Oxlint and fix React Compiler errors - #826
Conversation
dd32ad4 to
3acf811
Compare
d20d706 to
495ee90
Compare
ea19e05 to
844d19f
Compare
844d19f to
286ab07
Compare
Should fix1. Table filter row can drift out of line with the columns —
Minor2. Missing equality check in
3. Focus effect with no dependencies —
4. Behavior changes that go beyond lint fixes —
Looks good
|
Nexucis
left a comment
There was a problem hiding this comment.
🤖 This review was written by GitHub Copilot (an AI coding assistant) and posted by @Nexucis, who asked for it. It is based on the diff at
a5119b8plus checks against the PR branch and the published@perses-dev/components/@perses-dev/plugin-systempackages. Please read it as AI-generated feedback, not as a maintainer decision.
Summary
Overall this is a good cleanup. CI is green, and the points from the earlier self-review are fixed in a5119b8. I found nothing that should block merging. Item 1 is a behavior change, and item 2 is a policy question worth discussing.
Should address
1. table/.../EmbeddedPanelOptionsEditor.tsx: the effect now re-runs on every parent render
The latest-ref pattern was replaced by putting onChange in the effect deps. The only caller (ColumnEditor.tsx) passes an inline arrow, so while the spec is empty the effect re-runs (and calls onChange, capturing the current column) on every ColumnEditor render. If the parent applies updates asynchronously, this can overwrite concurrent edits. @perses-dev/plugin-system already exports useEvent, which is compiler-safe because it updates the ref in a layout effect. ResizableDivider already uses it, and it keeps the old behavior:
const handleChange = useEvent(onChange);
useEffect(() => {
if (!panelPlugin || !isSpecEmpty(spec)) return;
handleChange(mergeWithPluginDefaults(panelPlugin, spec));
}, [panelPlugin, spec, handleChange]);2. .oxlintrc.json: should react/todo be "error"?
todo reports React Compiler limitations, not incorrect code. The compiler is not actually enabled in the build: there is no babel-plugin-react-compiler or compiler option in rsbuild.shared.ts or the lockfile. Making todo an error is what forced these rewrites, which have no runtime benefit today:
??=rewritten asx = x ?? []try/finallymoved out intodownloadCsvtry/catchturned into a.then().catch().finally()chain
Suggest "warn" until the compiler is turned on. Note that the rest of the config is complete: refs, set-state-in-render, set-state-in-effect, purity and immutability are already errors through categories.correctness: "error".
3. PR hygiene
-
The title is
[IGNORE], but the PR has fixes users will notice:- TreeNode connectors now follow resizes and reset the opposite border style.
- PieChart now calls
resize({ width, height }). - The table filter row stays aligned after the header row is replaced.
- The Gantt divider gap is correct on the first render.
Consider
[BUGFIX]/[ENHANCEMENT], or at least list these in the description, which is still a single line. The checklist is also unchecked. -
Optional: splitting would make review and revert easier: (a) oxlint/oxfmt bump + formatting churn, (b)
no-explicit-any, (c) React Compiler fixes.
Minor / nits
-
prometheus/.../FilterInputs.tsx(ListboxComponent): the inline ref callback is detached and re-attached on every commit. Each time it runsgetComputedStyle, which forces a style recalculation, and may trigger an extra render. Consider:- a stable callback (
useCallback) with a functional update:setMaxHeight((prev) => (m && m !== prev ? m : prev)) - MUI's
useForkRefto merge the forwarded ref, instead of the hand-writtenref.current = …
- a stable callback (
-
table/src/components/TablePanel.test.tsx:expect(currentHeaderRow).not.toBe(initialHeaderRow)depends on howVirtualizedTable(in@perses-dev/components) builds its inlineTablecomponent. If that changes upstream, the test fails even though the feature still works. Keep only the "the current header row is observed / the old one is not" assertions. -
table/src/components/TablePanel.tsx: theMutationObserverwatches the whole scroll container withsubtree: true, so it fires on every virtualized scroll as rows mount and unmount. The callback is cheap, but it could skip mutation records that don't touch thethead. -
alertmanager/.../AlertTablePanel.tsx:currentKeys(groups.map(...).join('\0')) is now recomputed on every render. Wrap it inuseMemo(..., [groups]). -
datasourcevariable/src/DatasourceVariable.tsx:selectedKindis a new object every render, so the effect still runs every render. This was already the case before, but since this deps array is being changed, useselectedKind.value. -
alertmanager/.../SilenceTablePanel.tsx: mixingawaitwith.then/.catch/.finallyis harder to read.try { … } catch (e) { exceptionSnackbar(e); }followed bysetIsExpiring(false);avoidsfinallyand stays compiler-friendly. -
prometheus/src/components/TreeNode.tsx:ResizeObserverreacts to size changes, not position changes. If a sibling above the node grows, the connector can still be stale (still better than before).- Consider an explicit return type on the
resultStatsuseMemo; the olduseStatehad one.
-
tracingganttchart/.../ResizableDivider.tsx:getComputedStyle(...).columnGapis now read on everymousemove. Read it once when the drag starts. -
piechart/src/PieChartBase.tsx:useMemofor{ width, height }passed to a non-memoizedBoxadds little.- Please explain why
resize({ width, height })was needed (commit "Fix PieChart").
-
Import aliases (
LogQLExtension as createLogQLExtension, and the same in VictoriaLogs):- The Loki and VictoriaLogs factories are internal, so they could be renamed at the source.
- Tempo's
TraceQLExtensionis public (re-exported fromtempo/src/index.ts), so the alias is the right choice there.
-
Small cleanups:
MiniGanttChart/Canvas.tsxmixesheightandCANVAS_HEIGHT; pick one.tracingganttchart/src/test/convert/jaeger.ts: useswitch (tagType).canvas/.../EditorCanvas.tsxstill useseslint-disable, whileStaticListVariablemoved tooxlint-disable.
-
Follow-up idea: the same "previous value" draft-sync code (store the last prop in state, reset the draft when it changes) appears 6 times (alertmanager
LazyTextField, Jaeger ×2, TempoLazyTextInput, VictoriaLogs ×2). A shareduseDraftValue(value)hook would remove the duplication.
Checked and fine
useIdmigration: Perses'useIdis a thin wrapper around React'suseIdwith a prefix. Nothing (tests, e2e, CSS selectors) depends on the old prefixed ids, so only debugging readability is lost.TimeSeriesChartBasepin logic:seriesMappingis a prop, so updating state during render cannot loop, and the behavior matches the old effect.- TablePanel scroll-sync deps: the scroll container (
Scroller: VirtualizedTableContainer) is a stable module-level component, so droppingcolumns/contentDimensionsfrom that effect is safe. - Derived state instead of effects: TreeNode
resultStatswithuseMemo,renderItemhoisted in FlameChart, theQuerySettingsEditorcallback ref, andColumnsEditorid tracking (with new tests) all look good. - Type cleanup (Scatterplot formatter,
markPointData, exemplar params) is a nice improvement.
🤖 Generated by GitHub Copilot. Please double-check before acting on it.
Nexucis
left a comment
There was a problem hiding this comment.
🤖 This follow-up review was written by GitHub Copilot (an AI coding assistant) and posted by @Nexucis, who asked for it. It covers
095ec3eand uses React best-practice guidelines. Please read it as AI-generated feedback, not as a maintainer decision.
Summary
Thanks for the quick follow-up. 095ec3e fixes items 1, 4, 7, 8, 11 and 13 from the previous review. The title and description now list the user-facing changes (item 3), and CI is green. Nothing blocks merging. One policy question is still open (react/todo). The other comments are inline nits; resolve or dismiss each one as you prefer.
Checked in 095ec3e
EmbeddedPanelOptionsEditor:useEvent(onChange)restores the old "always call the latest callback" behavior. The effect no longer re-runs on every parent render. ✅FilterInputs: the callback ref is now stable and uses a functional update. Object refs are now forwarded too, which the old code missed. ✅AlertTablePanel:currentKeysis memoized ongroups. ✅DatasourceVariable: the effect now depends onselectedKind.value, so it no longer runs on every render. ✅ResizableDivider: the gap is read once when a drag starts. ✅ (one inline nit)- Loki / VictoriaLogs: the factories are renamed where they're defined, and no old names are left. Tempo keeps the alias because
TraceQLExtensionis part of its public API. ✅ EditorCanvas: now usesoxlint-disable. ✅
React-specific checks
- State reset during render: these components now store the previous prop in state and reset the draft during render, instead of in an effect:
LazyTextField×2, Jaeger ×2, Tempo, VictoriaLogs ×2,FlameChartPanel,AlertTablePanel, theTablePanelpagination and theTimeSeriesChartBasepin. All of them follow the React docs pattern for adjusting state when a prop changes. Each one is guarded, compares against a stable value (a primitive, a memoized value or a prop), and only updates its own component's state, so I found no loop risk.resolvedDefaultGroupByis auseMemo, so the reference comparison there is safe. QuerySettingsEditor: the callback ref is only attached to the last input. It fires once per added row, andfocusRefstops it from focusing after a delete. ✅FlameChart:handleItemClickwith[]deps only uses state setters, so the empty deps are correct. MovingrenderItemto module scope gives ECharts a stable function. ✅TablePanel: keeping the container in state (ref={setPanelContainer}) means the effects run again when the table comes back after the "No data" view. The old ref-based version didn't do that. ✅
Still open from the previous review
Items 2, 6, 9, 10 and 14 are inline below. Item 15 (a shared useDraftValue hook for the 6 copies of the draft-reset code) can wait for a follow-up PR. I dropped item 5: if VirtualizedTable stopped replacing the header row, the observedElements checks would fail as well, so that assertion just states what the test assumes.
🤖 Generated by GitHub Copilot. Please double-check before acting on it.
| const { parentRef, spacing = 0, onMove } = props; | ||
| const { parentRef, onMove } = props; | ||
| const [isResizing, setResizing] = useState(false); | ||
| const [spacing, setSpacing] = useState(0); |
There was a problem hiding this comment.
Nit (rerender-use-ref-transient-values): spacing is never rendered. It is only read in handleMouseMove, so a ref fits better than state:
const spacingRef = useRef(0);
// handleMouseDown
spacingRef.current = parentRef.current ? parseFloat(getComputedStyle(parentRef.current).columnGap) || 0 : 0;
// handleMouseMove
const offsetX = e.clientX - parentRect.left + spacingRef.current;The compiler rules allow writing a ref in an event handler. There's no extra render today, because setSpacing is batched with setResizing. The change only makes the intent clearer.
| })); | ||
| } | ||
| }, [parentEl, nodeEl, reverse, nodeRef, setConnectorStyle]); | ||
| const updateConnector = (): void => { |
There was a problem hiding this comment.
Nit: updateConnector always returns a new style object, so every ResizeObserver callback re-renders the node, even when top and bottom haven't changed. In a large query tree, resizing the panel re-renders every node. Consider returning prevStyle unchanged when the computed top/bottom and border values are the same.
(From item 10, for reference: ResizeObserver doesn't fire when a node only moves without resizing. Still better than before, and fine to leave as is.)
|
|
||
| if (reportNodeState) { | ||
| reportNodeState(childIdx, 'success'); | ||
| const resultStats = useMemo(() => { |
There was a problem hiding this comment.
Nit (item 10): give this useMemo an explicit type, as the old useState had. Right now the early-return branch infers labelExamples: {} and sortedLabelCards: never[], so the result type is a union of the two branches. You can also move the empty value to module scope so it is the same object every time (rerender-memo-with-default-value):
interface ResultStats {
numSeries: number;
labelExamples: Record<string, Array<{ value: string; count: number }>>;
sortedLabelCards: Array<[string, number]>;
}
const EMPTY_RESULT_STATS: ResultStats = { numSeries: 0, labelExamples: {}, sortedLabelCards: [] };
const resultStats = useMemo<ResultStats>(() => {
if (instantQueryResponse?.status !== 'success') return EMPTY_RESULT_STATS;
// …
}, [instantQueryResponse]);| } finally { | ||
| setIsExpiring(false); | ||
| } | ||
| await amClient |
There was a problem hiding this comment.
Nit (item 9): mixing await with .then/.catch/.finally is harder to read. The compiler limitation is about finally, so a plain try/catch with the reset after it should behave the same and still pass react/todo (please confirm with lint):
try {
await amClient.deleteSilence(expireTarget.id);
setExpireTarget(null);
successSnackbar('Silence expired successfully');
queryClient.invalidateQueries({ queryKey: ['query', 'AlertsQuery'] });
queryClient.invalidateQueries({ queryKey: ['query', 'SilencesQuery'] });
} catch (err) {
exceptionSnackbar(err);
}
setIsExpiring(false);If you keep the promise chain, you can remove async/await and return the chain instead.
|
|
||
| observeHeaderRow(); | ||
| const mutationObserver = new MutationObserver(observeHeaderRow); | ||
| mutationObserver.observe(scrollContainer, { childList: true, subtree: true }); |
There was a problem hiding this comment.
Nit (item 6): with subtree: true, this callback fires on every virtualized scroll, because rows mount and unmount. It's cheap, but each call still runs querySelector. A simple guard skips that until the observed row is detached:
const observeHeaderRow = (): void => {
if (headerRow?.isConnected) {
return;
}
const currentHeaderRow = scrollContainer.querySelector('thead tr');
// …
};| if (typeof ref === 'function') { | ||
| ref(reference); | ||
| } else if (ref) { | ||
| ref.current = reference; |
There was a problem hiding this comment.
Nit (second half of item 4): MUI's useForkRef (@mui/material/utils) does this ref merge for you, for both callback and object refs:
const measureRef = useCallback((node: HTMLUListElement | null) => {
if (!node) return;
const measured = getComputedStyle(node).maxHeight;
setMaxHeight((prev) => (measured && measured !== prev ? measured : prev));
}, []);
const listRef = useForkRef(ref, measureRef);|
|
||
| drawSpans(ctx, width, height, trace, spanColorGenerator); | ||
| }, [width, height, trace, spanColorGenerator]); | ||
| drawSpans(ctx, width, CANVAS_HEIGHT, trace, spanColorGenerator); |
There was a problem hiding this comment.
Nit (item 14): line 49 still defines const height = CANVAS_HEIGHT, while this line uses CANVAS_HEIGHT directly. Pick one: either use height here, or remove the local variable and use CANVAS_HEIGHT everywhere.
|
|
||
| function buildAnyValue(tags: Tag): otlpcommonv1.AnyValue { | ||
| const tagType = tags.type; | ||
| switch (tags.type) { |
There was a problem hiding this comment.
Nit (item 14): tagType is only used in the default error message. switch (tagType) would be more consistent, and TypeScript should still narrow tags through the aliased discriminant (TS ≥ 4.4). Ignore this if type-check disagrees.
- Enforce React Compiler rules as errors - Preserve component state across rerenders and list updates - Add regression tests for draft and column state Signed-off-by: Guillaume LADORME <gladorme@gmail.com> Signed-off-by: Guillaume LADORME <Gladorme@users.noreply.github.com>
Signed-off-by: Guillaume LADORME <gladorme@gmail.com> Signed-off-by: Guillaume LADORME <Gladorme@users.noreply.github.com>
Signed-off-by: Guillaume LADORME <gladorme@gmail.com> Signed-off-by: Guillaume LADORME <Gladorme@users.noreply.github.com>
Signed-off-by: Guillaume <gladorme@gmail.com> Signed-off-by: Guillaume LADORME <Gladorme@users.noreply.github.com>
Signed-off-by: Guillaume <gladorme@gmail.com> Signed-off-by: Guillaume LADORME <Gladorme@users.noreply.github.com>
Signed-off-by: Guillaume LADORME <Gladorme@users.noreply.github.com>
Signed-off-by: Guillaume LADORME <gladorme@gmail.com>
- Table: follow the current header row when syncing filter cell widths, since the virtualized table remounts it on width, row, or column changes - Prometheus: only update the listbox max height when it changes - Time series: focus newly added query settings via a callback ref instead of an effect that ran on every render Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Guillaume LADORME <Gladorme@users.noreply.github.com>
Signed-off-by: Guillaume LADORME <Gladorme@users.noreply.github.com>
095ec3e to
4e1216f
Compare
Nexucis
left a comment
There was a problem hiding this comment.
🤖 This follow-up review was written by GitHub Copilot (an AI coding assistant) and posted by @Nexucis, who asked for it. It covers
4e1216f. Please read it as AI-generated feedback, not as a maintainer decision.
Summary
Since 095ec3e, the branch was rebased onto main. A git range-diff shows the 9 PR commits are unchanged, apart from conflict resolution in two files. No new inline comments: nothing new to flag. CI is green on 4e1216f.
Rebase check
AnnotationTooltip.tsx/TimeSeriesChartBase.tsx(conflict with #855): resolved correctly. #855'sANNOTATION_Y_AXIS, theannotationYAxisIndexparameter andyAxisIndexon the annotation series are all kept. The PR's typedmarkPointData,name: ''entries and typedmouseover/mouseouthandlers are added on top of them. ✅- Code merged from
mainsince the last review (palette selector, pyroscope label lookups, statchart auto sizing, datasource fetch provider): it passes the stricter rules with no changes needed. No@perses-dev/componentsuseIdand noeslint-disablecomments are left in the repository. ✅
Open points
react/todoas"error": answered, it's intentional, to prepare for enabling the compiler. Thanks. The README already says these rules are enforced as errors, so the docs match. A sentence in the PR description would help future readers who wonder why??=andtry/finallywere rewritten.- The 8 nits from the last review are still open:
ResizableDividerref,TreeNodestyle bail-out anduseMemotype,SilenceTablePaneltry/catch,TablePanelisConnectedguard,FilterInputsuseForkRef,Canvasheight,jaeger.tsswitch. None of them block merging. Fix them, or reply/resolve the threads if you'd rather leave them for a follow-up.
Nothing blocks merging from my side.
🤖 Generated by GitHub Copilot. Please double-check before acting on it.
Description
Enforcing stricter rules around React to be compliant with React Compiler rules (https://oxc.rs/blog/2026-08-18-react-compiler-support)
Screenshots
User-facing fixes
resize({ width, height })), instead of letting ECharts measure the container itself.Behavior-preserving changes
Checklist
[<catalog_entry>] <commit message>naming convention using one of thefollowing
catalog_entryvalues:FEATURE,ENHANCEMENT,BUGFIX,BREAKINGCHANGE,DOC,IGNORE.UI Changes