From 4d1b8ecdfadb1ba4d0b74f328998d69bc2a2dfcd Mon Sep 17 00:00:00 2001 From: Guillaume Date: Sat, 19 Sep 2026 06:50:30 +0000 Subject: [PATCH 1/9] [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 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 +- .../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 | 382 ++++++++++-------- 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 +- 44 files changed, 682 insertions(+), 439 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 bc44cd608..7ea8072dd 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'; @@ -347,9 +347,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(''); @@ -416,9 +416,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, @@ -519,18 +521,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) => { @@ -701,7 +702,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/jaeger/src/plugins/JaegerTraceQueryEditor.test.tsx b/jaeger/src/plugins/JaegerTraceQueryEditor.test.tsx index 36279ab13..9209dda34 100644 --- a/jaeger/src/plugins/JaegerTraceQueryEditor.test.tsx +++ b/jaeger/src/plugins/JaegerTraceQueryEditor.test.tsx @@ -32,6 +32,9 @@ const mockedUseDatasourceSelectValueToSelector = useDatasourceSelectValueToSelec typeof useDatasourceSelectValueToSelector >; +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 34a82113a..68ad22319 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,8 +66,8 @@ "express": "^4.21.3", "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", @@ -2621,9 +2621,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" ], @@ -2638,9 +2638,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" ], @@ -2655,9 +2655,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" ], @@ -2672,9 +2672,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" ], @@ -2689,9 +2689,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" ], @@ -2706,9 +2706,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" ], @@ -2723,9 +2723,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" ], @@ -2740,13 +2740,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": [ @@ -2757,13 +2760,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": [ @@ -2774,13 +2780,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": [ @@ -2791,13 +2800,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": [ @@ -2808,13 +2820,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": [ @@ -2825,13 +2840,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": [ @@ -2842,13 +2860,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": [ @@ -2859,13 +2880,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": [ @@ -2876,9 +2900,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" ], @@ -2893,9 +2917,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" ], @@ -2910,9 +2934,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" ], @@ -2927,9 +2951,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" ], @@ -2944,9 +2968,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" ], @@ -2961,9 +2985,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" ], @@ -2978,9 +3002,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" ], @@ -2995,9 +3019,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" ], @@ -3012,9 +3036,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" ], @@ -3029,9 +3053,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" ], @@ -3046,9 +3070,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" ], @@ -3063,13 +3087,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": [ @@ -3080,13 +3107,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": [ @@ -3097,13 +3127,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": [ @@ -3114,13 +3147,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": [ @@ -3131,13 +3167,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": [ @@ -3148,13 +3187,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": [ @@ -3165,13 +3207,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": [ @@ -3182,13 +3227,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": [ @@ -3199,9 +3247,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" ], @@ -3216,9 +3264,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" ], @@ -3233,9 +3281,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" ], @@ -3250,9 +3298,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" ], @@ -9065,13 +9113,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" @@ -9080,28 +9128,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", @@ -9117,9 +9165,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": { @@ -9129,31 +9177,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" + "@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": ">=0.22.1", + "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "peerDependenciesMeta": { @@ -10447,9 +10495,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 e80c5b4a0..89a9a5028 100644 --- a/package.json +++ b/package.json @@ -92,8 +92,8 @@ "express": "^4.21.3", "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 74ce1c70a..e682ea7b4 100644 --- a/statchart/src/StatChartBase.tsx +++ b/statchart/src/StatChartBase.tsx @@ -240,7 +240,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 b95728810..ee018aaf8 100644 --- a/table/src/components/TablePanel.tsx +++ b/table/src/components/TablePanel.tsx @@ -21,7 +21,7 @@ import { CalculationsMap, replaceVariablesInString, useAllVariableValues } from import type { JsonData, 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'; @@ -395,7 +395,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>([]); @@ -699,10 +699,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(() => { @@ -723,23 +723,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(() => { @@ -747,7 +746,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) { @@ -761,13 +760,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(() => { @@ -775,7 +774,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; } @@ -813,7 +812,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; @@ -835,7 +834,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 3be9ed12b..c3584ad32 100644 --- a/timeserieschart/src/TimeSeriesChartBase.tsx +++ b/timeserieschart/src/TimeSeriesChartBase.tsx @@ -69,7 +69,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 useResizeObserver from 'use-resize-observer'; import { ANNOTATION_Y_AXIS, AnnotationTooltip, buildAnnotationSeries } from './annotations/AnnotationTooltip'; @@ -420,18 +420,21 @@ 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); } // A pinned exemplar tooltip is also unpinned when a tooltip is pinned in another chart, // unless it is the one just pinned by this chart at these exact coordinates. @@ -443,9 +446,7 @@ export const TimeSeriesChartBase = forwardRef(fun setPinnedExemplar(null); setPinnedExemplarPos(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 c1f24cf744dfbcc0790c2a2aa7f1abaa02eb8eeb Mon Sep 17 00:00:00 2001 From: Guillaume Date: Sat, 19 Sep 2026 11:29:29 +0000 Subject: [PATCH 2/9] [IGNORE] Upgrade Oxlint and fix React Compiler diagnostics Signed-off-by: Guillaume LADORME 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 9746ea2a7f67cf0ad467de316f8a8a34b1162c08 Mon Sep 17 00:00:00 2001 From: Guillaume LADORME Date: Sat, 19 Sep 2026 22:05:51 +0200 Subject: [PATCH 3/9] Fable review Signed-off-by: Guillaume LADORME 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 c3584ad32..2d0f6a08f 100644 --- a/timeserieschart/src/TimeSeriesChartBase.tsx +++ b/timeserieschart/src/TimeSeriesChartBase.tsx @@ -248,7 +248,6 @@ export const TimeSeriesChartBase = forwardRef(fun enableDataZoom(chartRef.current); } }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any mouseover: (params: any): void => { if ( params.componentType === 'series' && @@ -278,7 +277,6 @@ export const TimeSeriesChartBase = forwardRef(fun } setHoveredAnnotation(null); }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any mouseout: (params: any): void => { if ( params.componentType === 'series' && diff --git a/timeserieschart/src/annotations/AnnotationTooltip.tsx b/timeserieschart/src/annotations/AnnotationTooltip.tsx index 07d7691fa..4542fc1eb 100644 --- a/timeserieschart/src/annotations/AnnotationTooltip.tsx +++ b/timeserieschart/src/annotations/AnnotationTooltip.tsx @@ -183,7 +183,6 @@ export function buildAnnotationSeries( 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 86a8f464d..b1caa3853 100644 --- a/timeserieschart/src/utils/data-transform.ts +++ b/timeserieschart/src/utils/data-transform.ts @@ -252,7 +252,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({ From 64a0e7db3420ac4bae800365ec54a913d90511fa Mon Sep 17 00:00:00 2001 From: Guillaume Date: Mon, 21 Sep 2026 09:32:52 +0000 Subject: [PATCH 4/9] Replace Perses useId with React useId Signed-off-by: Guillaume Signed-off-by: Guillaume LADORME --- .../AlertManagerAlertsQueryEditor.tsx | 5 ++--- .../AlertManagerSilencesQueryEditor.tsx | 5 ++--- jaeger/src/plugins/JaegerTraceQueryEditor.test.tsx | 4 ---- jaeger/src/plugins/JaegerTraceQueryEditor.tsx | 5 ++--- piechart/src/PieChartPanel.tsx | 6 +++--- .../annotations/PrometheusPromQLAnnotationOptionEditor.tsx | 4 ++-- .../PrometheusTimeSeriesQueryEditor.tsx | 5 ++--- .../pyroscope-profile-query/PyroscopeProfileQueryEditor.tsx | 4 ++-- .../src/plugins/tempo-trace-query/TempoTraceQueryEditor.tsx | 5 ++--- timeserieschart/src/TimeSeriesChartPanel.tsx | 5 ++--- 10 files changed, 19 insertions(+), 29 deletions(-) diff --git a/alertmanager/src/plugins/alertmanager-alerts-query/AlertManagerAlertsQueryEditor.tsx b/alertmanager/src/plugins/alertmanager-alerts-query/AlertManagerAlertsQueryEditor.tsx index e700865b9..c75193486 100644 --- a/alertmanager/src/plugins/alertmanager-alerts-query/AlertManagerAlertsQueryEditor.tsx +++ b/alertmanager/src/plugins/alertmanager-alerts-query/AlertManagerAlertsQueryEditor.tsx @@ -12,12 +12,11 @@ // limitations under the License. import { Checkbox, FormControl, FormControlLabel, Stack } from '@mui/material'; -import { useId } from '@perses-dev/components'; import type { DatasourceSelectProps } from '@perses-dev/plugin-system'; import { DatasourceSelect } from '@perses-dev/plugin-system'; import { produce } from 'immer'; import type { ReactElement } from 'react'; -import { useCallback } from 'react'; +import { useId, useCallback } from 'react'; import { LazyTextField } from '../../components/LazyTextField'; import { @@ -36,7 +35,7 @@ interface AlertManagerAlertsQueryEditorProps { export function AlertManagerAlertsQueryEditor(props: AlertManagerAlertsQueryEditorProps): ReactElement { const { onChange, value } = props; const datasourceSelectValue = value.datasource ?? DEFAULT_ALERTMANAGER; - const datasourceSelectLabelID = useId('alertmanager-datasource-label'); + const datasourceSelectLabelID = useId(); const handleDatasourceChange: DatasourceSelectProps['onChange'] = (next) => { if (isAlertManagerDatasourceSelector(next)) { diff --git a/alertmanager/src/plugins/alertmanager-silences-query/AlertManagerSilencesQueryEditor.tsx b/alertmanager/src/plugins/alertmanager-silences-query/AlertManagerSilencesQueryEditor.tsx index 6831ef6b0..29880ea16 100644 --- a/alertmanager/src/plugins/alertmanager-silences-query/AlertManagerSilencesQueryEditor.tsx +++ b/alertmanager/src/plugins/alertmanager-silences-query/AlertManagerSilencesQueryEditor.tsx @@ -12,12 +12,11 @@ // limitations under the License. import { FormControl, Stack } from '@mui/material'; -import { useId } from '@perses-dev/components'; import type { DatasourceSelectProps } from '@perses-dev/plugin-system'; import { DatasourceSelect } from '@perses-dev/plugin-system'; import { produce } from 'immer'; import type { ReactElement } from 'react'; -import { useCallback } from 'react'; +import { useId, useCallback } from 'react'; import { LazyTextField } from '../../components/LazyTextField'; import { @@ -36,7 +35,7 @@ interface AlertManagerSilencesQueryEditorProps { export function AlertManagerSilencesQueryEditor(props: AlertManagerSilencesQueryEditorProps): ReactElement { const { onChange, value } = props; const datasourceSelectValue = value.datasource ?? DEFAULT_ALERTMANAGER; - const datasourceSelectLabelID = useId('alertmanager-silences-datasource-label'); + const datasourceSelectLabelID = useId(); const handleDatasourceChange: DatasourceSelectProps['onChange'] = (next) => { if (isAlertManagerDatasourceSelector(next)) { diff --git a/jaeger/src/plugins/JaegerTraceQueryEditor.test.tsx b/jaeger/src/plugins/JaegerTraceQueryEditor.test.tsx index 9209dda34..e403a33cf 100644 --- a/jaeger/src/plugins/JaegerTraceQueryEditor.test.tsx +++ b/jaeger/src/plugins/JaegerTraceQueryEditor.test.tsx @@ -17,10 +17,6 @@ import type { MockedFunction } from 'vitest'; import { JaegerTraceQueryEditor } from './JaegerTraceQueryEditor'; -vi.mock('@perses-dev/components', () => ({ - useId: vi.fn(() => 'jaeger-datasource-label'), -})); - vi.mock('@perses-dev/plugin-system', () => ({ DatasourceSelect: vi.fn(() => null), useDatasourceClient: vi.fn(), diff --git a/jaeger/src/plugins/JaegerTraceQueryEditor.tsx b/jaeger/src/plugins/JaegerTraceQueryEditor.tsx index 8f4899a84..37c4230a9 100644 --- a/jaeger/src/plugins/JaegerTraceQueryEditor.tsx +++ b/jaeger/src/plugins/JaegerTraceQueryEditor.tsx @@ -13,12 +13,11 @@ import type { TextFieldProps } from '@mui/material'; import { Autocomplete, FormControl, InputLabel, MenuItem, Select, Stack, TextField } from '@mui/material'; -import { useId } from '@perses-dev/components'; import type { DatasourceSelectProps } from '@perses-dev/plugin-system'; import { DatasourceSelect, useDatasourceClient, useDatasourceSelectValueToSelector } from '@perses-dev/plugin-system'; import { produce } from 'immer'; import type { ChangeEvent, ReactElement, SyntheticEvent } from 'react'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useId, useCallback, useEffect, useMemo, useState } from 'react'; import type { JaegerClient, JaegerTraceQuerySpec } from '../model'; import { DEFAULT_JAEGER, isDefaultJaegerSelector, isJaegerDatasourceSelector, JAEGER_DATASOURCE_KIND } from '../model'; @@ -46,7 +45,7 @@ export function JaegerTraceQueryEditor(props: JaegerTraceQueryEditorProps): Reac const { onChange, value } = props; const datasourceSelectValue = value.datasource ?? DEFAULT_JAEGER; const selectedDatasource = useDatasourceSelectValueToSelector(datasourceSelectValue, JAEGER_DATASOURCE_KIND); - const datasourceSelectLabelID = useId('jaeger-datasource-label'); + const datasourceSelectLabelID = useId(); const { data: client } = useDatasourceClient(selectedDatasource); const serviceOptions = useServiceOptions(client); const operationOptions = useOperationOptions(client, value.service); diff --git a/piechart/src/PieChartPanel.tsx b/piechart/src/PieChartPanel.tsx index 54b674fff..b24f7b5d8 100644 --- a/piechart/src/PieChartPanel.tsx +++ b/piechart/src/PieChartPanel.tsx @@ -13,13 +13,13 @@ import { Box } from '@mui/material'; import type { ChartInstance, LegendProps, SelectedLegendItemState } from '@perses-dev/components'; -import { ContentWithLegend, DEFAULT_LEGEND, useChartsTheme, useId } from '@perses-dev/components'; +import { ContentWithLegend, DEFAULT_LEGEND, useChartsTheme } from '@perses-dev/components'; import type { CalculationType, PanelProps } from '@perses-dev/plugin-system'; import { CalculationsMap, validateLegendSpec } from '@perses-dev/plugin-system'; import type { TimeSeriesData } from '@perses-dev/spec'; import merge from 'lodash/merge'; import type { ReactElement } from 'react'; -import { useMemo, useRef, useState } from 'react'; +import { useId, useMemo, useRef, useState } from 'react'; import { getSeriesColor } from './colors'; import { resolvePieChartVisualOptions } from './pie-chart-model'; @@ -39,7 +39,7 @@ export function PieChartPanel(props: PieChartPanelProps): ReactElement | null { } = props; const { colorPalette, innerRadius, outerRadius } = resolvePieChartVisualOptions(props.spec); const chartsTheme = useChartsTheme(); - const chartId = useId('time-series-panel'); + const chartId = useId(); const seriesNames = queryResults.flatMap((result) => result?.data.series?.map((series) => series.name) || []); // Memoize the color list so it only regenerates when color/palette/series count changes diff --git a/prometheus/src/annotations/PrometheusPromQLAnnotationOptionEditor.tsx b/prometheus/src/annotations/PrometheusPromQLAnnotationOptionEditor.tsx index 6998243b4..c40711317 100644 --- a/prometheus/src/annotations/PrometheusPromQLAnnotationOptionEditor.tsx +++ b/prometheus/src/annotations/PrometheusPromQLAnnotationOptionEditor.tsx @@ -12,11 +12,11 @@ // limitations under the License. import { Autocomplete, FormControl, Stack, TextField } from '@mui/material'; -import { useId } from '@perses-dev/components'; import type { DatasourceSelectProps, DatasourceSelectValue, OptionsEditorProps } from '@perses-dev/plugin-system'; import { DatasourceSelect, useDatasourceClient, useDatasourceSelectValueToSelector } from '@perses-dev/plugin-system'; import { produce } from 'immer'; import type { ReactElement } from 'react'; +import { useId } from 'react'; import { PromQLEditor } from '../components'; import type { PrometheusClient, PrometheusDatasourceSelector } from '../model'; @@ -42,7 +42,7 @@ export function PrometheusPromQLAnnotationOptionEditor(props: PrometheusAnnotati const datasourceSelectValue = datasource ?? DEFAULT_PROM; - const datasourceSelectLabelID = useId('prom-datasource-label'); // for panels with multiple queries, this component is rendered multiple times on the same page + const datasourceSelectLabelID = useId(); // for panels with multiple queries, this component is rendered multiple times on the same page const selectedDatasource = useDatasourceSelectValueToSelector( datasourceSelectValue, diff --git a/prometheus/src/plugins/prometheus-time-series-query/PrometheusTimeSeriesQueryEditor.tsx b/prometheus/src/plugins/prometheus-time-series-query/PrometheusTimeSeriesQueryEditor.tsx index 84944addc..6153f0fd4 100644 --- a/prometheus/src/plugins/prometheus-time-series-query/PrometheusTimeSeriesQueryEditor.tsx +++ b/prometheus/src/plugins/prometheus-time-series-query/PrometheusTimeSeriesQueryEditor.tsx @@ -12,7 +12,6 @@ // limitations under the License. import { Button, ButtonGroup, FormControl, Stack, TextField } from '@mui/material'; -import { useId } from '@perses-dev/components'; import { PanelEditorContext } from '@perses-dev/dashboards'; import type { DatasourceSelectProps } from '@perses-dev/plugin-system'; import { @@ -27,7 +26,7 @@ import { } from '@perses-dev/plugin-system'; import { produce } from 'immer'; import type { ReactElement } from 'react'; -import { useContext, useMemo } from 'react'; +import { useId, useContext, useMemo } from 'react'; import { PromQLEditor } from '../../components'; import type { DurationString, PrometheusClient, PrometheusDatasourceSelector } from '../../model'; @@ -57,7 +56,7 @@ export function PrometheusTimeSeriesQueryEditor(props: PrometheusTimeSeriesQuery const datasourceSelectValue = datasource ?? DEFAULT_PROM; - const datasourceSelectLabelID = useId('prom-datasource-label'); // for panels with multiple queries, this component is rendered multiple times on the same page + const datasourceSelectLabelID = useId(); // for panels with multiple queries, this component is rendered multiple times on the same page const selectedDatasource = useDatasourceSelectValueToSelector( datasourceSelectValue, diff --git a/pyroscope/src/plugins/pyroscope-profile-query/PyroscopeProfileQueryEditor.tsx b/pyroscope/src/plugins/pyroscope-profile-query/PyroscopeProfileQueryEditor.tsx index 91269ab98..e3e6e26b1 100644 --- a/pyroscope/src/plugins/pyroscope-profile-query/PyroscopeProfileQueryEditor.tsx +++ b/pyroscope/src/plugins/pyroscope-profile-query/PyroscopeProfileQueryEditor.tsx @@ -12,11 +12,11 @@ // limitations under the License. import { FormControl, InputLabel, Stack, TextField, useTheme } from '@mui/material'; -import { useId } from '@perses-dev/components'; import type { DatasourceSelectProps } from '@perses-dev/plugin-system'; import { DatasourceSelect, useDatasourceSelectValueToSelector } from '@perses-dev/plugin-system'; import { produce } from 'immer'; import type { ReactElement } from 'react'; +import { useId } from 'react'; import { ProfileTypeSelector, Service, Filters } from '../../components'; import type { PyroscopeDatasourceSelector } from '../../model'; @@ -37,7 +37,7 @@ export function PyroscopeProfileQueryEditor(props: ProfileQueryEditorProps): Rea datasourceSelectValue, PYROSCOPE_DATASOURCE_KIND, ) as PyroscopeDatasourceSelector; - const datasourceSelectLabelID = useId('pyroscope-datasource-label'); + const datasourceSelectLabelID = useId(); const { maxNodes, handleMaxNodesChange, maxNodesHasError } = useMaxNodesState(props); const { profileType, handleProfileTypeChange } = useProfileTypeState(props); diff --git a/tempo/src/plugins/tempo-trace-query/TempoTraceQueryEditor.tsx b/tempo/src/plugins/tempo-trace-query/TempoTraceQueryEditor.tsx index 450a4d887..a949e5f72 100644 --- a/tempo/src/plugins/tempo-trace-query/TempoTraceQueryEditor.tsx +++ b/tempo/src/plugins/tempo-trace-query/TempoTraceQueryEditor.tsx @@ -12,12 +12,11 @@ // limitations under the License. import { Box, Button, FormControl, InputLabel, MenuItem, Select, Stack } from '@mui/material'; -import { useId } from '@perses-dev/components'; import type { DatasourceSelectProps } from '@perses-dev/plugin-system'; import { DatasourceSelect, useDatasourceClient, useDatasourceSelectValueToSelector } from '@perses-dev/plugin-system'; import { produce } from 'immer'; import type { ReactElement } from 'react'; -import { useCallback, useState } from 'react'; +import { useId, useCallback, useState } from 'react'; import { TraceQLEditor, filterToTraceQL, traceQLToFilter } from '../../components'; import { AttributeFilters } from '../../components/AttributeFilters'; @@ -35,7 +34,7 @@ export function TempoTraceQueryEditor(props: TraceQueryEditorProps): ReactElemen const datasourceSelectValue = datasource ?? DEFAULT_TEMPO; const selectedDatasource = useDatasourceSelectValueToSelector(datasourceSelectValue, TEMPO_DATASOURCE_KIND); - const datasourceSelectLabelID = useId('tempo-datasource-label'); // for panels with multiple queries, this component is rendered multiple times on the same page + const datasourceSelectLabelID = useId(); // for panels with multiple queries, this component is rendered multiple times on the same page const { data: client } = useDatasourceClient(selectedDatasource); const { query, handleQueryChange, handleQueryBlur } = useQueryState(props); diff --git a/timeserieschart/src/TimeSeriesChartPanel.tsx b/timeserieschart/src/TimeSeriesChartPanel.tsx index f1a831805..d202cd621 100644 --- a/timeserieschart/src/TimeSeriesChartPanel.tsx +++ b/timeserieschart/src/TimeSeriesChartPanel.tsx @@ -27,7 +27,6 @@ import { YAxisLabel, useChartsTheme, ContentWithLegend, - useId, DEFAULT_TOOLTIP_CONFIG, getFormattedMultipleYAxes, DEFAULT_LEGEND, @@ -47,7 +46,7 @@ import type { Labels, TimeSeries, TimeSeriesData, TimeSeriesValueTuple } from '@ import type { GridComponentOption } from 'echarts'; import merge from 'lodash/merge'; import type { ReactElement } from 'react'; -import { memo, useCallback, useMemo, useRef, useState } from 'react'; +import { memo, useCallback, useId, useMemo, useRef, useState } from 'react'; import type { TimeSeriesChartOptions } from './time-series-chart-model'; import { DEFAULT_FORMAT, DEFAULT_VISUAL, THRESHOLD_PLOT_INTERVAL } from './time-series-chart-model'; @@ -95,7 +94,7 @@ function TimeSeriesChartPanelComponent(props: TimeSeriesChartProps): ReactElemen } = props; const chartsTheme = useChartsTheme(); const muiTheme = useTheme(); - const chartId = useId('time-series-panel'); + const chartId = useId(); const chartRef = useRef(null); From 8b6cbfd9821c7f3e46d30880fdeb0ea11d00cd6a Mon Sep 17 00:00:00 2001 From: Guillaume Date: Wed, 23 Sep 2026 09:45:33 +0000 Subject: [PATCH 5/9] [IGNORE] Enforce no explicit any in TypeScript Signed-off-by: Guillaume Signed-off-by: Guillaume LADORME --- .oxlintrc.json | 1 + scatterchart/src/Scatterplot.tsx | 9 ++++---- .../src/utils/data-transform.test.ts | 5 ++++- timeserieschart/src/TimeSeriesChartBase.tsx | 22 +++++++++++-------- .../src/annotations/AnnotationTooltip.tsx | 13 +++++++++-- tracingganttchart/src/test/convert/jaeger.ts | 3 ++- 6 files changed, 36 insertions(+), 17 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 5062b47a4..5e10cd949 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -14,6 +14,7 @@ "browser": true }, "rules": { + "typescript/no-explicit-any": "error", "typescript/explicit-function-return-type": "error", "typescript/explicit-module-boundary-types": "error", "typescript/array-type": ["error", { "default": "array-simple" }], diff --git a/scatterchart/src/Scatterplot.tsx b/scatterchart/src/Scatterplot.tsx index 9a978d113..c115b3b25 100644 --- a/scatterchart/src/Scatterplot.tsx +++ b/scatterchart/src/Scatterplot.tsx @@ -19,7 +19,7 @@ import { useRouterContext, useTimeRange, } from '@perses-dev/plugin-system'; -import type { EChartsOption, ScatterSeriesOption } from 'echarts'; +import type { EChartsOption, ScatterSeriesOption, TooltipComponentFormatterCallbackParams } from 'echarts'; import { ScatterChart as EChartsScatterChart } from 'echarts/charts'; import { DatasetComponent, @@ -119,9 +119,10 @@ export function Scatterplot(props: ScatterplotProps): ReactElement { axisPointer: { type: 'cross', }, - formatter: function (params: any) { - // TODO: import type from ECharts instead of using any - const data = params[0].data as EChartTraceValue; + formatter: function (params: TooltipComponentFormatterCallbackParams): string { + const item = Array.isArray(params) ? params[0] : params; + if (!item) return ''; + const data = item.data as EChartTraceValue; return [ `Service name: ${data.rootServiceName}
`, `Span name: ${data.rootTraceName}
`, diff --git a/statushistorychart/src/utils/data-transform.test.ts b/statushistorychart/src/utils/data-transform.test.ts index 224e213a4..a7e657203 100644 --- a/statushistorychart/src/utils/data-transform.test.ts +++ b/statushistorychart/src/utils/data-transform.test.ts @@ -51,7 +51,10 @@ describe('useStatusHistoryDataModel', () => { }, ], }, - definition: { query: 'some-query' } as any, + definition: { + kind: 'TimeSeriesQuery', + spec: { plugin: { kind: 'PrometheusTimeSeriesQuery', spec: { query: 'some-query' } } }, + }, }, ]; const colors = ['#ff0000', '#00ff00']; diff --git a/timeserieschart/src/TimeSeriesChartBase.tsx b/timeserieschart/src/TimeSeriesChartBase.tsx index 2d0f6a08f..dcb4ec87a 100644 --- a/timeserieschart/src/TimeSeriesChartBase.tsx +++ b/timeserieschart/src/TimeSeriesChartBase.tsx @@ -223,7 +223,7 @@ export const TimeSeriesChartBase = forwardRef(fun }; }, []); - const handleEvents: OnEventsType = useMemo(() => { + const handleEvents = useMemo>(() => { return { datazoom: (params): void => { if (onDataZoom === undefined) { @@ -248,18 +248,21 @@ export const TimeSeriesChartBase = forwardRef(fun enableDataZoom(chartRef.current); } }, - mouseover: (params: any): void => { + mouseover: (params): void => { if ( params.componentType === 'series' && params.seriesType === 'scatter' && + 'seriesId' in params && typeof params.seriesId === 'string' && params.seriesId.startsWith(EXEMPLAR_SERIES_ID_PREFIX) ) { - if (params.data?.exemplar) { + // This series ID identifies the metadata embedded by getExemplarSeries. + const point = params.data as { exemplar?: Exemplar; seriesLabels?: Labels; value?: number[] }; + if (point?.exemplar) { setHoveredExemplar({ - exemplar: params.data.exemplar, - seriesLabels: params.data.seriesLabels, - plottedValue: params.data?.value?.[1] ?? params.data.exemplar.value, + exemplar: point.exemplar, + seriesLabels: point.seriesLabels, + plottedValue: point.value?.[1] ?? point.exemplar.value, }); return; } @@ -268,7 +271,7 @@ export const TimeSeriesChartBase = forwardRef(fun // Only markPoint (triangles under the X-axis) opens the annotation tooltip. // Hovering markLine or anything else keeps the regular TimeSeries tooltip visible // and clears any stale hovered annotation (mouseout is sometimes missed by ECharts). - if (annotations && params.componentType === 'markPoint' && params.data?.annotationIndex !== undefined) { + if (annotations && params.componentType === 'markPoint' && typeof params.data?.annotationIndex === 'number') { const matchedAnnotation = annotations[params.data.annotationIndex] || null; if (matchedAnnotation) { setHoveredAnnotation(matchedAnnotation); @@ -277,10 +280,11 @@ export const TimeSeriesChartBase = forwardRef(fun } setHoveredAnnotation(null); }, - mouseout: (params: any): void => { + mouseout: (params): void => { if ( params.componentType === 'series' && params.seriesType === 'scatter' && + 'seriesId' in params && typeof params.seriesId === 'string' && params.seriesId.startsWith(EXEMPLAR_SERIES_ID_PREFIX) ) { @@ -290,7 +294,7 @@ export const TimeSeriesChartBase = forwardRef(fun if ( annotations && params.componentType === 'markPoint' && - params.data?.annotationIndex !== undefined && + typeof params.data?.annotationIndex === 'number' && annotations ) { // Only clear if the mouseout corresponds to the currently hovered annotation, so that diff --git a/timeserieschart/src/annotations/AnnotationTooltip.tsx b/timeserieschart/src/annotations/AnnotationTooltip.tsx index 4542fc1eb..d6f4b53ad 100644 --- a/timeserieschart/src/annotations/AnnotationTooltip.tsx +++ b/timeserieschart/src/annotations/AnnotationTooltip.tsx @@ -24,7 +24,7 @@ import { useMousePosition, } from '@perses-dev/components'; import { DEFAULT_ANNOTATION_COLOR } from '@perses-dev/plugin-system'; -import type { LineSeriesOption, YAXisComponentOption } from 'echarts'; +import type { LineSeriesOption, MarkPointComponentOption, YAXisComponentOption } from 'echarts'; import Pin from 'mdi-material-ui/Pin'; import PinOutline from 'mdi-material-ui/PinOutline'; import useResizeObserver from 'use-resize-observer'; @@ -183,7 +183,14 @@ export function buildAnnotationSeries( label?: { show: boolean }; annotationIndex?: number; }> = []; - const markPointData: any[] = []; + const markPointData: Array< + NonNullable[number] & { + annotationIndex: number; + isStart?: boolean; + isEnd?: boolean; + isPoint?: boolean; + } + > = []; annotations.forEach((annotation, index) => { const color = annotation.color ?? DEFAULT_ANNOTATION_COLOR; @@ -216,6 +223,7 @@ export function buildAnnotationSeries( // Add start and end markers for (const isStart of [true, false]) { markPointData.push({ + name: '', coord: [isStart ? annotation.start : annotation.end, 0], symbol: 'triangle', symbolSize: [12, 12], @@ -241,6 +249,7 @@ export function buildAnnotationSeries( // Add point marker markPointData.push({ + name: '', coord: [annotation.start, 0], symbol: 'triangle', symbolSize: [12, 10], diff --git a/tracingganttchart/src/test/convert/jaeger.ts b/tracingganttchart/src/test/convert/jaeger.ts index 7b40bbac8..334aef27b 100644 --- a/tracingganttchart/src/test/convert/jaeger.ts +++ b/tracingganttchart/src/test/convert/jaeger.ts @@ -107,12 +107,13 @@ function buildKeyValue(tag: Tag): otlpcommonv1.KeyValue { } function buildAnyValue(tags: Tag): otlpcommonv1.AnyValue { + const tagType = tags.type; switch (tags.type) { case 'string': return { stringValue: tags.value }; case 'int64': return { intValue: tags.value.toString() }; default: - throw new Error(`unknown jaeger tag type ${(tags as any).type}`); + throw new Error(`unknown jaeger tag type ${tagType}`); } } From af92114a373e72e1233e43ae644cb63cc95544c4 Mon Sep 17 00:00:00 2001 From: Guillaume LADORME Date: Wed, 23 Sep 2026 14:56:15 +0200 Subject: [PATCH 6/9] Bump version Signed-off-by: Guillaume LADORME --- package-lock.json | 320 +++++++++++++++++++++++----------------------- package.json | 4 +- 2 files changed, 162 insertions(+), 162 deletions(-) diff --git a/package-lock.json b/package-lock.json index 68ad22319..ce3ddb560 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,8 +66,8 @@ "express": "^4.21.3", "jsdom": "^27.0.0", "mdi-material-ui": "^7.9.2", - "oxfmt": "0.68.0", - "oxlint": "1.83.0", + "oxfmt": "0.70.0", + "oxlint": "1.85.0", "oxlint-plugin-react-doctor": "0.9.12", "react": "^18.3.0", "react-dom": "^18.3.0", @@ -2621,9 +2621,9 @@ } }, "node_modules/@oxfmt/binding-android-arm-eabi": { - "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==", + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.70.0.tgz", + "integrity": "sha512-Xd7YO4/T2axEj6FTLcj4Why3mTBqFMg+x24xtorT4Lb2+1g82090GH0a/4U1m0pABGYiix2bq1pqkYrmV3f0Sw==", "cpu": [ "arm" ], @@ -2638,9 +2638,9 @@ } }, "node_modules/@oxfmt/binding-android-arm64": { - "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==", + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.70.0.tgz", + "integrity": "sha512-x9rlMYyKXdgKdYyUJzGsK1ZV8P4di/J32ipzcS6Jet6p9r9UAh28neXIMtdlSaJJycdi61Z4YkcLKLpk8ueFjg==", "cpu": [ "arm64" ], @@ -2655,9 +2655,9 @@ } }, "node_modules/@oxfmt/binding-darwin-arm64": { - "version": "0.68.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.68.0.tgz", - "integrity": "sha512-ei4MCMzHFREmZwPJ7KuWUB4kBuHdsgDrnXGJVcEAopU7fj7S42I8BChdFILWdHvhFqR08FLJtOfbZIr2CDw0cA==", + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.70.0.tgz", + "integrity": "sha512-IUTUPvrBVYy7POh4stXzRdz4IVC/1QSaviCWoyenSlOhGu0X9j5K07vCTM9biLjAA2Zs31l0Rj5vvRpj9n95wA==", "cpu": [ "arm64" ], @@ -2672,9 +2672,9 @@ } }, "node_modules/@oxfmt/binding-darwin-x64": { - "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==", + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.70.0.tgz", + "integrity": "sha512-vw745q870oTd6J517O24asoX4/E+eK0nxYIFoedSLqgJ+nI5En7+ZS82iZSHZ69zevQrnOXiyHP01dA+t8xD8w==", "cpu": [ "x64" ], @@ -2689,9 +2689,9 @@ } }, "node_modules/@oxfmt/binding-freebsd-x64": { - "version": "0.68.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.68.0.tgz", - "integrity": "sha512-6jrEKgpJbilM1QaRv7hEtKXr4p4AK4jvvyOtajwyhu0kOz3e0O7OLnSTk6tBotRqCcUC4ehZRJ1Zx+Y99wieLw==", + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.70.0.tgz", + "integrity": "sha512-NO14EgSM9dFkcg+MfGPxvsKqXYs9LKaxPrOKXpv1R0rLokGGFDcCq6dBMq18dE4wlpFOovX0UZY2uh1P30O7QA==", "cpu": [ "x64" ], @@ -2706,9 +2706,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { - "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==", + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.70.0.tgz", + "integrity": "sha512-139OEhHarj9CYoJ/i9gXlPv4KLBGtLj2toseOWYFf09QwlhklaZk+wW3aOvlqoeZtuayvkoSNVWra7WJ21s3VQ==", "cpu": [ "arm" ], @@ -2723,9 +2723,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-musleabihf": { - "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==", + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.70.0.tgz", + "integrity": "sha512-GEh2PY3IWTE0M24eNhTduountANSbWyDmMnzFSQE/nGg/bjPugbUgiGuFu+xdqcQSd/HKSwH80/F2yVVD48yhA==", "cpu": [ "arm" ], @@ -2740,9 +2740,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-gnu": { - "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==", + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.70.0.tgz", + "integrity": "sha512-En5i+UJmZSPxuSf47F2Hl5YOzKB0bicQLnGQkeTCMQ35cWLtbrSwACJKfiLqRZrk05DwSnsJkhBRaM3OURtIaA==", "cpu": [ "arm64" ], @@ -2760,9 +2760,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-musl": { - "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==", + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.70.0.tgz", + "integrity": "sha512-WWOoV5W9Im3flVwOVrWn/2DUlOF8v5vcCip+kcNuaMpulRCh6nzzt1Su2vcL2F908YJIXNV3HvegbBHuyLwKHg==", "cpu": [ "arm64" ], @@ -2780,9 +2780,9 @@ } }, "node_modules/@oxfmt/binding-linux-ppc64-gnu": { - "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==", + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.70.0.tgz", + "integrity": "sha512-YUouneIqW+5n7aE8xx/zeZ6/utr/KH7oykcGoFyd8Uz8uh591T1oKlnoWA3BsRq/ZR42oY1w4MUYvS/0e/MQOA==", "cpu": [ "ppc64" ], @@ -2800,9 +2800,9 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-gnu": { - "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==", + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.70.0.tgz", + "integrity": "sha512-iEnMf21S5aGVa4hViDGY8sAQ/AHyCu2JPyrQF8P06wtHhSkD1YJBeT4m/KiGewgf7+a5XCYSCRIPcRQa1xwEoQ==", "cpu": [ "riscv64" ], @@ -2820,9 +2820,9 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-musl": { - "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==", + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.70.0.tgz", + "integrity": "sha512-91Sdniaj20fQzyMeCxMDzTP4c9s4RB8dGQ308xHhDR0n6U7+1Xq7N9klE7mfXq8iV3lRmIGSXi5X23Hn/0XX/g==", "cpu": [ "riscv64" ], @@ -2840,9 +2840,9 @@ } }, "node_modules/@oxfmt/binding-linux-s390x-gnu": { - "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==", + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.70.0.tgz", + "integrity": "sha512-uUV30M6E+2TKKGMaKiwfeL4RZrviHXlUxsrYJ/jFBb+1EZy+pnFT+hF73eeWdzh5NqPOAnw0iiMAIqjqiLZPFg==", "cpu": [ "s390x" ], @@ -2860,9 +2860,9 @@ } }, "node_modules/@oxfmt/binding-linux-x64-gnu": { - "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==", + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.70.0.tgz", + "integrity": "sha512-ivMcX6kNDPhqtbOaBt/ItFlLlTlXNHLgRuNmxP6Na6UuYXRT10llpJcAPbGeRgjjb3Qzv4jwPp3fB0hui50WNQ==", "cpu": [ "x64" ], @@ -2880,9 +2880,9 @@ } }, "node_modules/@oxfmt/binding-linux-x64-musl": { - "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==", + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.70.0.tgz", + "integrity": "sha512-w+S+fERxYmlZSyZlJK/U292FjyBoH8cCEj21/tYJX6atX5kNSn+HDkhlFQKT2zcMwUW0uAtUL/bOrlwJZwqRdA==", "cpu": [ "x64" ], @@ -2900,9 +2900,9 @@ } }, "node_modules/@oxfmt/binding-openharmony-arm64": { - "version": "0.68.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.68.0.tgz", - "integrity": "sha512-x2X5AZez7OgyLLFpwIgItoXBUqudDM7yiaTsxv8R8vKQ6e81l0jVw0NFeUCXzcl1sAJq8h+tC8N4mY8EiMeL4w==", + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.70.0.tgz", + "integrity": "sha512-Zlom1Xkx257R8bk4ZI4zJsrGno2opknz1+5v5baka3nn4FPyvNSdh8JUL4CdN1S1OWRMvJ9UJQ+RfIqGGCUEfA==", "cpu": [ "arm64" ], @@ -2917,9 +2917,9 @@ } }, "node_modules/@oxfmt/binding-win32-arm64-msvc": { - "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==", + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.70.0.tgz", + "integrity": "sha512-FQgPW5R17vzt7cgrJ8eG/dqX00o2xHsqFeLfw4xzA9FRHpN/DjFo9YDonvIIXGxiEuS9F/jZPnGO+KHNKuCo4Q==", "cpu": [ "arm64" ], @@ -2934,9 +2934,9 @@ } }, "node_modules/@oxfmt/binding-win32-ia32-msvc": { - "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==", + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.70.0.tgz", + "integrity": "sha512-ZfZublNhZ+XBndMiXhkiLlPE+XyGRDa0CweeTL6t1fZypfCh1LTg7e5CvnOeTBunq15MskOcRempumSPGAaCQA==", "cpu": [ "ia32" ], @@ -2951,9 +2951,9 @@ } }, "node_modules/@oxfmt/binding-win32-x64-msvc": { - "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==", + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.70.0.tgz", + "integrity": "sha512-HlIZEn+WzLQL0DszNzldiRl/DPRCX5R0Vkt6qeUPR1YHwy52hZZo4x6HoTOVmKRP2wUiwPGtKsihNY/f8KRaBg==", "cpu": [ "x64" ], @@ -2968,9 +2968,9 @@ } }, "node_modules/@oxlint/binding-android-arm-eabi": { - "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==", + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.85.0.tgz", + "integrity": "sha512-q2KO/Zso9UT+OMn0NF9ywn4E4t0MI3yxiDhNyhsQ7DyQJrC4FhFE4TXOi4bktFnOWXTMds8qZSbpv2XwRaNOBg==", "cpu": [ "arm" ], @@ -2985,9 +2985,9 @@ } }, "node_modules/@oxlint/binding-android-arm64": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.83.0.tgz", - "integrity": "sha512-hHfJ0vc17A4iUjH5p9BsTUPYbYRNxGpvD2lbu1aBRk54bzNIx9o5TtYF39QPZcV95DagZd+4DEAw2RH3G2ZsMg==", + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.85.0.tgz", + "integrity": "sha512-SxLN3ALjoT9NNdvpjEevGeHvfzTAFrF0NBYB5tzK7/GtCKMze3j1e/m/X2ozqGj2U9hfGG/dg/OG8vpVK4PiDA==", "cpu": [ "arm64" ], @@ -3002,9 +3002,9 @@ } }, "node_modules/@oxlint/binding-darwin-arm64": { - "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==", + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.85.0.tgz", + "integrity": "sha512-Y/Sup/J4f0f9UGsSd/xyCNTeWL+gepO63GBdEDAfue9nBsnk9zMmnIXx1O6b1V8C90vB5nucYNZ0pbMXAp8zJA==", "cpu": [ "arm64" ], @@ -3019,9 +3019,9 @@ } }, "node_modules/@oxlint/binding-darwin-x64": { - "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==", + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.85.0.tgz", + "integrity": "sha512-ApOSNC04ynpDTwvBD+//0wyfODRSbEzvRoKpX8teffmc27z8AockwSNeMXGJXn5KP85eahDgR/2llICWLkzcnw==", "cpu": [ "x64" ], @@ -3036,9 +3036,9 @@ } }, "node_modules/@oxlint/binding-freebsd-x64": { - "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==", + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.85.0.tgz", + "integrity": "sha512-bNrVrCOA/kHky3Tu79IXWXe5bhIgLXfUuUEDHlAGOHUk96MkvDZ1ecaQF19rwstrnaqfP1o9nBTqzIr9+ZHkUg==", "cpu": [ "x64" ], @@ -3053,9 +3053,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-gnueabihf": { - "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==", + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.85.0.tgz", + "integrity": "sha512-NUrzOJ1s/EqsVvfn2L/1D8Wro2LPIZUbihL8kOJLh5fEdGEN3rdOGUYq3HwnUIL8sjpoP+4N6RaGrgmMJnaMPw==", "cpu": [ "arm" ], @@ -3070,9 +3070,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-musleabihf": { - "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==", + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.85.0.tgz", + "integrity": "sha512-UJXrAT3E/RWkEqXLIs2ehETja1qfgkPb+5gwLIIS+o/6cf+grHvoOXTa5997a/YNQfcJS0DRBTOfZt95cvOI1g==", "cpu": [ "arm" ], @@ -3087,9 +3087,9 @@ } }, "node_modules/@oxlint/binding-linux-arm64-gnu": { - "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==", + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.85.0.tgz", + "integrity": "sha512-lK40QLjI0HxigO7CjDDshEtfYIeiYS0020v5BHFPqN4uuQBQxd2K9LNom2dW15o9F1937quSCRVp4ZsVhdbYdg==", "cpu": [ "arm64" ], @@ -3107,9 +3107,9 @@ } }, "node_modules/@oxlint/binding-linux-arm64-musl": { - "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==", + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.85.0.tgz", + "integrity": "sha512-c2zbdBwGKreHXwRx3gWBuFGJxLhxgsg6YlZ+3H+RgRusU/UEV9jNwJ3HGYK+nRo0LvBa7mt6Kj86xoVotUo8cw==", "cpu": [ "arm64" ], @@ -3127,9 +3127,9 @@ } }, "node_modules/@oxlint/binding-linux-ppc64-gnu": { - "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==", + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.85.0.tgz", + "integrity": "sha512-tlt/Hy8lZ97/lCPmCgw/B3k/mwh+BzaIPbPkldZEly7TwLmx0xe2CQcaW2g/rR0dOgS9JNGCZsMEqLhUNMGvaw==", "cpu": [ "ppc64" ], @@ -3147,9 +3147,9 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-gnu": { - "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==", + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.85.0.tgz", + "integrity": "sha512-3tNR9Xey82X0zKuY1d8hJ6Rc9gwRDurmqGLnQZa5xqOXy8/YyiqFXjAtugkKLY82obOlpK1eSiDRlgcNPuxtIg==", "cpu": [ "riscv64" ], @@ -3167,9 +3167,9 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-musl": { - "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==", + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.85.0.tgz", + "integrity": "sha512-wbGRd5PqCcjkJFHhZuZ2OBSUQY9czlQsoA/cQQB9JK/L9mC5MQgGoKAh+xd8QjA5V+0D3j+Qd1lAWn1I8zlelA==", "cpu": [ "riscv64" ], @@ -3187,9 +3187,9 @@ } }, "node_modules/@oxlint/binding-linux-s390x-gnu": { - "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==", + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.85.0.tgz", + "integrity": "sha512-3Sn0kSrE4DPZCWV/8o+n4x3aFZxI9ulMnkYlwCbJ8eUVkwRK2IerohE/A/z3SNbCwoPFOCJmGE5Avrq0rrvdvQ==", "cpu": [ "s390x" ], @@ -3207,9 +3207,9 @@ } }, "node_modules/@oxlint/binding-linux-x64-gnu": { - "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==", + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.85.0.tgz", + "integrity": "sha512-JY2pxxYfB62bAGfejljVCqc44etItehPuAyaeSAdMuEMtwNA00ggMnS66lC1oIhos6oOXUkuU6mZ9bpFh3BqWg==", "cpu": [ "x64" ], @@ -3227,9 +3227,9 @@ } }, "node_modules/@oxlint/binding-linux-x64-musl": { - "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==", + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.85.0.tgz", + "integrity": "sha512-5k74vZ6qJBjBHEOlBk9B/iv68Yu0F1Afw/vvT2ar6OGCqEeXLaSjXz2n/IPCbhLG22UoKoYEJTzpYraRdcp6PA==", "cpu": [ "x64" ], @@ -3247,9 +3247,9 @@ } }, "node_modules/@oxlint/binding-openharmony-arm64": { - "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==", + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.85.0.tgz", + "integrity": "sha512-GbAl5qt5TCkPLXTaIISZJnugrcBhra6rodcXc9jYt620UtdsTt71NlNmJmm0frxzFpd54x/G+MkitEJA8I/BoA==", "cpu": [ "arm64" ], @@ -3264,9 +3264,9 @@ } }, "node_modules/@oxlint/binding-win32-arm64-msvc": { - "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==", + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.85.0.tgz", + "integrity": "sha512-kjmws5MK0et2swk4ND85D7NVQyDHw162i6whtZDLUA/lo6FQyBZDcmMRCMcVZcNrAhIaftVb00x9ChGDOjjNJA==", "cpu": [ "arm64" ], @@ -3281,9 +3281,9 @@ } }, "node_modules/@oxlint/binding-win32-ia32-msvc": { - "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==", + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.85.0.tgz", + "integrity": "sha512-eSsIJx9n4yxvOqYTZyPEMyEXRmE60XH7xGAU7i0Qbsn1lf6Za3CWJ9aRd82oSFKXaxhp+sA6/yMJVRIpLpna6A==", "cpu": [ "ia32" ], @@ -3298,9 +3298,9 @@ } }, "node_modules/@oxlint/binding-win32-x64-msvc": { - "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==", + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.85.0.tgz", + "integrity": "sha512-pBebIPUpKKhWrhSMWhy8TdAZBewiXnfxmaAGxhzxM1068GagqFaTwgKlU6e+UyJ2sPR+VoHouhXuGJkQjsrDvA==", "cpu": [ "x64" ], @@ -9113,9 +9113,9 @@ } }, "node_modules/oxfmt": { - "version": "0.68.0", - "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.68.0.tgz", - "integrity": "sha512-Z0XMofcXCGUXbcpBHnWyUiX93BGiw1B+lcHNbQDWEtOhX06ewoFfu4zXkyiLhRrNnMq0twqXRHUcJetf+GsiQQ==", + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.70.0.tgz", + "integrity": "sha512-IsHxZ4y0wQLLMhnrJblBJgZsLDzfULrJnAw5j/QqsTlMa/m3AqsbToi+W71uhBGaqlqq/PbbjvHc09TJwdv3Tw==", "dev": true, "license": "MIT", "dependencies": { @@ -9131,25 +9131,25 @@ "url": "https://github.com/sponsors/oxc-project" }, "optionalDependencies": { - "@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" + "@oxfmt/binding-android-arm-eabi": "0.70.0", + "@oxfmt/binding-android-arm64": "0.70.0", + "@oxfmt/binding-darwin-arm64": "0.70.0", + "@oxfmt/binding-darwin-x64": "0.70.0", + "@oxfmt/binding-freebsd-x64": "0.70.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.70.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.70.0", + "@oxfmt/binding-linux-arm64-gnu": "0.70.0", + "@oxfmt/binding-linux-arm64-musl": "0.70.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.70.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.70.0", + "@oxfmt/binding-linux-riscv64-musl": "0.70.0", + "@oxfmt/binding-linux-s390x-gnu": "0.70.0", + "@oxfmt/binding-linux-x64-gnu": "0.70.0", + "@oxfmt/binding-linux-x64-musl": "0.70.0", + "@oxfmt/binding-openharmony-arm64": "0.70.0", + "@oxfmt/binding-win32-arm64-msvc": "0.70.0", + "@oxfmt/binding-win32-ia32-msvc": "0.70.0", + "@oxfmt/binding-win32-x64-msvc": "0.70.0" }, "peerDependencies": { "svelte": "^5.0.0", @@ -9165,9 +9165,9 @@ } }, "node_modules/oxlint": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.83.0.tgz", - "integrity": "sha512-cyDzSzaw3uzP0TeCeq3lLRPPoaUxkbB4ZOXj+kn+5r+BX9V+4bNVGk9lxer+WrgcpebH4JxLlJ3KQjveVztOLQ==", + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.85.0.tgz", + "integrity": "sha512-bc26s97nuvPj1ViyPsqmKecVkUWFMEdtayO8MaQ6oiLfs1pj94cQlZZhrh4BPNlr9HQosjhIlwgZKsfcwmcNgg==", "dev": true, "license": "MIT", "bin": { @@ -9180,25 +9180,25 @@ "url": "https://github.com/sponsors/oxc-project" }, "optionalDependencies": { - "@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" + "@oxlint/binding-android-arm-eabi": "1.85.0", + "@oxlint/binding-android-arm64": "1.85.0", + "@oxlint/binding-darwin-arm64": "1.85.0", + "@oxlint/binding-darwin-x64": "1.85.0", + "@oxlint/binding-freebsd-x64": "1.85.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.85.0", + "@oxlint/binding-linux-arm-musleabihf": "1.85.0", + "@oxlint/binding-linux-arm64-gnu": "1.85.0", + "@oxlint/binding-linux-arm64-musl": "1.85.0", + "@oxlint/binding-linux-ppc64-gnu": "1.85.0", + "@oxlint/binding-linux-riscv64-gnu": "1.85.0", + "@oxlint/binding-linux-riscv64-musl": "1.85.0", + "@oxlint/binding-linux-s390x-gnu": "1.85.0", + "@oxlint/binding-linux-x64-gnu": "1.85.0", + "@oxlint/binding-linux-x64-musl": "1.85.0", + "@oxlint/binding-openharmony-arm64": "1.85.0", + "@oxlint/binding-win32-arm64-msvc": "1.85.0", + "@oxlint/binding-win32-ia32-msvc": "1.85.0", + "@oxlint/binding-win32-x64-msvc": "1.85.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", diff --git a/package.json b/package.json index 89a9a5028..9ad24cc32 100644 --- a/package.json +++ b/package.json @@ -92,8 +92,8 @@ "express": "^4.21.3", "jsdom": "^27.0.0", "mdi-material-ui": "^7.9.2", - "oxfmt": "0.68.0", - "oxlint": "1.83.0", + "oxfmt": "0.70.0", + "oxlint": "1.85.0", "oxlint-plugin-react-doctor": "0.9.12", "react": "^18.3.0", "react-dom": "^18.3.0", From f405863cce40714834b411b2b1f7dffe53d29f24 Mon Sep 17 00:00:00 2001 From: Guillaume LADORME Date: Thu, 24 Sep 2026 01:14:22 +0200 Subject: [PATCH 7/9] Fix PieChart Signed-off-by: Guillaume LADORME --- piechart/src/PieChartBase.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/piechart/src/PieChartBase.tsx b/piechart/src/PieChartBase.tsx index 47d06d880..31a37cfa5 100644 --- a/piechart/src/PieChartBase.tsx +++ b/piechart/src/PieChartBase.tsx @@ -36,6 +36,7 @@ registerECharts([ ]); const CHART_SX = { width: '100%', height: '100%' }; +const CONTAINER_SX = { overflow: 'auto' }; const EMPHASIS_SHADOW_BLUR = 10; const EMPHASIS_SCALE_SIZE = 5; const PIE_INSET = EMPHASIS_SHADOW_BLUR + EMPHASIS_SCALE_SIZE; @@ -75,9 +76,11 @@ export function PieChartBase(props: PieChartBaseProps): ReactElement { const chartRef = useRef(); useLayoutEffect(() => { - chartRef.current?.resize(); + chartRef.current?.resize({ width, height }); }, [height, width]); + const containerStyle = useMemo(() => ({ width, height }), [width, height]); + const option = useMemo(() => { // ECharts treats numeric radii as pixels, so convert persisted percentages at the rendering boundary. const radius = innerRadius === undefined ? `${outerRadius}%` : [`${innerRadius}%`, `${outerRadius}%`]; @@ -125,13 +128,7 @@ export function PieChartBase(props: PieChartBaseProps): ReactElement { }, [data, formatOptions, innerRadius, mode, muiTheme.palette.background.default, outerRadius, showLabels]); return ( - + ); From ac135fa17d5fb07276e8e66ab8441f5b174998d9 Mon Sep 17 00:00:00 2001 From: Guillaume LADORME Date: Fri, 2 Oct 2026 14:52:17 +0200 Subject: [PATCH 8/9] [IGNORE] Fix review feedback on React Compiler changes - Table: follow the current header row when syncing filter cell widths, since the virtualized table remounts it on width, row, or column changes - Prometheus: only update the listbox max height when it changes - Time series: focus newly added query settings via a callback ref instead of an effect that ran on every render Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Guillaume LADORME --- .../filter/FilterInputs.tsx | 2 +- table/src/components/TablePanel.test.tsx | 84 +++++++++++++++++++ table/src/components/TablePanel.tsx | 34 +++++--- timeserieschart/src/QuerySettingsEditor.tsx | 13 ++- 4 files changed, 114 insertions(+), 19 deletions(-) diff --git a/prometheus/src/explore/PrometheusMetricsFinder/filter/FilterInputs.tsx b/prometheus/src/explore/PrometheusMetricsFinder/filter/FilterInputs.tsx index f9fe8c794..f6ed885d4 100644 --- a/prometheus/src/explore/PrometheusMetricsFinder/filter/FilterInputs.tsx +++ b/prometheus/src/explore/PrometheusMetricsFinder/filter/FilterInputs.tsx @@ -85,7 +85,7 @@ export const ListboxComponent = forwardRef { if (reference) { const measuredMaxHeight = getComputedStyle(reference).maxHeight; - if (measuredMaxHeight) setMaxHeight(measuredMaxHeight); + if (measuredMaxHeight && measuredMaxHeight !== maxHeight) setMaxHeight(measuredMaxHeight); } if (typeof ref === 'function') { diff --git a/table/src/components/TablePanel.test.tsx b/table/src/components/TablePanel.test.tsx index 3f3156c26..1ab095e36 100644 --- a/table/src/components/TablePanel.test.tsx +++ b/table/src/components/TablePanel.test.tsx @@ -19,6 +19,7 @@ import type { TimeSeriesData } from '@perses-dev/spec'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import type { ReactElement } from 'react'; import { VirtuosoMockContext } from 'react-virtuoso'; import type { TableOptions, TimeSeriesTableProps } from '../models'; @@ -54,6 +55,36 @@ const TEST_TIME_SERIES_TABLE_PROPS: Omit = spec: {}, }; +const VIRTUOSO_MOCK_CONTEXT = { viewportHeight: 600, itemHeight: 100 }; +const FILTERING_SPEC: TableOptions = { enableFiltering: true }; +const FILTERING_QUERY_RESULTS = [ + { definition: MOCK_TIME_SERIES_QUERY_DEFINITION, data: MOCK_TIME_SERIES_DATA_SINGLEVALUE }, +]; +const NARROW_DIMENSIONS = { width: 500, height: 500 }; +const WIDE_DIMENSIONS = { width: 600, height: 500 }; + +function renderFilteringTable(contentDimensions: TimeSeriesTableProps['contentDimensions']): ReactElement { + return ( + + + + + + + + + + ); +} + +function getFirstFilterCell(): HTMLElement { + return screen.getAllByRole('button', { name: '▼' })[0]!.parentElement!; +} + describe('TablePanel', () => { // Helper to render the panel with some context set const renderPanel = (data: TimeSeriesData, options?: TableOptions): void => { @@ -522,4 +553,57 @@ describe('TablePanel', () => { TEST_TIMEOUT, ); }); + + describe('filter row alignment', () => { + let headerCellWidth = 0; + const observedElements = new Set(); + + beforeEach(() => { + headerCellWidth = 120; + observedElements.clear(); + vi.spyOn(HTMLTableCellElement.prototype, 'getBoundingClientRect').mockImplementation( + () => ({ width: headerCellWidth }) as DOMRect, + ); + vi.stubGlobal( + 'ResizeObserver', + class { + observe(element: Element): void { + observedElements.add(element); + } + unobserve(element: Element): void { + observedElements.delete(element); + } + disconnect(): void { + observedElements.clear(); + } + }, + ); + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it( + 'should re-sync filter cell widths when the table replaces its header row', + async () => { + const { container, rerender } = render(renderFilteringTable(NARROW_DIMENSIONS)); + + await waitFor(() => expect(getFirstFilterCell().style.width).toBe('120px')); + const initialHeaderRow = container.querySelector('thead tr'); + expect(observedElements.has(initialHeaderRow!)).toBe(true); + + headerCellWidth = 150; + rerender(renderFilteringTable(WIDE_DIMENSIONS)); + + await waitFor(() => expect(getFirstFilterCell().style.width).toBe('150px')); + const currentHeaderRow = container.querySelector('thead tr'); + expect(currentHeaderRow).not.toBe(initialHeaderRow); + expect(observedElements.has(currentHeaderRow!)).toBe(true); + expect(observedElements.has(initialHeaderRow!)).toBe(false); + }, + TEST_TIMEOUT, + ); + }); }); diff --git a/table/src/components/TablePanel.tsx b/table/src/components/TablePanel.tsx index ee018aaf8..156e725c1 100644 --- a/table/src/components/TablePanel.tsx +++ b/table/src/components/TablePanel.tsx @@ -779,11 +779,6 @@ export function TablePanel({ contentDimensions, spec, queryResults }: TableProps return; } - const headerRow = scrollContainer.querySelector('thead tr'); - if (!headerRow) { - return; - } - const syncColumnWidths = (): void => { const headerCells = scrollContainer.querySelectorAll('thead tr th'); @@ -801,15 +796,32 @@ export function TablePanel({ contentDimensions, spec, queryResults }: TableProps }); }; - syncColumnWidths(); - - // Re-sync whenever the header row's size changes + // Re-sync whenever the header row's size changes. const resizeObserver = new ResizeObserver(syncColumnWidths); - if (headerRow) { - resizeObserver.observe(headerRow); - } + // The virtualized table remounts its header row when its width, rows, or columns change, + // so follow the current header row instead of observing a detached one. + let headerRow: Element | null = null; + const observeHeaderRow = (): void => { + const currentHeaderRow = scrollContainer.querySelector('thead tr'); + if (currentHeaderRow === headerRow) { + return; + } + if (headerRow) { + resizeObserver.unobserve(headerRow); + } + headerRow = currentHeaderRow; + if (headerRow) { + resizeObserver.observe(headerRow); + } + syncColumnWidths(); + }; + + observeHeaderRow(); + const mutationObserver = new MutationObserver(observeHeaderRow); + mutationObserver.observe(scrollContainer, { childList: true, subtree: true }); return (): void => { + mutationObserver.disconnect(); resizeObserver.disconnect(); }; }, [panelContainer, spec.enableFiltering, columns]); diff --git a/timeserieschart/src/QuerySettingsEditor.tsx b/timeserieschart/src/QuerySettingsEditor.tsx index 6b5a22ef5..efdbc4199 100644 --- a/timeserieschart/src/QuerySettingsEditor.tsx +++ b/timeserieschart/src/QuerySettingsEditor.tsx @@ -34,7 +34,7 @@ import CloseIcon from 'mdi-material-ui/Close'; import DeleteIcon from 'mdi-material-ui/DeleteOutline'; import AddIcon from 'mdi-material-ui/Plus'; import type { ReactElement } from 'react'; -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import type { QuerySettingsOptions, @@ -58,13 +58,12 @@ export function QuerySettingsEditor(props: TimeSeriesChartOptionsEditorProps): R ); }; // Every time a new query settings input is added, we want to focus the recently added input - const recentlyAddedInputRef = useRef(null); const focusRef = useRef(false); - useEffect(() => { - if (!recentlyAddedInputRef.current || !focusRef.current) return; - recentlyAddedInputRef.current?.focus(); + const recentlyAddedInputRef = useCallback((input: HTMLInputElement | null): void => { + if (!input || !focusRef.current) return; + input.focus(); focusRef.current = false; - }); + }, []); const handleQueryIndexChange = (e: React.ChangeEvent, i: number): void => { if (querySettingsList !== undefined) { @@ -348,7 +347,7 @@ interface QuerySettingsInputProps { onLineStyleChange: (lineStyle: string) => void; onAreaOpacityChange: (event: Event, value: number | number[]) => void; onDelete: () => void; - inputRef?: React.RefObject; + inputRef?: React.Ref; // Optional control handlers onAddColor: () => void; onRemoveColor: () => void; From 4e1216ff6de0706ff551fdadb4f92e87c2e83eeb Mon Sep 17 00:00:00 2001 From: Guillaume LADORME Date: Wed, 7 Oct 2026 10:57:44 +0200 Subject: [PATCH 9/9] Fix issues 1, 4, 7, 8, 11, 13 Signed-off-by: Guillaume LADORME --- .../plugins/alert-table/AlertTablePanel.tsx | 2 +- canvas/src/components/editor/EditorCanvas.tsx | 4 +-- datasourcevariable/src/DatasourceVariable.tsx | 5 +-- loki/src/components/logql-editor.tsx | 2 +- loki/src/components/logql-extension.ts | 2 +- .../filter/FilterInputs.tsx | 31 +++++++++++-------- .../EmbeddedPanelOptionsEditor.tsx | 8 +++-- .../GanttTable/ResizableDivider.tsx | 4 ++- victorialogs/src/components/logsql-editor.tsx | 2 +- .../src/components/logsql-extension.ts | 2 +- 10 files changed, 36 insertions(+), 26 deletions(-) diff --git a/alertmanager/src/plugins/alert-table/AlertTablePanel.tsx b/alertmanager/src/plugins/alert-table/AlertTablePanel.tsx index 7ea8072dd..1c54ffee0 100644 --- a/alertmanager/src/plugins/alert-table/AlertTablePanel.tsx +++ b/alertmanager/src/plugins/alert-table/AlertTablePanel.tsx @@ -521,7 +521,7 @@ export function AlertTablePanel({ spec, queryResults, contentDimensions }: Alert return result; }, [alerts, groupBy, allTrackedKeys, sortState]); - const currentKeys = groups.map((g) => g.key).join('\0'); + const currentKeys = useMemo(() => groups.map((g) => g.key).join('\0'), [groups]); const [previousGroupKeys, setPreviousGroupKeys] = useState(''); if (currentKeys !== previousGroupKeys) { setPreviousGroupKeys(currentKeys); diff --git a/canvas/src/components/editor/EditorCanvas.tsx b/canvas/src/components/editor/EditorCanvas.tsx index 33b099d0e..67221f3a0 100644 --- a/canvas/src/components/editor/EditorCanvas.tsx +++ b/canvas/src/components/editor/EditorCanvas.tsx @@ -202,11 +202,11 @@ export function EditorCanvas({ return ( <> - {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */} + {/* oxlint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */} option.label === datasourcePluginKind) ?? EMPTY_SELECTED_KIND; // If there is no selected kind and there are available options, select the first one + const selectedKindValue = selectedKind.value; useEffect(() => { const datasourcePluginKindArray = Array.from(datasourcePluginKindSet); if ( - selectedKind.value === EMPTY_SELECTED_KIND.value && + selectedKindValue === EMPTY_SELECTED_KIND.value && datasourcePluginKindArray.length > 0 && datasourcePluginKindArray[0] ) { onChange({ datasourcePluginKind: datasourcePluginKindArray[0] }); } - }, [selectedKind, onChange, datasourcePluginKindSet]); + }, [selectedKindValue, onChange, datasourcePluginKindSet]); return ( & { completionConfig?: CompletionConfig; diff --git a/loki/src/components/logql-extension.ts b/loki/src/components/logql-extension.ts index 3d32c5920..fcfcd9f5c 100644 --- a/loki/src/components/logql-extension.ts +++ b/loki/src/components/logql-extension.ts @@ -41,7 +41,7 @@ export interface CompletionConfig { timeRange?: AbsoluteTimeRange; } -export function LogQLExtension(completionCfg?: CompletionConfig): Array { +export function createLogQLExtension(completionCfg?: CompletionConfig): Array { const language = logqlLanguage(); // Only add autocomplete if config is provided diff --git a/prometheus/src/explore/PrometheusMetricsFinder/filter/FilterInputs.tsx b/prometheus/src/explore/PrometheusMetricsFinder/filter/FilterInputs.tsx index f6ed885d4..133d98ea6 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, useState } from 'react'; +import { cloneElement, forwardRef, useCallback, useMemo, useState } from 'react'; import { Virtuoso } from 'react-virtuoso'; import type { LabelFilter, Operator } from '../types'; @@ -79,21 +79,26 @@ export const ListboxComponent = forwardRef { + if (reference) { + const measuredMaxHeight = getComputedStyle(reference).maxHeight; + setMaxHeight((prev) => (measuredMaxHeight && measuredMaxHeight !== prev ? measuredMaxHeight : prev)); + } + + if (typeof ref === 'function') { + ref(reference); + } else if (ref) { + ref.current = reference; + } + }, + [ref], + ); + return (
    { - if (reference) { - const measuredMaxHeight = getComputedStyle(reference).maxHeight; - if (measuredMaxHeight && measuredMaxHeight !== maxHeight) setMaxHeight(measuredMaxHeight); - } - - if (typeof ref === 'function') { - ref(reference); - } else if (ref) { - ref.current = reference; - } - }} + ref={listRef} {...rest} > { if (!panelPlugin || !isSpecEmpty(spec)) { return; } - onChange(mergeWithPluginDefaults(panelPlugin, spec)); - }, [panelPlugin, spec, onChange]); + handleChange(mergeWithPluginDefaults(panelPlugin, spec)); + }, [panelPlugin, spec, handleChange]); if (isLoading) { return ( diff --git a/tracingganttchart/src/TracingGanttChart/GanttTable/ResizableDivider.tsx b/tracingganttchart/src/TracingGanttChart/GanttTable/ResizableDivider.tsx index 70af9f077..b22365eae 100644 --- a/tracingganttchart/src/TracingGanttChart/GanttTable/ResizableDivider.tsx +++ b/tracingganttchart/src/TracingGanttChart/GanttTable/ResizableDivider.tsx @@ -24,11 +24,14 @@ interface ResizableDividerProps { export function ResizableDivider(props: ResizableDividerProps): ReactElement { const { parentRef, onMove } = props; const [isResizing, setResizing] = useState(false); + const [spacing, setSpacing] = useState(0); const handleMouseDown = (e: ReactMouseEvent): void => { // disable any default actions (text selection, etc.) e.preventDefault(); + // read the flex gap once per drag instead of on every mousemove + setSpacing(parentRef.current ? parseFloat(getComputedStyle(parentRef.current).columnGap) || 0 : 0); setResizing(true); }; @@ -37,7 +40,6 @@ 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/victorialogs/src/components/logsql-editor.tsx b/victorialogs/src/components/logsql-editor.tsx index f97ea087c..1fda790a5 100644 --- a/victorialogs/src/components/logsql-editor.tsx +++ b/victorialogs/src/components/logsql-editor.tsx @@ -17,7 +17,7 @@ import CodeMirror, { EditorView } from '@uiw/react-codemirror'; import type { ReactElement } from 'react'; import { useMemo } from 'react'; -import { LogsQLExtension as createLogsQLExtension } from './logsql-extension'; +import { createLogsQLExtension } from './logsql-extension'; export type LogsQLEditorProps = Omit; diff --git a/victorialogs/src/components/logsql-extension.ts b/victorialogs/src/components/logsql-extension.ts index c2809fc1a..636cdb89f 100644 --- a/victorialogs/src/components/logsql-extension.ts +++ b/victorialogs/src/components/logsql-extension.ts @@ -29,7 +29,7 @@ function logsqlLanguage(): LRLanguage { }); } -export function LogsQLExtension(): Array { +export function createLogsQLExtension(): Array { const language = logsqlLanguage(); return [language]; }