diff --git a/gen/proto/go/google/api/http.pb.go b/gen/proto/go/google/api/http.pb.go index 14f7b4eff11..32a3f2caa58 100644 --- a/gen/proto/go/google/api/http.pb.go +++ b/gen/proto/go/google/api/http.pb.go @@ -1,4 +1,4 @@ -// Copyright 2025 Google LLC +// Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/proto/buf.lock b/proto/buf.lock index 9ae79671931..04b8054b1ee 100644 --- a/proto/buf.lock +++ b/proto/buf.lock @@ -4,8 +4,8 @@ deps: - remote: buf.build owner: googleapis repository: googleapis - commit: 004180b77378443887d3b55cabc00384 - digest: shake256:d26c7c2fd95f0873761af33ca4a0c0d92c8577122b6feb74eb3b0a57ebe47a98ab24a209a0e91945ac4c77204e9da0c2de0020b2cedc27bdbcdea6c431eec69b + commit: 536964a08a534d51b8f30f2d6751f1f9 + digest: shake256:b6d518a50df43704333587967830344b49247ac8cf0953847d710f2d72246f677aeba56593dcd78f9199afff8ae9498f8dd5efe54107e5a09c60fff872456ca9 - remote: buf.build owner: grpc-ecosystem repository: grpc-gateway diff --git a/ui/babel.react-compiler.cjs b/ui/babel.react-compiler.cjs new file mode 100644 index 00000000000..aa319613ae2 --- /dev/null +++ b/ui/babel.react-compiler.cjs @@ -0,0 +1,28 @@ +// Shared Babel config for packages that need the React compiler. +// Used by shared packages to compile React components with the compiler plugin. +module.exports = { + ignore: [ + '**/*.test.ts', + '**/*.test.tsx', + '**/*.benchmark.ts', + '**/*.benchmark.tsx', + '**/benchdata/**', + '**/testdata/**', + '**/.DS_Store', + '**/*.md', + ], + presets: [ + ['@babel/preset-env', {modules: false}], + ['@babel/preset-react', {runtime: 'automatic'}], + '@babel/preset-typescript', + ], + plugins: [ + [ + 'babel-plugin-react-compiler', + { + target: '18', + compilationMode: 'infer', + }, + ], + ], +}; diff --git a/ui/package.json b/ui/package.json index b37c5e8fd55..77edf03d68b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -30,11 +30,14 @@ "tailwindcss": "3.2.4" }, "devDependencies": { + "@babel/cli": "^7.28.6", "@babel/core": "7.29.0", "@babel/node": "7.29.0", "@babel/plugin-proposal-export-default-from": "7.27.1", "@babel/plugin-proposal-private-property-in-object": "7.21.11", "@babel/preset-env": "7.29.2", + "@babel/preset-react": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", "@chromatic-com/storybook": "1.9.0", "@ianvs/prettier-plugin-sort-imports": "3.7.2", "@mdx-js/loader": "2.3.0", @@ -65,6 +68,7 @@ "@typescript-eslint/eslint-plugin": "5.62.0", "@typescript-eslint/parser": "5.62.0", "arg": "5.0.2", + "babel-plugin-react-compiler": "1.0.0", "chromatic": "11.29.0", "css-loader": "6.11.0", "eslint": "8.45.0", @@ -78,7 +82,7 @@ "eslint-plugin-prettier": "5.5.5", "eslint-plugin-promise": "6.6.0", "eslint-plugin-react": "7.37.5", - "eslint-plugin-react-hooks": "4.6.2", + "eslint-plugin-react-hooks": "7.0.1", "eslint-plugin-standard": "5.0.0", "eslint-plugin-storybook": "0.12.0", "eslint-plugin-typescript-enum": "2.1.0", @@ -98,6 +102,7 @@ "postcss": "8.5.8", "prettier": "3.8.1", "prettier-plugin-tailwindcss": "^0.4.0", + "react-compiler-runtime": "1.0.0", "react-is": "18.3.1", "react-test-renderer": "18.3.1", "replace-in-files": "3.0.0", diff --git a/ui/packages/app/web/package.json b/ui/packages/app/web/package.json index 3c0cf27713e..d3ca0e64b67 100644 --- a/ui/packages/app/web/package.json +++ b/ui/packages/app/web/package.json @@ -63,12 +63,14 @@ }, "devDependencies": { "@types/lodash.throttle": "4.1.9", - "@vitejs/plugin-react-swc": "3.11.0", + "@vitejs/plugin-react": "4.7.0", + "babel-plugin-react-compiler": "1.0.0", "css-loader": "6.11.0", "eslint-config-prettier": "8.10.2", "eslint-plugin-import": "2.32.0", "jest": "29.7.0", "jest-runtime": "29.7.0", + "react-compiler-runtime": "1.0.0", "tslint": "6.1.3", "tslint-config-prettier": "1.18.0", "tslint-plugin-prettier": "2.3.0", diff --git a/ui/packages/app/web/src/components/ui/Navbar.tsx b/ui/packages/app/web/src/components/ui/Navbar.tsx index d16f3935de0..0f8d0470fc3 100644 --- a/ui/packages/app/web/src/components/ui/Navbar.tsx +++ b/ui/packages/app/web/src/components/ui/Navbar.tsx @@ -61,7 +61,7 @@ const Navbar = () => { const compareA = queryParams.get('compare_a'); const compareB = queryParams.get('compare_b'); - const queryParamsURL = parseParams(window.location.search); + const queryParamsURL = parseParams(location.search); /* eslint-disable @typescript-eslint/naming-convention */ const { diff --git a/ui/packages/app/web/src/pages/index.tsx b/ui/packages/app/web/src/pages/index.tsx index 063d3c38d3d..a1af24e394a 100644 --- a/ui/packages/app/web/src/pages/index.tsx +++ b/ui/packages/app/web/src/pages/index.tsx @@ -14,7 +14,7 @@ import {useCallback} from 'react'; import {GrpcWebFetchTransport} from '@protobuf-ts/grpcweb-transport'; -import {useNavigate} from 'react-router-dom'; +import {useLocation, useNavigate} from 'react-router-dom'; import {QueryServiceClient} from '@parca/client'; import {ParcaContextProvider, Spinner, URLStateProvider} from '@parca/components'; @@ -31,7 +31,12 @@ const queryClient = new QueryServiceClient( ); const Profiles = () => { + 'use no memo'; const navigate = useNavigate(); + // useLocation() subscribes to react-router location changes so this component + // re-renders on navigate(). 'use no memo' ensures the re-render propagates to + // URLStateProvider, whose no-deps effect syncs state from window.location.search. + useLocation(); const isDarkMode = useAppSelector(selectDarkMode); const navigateTo = useCallback( diff --git a/ui/packages/app/web/vite.config.ts b/ui/packages/app/web/vite.config.ts index 72a68fca6c4..6b1411786a0 100644 --- a/ui/packages/app/web/vite.config.ts +++ b/ui/packages/app/web/vite.config.ts @@ -11,14 +11,29 @@ // See the License for the specific language governing permissions and // limitations under the License. -import react from '@vitejs/plugin-react-swc'; +import react from '@vitejs/plugin-react'; import {defineConfig} from 'vite'; import svgr from 'vite-plugin-svgr'; // https://vitejs.dev/config/ export default defineConfig({ - // @ts-expect-error - plugins: [react(), svgr()], + // cast needed: dual @types/node versions create incompatible vite Plugin types + plugins: [ + react({ + babel: { + plugins: [ + [ + 'babel-plugin-react-compiler', + { + target: '18', + compilationMode: 'infer', + }, + ], + ], + }, + }), + svgr(), + ] as any, base: './', server: { port: 3000, diff --git a/ui/packages/shared/client/src/google/api/http.ts b/ui/packages/shared/client/src/google/api/http.ts index 8c211611541..4cde4df7f0e 100644 --- a/ui/packages/shared/client/src/google/api/http.ts +++ b/ui/packages/shared/client/src/google/api/http.ts @@ -2,7 +2,7 @@ // @generated from protobuf file "google/api/http.proto" (package "google.api", syntax proto3) // tslint:disable // -// Copyright 2025 Google LLC +// Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/ui/packages/shared/components/package.json b/ui/packages/shared/components/package.json index f1a45373122..62892997153 100644 --- a/ui/packages/shared/components/package.json +++ b/ui/packages/shared/components/package.json @@ -6,7 +6,7 @@ "scripts": { "test": "jest --coverage --config ../../../jest.config.js ./src/*", "prepublish": "pnpm run build", - "build": "tsc && tailwindcss -o dist/styles.css --minify", + "build": "babel src --out-dir dist --config-file ../../../babel.react-compiler.cjs --extensions .ts,.tsx --copy-files --no-copy-ignored && tsc --emitDeclarationOnly && tailwindcss -o dist/styles.css --minify", "build-swc": "swc ./src -d dist --copy-files && tailwindcss -o dist/styles.css --minify", "watch": "tsc-watch --onCompilationComplete 'tailwindcss -o dist/styles.css'" }, diff --git a/ui/packages/shared/components/src/DateTimeRangePicker/AbsoluteDatePicker/index.test.tsx b/ui/packages/shared/components/src/DateTimeRangePicker/AbsoluteDatePicker/index.test.tsx new file mode 100644 index 00000000000..cad87483ac3 --- /dev/null +++ b/ui/packages/shared/components/src/DateTimeRangePicker/AbsoluteDatePicker/index.test.tsx @@ -0,0 +1,45 @@ +// Copyright 2022 The Parca 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. + +/* eslint-disable jest-dom/prefer-to-have-value */ + +import {render} from '@testing-library/react'; +import {describe, expect, it, vi} from 'vitest'; + +import {AbsoluteDate, DateTimeRange} from '../utils'; +import AbsoluteDatePicker from './index'; + +describe('AbsoluteDatePicker', () => { + it('resyncs when an existing DateTimeRange instance is mutated', () => { + const range = new DateTimeRange( + new AbsoluteDate(new Date('2023-12-01T10:00:00Z')), + new AbsoluteDate(new Date('2023-12-01T15:30:00Z')) + ); + + const {rerender, getAllByRole} = render( + + ); + + const [startInput, endInput] = getAllByRole('textbox'); + expect((startInput as HTMLInputElement).value).toBe('2023-12-01 10:00:00'); + expect((endInput as HTMLInputElement).value).toBe('2023-12-01 15:30:00'); + + range.from = new AbsoluteDate(new Date('2023-12-02T08:15:00Z')); + range.to = new AbsoluteDate(new Date('2023-12-02T09:45:00Z')); + + rerender(); + + expect((startInput as HTMLInputElement).value).toBe('2023-12-02 08:15:00'); + expect((endInput as HTMLInputElement).value).toBe('2023-12-02 09:45:00'); + }); +}); diff --git a/ui/packages/shared/components/src/DateTimeRangePicker/AbsoluteDatePicker/index.tsx b/ui/packages/shared/components/src/DateTimeRangePicker/AbsoluteDatePicker/index.tsx index 6ad9206d586..5bdc43f3664 100644 --- a/ui/packages/shared/components/src/DateTimeRangePicker/AbsoluteDatePicker/index.tsx +++ b/ui/packages/shared/components/src/DateTimeRangePicker/AbsoluteDatePicker/index.tsx @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {useEffect, useMemo, useState} from 'react'; +import {useState} from 'react'; import {DateTimePicker} from '../../DateTimePicker'; import {AbsoluteDate, DateTimeRange, RelativeDate, getHistoricalDate} from '../utils'; @@ -22,52 +22,24 @@ interface AbsoluteDatePickerProps { } const AbsoluteDatePicker = ({range, onChange}: AbsoluteDatePickerProps): JSX.Element => { - const dateFromInRelative = useMemo(() => range.from as RelativeDate, [range.from]); - const dateToInRelative = useMemo(() => range.to as RelativeDate, [range.to]); - - const [from, setFrom] = useState( - range.from.isRelative() - ? new AbsoluteDate( - getHistoricalDate({ - unit: dateFromInRelative.unit, - value: dateFromInRelative.value, - }) - ) - : (range.from as AbsoluteDate) - ); - const [to, setTo] = useState( - range.to.isRelative() + const toAbsolute = (d: RelativeDate | AbsoluteDate): AbsoluteDate => + d.isRelative() ? new AbsoluteDate( - getHistoricalDate({ - unit: dateToInRelative.unit, - value: dateToInRelative.value, - }) + getHistoricalDate({unit: (d as RelativeDate).unit, value: (d as RelativeDate).value}) ) - : (range.to as AbsoluteDate) - ); + : (d as AbsoluteDate); + + const [from, setFrom] = useState(() => toAbsolute(range.from)); + const [to, setTo] = useState(() => toAbsolute(range.to)); + const [prevRangeFrom, setPrevRangeFrom] = useState(range.from); + const [prevRangeTo, setPrevRangeTo] = useState(range.to); - useEffect(() => { - setFrom( - range.from.isRelative() - ? new AbsoluteDate( - getHistoricalDate({ - unit: dateFromInRelative.unit, - value: dateFromInRelative.value, - }) - ) - : (range.from as AbsoluteDate) - ); - setTo( - range.to.isRelative() - ? new AbsoluteDate( - getHistoricalDate({ - unit: dateToInRelative.unit, - value: dateToInRelative.value, - }) - ) - : (range.to as AbsoluteDate) - ); - }, [dateFromInRelative, dateToInRelative, range.from, range.to]); + if (prevRangeFrom !== range.from || prevRangeTo !== range.to) { + setPrevRangeFrom(range.from); + setPrevRangeTo(range.to); + setFrom(toAbsolute(range.from)); + setTo(toAbsolute(range.to)); + } return (
diff --git a/ui/packages/shared/components/src/ResponsiveSvg/index.tsx b/ui/packages/shared/components/src/ResponsiveSvg/index.tsx index 6daec421439..f491b231822 100644 --- a/ui/packages/shared/components/src/ResponsiveSvg/index.tsx +++ b/ui/packages/shared/components/src/ResponsiveSvg/index.tsx @@ -11,6 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +/* eslint-disable react-hooks/set-state-in-effect */ + import {Children, useEffect, useState} from 'react'; import {useContainerDimensions} from '@parca/hooks'; @@ -33,6 +35,7 @@ const addPropsToChildren = (children: JSX.Element, props: {[x: string]: any}): J }; const ResponsiveSvg = (props: Props): JSX.Element => { + 'use no memo'; const {children} = props; const {ref, dimensions} = useContainerDimensions(); const {width} = dimensions ?? {width: 0}; diff --git a/ui/packages/shared/components/src/Table/index.tsx b/ui/packages/shared/components/src/Table/index.tsx index f11d4aea0df..4c708675bfc 100644 --- a/ui/packages/shared/components/src/Table/index.tsx +++ b/ui/packages/shared/components/src/Table/index.tsx @@ -139,6 +139,7 @@ const Table = ({ scrollToIndex, estimatedRowHeight = 26, }: Props): JSX.Element => { + 'use no memo'; const [sorting, setSorting] = useState(initialSorting); const tableContainerRef = useRef(null); const scrollingRef = useRef(); diff --git a/ui/packages/shared/hooks/package.json b/ui/packages/shared/hooks/package.json index ad1d6b9d701..d5d568987b4 100644 --- a/ui/packages/shared/hooks/package.json +++ b/ui/packages/shared/hooks/package.json @@ -6,7 +6,7 @@ "scripts": { "test": "jest --coverage --config ../../../jest.config.js ./src/*", "prepublish": "pnpm run build", - "build": "tsc", + "build": "babel src --out-dir dist --config-file ../../../babel.react-compiler.cjs --extensions .ts,.tsx --copy-files --no-copy-ignored && tsc --emitDeclarationOnly", "build-swc": "swc ./src -d dist --copy-files", "watch": "tsc-watch" }, diff --git a/ui/packages/shared/icons/package.json b/ui/packages/shared/icons/package.json index 7c7ceee815f..8775487733d 100644 --- a/ui/packages/shared/icons/package.json +++ b/ui/packages/shared/icons/package.json @@ -7,7 +7,7 @@ "test": "jest --coverage --config ../../../jest.config.js ./src/*", "watch": "tsc-watch --onCompilationComplete 'pnpm run compileStaticFiles'", "build-swc": "swc ./src -d dist --copy-files && pnpm run compileStaticFiles", - "build": "tsc && pnpm run compileStaticFiles", + "build": "babel src --out-dir dist --config-file ../../../babel.react-compiler.cjs --extensions .ts,.tsx --copy-files --no-copy-ignored && tsc --emitDeclarationOnly && pnpm run compileStaticFiles", "compileStaticFiles": "tailwindcss -o dist/styles.css && mkdir -p ./dist/assets && cp ./src/assets/* ./dist/assets/" }, "keywords": [], diff --git a/ui/packages/shared/profile/package.json b/ui/packages/shared/profile/package.json index dd1e6aaae6b..21c1df07676 100644 --- a/ui/packages/shared/profile/package.json +++ b/ui/packages/shared/profile/package.json @@ -71,7 +71,7 @@ "scripts": { "test": "jest --coverage --config ../../../jest.config.js ./src/*", "prepublish": "pnpm run build", - "build": "tsc && pnpm run compile:styles", + "build": "babel src --out-dir dist --config-file ../../../babel.react-compiler.cjs --extensions .ts,.tsx --copy-files --no-copy-ignored && tsc --emitDeclarationOnly && pnpm run compile:styles", "build-swc": "swc ./src -d dist --copy-files && pnpm run compile:styles", "watch": "tsc-watch --onCompilationComplete 'pnpm run compile:styles'", "compile:styles": "tailwindcss -o dist/styles.css --minify" diff --git a/ui/packages/shared/profile/src/GraphTooltipArrow/index.tsx b/ui/packages/shared/profile/src/GraphTooltipArrow/index.tsx index 969d466fc6a..1fa035cba37 100644 --- a/ui/packages/shared/profile/src/GraphTooltipArrow/index.tsx +++ b/ui/packages/shared/profile/src/GraphTooltipArrow/index.tsx @@ -11,6 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +/* eslint-disable react-hooks/refs */ + import React, {useEffect, useState} from 'react'; import {flip, offset, shift, useFloating, type VirtualElement} from '@floating-ui/react'; @@ -39,6 +41,7 @@ function createPositionedVirtualElement(contextElement: Element, x = 0, y = 0): } const GraphTooltip = ({children, contextElement}: GraphTooltipProps): React.JSX.Element => { + 'use no memo'; const [isPositioned, setIsPositioned] = useState(false); const {refs, floatingStyles, update} = useFloating({ diff --git a/ui/packages/shared/profile/src/MatchersInput/SuggestionsList.test.tsx b/ui/packages/shared/profile/src/MatchersInput/SuggestionsList.test.tsx new file mode 100644 index 00000000000..50a08fcf8c7 --- /dev/null +++ b/ui/packages/shared/profile/src/MatchersInput/SuggestionsList.test.tsx @@ -0,0 +1,70 @@ +// Copyright 2022 The Parca 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 {useRef} from 'react'; + +import {act, fireEvent, render} from '@testing-library/react'; +import {beforeAll, describe, expect, it, vi} from 'vitest'; + +import SuggestionsList, {Suggestion, Suggestions} from './SuggestionsList'; + +vi.mock('@parca/components', () => ({ + RefreshButton: ({title}: {title: string}) => , + useParcaContext: () => ({ + loader:
loading
, + }), +})); + +beforeAll(() => { + Element.prototype.scrollIntoView = vi.fn(); +}); + +const TestHarness = ({inputKey = 'initial'}: {inputKey?: string}): JSX.Element => { + const inputRef = useRef(null); + const suggestions = new Suggestions(); + suggestions.labelNames.push(new Suggestion('labelName', 'na', 'namespace')); + + return ( +
+