From 324e3c0cf74b0d8cafe54369440d987690483930 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Sat, 19 Sep 2026 06:50:30 +0000 Subject: [PATCH 1/3] [IGNORE] Upgrade Oxlint and fix React Compiler diagnostics - 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 --- .oxlintrc.json | 23 +- README.md | 2 +- STYLEGUIDE.md | 2 +- .../src/components/ColumnsEditor.test.tsx | 84 ++++ alertmanager/src/components/ColumnsEditor.tsx | 47 ++- .../src/components/LazyTextField.test.tsx | 42 ++ alertmanager/src/components/LazyTextField.tsx | 8 +- .../explore/AlertManagerSilencesExplorer.tsx | 8 +- .../plugins/alert-table/AlertTablePanel.tsx | 25 +- .../silence-table/SilenceTablePanel.tsx | 25 +- .../settings/EdgeThicknessSettings.tsx | 2 +- canvas/src/contexts/SpecContext.tsx | 4 +- canvas/src/hooks/useEdgeConnect.ts | 2 +- datasourcevariable/src/DatasourceVariable.tsx | 2 +- flamechart/src/components/FlameChart.tsx | 102 ++--- flamechart/src/components/FlameChartPanel.tsx | 9 +- flamechart/src/components/Settings.tsx | 8 +- gaugechart/src/GaugeChartBase.tsx | 8 +- .../plugins/JaegerTraceQueryEditor.test.tsx | 19 + jaeger/src/plugins/JaegerTraceQueryEditor.tsx | 26 +- jaeger/src/plugins/get-trace-data.test.ts | 16 +- loki/src/components/logql-editor.tsx | 4 +- package-lock.json | 386 ++++++++++-------- package.json | 4 +- prometheus/src/components/TreeNode.tsx | 79 ++-- .../filter/FilterInputs.tsx | 14 +- .../plugins/PrometheusDatasourceEditor.tsx | 6 +- statchart/src/StatChartBase.tsx | 2 +- table/src/TableExportAction.tsx | 26 +- .../EmbeddedPanelOptionsEditor.tsx | 9 +- table/src/components/TablePanel.test.tsx | 10 +- table/src/components/TablePanel.tsx | 39 +- tempo/src/components/AttributeFilters.tsx | 8 +- tempo/src/components/TraceQLEditor.tsx | 4 +- timeserieschart/src/QuerySettingsEditor.tsx | 2 +- timeserieschart/src/TimeSeriesChartBase.tsx | 33 +- timeserieschart/src/TimeSeriesChartPanel.tsx | 4 +- timeseriestable/src/components/DataTable.tsx | 2 +- tracetable/src/DataTable.tsx | 2 +- .../GanttTable/ResizableDivider.tsx | 4 +- .../MiniGanttChart/Canvas.tsx | 6 +- .../TracingGanttChart/TracingGanttChart.tsx | 3 +- victorialogs/src/components/logsql-editor.tsx | 4 +- .../VictoriaLogsLogQueryEditor.tsx | 9 +- .../VictoriaLogsTimeSeriesQueryEditor.tsx | 9 +- 45 files changed, 688 insertions(+), 445 deletions(-) create mode 100644 alertmanager/src/components/ColumnsEditor.test.tsx create mode 100644 alertmanager/src/components/LazyTextField.test.tsx diff --git a/.oxlintrc.json b/.oxlintrc.json index 2ec28ddcb..24baa71b9 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -44,7 +44,28 @@ "react/jsx-no-useless-fragment": "warn", "react/no-unescaped-entities": "error", "react/exhaustive-deps": "error", - "react/react-compiler": "warn", + "react/error-boundaries": "error", + "react/globals": "error", + "react/immutability": "error", + "react/incompatible-library": "error", + "react/preserve-manual-memoization": "error", + "react/purity": "error", + "react/refs": "error", + "react/set-state-in-effect": "error", + "react/set-state-in-render": "error", + "react/static-components": "error", + "react/use-memo": "error", + "react/unsupported-syntax": "error", + "react/void-use-memo": "error", + "react/no-deriving-state-in-effects": "error", + "react/invariant": "error", + "react/rule-suppression": "error", + "react/syntax": "error", + "react/todo": "error", + "react/capitalized-calls": "error", + "react/exhaustive-effect-dependencies": "error", + "react/hooks": "error", + "react/memo-dependencies": "error", "react-doctor/no-fetch-in-effect": "warn", "react-doctor/no-derived-state": "warn", "react/jsx-curly-brace-presence": [ diff --git a/README.md b/README.md index 1153f25ea..99bb99622 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Then in [`perses`](https://github.com/perses/perses) repository: Run `npm run lint` for the regular Oxlint checks, including the React Doctor rules configured in `.oxlintrc.json`. Run `npm run doctor` for the full React Doctor project scan. Pull requests and pushes to `main` also run the scan in GitHub -Actions. +Actions. React Compiler rules are enforced as errors; resolve their diagnostics before merging. ### Working with Snapshots diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md index 8600e18d0..886612521 100644 --- a/STYLEGUIDE.md +++ b/STYLEGUIDE.md @@ -37,7 +37,7 @@ touch without expanding the task into unrelated cleanup. for dynamic values passed across memoized boundaries. Do not add memoization blindly or omit dependencies. - Memoize React context values when they contain objects or functions whose identity would otherwise change each render. - Avoid array-index keys when a stable domain identifier exists. -- Treat `react/react-compiler` and `react-perf` diagnostics as design feedback. Fix new warnings rather than disabling +- Treat React Compiler and `react-perf` diagnostics as design feedback. Fix errors and new warnings rather than disabling the rule or increasing the repository warning ceiling. ## Components, state, and accessibility diff --git a/alertmanager/src/components/ColumnsEditor.test.tsx b/alertmanager/src/components/ColumnsEditor.test.tsx new file mode 100644 index 000000000..2a5f84ced --- /dev/null +++ b/alertmanager/src/components/ColumnsEditor.test.tsx @@ -0,0 +1,84 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { fireEvent, render, screen } from '@testing-library/react'; +import type { ReactElement } from 'react'; +import { useCallback, useState } from 'react'; + +import type { BaseColumnDefinition } from './ColumnsEditor'; +import { ColumnsEditor } from './ColumnsEditor'; + +const SORT_MODES = { alphabetical: 'Alphabetical' }; +const INITIAL_COLUMNS = [{ name: 'first' }, { name: 'second' }]; +const getName = (column: BaseColumnDefinition): string => column.name; +const renderName = (column: BaseColumnDefinition): ReactElement => ( + +); +const onUpdate = vi.fn(); + +function Editor(): ReactElement { + const [columns, setColumns] = useState(INITIAL_COLUMNS); + const onAdd = useCallback((): void => setColumns((previous) => [...previous, { name: 'added' }]), []); + const onRemove = useCallback( + (index: number): void => setColumns((previous) => previous.filter((_, i) => i !== index)), + [], + ); + const move = useCallback((index: number, offset: number): void => { + setColumns((previous) => { + const next = [...previous]; + const [column] = next.splice(index, 1); + if (column) next.splice(index + offset, 0, column); + return next; + }); + }, []); + const onMoveUp = useCallback((index: number): void => move(index, -1), [move]); + const onMoveDown = useCallback((index: number): void => move(index, 1), [move]); + + return ( + + ); +} + +describe('ColumnsEditor', () => { + it('keeps local edits with their column when moving, removing, and adding columns', () => { + render(); + const firstInput = screen.getAllByRole('textbox', { name: 'Column name' })[0]!; + fireEvent.change(firstInput, { target: { value: 'draft' } }); + + fireEvent.click(screen.getAllByRole('button', { name: 'Move column down' })[0]!); + expect(screen.getAllByRole('textbox', { name: 'Column name' })[1]).toBe(firstInput); + expect(firstInput).toHaveValue('draft'); + + fireEvent.click(screen.getAllByRole('button', { name: 'Move column up' })[1]!); + expect(screen.getAllByRole('textbox', { name: 'Column name' })[0]).toBe(firstInput); + + fireEvent.click(screen.getAllByRole('button', { name: 'Remove column' })[1]!); + fireEvent.click(screen.getByRole('button', { name: 'Add column' })); + expect(screen.getAllByRole('textbox', { name: 'Column name' })[0]).toBe(firstInput); + expect(firstInput).toHaveValue('draft'); + expect(screen.getAllByRole('textbox', { name: 'Column name' })[1]).toHaveValue('added'); + }); +}); diff --git a/alertmanager/src/components/ColumnsEditor.tsx b/alertmanager/src/components/ColumnsEditor.tsx index e3abb391f..76c906850 100644 --- a/alertmanager/src/components/ColumnsEditor.tsx +++ b/alertmanager/src/components/ColumnsEditor.tsx @@ -32,7 +32,7 @@ import ArrowUpIcon from 'mdi-material-ui/ArrowUp'; import DeleteIcon from 'mdi-material-ui/Delete'; import PlusIcon from 'mdi-material-ui/Plus'; import type { ReactElement } from 'react'; -import { useCallback, useRef } from 'react'; +import { useCallback, useState } from 'react'; export interface BaseColumnDefinition { name: string; @@ -202,22 +202,27 @@ export function ColumnsEditor(props: ColumnsEdit renderNameField, } = props; - const idCounterRef = useRef(0); - const idsRef = useRef([]); - - while (idsRef.current.length < columns.length) { - idsRef.current.push(idCounterRef.current++); + const [columnIds, setColumnIds] = useState(() => ({ + ids: columns.map((_, index) => index), + nextId: columns.length, + })); + if (columnIds.ids.length !== columns.length) { + const ids = columnIds.ids.slice(0, columns.length); + let nextId = columnIds.nextId; + while (ids.length < columns.length) { + ids.push(nextId++); + } + setColumnIds({ ids, nextId }); } - idsRef.current.length = columns.length; const handleAdd = useCallback((): void => { - idsRef.current.push(idCounterRef.current++); + setColumnIds(({ ids, nextId }) => ({ ids: [...ids, nextId], nextId: nextId + 1 })); onAdd(); }, [onAdd]); const handleRemove = useCallback( (index: number): void => { - idsRef.current.splice(index, 1); + setColumnIds((previous) => ({ ...previous, ids: previous.ids.filter((_, i) => i !== index) })); onRemove(index); }, [onRemove], @@ -226,9 +231,12 @@ export function ColumnsEditor(props: ColumnsEdit const handleMoveUp = useCallback( (index: number): void => { if (index <= 0) return; - const ids = idsRef.current; - const id = ids.splice(index, 1)[0]!; - ids.splice(index - 1, 0, id); + setColumnIds((previous) => { + const ids = [...previous.ids]; + const id = ids.splice(index, 1)[0]!; + ids.splice(index - 1, 0, id); + return { ...previous, ids }; + }); onMoveUp(index); }, [onMoveUp], @@ -236,13 +244,16 @@ export function ColumnsEditor(props: ColumnsEdit const handleMoveDown = useCallback( (index: number): void => { - const ids = idsRef.current; - if (index >= ids.length - 1) return; - const id = ids.splice(index, 1)[0]!; - ids.splice(index + 1, 0, id); + if (index >= columns.length - 1) return; + setColumnIds((previous) => { + const ids = [...previous.ids]; + const id = ids.splice(index, 1)[0]!; + ids.splice(index + 1, 0, id); + return { ...previous, ids }; + }); onMoveDown(index); }, - [onMoveDown], + [onMoveDown, columns.length], ); return ( @@ -252,7 +263,7 @@ export function ColumnsEditor(props: ColumnsEdit {description} {columns.map((column, index) => ( - + {index > 0 && } { + it('preserves an uncommitted draft across renders and commits it on blur', () => { + const onCommit = vi.fn(); + const { rerender } = render(); + fireEvent.change(screen.getByRole('textbox'), { target: { value: 'draft' } }); + rerender(); + + expect(screen.getByRole('textbox')).toHaveValue('draft'); + expect(onCommit).not.toHaveBeenCalled(); + fireEvent.blur(screen.getByRole('textbox')); + expect(onCommit).toHaveBeenCalledWith('draft'); + }); + + it('resets a draft when the committed value changes or is cleared', () => { + const onCommit = vi.fn(); + const { rerender } = render(); + fireEvent.change(screen.getByRole('textbox'), { target: { value: 'draft' } }); + rerender(); + expect(screen.getByRole('textbox')).toHaveValue('updated'); + + rerender(); + expect(screen.getByRole('textbox')).toHaveValue(''); + expect(onCommit).not.toHaveBeenCalled(); + }); +}); diff --git a/alertmanager/src/components/LazyTextField.tsx b/alertmanager/src/components/LazyTextField.tsx index 02bcab7fe..0232bb5ee 100644 --- a/alertmanager/src/components/LazyTextField.tsx +++ b/alertmanager/src/components/LazyTextField.tsx @@ -13,7 +13,7 @@ import { TextField } from '@mui/material'; import type { ChangeEvent, ReactElement } from 'react'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useState } from 'react'; export interface LazyTextFieldProps { label: string; @@ -29,9 +29,11 @@ export function LazyTextField(props: LazyTextFieldProps): ReactElement { const { value, onCommit, ...textFieldProps } = props; const [draftValue, setDraftValue] = useState(value ?? ''); - useEffect(() => { + const [previousValue, setPreviousValue] = useState(value); + if (value !== previousValue) { + setPreviousValue(value); setDraftValue(value ?? ''); - }, [value]); + } const handleChange = useCallback((event: ChangeEvent): void => { setDraftValue(event.target.value); diff --git a/alertmanager/src/explore/AlertManagerSilencesExplorer.tsx b/alertmanager/src/explore/AlertManagerSilencesExplorer.tsx index 86ebda342..790f43be4 100644 --- a/alertmanager/src/explore/AlertManagerSilencesExplorer.tsx +++ b/alertmanager/src/explore/AlertManagerSilencesExplorer.tsx @@ -20,7 +20,7 @@ import type { DatasourceSelector, QueryDefinition } from '@perses-dev/spec'; import { useQueryClient } from '@tanstack/react-query'; import BellOffIcon from 'mdi-material-ui/BellOff'; import type { ReactElement } from 'react'; -import { useCallback, useMemo, useRef, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { SilenceForm } from '../components/SilenceForm'; import type { AlertManagerClient, PostableSilence } from '../model'; @@ -50,10 +50,10 @@ function CreateSilenceButton({ queries }: { queries: QueryDefinition[] }): React const { successSnackbar, exceptionSnackbar } = useSnackbar(); const [open, setOpen] = useState(false); - const formKeyRef = useRef(0); + const [formKey, setFormKey] = useState(0); const handleOpen = useCallback(() => { - formKeyRef.current++; + setFormKey((previous) => previous + 1); setOpen(true); }, []); @@ -77,7 +77,7 @@ function CreateSilenceButton({ queries }: { queries: QueryDefinition[] }): React - setOpen(false)} onSubmit={handleSubmit} /> + setOpen(false)} onSubmit={handleSubmit} /> ); } diff --git a/alertmanager/src/plugins/alert-table/AlertTablePanel.tsx b/alertmanager/src/plugins/alert-table/AlertTablePanel.tsx index a1b3edd88..ae5be11fd 100644 --- a/alertmanager/src/plugins/alert-table/AlertTablePanel.tsx +++ b/alertmanager/src/plugins/alert-table/AlertTablePanel.tsx @@ -51,7 +51,7 @@ import MagnifyIcon from 'mdi-material-ui/Magnify'; import UnfoldLessHorizontalIcon from 'mdi-material-ui/UnfoldLessHorizontal'; import UnfoldMoreHorizontalIcon from 'mdi-material-ui/UnfoldMoreHorizontal'; import type { ReactElement } from 'react'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { SilenceForm } from '../../components/SilenceForm'; import { StatusBadge } from '../../components/StatusBadge'; @@ -345,9 +345,9 @@ export function AlertTablePanel({ spec, queryResults, contentDimensions }: Alert const queryClient = useQueryClient(); const [silenceTarget, setSilenceTarget] = useState(null); - const silenceKeyRef = useRef(0); + const [silenceKey, setSilenceKey] = useState(0); const handleSetSilenceTarget = useCallback((alert: Alert) => { - silenceKeyRef.current++; + setSilenceKey((previous) => previous + 1); setSilenceTarget(alert); }, []); const [search, setSearch] = useState(''); @@ -414,9 +414,11 @@ export function AlertTablePanel({ spec, queryResults, contentDimensions }: Alert const [groupBy, setGroupBy] = useState(resolvedDefaultGroupBy); - useEffect(() => { + const [previousDefaultGroupBy, setPreviousDefaultGroupBy] = useState(resolvedDefaultGroupBy); + if (resolvedDefaultGroupBy !== previousDefaultGroupBy) { + setPreviousDefaultGroupBy(resolvedDefaultGroupBy); setGroupBy(resolvedDefaultGroupBy); - }, [resolvedDefaultGroupBy]); + } const effectiveActions = useMemo( () => spec.allowedActions ?? ALL_ALERT_ACTIONS, @@ -517,18 +519,17 @@ export function AlertTablePanel({ spec, queryResults, contentDimensions }: Alert return result; }, [alerts, groupBy, allTrackedKeys, sortState]); - const prevGroupKeysRef = useRef(''); - useEffect(() => { - const currentKeys = groups.map((g) => g.key).join('\0'); - if (currentKeys === prevGroupKeysRef.current) return; - prevGroupKeysRef.current = currentKeys; + const currentKeys = groups.map((g) => g.key).join('\0'); + const [previousGroupKeys, setPreviousGroupKeys] = useState(''); + if (currentKeys !== previousGroupKeys) { + setPreviousGroupKeys(currentKeys); if (groups.length === 1) { setExpandedGroups(new Set(groups.map((g) => g.key))); } else { setExpandedGroups(new Set()); } - }, [groups]); + } const handleToggleGroup = useCallback((key: string) => { setExpandedGroups((prev) => { @@ -699,7 +700,7 @@ export function AlertTablePanel({ spec, queryResults, contentDimensions }: Alert setSilenceTarget(null)} onSubmit={handleSilenceSubmit} diff --git a/alertmanager/src/plugins/silence-table/SilenceTablePanel.tsx b/alertmanager/src/plugins/silence-table/SilenceTablePanel.tsx index 6f1dd5283..efa5ea40e 100644 --- a/alertmanager/src/plugins/silence-table/SilenceTablePanel.tsx +++ b/alertmanager/src/plugins/silence-table/SilenceTablePanel.tsx @@ -148,24 +148,23 @@ export function SilenceTablePanel({ spec, queryResults, contentDimensions }: Sil const handleExpire = useCallback(async () => { if (!amClient || !expireTarget) return; setIsExpiring(true); - 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); - } finally { - setIsExpiring(false); - } + await amClient + .deleteSilence(expireTarget.id) + .then(() => { + setExpireTarget(null); + successSnackbar('Silence expired successfully'); + queryClient.invalidateQueries({ queryKey: ['query', 'AlertsQuery'] }); + queryClient.invalidateQueries({ queryKey: ['query', 'SilencesQuery'] }); + }) + .catch(exceptionSnackbar) + .finally(() => setIsExpiring(false)); }, [amClient, expireTarget, queryClient, successSnackbar, exceptionSnackbar]); const [search, setSearch] = useState(''); const effectiveActions = useMemo( - () => spec?.allowedActions ?? ALL_SILENCE_ACTIONS, - [spec?.allowedActions], + () => spec.allowedActions ?? ALL_SILENCE_ACTIONS, + [spec.allowedActions], ); const showActionsColumn = effectiveActions.length > 0; diff --git a/canvas/src/components/settings/EdgeThicknessSettings.tsx b/canvas/src/components/settings/EdgeThicknessSettings.tsx index becce6a47..27273092b 100644 --- a/canvas/src/components/settings/EdgeThicknessSettings.tsx +++ b/canvas/src/components/settings/EdgeThicknessSettings.tsx @@ -92,7 +92,7 @@ export function EdgeThicknessSettings({ value, onChange }: EdgeThicknessSettings (stepValue: number, strokeWidth: number | undefined): void => { onChange( produce(value, (draft) => { - draft.edgeThresholdWidths ??= []; + draft.edgeThresholdWidths = draft.edgeThresholdWidths ?? []; const existingIdx = draft.edgeThresholdWidths.findIndex((w) => w.value === stepValue); if (strokeWidth !== undefined) { if (existingIdx >= 0) { diff --git a/canvas/src/contexts/SpecContext.tsx b/canvas/src/contexts/SpecContext.tsx index 802fc7af9..4567fc4ab 100644 --- a/canvas/src/contexts/SpecContext.tsx +++ b/canvas/src/contexts/SpecContext.tsx @@ -74,7 +74,7 @@ export function SpecProvider({ spec, onChange, children }: SpecProviderProps): R const id = generateId('node'); onChange( produce(spec, (draft) => { - (draft.nodes ??= []).push({ + (draft.nodes = draft.nodes ?? []).push({ id, position, width: DEFAULT_NODE_WIDTH, @@ -93,7 +93,7 @@ export function SpecProvider({ spec, onChange, children }: SpecProviderProps): R const id = generateId('bg'); onChange( produce(spec, (draft) => { - (draft.backgrounds ??= []).push({ id, position, width, height }); + (draft.backgrounds = draft.backgrounds ?? []).push({ id, position, width, height }); }), ); selectItems(new Set([id])); diff --git a/canvas/src/hooks/useEdgeConnect.ts b/canvas/src/hooks/useEdgeConnect.ts index 2e8aecd94..e044ec325 100644 --- a/canvas/src/hooks/useEdgeConnect.ts +++ b/canvas/src/hooks/useEdgeConnect.ts @@ -199,7 +199,7 @@ export function useEdgeConnect(): UseEdgeConnectResult { if (!snap && sourceNode && pointInsideNode(sourceNode, pt, SNAP_RADIUS)) { return; } - (draft.edges ??= []).push(buildNewEdge(dragEdge, snap, pt)); + (draft.edges = draft.edges ?? []).push(buildNewEdge(dragEdge, snap, pt)); } }, [dragEdge, nodeById], diff --git a/datasourcevariable/src/DatasourceVariable.tsx b/datasourcevariable/src/DatasourceVariable.tsx index af531e818..f7777be63 100644 --- a/datasourcevariable/src/DatasourceVariable.tsx +++ b/datasourcevariable/src/DatasourceVariable.tsx @@ -50,7 +50,7 @@ export const DatasourceVariableOptionEditor = (props: OptionsEditorProps { + const level = api.value(0); + const start = api.coord([api.value(1), level]); + const end = api.coord([api.value(2), level]); + const height = (((api.size && api.size([0, 1])) || [0, 20]) as number[])[1]; + const width = (end?.[0] ?? 0) - (start?.[0] ?? 0); + + return { + type: 'rect', + transition: ['shape'], + shape: { + x: start?.[0], + y: (start?.[1] ?? 0) - (height ?? 0) / 2, + width, + height: (height ?? ITEM_GAP) - ITEM_GAP, + r: 0, + }, + style: { + fill: api.visual('color'), + }, + emphasis: { + style: { + stroke: '#000', + }, + }, + textConfig: { + position: 'insideLeft', + }, + textContent: { + style: { + text: api.value(3), + fill: '#000', + width: width - 4, + overflow: 'truncate', + ellipsis: '..', + truncateMinChar: 1, + }, + emphasis: { + style: { + stroke: '#000', + lineWidth: 0.5, + }, + }, + }, + } as CustomSeriesRenderItemReturn; +}; + export interface FlameChartProps { width: number; height: number; @@ -63,7 +110,7 @@ export function FlameChart(props: FlameChartProps): ReactElement { [palette, data.metadata, data.profile.stackTrace, selectedId, searchValue], ); - const handleItemClick = (params: MouseEventsParameters): void => { + const handleItemClick = useCallback((params: MouseEventsParameters): void => { const data: Sample = params.data; const functionName = data.value[6]; const functionId = data.name; @@ -78,7 +125,7 @@ export function FlameChart(props: FlameChartProps): ReactElement { mouseY: mouseEvent.event.clientY - 4, }); } - }; + }, []); const handleFocusBlock = (): void => { onSelectedIdChange(selectedItem.id); @@ -104,53 +151,6 @@ export function FlameChart(props: FlameChartProps): ReactElement { if (isCopied) setIsCopied(false); }; - const renderItem: CustomSeriesRenderItem = (params: CustomSeriesRenderItemParams, api: CustomSeriesRenderItemAPI) => { - const level = api.value(0); - const start = api.coord([api.value(1), level]); - const end = api.coord([api.value(2), level]); - const height = (((api.size && api.size([0, 1])) || [0, 20]) as number[])[1]; - const width = (end?.[0] ?? 0) - (start?.[0] ?? 0); - - return { - type: 'rect', - transition: ['shape'], - shape: { - x: start?.[0], - y: (start?.[1] ?? 0) - (height ?? 0) / 2, - width, - height: (height ?? ITEM_GAP) - ITEM_GAP, - r: 0, - }, - style: { - fill: api.visual('color'), - }, - emphasis: { - style: { - stroke: '#000', - }, - }, - textConfig: { - position: 'insideLeft', - }, - textContent: { - style: { - text: api.value(3), - fill: '#000', - width: width - 4, - overflow: 'truncate', - ellipsis: '..', - truncateMinChar: 1, - }, - emphasis: { - style: { - stroke: '#000', - lineWidth: 0.5, - }, - }, - }, - } as CustomSeriesRenderItemReturn; - }; - const option: EChartsCoreOption = useMemo(() => { if (data.profile.stackTrace === undefined) return chartsTheme.noDataOption; @@ -234,7 +234,7 @@ export function FlameChart(props: FlameChartProps): ReactElement { }} /> ), - [chartsTheme.echartsTheme, height, option, width], + [chartsTheme.echartsTheme, height, option, width, handleItemClick], ); return ( diff --git a/flamechart/src/components/FlameChartPanel.tsx b/flamechart/src/components/FlameChartPanel.tsx index 6ac9995b2..d06b50ce9 100644 --- a/flamechart/src/components/FlameChartPanel.tsx +++ b/flamechart/src/components/FlameChartPanel.tsx @@ -18,7 +18,7 @@ import type { PanelProps } from '@perses-dev/plugin-system'; import type { ProfileData, StackTrace } from '@perses-dev/spec'; import type { TitleComponentOption } from 'echarts'; import type { FC } from 'react'; -import { useState, useEffect, useMemo } from 'react'; +import { useState, useMemo } from 'react'; import type { FlameChartOptions } from '../flame-chart-model'; import { filterStackTraceById, getMaxDepth } from '../utils/data-transform'; @@ -45,12 +45,13 @@ export const FlameChartPanel: FC = (props) => { // This spec is used to manage settings temporarily const [liveSpec, setLiveSpec] = useState(spec); - // keep liveSpec up to date - useEffect(() => { + const [previousSpec, setPreviousSpec] = useState(spec); + if (spec !== previousSpec) { + setPreviousSpec(spec); setLiveSpec(spec); setSelectedId(0); setSearchValue(''); - }, [spec]); + } const chartsTheme = useChartsTheme(); const flameChartData = useMemo(() => { diff --git a/flamechart/src/components/Settings.tsx b/flamechart/src/components/Settings.tsx index 4ded9580e..2295b0e41 100644 --- a/flamechart/src/components/Settings.tsx +++ b/flamechart/src/components/Settings.tsx @@ -59,10 +59,6 @@ export function Settings(props: SettingsProps): ReactElement { setAnchorEl(null); }; - const isTableSelected = (): boolean => selectedView === 'table'; - const isFlameGraphSelected = (): boolean => selectedView === 'flame-graph'; - const isBothSelected = (): boolean => selectedView === 'both'; - // Update selected view based on the value of showTable and showFlameGraph const selectedView: 'table' | 'flame-graph' | 'both' | 'none' = useMemo(() => { if (!value.showTable && !value.showFlameGraph) { @@ -76,6 +72,10 @@ export function Settings(props: SettingsProps): ReactElement { } }, [value.showTable, value.showFlameGraph]); + const isTableSelected = (): boolean => selectedView === 'table'; + const isFlameGraphSelected = (): boolean => selectedView === 'flame-graph'; + const isBothSelected = (): boolean => selectedView === 'both'; + return ( {selectedId !== 0 && ( diff --git a/gaugechart/src/GaugeChartBase.tsx b/gaugechart/src/GaugeChartBase.tsx index 67df981bd..52da5c3a1 100644 --- a/gaugechart/src/GaugeChartBase.tsx +++ b/gaugechart/src/GaugeChartBase.tsx @@ -12,7 +12,7 @@ // limitations under the License. import type { FormatOptions } from '@perses-dev/components'; -import { EChart, formatValue, useChartsTheme, useDeepMemo } from '@perses-dev/components'; +import { EChart, formatValue, useChartsTheme } from '@perses-dev/components'; import type { GaugeSeriesOption } from 'echarts/charts'; import { GaugeChart as EChartsGaugeChart } from 'echarts/charts'; import { GridComponent, TitleComponent, TooltipComponent } from 'echarts/components'; @@ -20,6 +20,7 @@ import type { EChartsCoreOption } from 'echarts/core'; import { use as registerECharts } from 'echarts/core'; import { CanvasRenderer } from 'echarts/renderers'; import type { ReactElement } from 'react'; +import { useMemo } from 'react'; registerECharts([EChartsGaugeChart, GridComponent, TitleComponent, TooltipComponent, CanvasRenderer]); @@ -49,8 +50,7 @@ export function GaugeChartBase(props: GaugeChartBaseProps): ReactElement { const { width, height, data, format, axisLine, max, valueFontSize, progressWidth, titleFontSize } = props; const chartsTheme = useChartsTheme(); - // useDeepMemo ensures value size util does not rerun everytime you hover on the chart - const option: EChartsCoreOption = useDeepMemo(() => { + const option: EChartsCoreOption = useMemo(() => { if (data.value === undefined) return chartsTheme.noDataOption; // Base configuration shared by both series (= progress & scale) @@ -170,7 +170,7 @@ export function GaugeChartBase(props: GaugeChartBaseProps): ReactElement { }, ], }; - }, [data, width, height, chartsTheme, format, axisLine, max, valueFontSize, progressWidth, titleFontSize]); + }, [data, width, chartsTheme, format, axisLine, max, valueFontSize, progressWidth, titleFontSize]); return ( ; +const initialValue = { service: 'frontend', traceId: 'initial' }; +const updatedValue = { service: 'backend', traceId: 'updated' }; + describe('JaegerTraceQueryEditor', () => { beforeEach(() => { mockedUseDatasourceSelectValueToSelector.mockImplementation( @@ -119,4 +122,20 @@ describe('JaegerTraceQueryEditor', () => { await waitFor(() => expect(searchServices).toHaveBeenCalledTimes(1)); await waitFor(() => expect(searchOperations).toHaveBeenCalledWith('frontend')); }); + it('preserves drafts until committed values change', () => { + mockedUseDatasourceClient.mockReturnValue({ data: undefined } as never); + const onChange = vi.fn(); + const { rerender } = render(); + + fireEvent.change(screen.getByLabelText('Trace ID'), { target: { value: 'draft' } }); + fireEvent.change(screen.getByLabelText('Service'), { target: { value: 'draft-service' } }); + rerender(); + expect(screen.getByLabelText('Trace ID')).toHaveValue('draft'); + expect(screen.getByLabelText('Service')).toHaveValue('draft-service'); + expect(onChange).not.toHaveBeenCalled(); + + rerender(); + expect(screen.getByLabelText('Trace ID')).toHaveValue('updated'); + expect(screen.getByLabelText('Service')).toHaveValue('backend'); + }); }); diff --git a/jaeger/src/plugins/JaegerTraceQueryEditor.tsx b/jaeger/src/plugins/JaegerTraceQueryEditor.tsx index b2ce00aaf..8f4899a84 100644 --- a/jaeger/src/plugins/JaegerTraceQueryEditor.tsx +++ b/jaeger/src/plugins/JaegerTraceQueryEditor.tsx @@ -201,9 +201,11 @@ function LazyTextField(props: LazyTextFieldProps): ReactElement { const { value, onCommit, ...textFieldProps } = props; const [draftValue, setDraftValue] = useState(value ?? ''); - useEffect(() => { + const [previousValue, setPreviousValue] = useState(value); + if (value !== previousValue) { + setPreviousValue(value); setDraftValue(value ?? ''); - }, [value]); + } const handleChange = useCallback((event: ChangeEvent): void => { setDraftValue(event.target.value); @@ -226,9 +228,11 @@ function LazyAutocompleteTextField(props: LazyAutocompleteTextFieldProps): React const { value, options, onCommit, ...textFieldProps } = props; const [draftValue, setDraftValue] = useState(value ?? ''); - useEffect(() => { + const [previousValue, setPreviousValue] = useState(value); + if (value !== previousValue) { + setPreviousValue(value); setDraftValue(value ?? ''); - }, [value]); + } const commitValue = useCallback( (nextValue: string): void => { @@ -271,11 +275,16 @@ function LazyAutocompleteTextField(props: LazyAutocompleteTextFieldProps): React function useServiceOptions(client: JaegerClient | undefined): string[] { const [serviceOptions, setServiceOptions] = useState([]); + const [previousClient, setPreviousClient] = useState(client); + if (client !== previousClient) { + setPreviousClient(client); + if (!client) setServiceOptions([]); + } + useEffect(() => { let ignore = false; if (!client) { - setServiceOptions([]); return; } @@ -306,11 +315,16 @@ function useOperationOptions(client: JaegerClient | undefined, service: string | const [operationOptions, setOperationOptions] = useState([]); const normalizedService = useMemo(() => service?.trim(), [service]); + const [previousSource, setPreviousSource] = useState({ client, normalizedService }); + if (client !== previousSource.client || normalizedService !== previousSource.normalizedService) { + setPreviousSource({ client, normalizedService }); + if (!client || !normalizedService) setOperationOptions([]); + } + useEffect(() => { let ignore = false; if (!client || normalizedService === undefined || normalizedService === '') { - setOperationOptions([]); return; } diff --git a/jaeger/src/plugins/get-trace-data.test.ts b/jaeger/src/plugins/get-trace-data.test.ts index daa7438ed..9160c6d54 100644 --- a/jaeger/src/plugins/get-trace-data.test.ts +++ b/jaeger/src/plugins/get-trace-data.test.ts @@ -90,15 +90,13 @@ const makeClient = (): JaegerClient => { return client; }; -const getDatasource: Mock = vi.fn( - (): DatasourceSpec => ({ - default: false, - plugin: { - kind: 'JaegerDatasource', - spec: datasource, - }, - }), -); +const getDatasource: Mock = vi.fn((): DatasourceSpec => ({ + default: false, + plugin: { + kind: 'JaegerDatasource', + spec: datasource, + }, +})); function createContext(client: JaegerClient): TraceQueryContext { return { diff --git a/loki/src/components/logql-editor.tsx b/loki/src/components/logql-editor.tsx index c42a4f1f1..3c9aa52ef 100644 --- a/loki/src/components/logql-editor.tsx +++ b/loki/src/components/logql-editor.tsx @@ -18,7 +18,7 @@ import type { ReactElement } from 'react'; import { useMemo } from 'react'; import type { CompletionConfig } from './logql-extension'; -import { LogQLExtension } from './logql-extension'; +import { LogQLExtension as createLogQLExtension } from './logql-extension'; export type LogQLEditorProps = Omit & { completionConfig?: CompletionConfig; @@ -30,7 +30,7 @@ export function LogQLEditor(props: LogQLEditorProps): ReactElement { const isDarkMode = theme.palette.mode === 'dark'; const logqlExtension = useMemo(() => { - return LogQLExtension(completionConfig); + return createLogQLExtension(completionConfig); }, [completionConfig]); const codemirrorTheme = useMemo(() => { diff --git a/package-lock.json b/package-lock.json index 0bd85bc3b..fe9f2591d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,8 +65,8 @@ "express": "^4.21.2", "jsdom": "^27.0.0", "mdi-material-ui": "^7.9.2", - "oxfmt": "0.55.0", - "oxlint": "1.70.0", + "oxfmt": "0.68.0", + "oxlint": "1.83.0", "oxlint-plugin-react-doctor": "0.9.12", "react": "^18.3.0", "react-dom": "^18.3.0", @@ -3232,9 +3232,9 @@ } }, "node_modules/@oxfmt/binding-android-arm-eabi": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.55.0.tgz", - "integrity": "sha512-+rFDOqQe5LOWgxrAJaZgLRudr6GQm0wGI6gtu7vVkrdLGjNMUSGbAlaCr8j7F2H2Er97vYQCU8WDb30onqMM1g==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.68.0.tgz", + "integrity": "sha512-dhfYPbzv/h9JgHjNkl2R6sOjUfxDyLGOZVb3g8/ScaTNwwJcYgmHh8kcYFDUhinuy1QAoANCWUvw1jlk+z6gAg==", "cpu": [ "arm" ], @@ -3249,9 +3249,9 @@ } }, "node_modules/@oxfmt/binding-android-arm64": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.55.0.tgz", - "integrity": "sha512-ctulLq8s3x8Zmvw6+iccB09TIKERAklRSmbJ10gk8mlAn05qZxoyo52dj3Hi9IJcmDSwF54fQaTVh2CbL6PInw==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.68.0.tgz", + "integrity": "sha512-v3Njdi6qY0O/5eGfg01ww2w6gTn2mUvZ72Bnx1/UN53A9wruh3Nk6otc3WkgJLkXD4Qgz1SOcVQieH1oD03V9Q==", "cpu": [ "arm64" ], @@ -3266,9 +3266,9 @@ } }, "node_modules/@oxfmt/binding-darwin-arm64": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.55.0.tgz", - "integrity": "sha512-xDQczLH9pw/RBk1h/GH0qcGMm8hQtmtVHBNLSH3lk1gEIR09hZ4L+mJQl4VqiVAvPK9VG9PYrWWuSQLt7xTbiA==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.68.0.tgz", + "integrity": "sha512-ei4MCMzHFREmZwPJ7KuWUB4kBuHdsgDrnXGJVcEAopU7fj7S42I8BChdFILWdHvhFqR08FLJtOfbZIr2CDw0cA==", "cpu": [ "arm64" ], @@ -3283,9 +3283,9 @@ } }, "node_modules/@oxfmt/binding-darwin-x64": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.55.0.tgz", - "integrity": "sha512-JaNoFCkF2CJdGgpPSMbuO9HVyXyoNGIhMHPvp6NYAjeVKw9XEYc0HcUWJLPQa3Q69WV5wMa9m5jPMJPtbLtcRg==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.68.0.tgz", + "integrity": "sha512-UrKgzZxYhwB9DSvTX+vdgl9M32wLUNKJcAKIoiyx/Kzn/zveqi7W6kYVcRroFMhS3Kwz0KhTk3WBeSuQn4YCTg==", "cpu": [ "x64" ], @@ -3300,9 +3300,9 @@ } }, "node_modules/@oxfmt/binding-freebsd-x64": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.55.0.tgz", - "integrity": "sha512-DNbszhpg6S2MIzax5azdHFTTBIVkR5xr8yyRZuA4yoDAwOkzIp3tmldgKZM2+VlT+hJIG0xUksA+elISzMEAfA==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.68.0.tgz", + "integrity": "sha512-6jrEKgpJbilM1QaRv7hEtKXr4p4AK4jvvyOtajwyhu0kOz3e0O7OLnSTk6tBotRqCcUC4ehZRJ1Zx+Y99wieLw==", "cpu": [ "x64" ], @@ -3317,9 +3317,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.55.0.tgz", - "integrity": "sha512-2snoaoRfFFyGnbOcKUK36rREBYxe/Xgz3uHbiA5zbCB/s6R4DQj4mHqYAaWWhgizCUSDxV8cE9zAZ0XleNpKGw==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.68.0.tgz", + "integrity": "sha512-YOIVnKOBaLeGullskS179N12hjSAdFYnzLjOaKiLhAKNWgnShq9w4xRdtmUm6BlnP65l2/EA9Aw/KlftNxDM7Q==", "cpu": [ "arm" ], @@ -3334,9 +3334,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-musleabihf": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.55.0.tgz", - "integrity": "sha512-q1aktHF/WRpSK81BX1dE/9vWrS2jGw1Nax2kb4DBLGAewubCLcoNyp4Zl/NSMgbv3vUS46Z33wIQkBVYOP3PYg==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.68.0.tgz", + "integrity": "sha512-xW5XoEHVNqydPBv2KXvk9lmEzyAlOQHVEazKoXUuAacqekjya+OdiaFjjEBl0oJD02raG8g3TRl9OVCh9PDIHA==", "cpu": [ "arm" ], @@ -3351,13 +3351,16 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-gnu": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.55.0.tgz", - "integrity": "sha512-VD0y36aENezl/3tsclA/4G53Cc7iV+7Uoh7gz4yvcOTaEYBtJpQsE6PKDGTtUtOvGS4kv51ybfXY/nWZejO5IA==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.68.0.tgz", + "integrity": "sha512-QCvYwVVQieu6oyJglAgV9vH/YMDxZyR4cwVSYtoq9oOXd5N+D3TDUBjNwxFrLn5AdcJZOcvn/7IB17vJGp+2Og==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3368,13 +3371,16 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-musl": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.55.0.tgz", - "integrity": "sha512-r8xlKJFcsRmn0H5jZrdORae6RX9jDBrZVvOoxF+bCQtampQJClv80aZEHsv+NsLsp2KCE5ql79O7DpPVzYWpXA==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.68.0.tgz", + "integrity": "sha512-4TVz5iFQ8ndrHnhX50UXiz9BIWAtUSOHJ6Nus4qWFfJBXq/Ed/krXbF/ehJu42BXM5tIvBs99jNIM22s9agY5A==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3385,13 +3391,16 @@ } }, "node_modules/@oxfmt/binding-linux-ppc64-gnu": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.55.0.tgz", - "integrity": "sha512-GRKv/HXHcwIVld/WU61rF0g0R16hl5EJ+ScKdpjevT57lnLnagj/U2YUbXf2mT+2Pg1uCzWC+mvGicPV3CDdLQ==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.68.0.tgz", + "integrity": "sha512-qLe3ao0RP84bnPxBvRI+GnlK/jybo538NWu0Xrm+zYeTmJtpzqLhnnd5BH21NafqKnplbGZjtN1cnrOlWF73lw==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3402,13 +3411,16 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-gnu": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.55.0.tgz", - "integrity": "sha512-rdv57enTiPtpSYRMKfAiEbQb0Puw5t9N7isVinDoo5qeLDScro2gznmZqSgSWbVZRzLisTeCTW8Qwgw0bOHv3A==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.68.0.tgz", + "integrity": "sha512-Yvyl7a6gbb0vM6r925KW2dO+/CmXySO5TVbcX7o/uZJ+d108HFOG0TxIyHApmn5USuj5mhDPxzhiVwOlGP7uSA==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3419,13 +3431,16 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-musl": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.55.0.tgz", - "integrity": "sha512-7v1nNrlD43VY6+sYQ6efYyb3lE6QY182304PD/768ZxTjOmFd/3dQa3u/nGBUAXYdGSWOQc5N3PnS0QzUXyEIA==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.68.0.tgz", + "integrity": "sha512-mJlFuFVCxzrYM5sFStN433D/s/mb6Wq4aCQAM02vs/OudHywnaSAd2rb1vlYUJtqdYIciJtiasuxvfbYkv5fLg==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3436,13 +3451,16 @@ } }, "node_modules/@oxfmt/binding-linux-s390x-gnu": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.55.0.tgz", - "integrity": "sha512-f4lJLUSPOgScjFl9LiflKCTocyNRwE25JmTMbN4XQdDjoZzEHjqf3wA3VESF1/csg7i8m7+EQLbrZyYDqe10UQ==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.68.0.tgz", + "integrity": "sha512-RlfSg++qs1hbKltRR6lYvV9EoI3MdlfSQD9w1hdHVYjHqjIn1tkH4FWOpMSmjKGN20zr+nI+W9o4ARogCDudGQ==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3453,13 +3471,16 @@ } }, "node_modules/@oxfmt/binding-linux-x64-gnu": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.55.0.tgz", - "integrity": "sha512-MihqiPziJNoWy4MqNSV+jVA1g+07iQDjZiR0vaCaDoPgFEiJpCMsxamktzLV07cEeQsSJ04vQaU4CzCQwIvtDA==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.68.0.tgz", + "integrity": "sha512-nyzRB9U+dlYUKu3pMo3afHzZBUv/oTHZMG36ZfJViNVfOIzp70Q4GS8FFRGgYJ/p0zcyDCgpBvYISOdJOMh+jQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3470,13 +3491,16 @@ } }, "node_modules/@oxfmt/binding-linux-x64-musl": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.55.0.tgz", - "integrity": "sha512-Yqghym7KYAVjP9MmSrNZiDeerMuoejNjo0r3ox5H3GDKk8eAfl8VyJm9i+pWCLDCTnAbcTUMMN2ZKjUYXH1v3g==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.68.0.tgz", + "integrity": "sha512-iCx3sbZRIvGrL1RafphEiUKBaW1lc0/tAjKOIB/Wjw2+STRBEdu5+fH1Gc1faEWEmc2k5Ks4iUUV54Zd5C9a1A==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3487,9 +3511,9 @@ } }, "node_modules/@oxfmt/binding-openharmony-arm64": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.55.0.tgz", - "integrity": "sha512-s5SDvVVSbyQl1V5UU3Yl12M+XLUQ3rl5SglNqgAA2K4PXUtQhyNSS00wivONPEnNo5W01rCou8WkDNyvI/RGHg==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.68.0.tgz", + "integrity": "sha512-x2X5AZez7OgyLLFpwIgItoXBUqudDM7yiaTsxv8R8vKQ6e81l0jVw0NFeUCXzcl1sAJq8h+tC8N4mY8EiMeL4w==", "cpu": [ "arm64" ], @@ -3504,9 +3528,9 @@ } }, "node_modules/@oxfmt/binding-win32-arm64-msvc": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.55.0.tgz", - "integrity": "sha512-7p9FB5R32tw2KyyNX3wpQrR2WHwEHvMEiBlGXxeTCaRMCVNx3UtFMAUbaQ/pRNWIrEUZmYhJ6tcUH52uPTRYjQ==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.68.0.tgz", + "integrity": "sha512-AHVPjXkenLPQUh6kB8zSC8pX2ct9r4T1Edk9r/RNJyov6wPsS5uAfYtipwG4chn6+3bPFG5rI/3DxEeH8vib1w==", "cpu": [ "arm64" ], @@ -3521,9 +3545,9 @@ } }, "node_modules/@oxfmt/binding-win32-ia32-msvc": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.55.0.tgz", - "integrity": "sha512-ZYqj3fDnOT1IaVGMP5kpmkQl4F3tQIm2ZyAxvqkJYmI0xgWWak4ss4XYwv3VDfM+TWXeC9K4uQ/wW5jm/5XABA==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.68.0.tgz", + "integrity": "sha512-n09SjEk5VH7z8Hl4WVP7hho+cCwGViENkQFiM45vbW85dJd7kEhWaHRUobaPzEmrWyu6uumd4EuNfNyDKLtzDA==", "cpu": [ "ia32" ], @@ -3538,9 +3562,9 @@ } }, "node_modules/@oxfmt/binding-win32-x64-msvc": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.55.0.tgz", - "integrity": "sha512-eEYT5tivGnGbPHuOHuQpi6CGLObhh0re/5jcNQHihD2GRYkTM85dyi5a19zjP8Q00t1uqAx+/QGLUGdHeqzWyg==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.68.0.tgz", + "integrity": "sha512-gPe+dJLXaPuWPWqlpklDAJp0k+K9KhQPYiQLHfb+i2rmFuUGfJ/5Qlj6tr1mO6of5g0DiLjG/XCFHIaPhotqqA==", "cpu": [ "x64" ], @@ -3555,9 +3579,9 @@ } }, "node_modules/@oxlint/binding-android-arm-eabi": { - "version": "1.70.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.70.0.tgz", - "integrity": "sha512-zFh0P4cswmRvw6nkyb89dr18rRanuaCPAsEXsFDoQY8WdaquI8Pt4NWFjaMJg6L23cy5NeN8J9cBnREbWzZhaw==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.83.0.tgz", + "integrity": "sha512-0yGY24EwsLk5YDe6F+VkmZyRHSwJDALa3nIrPpq7FXmp2lV2d0TzvBCGeZk+wgiULRGr5blhyr4QMp5KCXJUqA==", "cpu": [ "arm" ], @@ -3572,9 +3596,9 @@ } }, "node_modules/@oxlint/binding-android-arm64": { - "version": "1.70.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.70.0.tgz", - "integrity": "sha512-qI8o4HZjeGiBrWv+pJv4lH0Yi2Gl/JSp/EumBUApezJprIKa5PS4nU0lQsQngtky8k+SplQIOjv6hwu0SSxeyg==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.83.0.tgz", + "integrity": "sha512-hHfJ0vc17A4iUjH5p9BsTUPYbYRNxGpvD2lbu1aBRk54bzNIx9o5TtYF39QPZcV95DagZd+4DEAw2RH3G2ZsMg==", "cpu": [ "arm64" ], @@ -3589,9 +3613,9 @@ } }, "node_modules/@oxlint/binding-darwin-arm64": { - "version": "1.70.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.70.0.tgz", - "integrity": "sha512-8KjgVVHI5F9nVwHCRwwA78Ty7zNKP4Wd9OeN5PSv3iu/F/u1RVXoOCgLhWqust6HmwQG6xc8c+RCyaWENy24+w==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.83.0.tgz", + "integrity": "sha512-hsOjYjszLb/3zym/TkzUMPAoQlTJcuzSyEPOAyA+skXJIX9M0o+4JfOtqopX/Vf4hSLrJ98j0nvFo23gzk8auQ==", "cpu": [ "arm64" ], @@ -3606,9 +3630,9 @@ } }, "node_modules/@oxlint/binding-darwin-x64": { - "version": "1.70.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.70.0.tgz", - "integrity": "sha512-WVydssv5PSUBXFJTdNBWlmGkbNmvPGaFt/2SUT/EZRB6bq6bEOHmMlbnupZD5jmlEvi9+mZJHi8TCw15lyfSfQ==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.83.0.tgz", + "integrity": "sha512-mjh5oH2EA+wl5yRJYT9K9G61O2zFlpuv+yf2JwZOi0+dq2FnTUtm1h8i+5Ik0fXPWIu/k84I1psZR9aQsLAnyA==", "cpu": [ "x64" ], @@ -3623,9 +3647,9 @@ } }, "node_modules/@oxlint/binding-freebsd-x64": { - "version": "1.70.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.70.0.tgz", - "integrity": "sha512-hJucmUf8OlinHNb1R7fI4Fw6WsAstOz7i8nmkWQfiHoZXtbufNm+MxiDTIMk1ggh2Ro4vLzgQ+bKvRY54MZoRA==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.83.0.tgz", + "integrity": "sha512-fNHr64/YaO8YssuoDVC8+F4Uk5enR86q5uxfHkQrjAPs1dbAILOrD2uaud+J7MO8Fx774g44ERLD0IGIvZE48w==", "cpu": [ "x64" ], @@ -3640,9 +3664,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-gnueabihf": { - "version": "1.70.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.70.0.tgz", - "integrity": "sha512-1BnS7wbCYDSXwWzJJ+mc3NURoha6m6m6RT5c6vgAY3oz7C3OVXP+S0awo2mRq97arrJkVvO3qRQfyAHL+76xtQ==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.83.0.tgz", + "integrity": "sha512-Qpwy3zzAwMj+8/lyYItHmkSMwbkprFNWTK7jPYDOxSyxEhaSLOWYUTCMkjF334J8/WD0nznCCsoBbIH6hpsuIw==", "cpu": [ "arm" ], @@ -3657,9 +3681,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-musleabihf": { - "version": "1.70.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.70.0.tgz", - "integrity": "sha512-yKy/UdbR55+M2yEcuiV5DCNC/gdQAjr/GioUy50QwBzSrKm8ueWADqyRLS9Xk+qjNeCYGg6A8FvUBds56ttfqg==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.83.0.tgz", + "integrity": "sha512-s+BirYLFq7JL2k9sP0XI3ZXJ9dYvJ8sX3jLCLoag7tt+zrSHpZxP0jqznfL+Gdgwu7ay0dYgGYJXrQvq3iWloA==", "cpu": [ "arm" ], @@ -3674,13 +3698,16 @@ } }, "node_modules/@oxlint/binding-linux-arm64-gnu": { - "version": "1.70.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.70.0.tgz", - "integrity": "sha512-0A5XJ4alvmqFUFP/4oYSyaO+qLto/HrKEWTSaegiVl+HOufFngK2BjYw9x4RbwBt/du5QG6l5q1zeWiJYYG5yg==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.83.0.tgz", + "integrity": "sha512-7lihXt3vKr+GIyapNbHrnFHm/biiW30le6Zv/DExbAFPF6YwCQXVFlONPFehxs0CpGO4CBfYPM9rdDT+XMoIlg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3691,13 +3718,16 @@ } }, "node_modules/@oxlint/binding-linux-arm64-musl": { - "version": "1.70.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.70.0.tgz", - "integrity": "sha512-JiylyurlB0CLSedNtx1gzv3FvfWPF1h/2Y3BJszPLNt5XQFlBsH5ke0Jle3iJb3uqu5m2e7A/DwzpuCAHdiU+A==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.83.0.tgz", + "integrity": "sha512-q63JalLYVkZiZvls1z3PPUnpmQluOMXp0khqQMznCeAPLGydfNY8JhvuA4WlK57JfrvikU8wB5lPVveqpIXvew==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3708,13 +3738,16 @@ } }, "node_modules/@oxlint/binding-linux-ppc64-gnu": { - "version": "1.70.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.70.0.tgz", - "integrity": "sha512-J8VPG7I3/HmgaU4u8pNU2kFx2+0U+vPLS1dXFxXOaR/2TQ0f8AC7DRz0SRGRI1bfphnX2hVYTTtLuhL4nYKL+Q==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.83.0.tgz", + "integrity": "sha512-krQmDF+dRbxvdqVPV88ZuOoPPu8X5BuqDA8Hd+qcS4YMRQCb+nexA57DazgGsc/rGdKBe3QmV0mnv0bdpW/p5g==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3725,13 +3758,16 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-gnu": { - "version": "1.70.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.70.0.tgz", - "integrity": "sha512-N2+4lV2KLN+oXTIIIwmWDhwkrnvqf5oX7Hw0zPjk+RuIVgiBQSOlJWF7uQoFx2siEYX0ZQ5cfSbEAHm+J3t7Wg==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.83.0.tgz", + "integrity": "sha512-MmOl8Y6txEAXZU1RG8Rr264jQ6D7VPmqFsU/45x/FeWsGe32hklTqGrLE6UxHzp5Rjt0wP+20tY8YXKgSFB3mw==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3742,13 +3778,16 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-musl": { - "version": "1.70.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.70.0.tgz", - "integrity": "sha512-1e2L7cFCvx9QDzq6NPP+0tABKb5z6nWHyddWTNKprEsjO9xNrAtPowuCGpjNXxkTdsMiZ4jc8YQ5SstZd4XK6g==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.83.0.tgz", + "integrity": "sha512-u1rMymh0W3JZkq370kzQsYPULGWqhE09pZRqnZvUSoYaI9pVO5yVX+iYIslmWuEgwuzH9YAaOsScJiobWCHoOw==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3759,13 +3798,16 @@ } }, "node_modules/@oxlint/binding-linux-s390x-gnu": { - "version": "1.70.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.70.0.tgz", - "integrity": "sha512-Kwu/l/8GcYibCWA9m9N5pRXMIKVSsL/YbgpLzYkqDhWTiqdRfnNJ/+nqIKRKQiFbHWsdlHEhzMwruJK+qcEruA==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.83.0.tgz", + "integrity": "sha512-y0zK3HNwGysu7rqtE+BQG/d0bx5gh/KwlOtghN8oWeK1KcWzeaLqtZrbm8owqdma1lFyrce/hTO5ismuNu+INQ==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3776,13 +3818,16 @@ } }, "node_modules/@oxlint/binding-linux-x64-gnu": { - "version": "1.70.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.70.0.tgz", - "integrity": "sha512-tap04CsHYOl0nSAQJfPNIuBxqEPB2HnhQqwaOXLg1jnp2XfRo8Fa814dA4QC4zpvTWXCjAAaCY1W5LOORkEQuQ==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.83.0.tgz", + "integrity": "sha512-rS5gM0NgD7ngmuJmbIehsidtrOwKkLFwCQbKEeb9KuyQrrWNq5Zkn0uV6AYdXOMJ0grrWEiLwBuvMxt8w5vsNw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3793,13 +3838,16 @@ } }, "node_modules/@oxlint/binding-linux-x64-musl": { - "version": "1.70.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.70.0.tgz", - "integrity": "sha512-hzJa/WgvtJpbBD9rgfy0qe+MjbxOXNUT0bfR1S6EQQzfTtBFA9xg5q8KSwRrQ2QfSS+TaP4j+4mVPQrfNc6UNg==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.83.0.tgz", + "integrity": "sha512-W2IH4EtpcPaWcvNGCA95YoDg4vxqE/ZiPCi3arrxEEpsK7+JQN9WYwrlYFx9pcdP6KPXqRqkv3zdQPHcx7b6YQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3810,9 +3858,9 @@ } }, "node_modules/@oxlint/binding-openharmony-arm64": { - "version": "1.70.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.70.0.tgz", - "integrity": "sha512-xbsaNSNzVSnaJACCUYr1HQMyY/Q/Q1LkePmHG3UvZPvGCYGNxrsZp9OmtA6ick8xH47ltRRbRrPCM1YXYcyC+A==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.83.0.tgz", + "integrity": "sha512-6LyKkUyoajssTPLlZmDbZIbu4IZ5B4bGuRUnBgCGpEvHP3FQMaYITncHA/unPUo7q+Z+pIu2HhdkQ+8d1SG7iA==", "cpu": [ "arm64" ], @@ -3827,9 +3875,9 @@ } }, "node_modules/@oxlint/binding-win32-arm64-msvc": { - "version": "1.70.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.70.0.tgz", - "integrity": "sha512-icAEsUI7JbW1TMRdEXV83mVAInhRVQYuuAlPpxdGwJ95chNdnCzjloRW8GglT0WvzOEZSio6fnYSk2DJ2Hv7LQ==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.83.0.tgz", + "integrity": "sha512-Uz/fObEtF0jmNJQJ8CGRBKfefYstS0/wjD3s6IGzP8nUwsJykHQJBiN3npHwKiGRGn/vvBEgNr4B3cCzmmatvg==", "cpu": [ "arm64" ], @@ -3844,9 +3892,9 @@ } }, "node_modules/@oxlint/binding-win32-ia32-msvc": { - "version": "1.70.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.70.0.tgz", - "integrity": "sha512-FHMSWbVsPVs/f+Jcl04ws4JJ2wUnauyTzlpxWRG/lSO/8GpX08Fo2gQZqdA6CrRFI+zvkxl+N/KwJGWfUwYVZA==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.83.0.tgz", + "integrity": "sha512-u7XcvPW6Bk58tY5iWs2ESb0vJjoE/kuSpHxopbwp/p3ZtWVQXZ6wor5w3ssVTHOqd/v8b+QdhSFWQ4grEUNWpA==", "cpu": [ "ia32" ], @@ -3861,9 +3909,9 @@ } }, "node_modules/@oxlint/binding-win32-x64-msvc": { - "version": "1.70.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.70.0.tgz", - "integrity": "sha512-ptOlKwCz7n4AKs5VweMqG6DAg677FmKOK+vBkkL9DMNgFATIQ+upqUYBTOEwRQyRAx1ncGlPlXleV2hIcm3z4g==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.83.0.tgz", + "integrity": "sha512-LZRubd7ph13QmAg4fFecTYVZkiYbROR2Htaxh/ufWRkDhPOm2wrwaEYR89e0YpPFD3dqBrPoxS7myBw5hmYA7Q==", "cpu": [ "x64" ], @@ -9837,13 +9885,13 @@ } }, "node_modules/oxfmt": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.55.0.tgz", - "integrity": "sha512-jSj2wCTakwgPMxkfiVZX0jf+nX+Nz6xlyAZjqNE0qXTFdCBPYlP6JAN+ODjmealw7DXBjOzYbdsqwBMAZnPZ6A==", + "version": "0.68.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.68.0.tgz", + "integrity": "sha512-Z0XMofcXCGUXbcpBHnWyUiX93BGiw1B+lcHNbQDWEtOhX06ewoFfu4zXkyiLhRrNnMq0twqXRHUcJetf+GsiQQ==", "dev": true, "license": "MIT", "dependencies": { - "tinypool": "2.1.0" + "tinypool": "2.1.2" }, "bin": { "oxfmt": "bin/oxfmt" @@ -9852,28 +9900,28 @@ "node": "^20.19.0 || >=22.12.0" }, "funding": { - "url": "https://github.com/sponsors/Boshen" + "url": "https://github.com/sponsors/oxc-project" }, "optionalDependencies": { - "@oxfmt/binding-android-arm-eabi": "0.55.0", - "@oxfmt/binding-android-arm64": "0.55.0", - "@oxfmt/binding-darwin-arm64": "0.55.0", - "@oxfmt/binding-darwin-x64": "0.55.0", - "@oxfmt/binding-freebsd-x64": "0.55.0", - "@oxfmt/binding-linux-arm-gnueabihf": "0.55.0", - "@oxfmt/binding-linux-arm-musleabihf": "0.55.0", - "@oxfmt/binding-linux-arm64-gnu": "0.55.0", - "@oxfmt/binding-linux-arm64-musl": "0.55.0", - "@oxfmt/binding-linux-ppc64-gnu": "0.55.0", - "@oxfmt/binding-linux-riscv64-gnu": "0.55.0", - "@oxfmt/binding-linux-riscv64-musl": "0.55.0", - "@oxfmt/binding-linux-s390x-gnu": "0.55.0", - "@oxfmt/binding-linux-x64-gnu": "0.55.0", - "@oxfmt/binding-linux-x64-musl": "0.55.0", - "@oxfmt/binding-openharmony-arm64": "0.55.0", - "@oxfmt/binding-win32-arm64-msvc": "0.55.0", - "@oxfmt/binding-win32-ia32-msvc": "0.55.0", - "@oxfmt/binding-win32-x64-msvc": "0.55.0" + "@oxfmt/binding-android-arm-eabi": "0.68.0", + "@oxfmt/binding-android-arm64": "0.68.0", + "@oxfmt/binding-darwin-arm64": "0.68.0", + "@oxfmt/binding-darwin-x64": "0.68.0", + "@oxfmt/binding-freebsd-x64": "0.68.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.68.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.68.0", + "@oxfmt/binding-linux-arm64-gnu": "0.68.0", + "@oxfmt/binding-linux-arm64-musl": "0.68.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.68.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.68.0", + "@oxfmt/binding-linux-riscv64-musl": "0.68.0", + "@oxfmt/binding-linux-s390x-gnu": "0.68.0", + "@oxfmt/binding-linux-x64-gnu": "0.68.0", + "@oxfmt/binding-linux-x64-musl": "0.68.0", + "@oxfmt/binding-openharmony-arm64": "0.68.0", + "@oxfmt/binding-win32-arm64-msvc": "0.68.0", + "@oxfmt/binding-win32-ia32-msvc": "0.68.0", + "@oxfmt/binding-win32-x64-msvc": "0.68.0" }, "peerDependencies": { "svelte": "^5.0.0", @@ -9889,9 +9937,9 @@ } }, "node_modules/oxlint": { - "version": "1.70.0", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.70.0.tgz", - "integrity": "sha512-D6JgHtzkhRwvEC+A0Nw5AEc5bk8x5i1pHzvZIEf/a0C4hOzmAACNGtkDGPyFaxxX3ZVGxCPeig3P3rMM8XU3/g==", + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.83.0.tgz", + "integrity": "sha512-cyDzSzaw3uzP0TeCeq3lLRPPoaUxkbB4ZOXj+kn+5r+BX9V+4bNVGk9lxer+WrgcpebH4JxLlJ3KQjveVztOLQ==", "dev": true, "license": "MIT", "bin": { @@ -9901,31 +9949,31 @@ "node": "^20.19.0 || >=22.12.0" }, "funding": { - "url": "https://github.com/sponsors/Boshen" + "url": "https://github.com/sponsors/oxc-project" }, "optionalDependencies": { - "@oxlint/binding-android-arm-eabi": "1.70.0", - "@oxlint/binding-android-arm64": "1.70.0", - "@oxlint/binding-darwin-arm64": "1.70.0", - "@oxlint/binding-darwin-x64": "1.70.0", - "@oxlint/binding-freebsd-x64": "1.70.0", - "@oxlint/binding-linux-arm-gnueabihf": "1.70.0", - "@oxlint/binding-linux-arm-musleabihf": "1.70.0", - "@oxlint/binding-linux-arm64-gnu": "1.70.0", - "@oxlint/binding-linux-arm64-musl": "1.70.0", - "@oxlint/binding-linux-ppc64-gnu": "1.70.0", - "@oxlint/binding-linux-riscv64-gnu": "1.70.0", - "@oxlint/binding-linux-riscv64-musl": "1.70.0", - "@oxlint/binding-linux-s390x-gnu": "1.70.0", - "@oxlint/binding-linux-x64-gnu": "1.70.0", - "@oxlint/binding-linux-x64-musl": "1.70.0", - "@oxlint/binding-openharmony-arm64": "1.70.0", - "@oxlint/binding-win32-arm64-msvc": "1.70.0", - "@oxlint/binding-win32-ia32-msvc": "1.70.0", - "@oxlint/binding-win32-x64-msvc": "1.70.0" - }, - "peerDependencies": { - "oxlint-tsgolint": ">=0.22.1", + "@oxlint/binding-android-arm-eabi": "1.83.0", + "@oxlint/binding-android-arm64": "1.83.0", + "@oxlint/binding-darwin-arm64": "1.83.0", + "@oxlint/binding-darwin-x64": "1.83.0", + "@oxlint/binding-freebsd-x64": "1.83.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.83.0", + "@oxlint/binding-linux-arm-musleabihf": "1.83.0", + "@oxlint/binding-linux-arm64-gnu": "1.83.0", + "@oxlint/binding-linux-arm64-musl": "1.83.0", + "@oxlint/binding-linux-ppc64-gnu": "1.83.0", + "@oxlint/binding-linux-riscv64-gnu": "1.83.0", + "@oxlint/binding-linux-riscv64-musl": "1.83.0", + "@oxlint/binding-linux-s390x-gnu": "1.83.0", + "@oxlint/binding-linux-x64-gnu": "1.83.0", + "@oxlint/binding-linux-x64-musl": "1.83.0", + "@oxlint/binding-openharmony-arm64": "1.83.0", + "@oxlint/binding-win32-arm64-msvc": "1.83.0", + "@oxlint/binding-win32-ia32-msvc": "1.83.0", + "@oxlint/binding-win32-x64-msvc": "1.83.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "peerDependenciesMeta": { @@ -11246,9 +11294,9 @@ } }, "node_modules/tinypool": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz", - "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.2.tgz", + "integrity": "sha512-9YodfrxS9g9IbFr/KOjE5bAeJ0p61n3bW6mqvy0jtoeKd1kTW1Cxm0oulm6KX2lyM9Gl6WIe8nEbY7LWv5ZJww==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index a14b35033..ebe9a292b 100644 --- a/package.json +++ b/package.json @@ -91,8 +91,8 @@ "express": "^4.21.2", "jsdom": "^27.0.0", "mdi-material-ui": "^7.9.2", - "oxfmt": "0.55.0", - "oxlint": "1.70.0", + "oxfmt": "0.68.0", + "oxlint": "1.83.0", "oxlint-plugin-react-doctor": "0.9.12", "react": "^18.3.0", "react-dom": "^18.3.0", diff --git a/prometheus/src/components/TreeNode.tsx b/prometheus/src/components/TreeNode.tsx index def175d77..7434378f2 100644 --- a/prometheus/src/components/TreeNode.tsx +++ b/prometheus/src/components/TreeNode.tsx @@ -90,16 +90,6 @@ export default function TreeNode({ const [nodeEl, setNodeEl] = useState(null); const nodeRef = useCallback((node: HTMLDivElement) => setNodeEl(node), []); - const [resultStats, setResultStats] = useState<{ - numSeries: number; - labelExamples: Record>; - sortedLabelCards: Array<[string, number]>; - }>({ - numSeries: 0, - labelExamples: {}, - sortedLabelCards: [], - }); - const [connectorStyle, setConnectorStyle] = useState({ borderColor: theme.palette.grey['500'], borderLeftStyle: 'solid', @@ -166,37 +156,46 @@ export default function TreeNode({ return; } - const parentRect = parentEl.getBoundingClientRect(); - const nodeRect = nodeEl.getBoundingClientRect(); - if (reverse) { - setConnectorStyle((prevStyle) => ({ - ...prevStyle, - top: 'calc(50% - 1px)', - bottom: nodeRect.bottom - parentRect.top, - borderTopLeftRadius: 10, - borderTopStyle: 'solid', - borderBottomLeftRadius: undefined, - })); - } else { - setConnectorStyle((prevStyle) => ({ - ...prevStyle, - top: parentRect.bottom - nodeRect.top, - bottom: 'calc(50% - 1px)', - borderBottomLeftRadius: 10, - borderBottomStyle: 'solid', - borderTopLeftRadius: undefined, - })); - } - }, [parentEl, nodeEl, reverse, nodeRef, setConnectorStyle]); + const updateConnector = (): void => { + const parentRect = parentEl.getBoundingClientRect(); + const nodeRect = nodeEl.getBoundingClientRect(); + if (reverse) { + setConnectorStyle((prevStyle) => ({ + ...prevStyle, + top: 'calc(50% - 1px)', + bottom: nodeRect.bottom - parentRect.top, + borderTopLeftRadius: 10, + borderTopStyle: 'solid', + borderBottomLeftRadius: undefined, + borderBottomStyle: undefined, + })); + } else { + setConnectorStyle((prevStyle) => ({ + ...prevStyle, + top: parentRect.bottom - nodeRect.top, + bottom: 'calc(50% - 1px)', + borderBottomLeftRadius: 10, + borderBottomStyle: 'solid', + borderTopLeftRadius: undefined, + borderTopStyle: undefined, + })); + } + }; + const observer = new ResizeObserver(updateConnector); + observer.observe(parentEl); + observer.observe(nodeEl); + return (): void => observer.disconnect(); + }, [parentEl, nodeEl, reverse]); - // Update the node info state based on the query result. useEffect(() => { - if (instantQueryResponse?.status !== 'success') { - return; + if (instantQueryResponse?.status === 'success') { + reportNodeState?.(childIdx, 'success'); } + }, [instantQueryResponse, reportNodeState, childIdx]); - if (reportNodeState) { - reportNodeState(childIdx, 'success'); + const resultStats = useMemo(() => { + if (instantQueryResponse?.status !== 'success') { + return { numSeries: 0, labelExamples: {}, sortedLabelCards: [] }; } let resultSeries = 0; @@ -233,12 +232,12 @@ export default function TreeNode({ .map(([lv, cnt]) => ({ value: lv, count: cnt })); }); - setResultStats({ + return { numSeries: resultSeries, sortedLabelCards: Object.entries(labelCardinalities).toSorted((a, b) => b[1] - a[1]), labelExamples, - }); - }, [instantQueryResponse, reportNodeState, childIdx]); + }; + }, [instantQueryResponse]); const innerNode = ( diff --git a/prometheus/src/explore/PrometheusMetricsFinder/filter/FilterInputs.tsx b/prometheus/src/explore/PrometheusMetricsFinder/filter/FilterInputs.tsx index 89daa9b57..f9fe8c794 100644 --- a/prometheus/src/explore/PrometheusMetricsFinder/filter/FilterInputs.tsx +++ b/prometheus/src/explore/PrometheusMetricsFinder/filter/FilterInputs.tsx @@ -25,7 +25,7 @@ import { import type { DatasourceSelector } from '@perses-dev/spec'; import DeleteIcon from 'mdi-material-ui/Delete'; import type { HTMLAttributes, ReactElement, SyntheticEvent } from 'react'; -import { cloneElement, forwardRef, useMemo, useRef, useState } from 'react'; +import { cloneElement, forwardRef, useMemo, useState } from 'react'; import { Virtuoso } from 'react-virtuoso'; import type { LabelFilter, Operator } from '../types'; @@ -75,7 +75,7 @@ export function LabelFilterInput({ export const ListboxComponent = forwardRef>( ({ children, ...rest }, ref) => { const data = children as ReactElement[]; - const localRef = useRef('500px'); + const [maxHeight, setMaxHeight] = useState('500px'); const [height, setHeight] = useState(0); @@ -83,19 +83,21 @@ export const ListboxComponent = forwardRef { - const maxHeight = reference ? getComputedStyle(reference).maxHeight : null; - if (maxHeight && maxHeight !== localRef.current) { - localRef.current = maxHeight; + if (reference) { + const measuredMaxHeight = getComputedStyle(reference).maxHeight; + if (measuredMaxHeight) setMaxHeight(measuredMaxHeight); } if (typeof ref === 'function') { ref(reference); + } else if (ref) { + ref.current = reference; } }} {...rest} > { diff --git a/prometheus/src/plugins/PrometheusDatasourceEditor.tsx b/prometheus/src/plugins/PrometheusDatasourceEditor.tsx index ab45be2f5..d0c6a445f 100644 --- a/prometheus/src/plugins/PrometheusDatasourceEditor.tsx +++ b/prometheus/src/plugins/PrometheusDatasourceEditor.tsx @@ -37,14 +37,14 @@ export function PrometheusDatasourceEditor(props: PrometheusDatasourceEditorProp const { value, onChange, isReadonly, testConnection } = props; // Counter for generating unique IDs - const nextIdRef = useRef(0); + const nextIdRef = useRef(Object.keys(value.queryParams ?? {}).length); // Use local state to maintain an array of entries during editing, instead of // manipulating a map directly which causes weird UX. const [entries, setEntries] = useState(() => { const queryParams: QueryParamValues = value.queryParams ?? {}; - return Object.entries(queryParams).map(([key, val]) => ({ - id: String(nextIdRef.current++), + return Object.entries(queryParams).map(([key, val], index) => ({ + id: String(index), key, value: Array.isArray(val) ? val.join(',') : val, })); diff --git a/statchart/src/StatChartBase.tsx b/statchart/src/StatChartBase.tsx index d64d21eb1..509f5d6f3 100644 --- a/statchart/src/StatChartBase.tsx +++ b/statchart/src/StatChartBase.tsx @@ -237,7 +237,7 @@ export const StatChartBase: FC = (props) => { {data.seriesData?.name} ); - }, [colorMode, showSeriesName, secondary, color, containerPadding, seriesNameFontSize, data?.seriesData?.name]); + }, [colorMode, showSeriesName, secondary, color, containerPadding, seriesNameFontSize, data.seriesData?.name]); return ( = ({ queryResults, spec, definition }) => { const tableData = useMemo(() => buildTableData(queryResults, spec), [queryResults, spec]); @@ -92,17 +106,7 @@ export const TableExportAction: React.FC = ({ queryResults, spec, de const baseFilename = sanitizeFilename(title); const filename = `${baseFilename}_data.csv`; - const url = URL.createObjectURL(csvBlob); - try { - const link = document.createElement('a'); - link.href = url; - link.download = filename; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - } finally { - URL.revokeObjectURL(url); - } + downloadCsv(csvBlob, filename); } catch (error) { console.error('Table CSV export failed:', error); } diff --git a/table/src/components/ColumnsEditor/EmbeddedPanelOptionsEditor.tsx b/table/src/components/ColumnsEditor/EmbeddedPanelOptionsEditor.tsx index 26456dece..c835d016a 100644 --- a/table/src/components/ColumnsEditor/EmbeddedPanelOptionsEditor.tsx +++ b/table/src/components/ColumnsEditor/EmbeddedPanelOptionsEditor.tsx @@ -17,7 +17,7 @@ import { OptionsEditorTabs, usePlugin } from '@perses-dev/plugin-system'; import type { UnknownSpec } from '@perses-dev/spec'; import merge from 'lodash/merge'; import type { ReactElement } from 'react'; -import { useEffect, useMemo, useRef } from 'react'; +import { useEffect, useMemo } from 'react'; export interface EmbeddedPanelOptionsEditorProps { kind: string; @@ -52,16 +52,13 @@ export function EmbeddedPanelOptionsEditor({ kind, spec, onChange }: EmbeddedPan return mergeWithPluginDefaults(panelPlugin, spec); }, [panelPlugin, spec]); - const onChangeRef = useRef(onChange); - onChangeRef.current = onChange; - // Persist plugin defaults when the column still has an empty spec (e.g. after switching panel kind). useEffect(() => { if (!panelPlugin || !isSpecEmpty(spec)) { return; } - onChangeRef.current(mergeWithPluginDefaults(panelPlugin, spec)); - }, [panelPlugin, kind, spec]); + onChange(mergeWithPluginDefaults(panelPlugin, spec)); + }, [panelPlugin, spec, onChange]); if (isLoading) { return ( diff --git a/table/src/components/TablePanel.test.tsx b/table/src/components/TablePanel.test.tsx index ca1b176ad..3f3156c26 100644 --- a/table/src/components/TablePanel.test.tsx +++ b/table/src/components/TablePanel.test.tsx @@ -34,12 +34,10 @@ import { import { TablePanel } from './TablePanel'; /* mock all variables */ -const MOCK_VARIABLE_STATE_MAP = vi.hoisted( - (): VariableStateMap => ({ - myproject: { loading: false, value: 'my_project' }, - __range: { loading: false, value: '1h' }, - }), -); +const MOCK_VARIABLE_STATE_MAP = vi.hoisted((): VariableStateMap => ({ + myproject: { loading: false, value: 'my_project' }, + __range: { loading: false, value: '1h' }, +})); vi.mock('@perses-dev/plugin-system', async (importOriginal) => ({ ...(await importOriginal()), // Return a stable reference (like the real hook, which memoizes) so consumers diff --git a/table/src/components/TablePanel.tsx b/table/src/components/TablePanel.tsx index 330b55894..746d66197 100644 --- a/table/src/components/TablePanel.tsx +++ b/table/src/components/TablePanel.tsx @@ -21,7 +21,7 @@ import { CalculationsMap, replaceVariablesInString, useAllVariableValues } from import type { QueryDataType, TimeSeriesData } from '@perses-dev/spec'; import type { ColumnFiltersState, PaginationState, RowSelectionState, SortingState } from '@tanstack/react-table'; import type { ReactElement } from 'react'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import type { CellSettings, ColumnSettings, TableOptions } from '../models'; @@ -393,7 +393,7 @@ export function TablePanel({ contentDimensions, spec, queryResults }: TableProps const filteredDataRef = useRef>>([]); // Refs used to keep the filter row in sync with the table's horizontal - const panelContainerRef = useRef(null); + const [panelContainer, setPanelContainer] = useState(null); const filterRowInnerRef = useRef(null); const filterCellRefs = useRef>([]); @@ -697,10 +697,10 @@ export function TablePanel({ contentDimensions, spec, queryResults }: TableProps setOpenFilterColumn(columnId); }; - const handleFilterClose = (): void => { + const handleFilterClose = useCallback((): void => { setFilterAnchorEl({}); setOpenFilterColumn(null); - }; + }, []); // Close filter when clicking outside useEffect(() => { @@ -721,23 +721,22 @@ export function TablePanel({ contentDimensions, spec, queryResults }: TableProps clearTimeout(timer); document.removeEventListener('click', handleClick); }; - }, [openFilterColumn]); + }, [openFilterColumn, handleFilterClose]); // Keep ref in sync with filtered data for use in selection handler - filteredDataRef.current = filteredData; + useLayoutEffect(() => { + filteredDataRef.current = filteredData; + }, [filteredData]); const [pagination, setPagination] = useState( spec.pagination ? { pageIndex: 0, pageSize: 10 } : undefined, ); - useEffect(() => { - // If the pagination setting changes from no pagination to pagination, but the pagination state is undefined, update the pagination state - if (spec.pagination && !pagination) { - setPagination({ pageIndex: 0, pageSize: 10 }); - } else if (!spec.pagination && pagination) { - setPagination(undefined); - } - }, [spec.pagination, pagination]); + if (spec.pagination && !pagination) { + setPagination({ pageIndex: 0, pageSize: 10 }); + } else if (!spec.pagination && pagination) { + setPagination(undefined); + } // Sync the filter row's horizontal position with the table scroll. useEffect(() => { @@ -745,7 +744,7 @@ export function TablePanel({ contentDimensions, spec, queryResults }: TableProps return; } - const scrollContainer = panelContainerRef.current?.querySelector('.MuiTableContainer-root'); + const scrollContainer = panelContainer?.querySelector('.MuiTableContainer-root'); const filterRowInner = filterRowInnerRef.current; if (!scrollContainer || !filterRowInner) { @@ -759,13 +758,13 @@ export function TablePanel({ contentDimensions, spec, queryResults }: TableProps setFilterAnchorEl((current) => (Object.keys(current).length === 0 ? current : {})); }; - syncFilterRowScroll(); + filterRowInner.style.transform = `translateX(-${scrollContainer.scrollLeft}px)`; scrollContainer.addEventListener('scroll', syncFilterRowScroll, { passive: true }); return (): void => { scrollContainer.removeEventListener('scroll', syncFilterRowScroll); }; - }, [spec.enableFiltering, columns, contentDimensions]); + }, [panelContainer, spec.enableFiltering]); // Sync filter cell widths with the actual rendered table column widths to keep them aligned. useEffect(() => { @@ -773,7 +772,7 @@ export function TablePanel({ contentDimensions, spec, queryResults }: TableProps return; } - const scrollContainer = panelContainerRef.current?.querySelector('.MuiTableContainer-root'); + const scrollContainer = panelContainer?.querySelector('.MuiTableContainer-root'); if (!scrollContainer) { return; } @@ -811,7 +810,7 @@ export function TablePanel({ contentDimensions, spec, queryResults }: TableProps return (): void => { resizeObserver.disconnect(); }; - }, [spec.enableFiltering, columns, contentDimensions, selectionEnabled, actionButtons]); + }, [panelContainer, spec.enableFiltering, columns]); if (contentDimensions === undefined) { return null; @@ -833,7 +832,7 @@ export function TablePanel({ contentDimensions, spec, queryResults }: TableProps } return ( -
+
{confirmDialog} {spec.enableFiltering && (
{ + const [previousValue, setPreviousValue] = useState(value); + if (value !== previousValue) { + setPreviousValue(value); setDraftValue(value); - }, [value, setDraftValue]); + } const handleChange = useCallback((event: React.ChangeEvent) => { setDraftValue(event.target.value); diff --git a/tempo/src/components/TraceQLEditor.tsx b/tempo/src/components/TraceQLEditor.tsx index ac4193399..814aa2a21 100644 --- a/tempo/src/components/TraceQLEditor.tsx +++ b/tempo/src/components/TraceQLEditor.tsx @@ -20,7 +20,7 @@ import type { ReactElement } from 'react'; import { useMemo } from 'react'; import type { TempoClient } from '../model'; -import { TraceQLExtension } from './TraceQLExtension'; +import { TraceQLExtension as createTraceQLExtension } from './TraceQLExtension'; export interface TraceQLEditorProps extends Omit { client?: TempoClient; @@ -32,7 +32,7 @@ export function TraceQLEditor({ client, ...rest }: TraceQLEditorProps): ReactEle const { absoluteTimeRange } = useTimeRange(); const traceQLExtension = useMemo(() => { - return TraceQLExtension({ client, timeRange: absoluteTimeRange }); + return createTraceQLExtension({ client, timeRange: absoluteTimeRange }); }, [client, absoluteTimeRange]); const codemirrorTheme = useMemo(() => { diff --git a/timeserieschart/src/QuerySettingsEditor.tsx b/timeserieschart/src/QuerySettingsEditor.tsx index 9d24c7395..6b5a22ef5 100644 --- a/timeserieschart/src/QuerySettingsEditor.tsx +++ b/timeserieschart/src/QuerySettingsEditor.tsx @@ -64,7 +64,7 @@ export function QuerySettingsEditor(props: TimeSeriesChartOptionsEditorProps): R if (!recentlyAddedInputRef.current || !focusRef.current) return; recentlyAddedInputRef.current?.focus(); focusRef.current = false; - }, [querySettingsList?.length]); + }); const handleQueryIndexChange = (e: React.ChangeEvent, i: number): void => { if (querySettingsList !== undefined) { diff --git a/timeserieschart/src/TimeSeriesChartBase.tsx b/timeserieschart/src/TimeSeriesChartBase.tsx index c4ecf7af5..dc935ab4b 100644 --- a/timeserieschart/src/TimeSeriesChartBase.tsx +++ b/timeserieschart/src/TimeSeriesChartBase.tsx @@ -64,7 +64,7 @@ import { CanvasRenderer } from 'echarts/renderers'; import isEqual from 'lodash/isEqual'; import merge from 'lodash/merge'; import type { MouseEvent } from 'react'; -import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; +import { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react'; import { AnnotationTooltip, buildAnnotationSeries } from './annotations/AnnotationTooltip'; import type { TimeSeriesAnnotation } from './utils/annotation'; @@ -340,22 +340,23 @@ export const TimeSeriesChartBase = forwardRef(fun getTimezoneAwareAxisFormatter, ]); - // Update adjacent charts so tooltip is unpinned when current chart is clicked. - useEffect(() => { - // Only allow pinning one tooltip at a time, subsequent tooltip click unpins previous. - // Multiple tooltips can only be pinned if Ctrl or Cmd key is pressed while clicking. - const multipleTooltipsPinned = tooltipPinnedCoords !== null && lastTooltipPinnedCoords !== null; - if (multipleTooltipsPinned) { - if (!isEqual(lastTooltipPinnedCoords, tooltipPinnedCoords)) { - setTooltipPinnedCoords(null); - if (tooltipPinnedCoords !== null && pinnedCrosshair !== null) { - setPinnedCrosshair(null); - } - } + // Only changes from another chart (or new series) can clear the local pin. + // A local click may update its coordinates without updating the shared pin. + const [previousPinState, setPreviousPinState] = useState({ lastTooltipPinnedCoords, seriesMapping }); + if ( + previousPinState.lastTooltipPinnedCoords !== lastTooltipPinnedCoords || + previousPinState.seriesMapping !== seriesMapping + ) { + setPreviousPinState({ lastTooltipPinnedCoords, seriesMapping }); + if ( + tooltipPinnedCoords !== null && + lastTooltipPinnedCoords !== null && + !isEqual(lastTooltipPinnedCoords, tooltipPinnedCoords) + ) { + setTooltipPinnedCoords(null); + setPinnedCrosshair(null); } - // tooltipPinnedCoords CANNOT be in dep array or tooltip pinning breaks in the current chart's onClick - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [lastTooltipPinnedCoords, seriesMapping]); + } return ( (itemActionsConfig?.enabled && itemActionsConfig.displayWithItem ? itemActionsConfig.actionsList : []), - [itemActionsConfig?.enabled, itemActionsConfig?.displayWithItem, itemActionsConfig?.actionsList], + [itemActionsConfig], ); const { getItemActionButtons, confirmDialog, actionButtons } = useSelectionItemActions({ diff --git a/tracetable/src/DataTable.tsx b/tracetable/src/DataTable.tsx index 2600e7e1a..ee3121dd1 100644 --- a/tracetable/src/DataTable.tsx +++ b/tracetable/src/DataTable.tsx @@ -108,7 +108,7 @@ export function DataTable(props: DataTableProps): ReactElement { } } return result_rows; - }, [result, options.links?.trace, variableValues]); + }, [result, options.links, variableValues]); const rowsById = useMemo(() => { const map = new Map(); diff --git a/tracingganttchart/src/TracingGanttChart/GanttTable/ResizableDivider.tsx b/tracingganttchart/src/TracingGanttChart/GanttTable/ResizableDivider.tsx index edcfbd8f4..70af9f077 100644 --- a/tracingganttchart/src/TracingGanttChart/GanttTable/ResizableDivider.tsx +++ b/tracingganttchart/src/TracingGanttChart/GanttTable/ResizableDivider.tsx @@ -18,12 +18,11 @@ import { useEffect, useState } from 'react'; interface ResizableDividerProps { parentRef: React.RefObject; - spacing?: number; onMove: (left: number) => void; } export function ResizableDivider(props: ResizableDividerProps): ReactElement { - const { parentRef, spacing = 0, onMove } = props; + const { parentRef, onMove } = props; const [isResizing, setResizing] = useState(false); const handleMouseDown = (e: ReactMouseEvent): void => { @@ -38,6 +37,7 @@ export function ResizableDivider(props: ResizableDividerProps): ReactElement { if (!parentRef.current) return; const parentRect = parentRef.current.getBoundingClientRect(); + const spacing = parseFloat(getComputedStyle(parentRef.current).columnGap) || 0; // The parent can be a flex row, for example: [leftPercent] [gap] [divider] [gap] [1-leftPercent]. // Without considering spacing, leftPercent would be wrong because it ignores the flex gap between the divider and the element. diff --git a/tracingganttchart/src/TracingGanttChart/MiniGanttChart/Canvas.tsx b/tracingganttchart/src/TracingGanttChart/MiniGanttChart/Canvas.tsx index 91c38cdc5..e72a4367f 100644 --- a/tracingganttchart/src/TracingGanttChart/MiniGanttChart/Canvas.tsx +++ b/tracingganttchart/src/TracingGanttChart/MiniGanttChart/Canvas.tsx @@ -60,13 +60,13 @@ export function Canvas(props: CanvasProps): ReactElement { ); useEffect(() => { - if (!canvasRef.current || !width || !height) return; + if (!canvasRef.current || !width) return; const ctx = canvasRef.current.getContext('2d'); if (!ctx) return; - drawSpans(ctx, width, height, trace, spanColorGenerator); - }, [width, height, trace, spanColorGenerator]); + drawSpans(ctx, width, CANVAS_HEIGHT, trace, spanColorGenerator); + }, [width, trace, spanColorGenerator]); const translateCursorToTime = (e: ReactMouseEvent | MouseEvent): number => { if (!canvasRef.current || !width) return 0; diff --git a/tracingganttchart/src/TracingGanttChart/TracingGanttChart.tsx b/tracingganttchart/src/TracingGanttChart/TracingGanttChart.tsx index f85a2b89b..a8b469bef 100644 --- a/tracingganttchart/src/TracingGanttChart/TracingGanttChart.tsx +++ b/tracingganttchart/src/TracingGanttChart/TracingGanttChart.tsx @@ -64,7 +64,6 @@ export function TracingGanttChart(props: TracingGanttChartProps): ReactElement { // setTableWidth() is only called by const [tableWidth, setTableWidth] = useState(0.82); const gap = 2; - const spacing = ganttChart.current ? parseFloat(getComputedStyle(ganttChart.current).columnGap) || 0 : 0; return ( @@ -86,7 +85,7 @@ export function TracingGanttChart(props: TracingGanttChartProps): ReactElement { {selectedSpan && ( <> - + ; @@ -26,7 +26,7 @@ export function LogsQLEditor(props: LogsQLEditorProps): ReactElement { const isDarkMode = theme.palette.mode === 'dark'; const logsqlExtension = useMemo(() => { - return LogsQLExtension(); + return createLogsQLExtension(); }, []); const codemirrorTheme = useMemo(() => { diff --git a/victorialogs/src/queries/victorialogs-log-query/VictoriaLogsLogQueryEditor.tsx b/victorialogs/src/queries/victorialogs-log-query/VictoriaLogsLogQueryEditor.tsx index 2c2451cfd..2e90cadd8 100644 --- a/victorialogs/src/queries/victorialogs-log-query/VictoriaLogsLogQueryEditor.tsx +++ b/victorialogs/src/queries/victorialogs-log-query/VictoriaLogsLogQueryEditor.tsx @@ -16,7 +16,7 @@ import { createModEnterHandler } from '@perses-dev/dashboards'; import type { DatasourceSelectProps, OptionsEditorProps } from '@perses-dev/plugin-system'; import { DatasourceSelect, isVariableDatasource, useDatasourceSelectValueToSelector } from '@perses-dev/plugin-system'; import type { ReactElement } from 'react'; -import { useCallback, useState, useEffect } from 'react'; +import { useCallback, useState } from 'react'; import { LogsQLEditor } from '../../components/logsql-editor'; import type { VictoriaLogsDatasourceSelector } from '../../model'; @@ -41,10 +41,11 @@ export function VictoriaLogsLogQueryEditor(props: VictoriaLogsQueryEditorProps): // Local state for editor value to prevent query_range calls on every keystroke const [localQuery, setLocalQuery] = useState(value.query); - // Update local state when prop changes - useEffect(() => { + const [previousQuery, setPreviousQuery] = useState(value.query); + if (value.query !== previousQuery) { + setPreviousQuery(value.query); setLocalQuery(value.query); - }, [value.query]); + } const handleDatasourceChange: DatasourceSelectProps['onChange'] = (newDatasourceSelection) => { if (!isVariableDatasource(newDatasourceSelection) && newDatasourceSelection.kind === DATASOURCE_KIND) { diff --git a/victorialogs/src/queries/victorialogs-time-series-query/VictoriaLogsTimeSeriesQueryEditor.tsx b/victorialogs/src/queries/victorialogs-time-series-query/VictoriaLogsTimeSeriesQueryEditor.tsx index b64c4a53a..f7d3a73ee 100644 --- a/victorialogs/src/queries/victorialogs-time-series-query/VictoriaLogsTimeSeriesQueryEditor.tsx +++ b/victorialogs/src/queries/victorialogs-time-series-query/VictoriaLogsTimeSeriesQueryEditor.tsx @@ -16,7 +16,7 @@ import { createModEnterHandler } from '@perses-dev/dashboards'; import type { DatasourceSelectProps, OptionsEditorProps } from '@perses-dev/plugin-system'; import { DatasourceSelect, isVariableDatasource, useDatasourceSelectValueToSelector } from '@perses-dev/plugin-system'; import type { ReactElement } from 'react'; -import { useCallback, useState, useEffect } from 'react'; +import { useCallback, useState } from 'react'; import { LogsQLEditor } from '../../components/logsql-editor'; import type { VictoriaLogsDatasourceSelector } from '../../model'; @@ -38,10 +38,11 @@ export function VictoriaLogsQueryEditor(props: VictoriaLogsQueryEditorProps): Re // Local state for editor value to prevent query_range calls on every keystroke const [localQuery, setLocalQuery] = useState(value.query); - // Update local state when prop changes - useEffect(() => { + const [previousQuery, setPreviousQuery] = useState(value.query); + if (value.query !== previousQuery) { + setPreviousQuery(value.query); setLocalQuery(value.query); - }, [value.query]); + } const handleDatasourceChange: DatasourceSelectProps['onChange'] = (newDatasourceSelection) => { if (!isVariableDatasource(newDatasourceSelection) && newDatasourceSelection.kind === DATASOURCE_KIND) { From f11dc24493685e691d7d8f7fcb5c2fba7d852b53 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Sat, 19 Sep 2026 11:29:29 +0000 Subject: [PATCH 2/3] [IGNORE] Upgrade Oxlint and fix React Compiler diagnostics Signed-off-by: Guillaume LADORME --- .oxlintrc.json | 16 ++-------------- README.md | 3 ++- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 24baa71b9..5062b47a4 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -44,19 +44,7 @@ "react/jsx-no-useless-fragment": "warn", "react/no-unescaped-entities": "error", "react/exhaustive-deps": "error", - "react/error-boundaries": "error", - "react/globals": "error", - "react/immutability": "error", - "react/incompatible-library": "error", - "react/preserve-manual-memoization": "error", - "react/purity": "error", - "react/refs": "error", - "react/set-state-in-effect": "error", - "react/set-state-in-render": "error", - "react/static-components": "error", - "react/use-memo": "error", "react/unsupported-syntax": "error", - "react/void-use-memo": "error", "react/no-deriving-state-in-effects": "error", "react/invariant": "error", "react/rule-suppression": "error", @@ -66,8 +54,8 @@ "react/exhaustive-effect-dependencies": "error", "react/hooks": "error", "react/memo-dependencies": "error", - "react-doctor/no-fetch-in-effect": "warn", - "react-doctor/no-derived-state": "warn", + "react-doctor/no-fetch-in-effect": "error", + "react-doctor/no-derived-state": "error", "react/jsx-curly-brace-presence": [ "error", { "props": "never", "children": "never", "propElementValues": "always" } diff --git a/README.md b/README.md index 99bb99622..b5459da4d 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,8 @@ Then in [`perses`](https://github.com/perses/perses) repository: Run `npm run lint` for the regular Oxlint checks, including the React Doctor rules configured in `.oxlintrc.json`. Run `npm run doctor` for the full React Doctor project scan. Pull requests and pushes to `main` also run the scan in GitHub -Actions. React Compiler rules are enforced as errors; resolve their diagnostics before merging. +Actions. React Compiler rules and the configured React Doctor rules are enforced as errors; resolve their diagnostics +before merging. ### Working with Snapshots From d5979d150e476360366ddd41a2c77b9158003e8c Mon Sep 17 00:00:00 2001 From: Guillaume LADORME Date: Sat, 19 Sep 2026 22:05:51 +0200 Subject: [PATCH 3/3] Fable review Signed-off-by: Guillaume LADORME --- rsbuild.shared.ts | 6 ++++-- scatterchart/src/Scatterplot.tsx | 1 - staticlistvariable/src/StaticListVariable.tsx | 2 +- statushistorychart/src/utils/data-transform.test.ts | 1 - table/src/models/model.ts | 1 - timeserieschart/src/TimeSeriesChartBase.tsx | 2 -- timeserieschart/src/annotations/AnnotationTooltip.tsx | 1 - timeserieschart/src/utils/data-transform.ts | 1 - tracingganttchart/src/test/convert/jaeger.ts | 1 - vitest.shared.ts | 2 +- 10 files changed, 6 insertions(+), 12 deletions(-) diff --git a/rsbuild.shared.ts b/rsbuild.shared.ts index f899762ca..14ce99913 100644 --- a/rsbuild.shared.ts +++ b/rsbuild.shared.ts @@ -14,8 +14,10 @@ import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; -import { ModuleFederationOptions, pluginModuleFederation } from '@module-federation/rsbuild-plugin'; -import { mergeRsbuildConfig, RsbuildConfig } from '@rsbuild/core'; +import type { ModuleFederationOptions } from '@module-federation/rsbuild-plugin'; +import { pluginModuleFederation } from '@module-federation/rsbuild-plugin'; +import type { RsbuildConfig } from '@rsbuild/core'; +import { mergeRsbuildConfig } from '@rsbuild/core'; /** The base path for all plugin assets. This should match the path where plugins are stored on the Perses server. * @see {@link https://github.com/perses/perses} diff --git a/scatterchart/src/Scatterplot.tsx b/scatterchart/src/Scatterplot.tsx index 2d8d20e5a..9a978d113 100644 --- a/scatterchart/src/Scatterplot.tsx +++ b/scatterchart/src/Scatterplot.tsx @@ -119,7 +119,6 @@ export function Scatterplot(props: ScatterplotProps): ReactElement { axisPointer: { type: 'cross', }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any formatter: function (params: any) { // TODO: import type from ECharts instead of using any const data = params[0].data as EChartTraceValue; diff --git a/staticlistvariable/src/StaticListVariable.tsx b/staticlistvariable/src/StaticListVariable.tsx index e5e892297..7b4052b06 100644 --- a/staticlistvariable/src/StaticListVariable.tsx +++ b/staticlistvariable/src/StaticListVariable.tsx @@ -12,7 +12,6 @@ // limitations under the License. import { Autocomplete, Chip, IconButton, TextField, Typography } from '@mui/material'; -/* eslint-disable jsx-a11y/no-autofocus */ import type { VariablePlugin, VariableOption, OptionsEditorProps } from '@perses-dev/plugin-system'; import PlusCircleIcon from 'mdi-material-ui/PlusCircle'; import type { ReactElement } from 'react'; @@ -186,6 +185,7 @@ function StaticListVariableOptionEditor(props: OptionsEditorProps { }, ], }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any definition: { query: 'some-query' } as any, }, ]; diff --git a/table/src/models/model.ts b/table/src/models/model.ts index 04065d032..9fb5bcae0 100644 --- a/table/src/models/model.ts +++ b/table/src/models/model.ts @@ -13,7 +13,6 @@ import type { PanelProps } from '@perses-dev/plugin-system'; -// eslint-disable-next-line @typescript-eslint/no-empty-object-type interface TimeSeriesTableOptions {} export type TimeSeriesTableProps = PanelProps; diff --git a/timeserieschart/src/TimeSeriesChartBase.tsx b/timeserieschart/src/TimeSeriesChartBase.tsx index dc935ab4b..5b98dd097 100644 --- a/timeserieschart/src/TimeSeriesChartBase.tsx +++ b/timeserieschart/src/TimeSeriesChartBase.tsx @@ -210,7 +210,6 @@ export const TimeSeriesChartBase = forwardRef(fun enableDataZoom(chartRef.current); } }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any mouseover: (params: any): void => { // Only markPoint (triangles under the X-axis) opens the annotation tooltip. // Hovering markLine or anything else keeps the regular TimeSeries tooltip visible @@ -224,7 +223,6 @@ export const TimeSeriesChartBase = forwardRef(fun } setHoveredAnnotation(null); }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any mouseout: (params: any): void => { if ( annotations && diff --git a/timeserieschart/src/annotations/AnnotationTooltip.tsx b/timeserieschart/src/annotations/AnnotationTooltip.tsx index 6574cca9b..ef6fc9e9d 100644 --- a/timeserieschart/src/annotations/AnnotationTooltip.tsx +++ b/timeserieschart/src/annotations/AnnotationTooltip.tsx @@ -171,7 +171,6 @@ export function buildAnnotationSeries(annotations: TimeSeriesAnnotation[] | unde label?: { show: boolean }; annotationIndex?: number; }> = []; - // eslint-disable-next-line @typescript-eslint/no-explicit-any const markPointData: any[] = []; annotations.forEach((annotation, index) => { diff --git a/timeserieschart/src/utils/data-transform.ts b/timeserieschart/src/utils/data-transform.ts index a7244f29a..4e50701f9 100644 --- a/timeserieschart/src/utils/data-transform.ts +++ b/timeserieschart/src/utils/data-transform.ts @@ -200,7 +200,6 @@ function findMax(data: LegacyTimeSeries[] | TimeSeries[]): number { if (data.length && data[0] !== undefined && (data as TimeSeries[])[0]?.values) { (data as TimeSeries[]).forEach((series) => { series.values.forEach((valueTuple: TimeSeriesValueTuple) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_, value] = valueTuple; // Use the absolute value so percent thresholds compute correctly against // negated series (e.g. when `querySettings[].negativeY` is enabled). diff --git a/tracingganttchart/src/test/convert/jaeger.ts b/tracingganttchart/src/test/convert/jaeger.ts index 673c9faf3..7b40bbac8 100644 --- a/tracingganttchart/src/test/convert/jaeger.ts +++ b/tracingganttchart/src/test/convert/jaeger.ts @@ -113,7 +113,6 @@ function buildAnyValue(tags: Tag): otlpcommonv1.AnyValue { case 'int64': return { intValue: tags.value.toString() }; default: - // eslint-disable-next-line @typescript-eslint/no-explicit-any throw new Error(`unknown jaeger tag type ${(tags as any).type}`); } } diff --git a/vitest.shared.ts b/vitest.shared.ts index fd58ce49a..35cfcfa6b 100644 --- a/vitest.shared.ts +++ b/vitest.shared.ts @@ -52,7 +52,7 @@ export function definePackageVitestConfig({ packageDir, setupFiles = [], passWithNoTests = false, -}: VitestPackageOptions) { +}: VitestPackageOptions): ReturnType { return mergeConfig( sharedConfig, defineConfig({