diff --git a/.eslintrc.js b/.eslintrc.js index 94364802..3428699d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -11,11 +11,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Base eslint configuration for typescript projects module.exports = { extends: [ 'eslint:recommended', 'plugin:react/recommended', - 'plugin:react-hooks/recommended', + 'plugin:react-hooks/recommended-legacy', 'plugin:jsx-a11y/recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', @@ -48,30 +49,73 @@ module.exports = { }, rules: { - 'prettier/prettier': 'error', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/explicit-function-return-type': 'error', + '@typescript-eslint/explicit-module-boundary-types': 'error', '@typescript-eslint/array-type': [ 'error', { default: 'array-simple', }, ], - 'import/order': 'error', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], // you must disable the base rule as it can report incorrect errors 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], - 'react/prop-types': 'off', - 'react-hooks/exhaustive-deps': 'error', - // Not necessary in React 17 - 'react/react-in-jsx-scope': 'off', - 'react/jsx-curly-brace-presence': ['error', { props: 'never', children: 'never', propElementValues: 'always' }], + 'prettier/prettier': 'error', + + eqeqeq: ['error', 'always'], // We use this rule instead of the core eslint `no-duplicate-imports` // because it avoids false errors on cases where we have a regular - // import and an `import type`. + // import and an `import type` 'import/no-duplicates': 'error', + 'import/order': 'error', + + // Not necessary in React 17 + 'react/react-in-jsx-scope': 'off', + 'react/jsx-curly-brace-presence': ['error', { props: 'never', children: 'never', propElementValues: 'always' }], + 'react-hooks/exhaustive-deps': 'error', + 'react-hooks/error-boundaries': 'error', + 'react-hooks/globals': 'error', + 'react-hooks/immutability': 'error', + 'react-hooks/purity': 'error', + 'react-hooks/refs': 'error', + 'react-hooks/set-state-in-effect': 'error', + 'react-hooks/set-state-in-render': 'error', + 'react-hooks/static-components': 'error', + 'react-hooks/unsupported-syntax': 'error', + 'react-hooks/use-memo': 'error', + + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + /** + * This library is gigantic and named imports end up slowing down builds/blowing out bundle sizes, + * so this prevents that style of import. + */ + group: ['mdi-material-ui', '!mdi-material-ui/'], + message: ` +Please use the default import from the icon file directly rather than using a named import. + +Good: +import IconName from 'mdi-material-ui/IconName'; + +Bad: +import { IconName } from 'mdi-material-ui'; +`, + }, + ], + }, + ], }, ignorePatterns: ['**/dist'], diff --git a/barchart/src/BarChartBase.tsx b/barchart/src/BarChartBase.tsx index 5628f986..919037b7 100644 --- a/barchart/src/BarChartBase.tsx +++ b/barchart/src/BarChartBase.tsx @@ -20,6 +20,7 @@ import { GridComponent, DatasetComponent, TitleComponent, TooltipComponent } fro import { CanvasRenderer } from 'echarts/renderers'; import { Box } from '@mui/material'; +// eslint-disable-next-line react-hooks/rules-of-hooks use([EChartsBarChart, GridComponent, DatasetComponent, TitleComponent, TooltipComponent, CanvasRenderer]); const BAR_WIN_WIDTH = 14; @@ -120,7 +121,7 @@ export function BarChartBase(props: BarChartBaseProps): ReactElement { sx={{ overflow: 'auto' }} > ) => { +export const DatasourceVariableOptionEditor = (props: OptionsEditorProps): ReactElement => { const { onChange, value } = props; const { datasourcePluginKind } = value; const { data: datasourcePlugins } = useListPluginMetadata(['Datasource']); diff --git a/flamechart/src/components/CustomBreadcrumb.tsx b/flamechart/src/components/CustomBreadcrumb.tsx index f5f8d5cc..0555e6ba 100644 --- a/flamechart/src/components/CustomBreadcrumb.tsx +++ b/flamechart/src/components/CustomBreadcrumb.tsx @@ -47,7 +47,7 @@ const StyledBreadcrumb = styled(Chip)(({ theme }) => { export function CustomBreadcrumb(props: CustomBreadcrumbProps): ReactElement { const { totalValue, totalSample, otherItemSample, onSelectedIdChange } = props; - const handleClick = (event: React.MouseEvent) => { + const handleClick = (event: React.MouseEvent): void => { event.preventDefault(); onSelectedIdChange(0); }; diff --git a/flamechart/src/components/FlameChart.tsx b/flamechart/src/components/FlameChart.tsx index e76b2e4c..d8f62569 100644 --- a/flamechart/src/components/FlameChart.tsx +++ b/flamechart/src/components/FlameChart.tsx @@ -220,7 +220,7 @@ export function FlameChart(props: FlameChartProps): ReactElement { const flameChart = useMemo( () => ( = (props) => { // keep liveSpec up to date useEffect(() => { + // TODO: improve this logic to avoid useState in useEffect + // eslint-disable-next-line react-hooks/set-state-in-effect setLiveSpec(spec); setSelectedId(0); setSearchValue(''); @@ -68,13 +70,13 @@ export const FlameChartPanel: FC = (props) => { const noDataTextStyle = (chartsTheme.noDataOption.title as TitleComponentOption).textStyle as SxProps; - const onChangePalette = (newPalette: 'package-name' | 'value') => { + const onChangePalette = (newPalette: 'package-name' | 'value'): void => { setLiveSpec((prev) => { return { ...prev, palette: newPalette }; }); }; - const onDisplayChange = (value: 'table' | 'flame-graph' | 'both' | 'none') => { + const onDisplayChange = (value: 'table' | 'flame-graph' | 'both' | 'none'): void => { let showTable = true; let showFlameGraph = true; if (value === 'table') { diff --git a/flamechart/src/components/Settings.tsx b/flamechart/src/components/Settings.tsx index 25fa9c24..1fb4b11f 100644 --- a/flamechart/src/components/Settings.tsx +++ b/flamechart/src/components/Settings.tsx @@ -39,27 +39,27 @@ export function Settings(props: SettingsProps): ReactElement { minWidth: 'auto', }; - const handleChangeColorShemeClick = (event: React.MouseEvent) => { + const handleChangeColorShemeClick = (event: React.MouseEvent): void => { setAnchorEl(event.currentTarget); }; - const handleByPackageNameClick = () => { + const handleByPackageNameClick = (): void => { onChangePalette('package-name'); handleClose(); }; - const handleByValueClick = () => { + const handleByValueClick = (): void => { onChangePalette('value'); handleClose(); }; - const handleClose = () => { + const handleClose = (): void => { setAnchorEl(null); }; - const isTableSelected = () => selectedView === 'table'; - const isFlameGraphSelected = () => selectedView === 'flame-graph'; - const isBothSelected = () => selectedView === 'both'; + 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(() => { diff --git a/flamechart/src/components/TableChart.tsx b/flamechart/src/components/TableChart.tsx index 018df0e3..1d61b467 100644 --- a/flamechart/src/components/TableChart.tsx +++ b/flamechart/src/components/TableChart.tsx @@ -58,7 +58,7 @@ export function TableChart(props: TableChartProps): ReactElement { align: 'left', enableSorting: true, width: 0.5 * availableWidth, - cell: (ctx) => { + cell: (ctx): ReactElement => { const cellValue = ctx.getValue(); return ( { + cell: (ctx): string => { const cellValue = ctx.getValue(); return formatItemValue(unit, cellValue); }, @@ -96,7 +96,7 @@ export function TableChart(props: TableChartProps): ReactElement { align: 'right', enableSorting: true, width: 0.25 * availableWidth, - cell: (ctx) => { + cell: (ctx): string => { const cellValue = ctx.getValue(); return formatItemValue(unit, cellValue); }, diff --git a/flamechart/src/utils/data-transform.test.ts b/flamechart/src/utils/data-transform.test.ts index 9df573c5..e65a367b 100644 --- a/flamechart/src/utils/data-transform.test.ts +++ b/flamechart/src/utils/data-transform.test.ts @@ -17,7 +17,7 @@ import { filterStackTraceById, buildSamples } from './data-transform'; import { getSpanColor } from './palette-gen'; // define the structuredClone function -global.structuredClone = (val) => JSON.parse(JSON.stringify(val)); +global.structuredClone = (val): unknown => JSON.parse(JSON.stringify(val)); describe('filterStackTraceById', () => { const emptyJson: StackTrace = {} as StackTrace; diff --git a/gaugechart/src/GaugeChartBase.tsx b/gaugechart/src/GaugeChartBase.tsx index d01c3e8a..042beec0 100644 --- a/gaugechart/src/GaugeChartBase.tsx +++ b/gaugechart/src/GaugeChartBase.tsx @@ -12,13 +12,14 @@ // limitations under the License. import { EChart, useChartsTheme } from '@perses-dev/components'; -import { formatValue, useDeepMemo, FormatOptions } from '@perses-dev/core'; +import { formatValue, FormatOptions } from '@perses-dev/core'; import { use, EChartsCoreOption } from 'echarts/core'; import { GaugeChart as EChartsGaugeChart, GaugeSeriesOption } from 'echarts/charts'; import { GridComponent, TitleComponent, TooltipComponent } from 'echarts/components'; import { CanvasRenderer } from 'echarts/renderers'; -import { ReactElement } from 'react'; +import { ReactElement, useMemo } from 'react'; +// eslint-disable-next-line react-hooks/rules-of-hooks use([EChartsGaugeChart, GridComponent, TitleComponent, TooltipComponent, CanvasRenderer]); // adjusts when to show pointer icon @@ -48,7 +49,7 @@ export function GaugeChartBase(props: GaugeChartBaseProps): ReactElement { const chartsTheme = useChartsTheme(); // useDeepMemo ensures value size util does not rerun everytime you hover on the chart - const option: EChartsCoreOption = useDeepMemo(() => { + const option: EChartsCoreOption = useMemo(() => { if (data.value === undefined) return chartsTheme.noDataOption; // Base configuration shared by both series (= progress & scale) @@ -168,13 +169,27 @@ export function GaugeChartBase(props: GaugeChartBaseProps): ReactElement { }, ], }; - }, [data, width, height, chartsTheme, format, axisLine, max, valueFontSize, progressWidth, titleFontSize]); + }, [ + axisLine, + chartsTheme.echartsTheme.textStyle?.color, + chartsTheme.noDataOption, + data.label, + data.value, + format, + max, + progressWidth, + titleFontSize, + valueFontSize, + width, + ]); return ( yellow->red gradient @@ -85,7 +86,7 @@ export function HeatMapChart({ return { tooltip: { appendToBody: true, - formatter: (params: { data: HeatMapDataItem; marker: string }) => { + formatter: (params: { data: HeatMapDataItem; marker: string }): string => { return generateTooltipHTML({ data: params.data.value, label: params.data.label, @@ -167,9 +168,11 @@ export function HeatMapChart({ const chart = useMemo( () => ( =16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://opencollective.com/pkgr" } }, "node_modules/@popperjs/core": { @@ -6078,18 +6078,20 @@ "license": "MIT" }, "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -7767,9 +7769,9 @@ } }, "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", "dev": true, "license": "MIT", "dependencies": { @@ -7777,18 +7779,18 @@ "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", @@ -7800,21 +7802,24 @@ "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", + "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", + "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", + "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -7823,7 +7828,7 @@ "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -8098,14 +8103,17 @@ } }, "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, "peerDependencies": { "eslint": ">=7.0.0" } @@ -8133,9 +8141,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, "license": "MIT", "dependencies": { @@ -8161,30 +8169,30 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", + "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", - "is-core-module": "^2.15.1", + "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", - "object.values": "^1.2.0", + "object.values": "^1.2.1", "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", + "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "engines": { @@ -8316,14 +8324,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.4.tgz", - "integrity": "sha512-SFtuYmnhwYCtuCDTKPoK+CEzCnEgKTU2qTLwoCxvrC0MFBTIXo1i6hDYOI4cwHaE5GZtlWmTN3YfucYi7KJwPw==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.10.2" + "synckit": "^0.11.7" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -8334,7 +8342,7 @@ "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", - "eslint-config-prettier": "*", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "peerDependenciesMeta": { @@ -8347,9 +8355,9 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.37.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", - "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, "license": "MIT", "dependencies": { @@ -8363,7 +8371,7 @@ "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.8", + "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", @@ -8380,16 +8388,45 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", - "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-6.1.1.tgz", + "integrity": "sha512-St9EKZzOAQF704nt2oJvAKZHjhrpg25ClQoaAlHmPZuajFldVLqRDW4VBNAS01NzeiQF0m0qhG1ZA807K6aVaQ==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "zod": "^3.22.4 || ^4.0.0", + "zod-validation-error": "^3.0.3 || ^4.0.0" + }, "engines": { - "node": ">=10" + "node": ">=18" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-hooks/node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/eslint-plugin-react-hooks/node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" } }, "node_modules/eslint-plugin-react/node_modules/brace-expansion": { @@ -10251,6 +10288,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -14311,6 +14361,20 @@ "node": ">= 0.8" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/streamroller": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", @@ -14650,20 +14714,19 @@ "license": "MIT" }, "node_modules/synckit": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.10.3.tgz", - "integrity": "sha512-R1urvuyiTaWfeCggqEvpDJwAlDVdsT9NM+IP//Tk2x7qHCkSvBk/fwFgw/TLAHzZlrAnnazMcRw0ZD8HlYFTEQ==", + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.2.0", - "tslib": "^2.8.1" + "@pkgr/core": "^0.2.9" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://opencollective.com/synckit" } }, "node_modules/tapable": { diff --git a/package.json b/package.json index 0b1829bc..825a368b 100644 --- a/package.json +++ b/package.json @@ -62,13 +62,13 @@ "@typescript-eslint/eslint-plugin": "^8.18.0", "concurrently": "^9.1.2", "cross-env": "^7.0.3", - "eslint": "^8.57.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jsx-a11y": "^6.8.0", - "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-react": "^7.34.1", - "eslint-plugin-react-hooks": "^4.6.2", + "eslint": "^8.57.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^6.1.1", "express": "^4.21.2", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", diff --git a/piechart/src/PieChartBase.tsx b/piechart/src/PieChartBase.tsx index 6ccc21e2..2928f7a1 100644 --- a/piechart/src/PieChartBase.tsx +++ b/piechart/src/PieChartBase.tsx @@ -25,6 +25,7 @@ import { Box } from '@mui/material'; import { ReactElement } from 'react'; import { EChart, useChartsTheme } from '@perses-dev/components'; +// eslint-disable-next-line react-hooks/rules-of-hooks use([EChartsPieChart, GridComponent, DatasetComponent, TitleComponent, TooltipComponent, CanvasRenderer]); const PIE_WIN_WIDTH = 12; @@ -91,7 +92,7 @@ export function PieChartBase(props: PieChartBaseProps): ReactElement { sx={{ overflow: 'auto' }} > ({ ...prevStyle, top: 'calc(50% - 1px)', @@ -230,6 +232,8 @@ export default function TreeNode({ .map(([lv, cnt]) => ({ value: lv, count: cnt })); }); + // TODO: improve this logic to avoid useState in useEffect + // eslint-disable-next-line react-hooks/set-state-in-effect setResultStats({ numSeries: resultSeries, sortedLabelCards: Object.entries(labelCardinalities).sort((a, b) => b[1] - a[1]), diff --git a/prometheus/src/explore/PrometheusMetricsFinder/filter/FilterInputs.tsx b/prometheus/src/explore/PrometheusMetricsFinder/filter/FilterInputs.tsx index 22876fbd..1dec5689 100644 --- a/prometheus/src/explore/PrometheusMetricsFinder/filter/FilterInputs.tsx +++ b/prometheus/src/explore/PrometheusMetricsFinder/filter/FilterInputs.tsx @@ -102,7 +102,12 @@ export const ListboxComponent = forwardRef { diff --git a/pyroscope/src/components/FilterItem.tsx b/pyroscope/src/components/FilterItem.tsx index 4b9dc89a..0ea5f606 100644 --- a/pyroscope/src/components/FilterItem.tsx +++ b/pyroscope/src/components/FilterItem.tsx @@ -30,19 +30,19 @@ export interface FilterItemProps { export function FilterItem(props: FilterItemProps): ReactElement { const { datasource, value, onChange, deleteItem } = props; - const handleLabelNameChange = (label: string) => { + const handleLabelNameChange = (label: string): void => { onChange?.({ labelName: label, labelValue: '', operator: value.operator }); }; - const handleOperatorChange = (op: OperatorType) => { + const handleOperatorChange = (op: OperatorType): void => { onChange?.({ labelName: value.labelName, labelValue: value.labelValue, operator: op }); }; - const handleLabelValueChange = (val: string) => { + const handleLabelValueChange = (val: string): void => { onChange?.({ labelName: value.labelName, labelValue: val, operator: value.operator }); }; - const handleDeleteClick = () => { + const handleDeleteClick = (): void => { deleteItem?.(); }; diff --git a/pyroscope/src/components/Filters.tsx b/pyroscope/src/components/Filters.tsx index e7f74bbc..ec0ef65f 100644 --- a/pyroscope/src/components/Filters.tsx +++ b/pyroscope/src/components/Filters.tsx @@ -28,19 +28,19 @@ export function Filters(props: FiltersProps): ReactElement { const theme = useTheme(); const { datasource, value, onChange } = props; - const addFilterItem = () => { + const addFilterItem = (): void => { const newItem: LabelFilter = { labelName: '', labelValue: '', operator: '=' }; const updatedFilters = [...value, newItem]; onChange?.(updatedFilters); }; - const updateFilter = (index: number, newValue: LabelFilter) => { + const updateFilter = (index: number, newValue: LabelFilter): void => { const nextFilters = [...value]; nextFilters[index] = newValue; onChange?.(nextFilters); }; - const deleteFilter = (index: number) => { + const deleteFilter = (index: number): void => { const nextFilters = [...value]; nextFilters.splice(index, 1); if (nextFilters.length === 0) { diff --git a/pyroscope/src/model/api-types.ts b/pyroscope/src/model/api-types.ts index a15fbd58..5ffd8c4f 100644 --- a/pyroscope/src/model/api-types.ts +++ b/pyroscope/src/model/api-types.ts @@ -62,7 +62,10 @@ export interface Timeline { /** * Request parameters of Pyroscope HTTP API endpoint POST /querier.v1.QuerierService/ProfileTypes */ -export type SearchProfileTypesParameters = Record; +export type SearchProfileTypesParameters = { + start?: number; + end?: number; +}; /** * Response of Pyroscope HTTP API endpoint POST /querier.v1.QuerierService/ProfileTypes @@ -83,7 +86,7 @@ export interface ProfileType { /** * Request parameters of Pyroscope HTTP API endpoint POST /querier.v1.QuerierService/LabelNames */ -export type SearchLabelNamesParameters = Record; +export type SearchLabelNamesParameters = Record; /** * Response of Pyroscope HTTP API endpoint POST /querier.v1.QuerierService/LabelNames @@ -95,7 +98,7 @@ export interface SearchLabelNamesResponse { /** * Request parameters of Pyroscope HTTP API endpoint POST /querier.v1.QuerierService/LabelValues */ -export type SearchLabelValuesParameters = Record; +export type SearchLabelValuesParameters = Record; /** * Response of Pyroscope HTTP API endpoint POST /querier.v1.QuerierService/LabelValues diff --git a/pyroscope/src/plugins/pyroscope-profile-query/get-profile-data.ts b/pyroscope/src/plugins/pyroscope-profile-query/get-profile-data.ts index d19ebc25..f89c7cf8 100644 --- a/pyroscope/src/plugins/pyroscope-profile-query/get-profile-data.ts +++ b/pyroscope/src/plugins/pyroscope-profile-query/get-profile-data.ts @@ -14,9 +14,14 @@ import { ProfileQueryPlugin } from '@perses-dev/plugin-system'; import { AbsoluteTimeRange, StackTrace, ProfileData } from '@perses-dev/core'; import { getUnixTime } from 'date-fns'; -import { PyroscopeProfileQuerySpec, PYROSCOPE_DATASOURCE_KIND, PyroscopeDatasourceSelector } from '../../model'; -import { PyroscopeClient } from '../../model/pyroscope-client'; -import { SearchProfilesParameters, SearchProfilesResponse } from '../../model/api-types'; +import { + PyroscopeProfileQuerySpec, + PYROSCOPE_DATASOURCE_KIND, + PyroscopeDatasourceSelector, + SearchProfilesParameters, + SearchProfilesResponse, + PyroscopeClient, +} from '../../model'; import { computeFilterExpr } from '../../utils/types'; export function getUnixTimeRange(timeRange: AbsoluteTimeRange): { start: number; end: number } { @@ -30,7 +35,7 @@ export function getUnixTimeRange(timeRange: AbsoluteTimeRange): { start: number; export const getProfileData: ProfileQueryPlugin['getProfileData'] = async ( spec, context -) => { +): Promise => { const defaultPyroscopeDatasource: PyroscopeDatasourceSelector = { kind: PYROSCOPE_DATASOURCE_KIND, }; @@ -39,7 +44,7 @@ export const getProfileData: ProfileQueryPlugin['getP spec.datasource ?? defaultPyroscopeDatasource ); - const buildQueryString = () => { + const buildQueryString = (): string => { let query: string = ''; if (spec.service) { query = `service_name="${spec.service}"`; diff --git a/scatterchart/src/Scatterplot.tsx b/scatterchart/src/Scatterplot.tsx index 0514b361..8c9ca13a 100644 --- a/scatterchart/src/Scatterplot.tsx +++ b/scatterchart/src/Scatterplot.tsx @@ -29,6 +29,7 @@ import { formatValue } from '@perses-dev/core'; import { replaceVariablesInString, useAllVariableValues, useRouterContext } from '@perses-dev/plugin-system'; import { EChartTraceValue } from './ScatterChartPanel'; +// eslint-disable-next-line react-hooks/rules-of-hooks use([ DatasetComponent, DataZoomComponent, @@ -124,7 +125,7 @@ export function Scatterplot(props: ScatterplotProps): ReactElement { return ( ) { - const value = (props.value.values || []).map((v) => { +function StaticListVariableOptionEditor(props: OptionsEditorProps): ReactElement { + const value = (props.value.values || []).map((v): string => { if (typeof v === 'string') { return v; } else { @@ -16,7 +17,7 @@ function StaticListVariableOptionEditor(props: OptionsEditorProps { + const onChange = (__: unknown, value: string[]): void => { props.onChange({ values: value.map((v) => { return { value: v, label: v }; diff --git a/statushistorychart/src/StatusHistoryChartBase.tsx b/statushistorychart/src/StatusHistoryChartBase.tsx index bd4cbe09..07f5014f 100644 --- a/statushistorychart/src/StatusHistoryChartBase.tsx +++ b/statushistorychart/src/StatusHistoryChartBase.tsx @@ -30,6 +30,7 @@ import { EChart, useChartsTheme, useTimeZone } from '@perses-dev/components'; import { getFormattedStatusHistoryAxisLabel } from './utils/get-formatted-axis-label'; import { generateTooltipHTML } from './StatusHistoryTooltip'; +// eslint-disable-next-line react-hooks/rules-of-hooks use([ EChartsHeatmapChart, VisualMapComponent, @@ -148,7 +149,7 @@ export const StatusHistoryChartBase: FC = (props) = return ( >; } -export function EmbeddedPanel({ kind, spec, queryResults }: EmbeddedPanelProps) { +export function EmbeddedPanel({ kind, spec, queryResults }: EmbeddedPanelProps): ReactElement { const { ref, width = 1, height = 1 } = useResizeObserver(); return (
diff --git a/table/src/TablePanel.tsx b/table/src/TablePanel.tsx index a21ef094..782f09ae 100644 --- a/table/src/TablePanel.tsx +++ b/table/src/TablePanel.tsx @@ -25,17 +25,17 @@ function generateCellContentConfig( const plugin = column.plugin; if (plugin !== undefined) { return { - cell: (ctx) => { + cell: (ctx): ReactElement => { const panelData: PanelData | undefined = ctx.getValue(); if (!panelData) return <>; return ; }, - cellDescription: column.cellDescription ? () => `${column.cellDescription}` : () => '', // disable hover text + cellDescription: column.cellDescription ? (): string => `${column.cellDescription}` : (): string => '', // disable hover text }; } return { - cell: (ctx) => { + cell: (ctx): unknown => { const cellValue = ctx.getValue(); return typeof cellValue === 'number' && column.format ? formatValue(cellValue, column.format) : cellValue; }, @@ -277,6 +277,8 @@ export function TablePanel({ contentDimensions, spec, queryResults }: TableProps 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) { + // TODO: improve this logic to avoid useState in useEffect + // eslint-disable-next-line react-hooks/set-state-in-effect setPagination({ pageIndex: 0, pageSize: 10 }); } else if (!spec.pagination && pagination) { setPagination(undefined); diff --git a/tempo/src/components/AttributeFilters.tsx b/tempo/src/components/AttributeFilters.tsx index c9ce6468..086acab4 100644 --- a/tempo/src/components/AttributeFilters.tsx +++ b/tempo/src/components/AttributeFilters.tsx @@ -15,7 +15,7 @@ import { ReactElement, SyntheticEvent, useCallback, useEffect, useState } from ' import { Autocomplete, Checkbox, Stack, TextField, TextFieldProps } from '@mui/material'; import { isAbsoluteTimeRange, toAbsoluteTimeRange } from '@perses-dev/core'; import { useTimeRange } from '@perses-dev/plugin-system'; -import { useQuery } from '@tanstack/react-query'; +import { useQuery, UseQueryResult } from '@tanstack/react-query'; import CheckboxOutline from 'mdi-material-ui/CheckboxOutline'; import CheckboxBlankOutline from 'mdi-material-ui/CheckboxBlankOutline'; import { TempoClient } from '../model'; @@ -35,7 +35,7 @@ export function AttributeFilters(props: AttributeFiltersProps): ReactElement { const { client, query, setQuery } = props; const filter = traceQLToFilter(query); - const setFilter = (filter: Filter) => { + const setFilter = (filter: Filter): void => { setQuery(filterToTraceQL(filter)); }; @@ -105,7 +105,7 @@ interface StringAttributeFilterProps { const checkboxBlankIcon = ; const checkedMarkedIcon = ; -function StringAttributeFilter(props: StringAttributeFilterProps) { +function StringAttributeFilter(props: StringAttributeFilterProps): ReactElement { const { label, width, options, value, setValue } = props; return ( @@ -117,10 +117,9 @@ function StringAttributeFilter(props: StringAttributeFilterProps) { value={value} onChange={(_event: SyntheticEvent, newValue: string[]) => setValue(newValue)} options={options} - renderOption={(props, option, { selected }) => { - const { key, ...optionProps } = props; + renderOption={({ key, ...props }, option, { selected }) => { return ( -
  • +
  • void; } -function DurationAttributeFilter(props: DurationAttributeFilterProps) { +function DurationAttributeFilter(props: DurationAttributeFilterProps): ReactElement { const { label, value, setValue } = props; const { min, max } = value; @@ -167,7 +166,7 @@ interface DurationTextInputProps { setValue: (value: string) => void; } -function DurationTextInput(props: DurationTextInputProps) { +function DurationTextInput(props: DurationTextInputProps): ReactElement { const { label, value, setValue } = props; return ( @@ -189,7 +188,7 @@ interface CustomAttributesFilterProps { setValue: (value: string[]) => void; } -function CustomAttributesFilter(props: CustomAttributesFilterProps) { +function CustomAttributesFilter(props: CustomAttributesFilterProps): ReactElement { const { label, value, setValue } = props; return ( @@ -212,12 +211,14 @@ interface LazyTextInputProps extends Omit { } /** A which calls props.setValue when the input field is blurred and the validation passes. */ -function LazyTextInput(props: LazyTextInputProps) { +function LazyTextInput(props: LazyTextInputProps): ReactElement { const { validationRegex, validationFailedMessage, value, setValue, ...otherProps } = props; const [draftValue, setDraftValue] = useState(value); - const isValidInput = draftValue == '' || validationRegex == undefined || validationRegex.test(draftValue); + const isValidInput = draftValue === '' || validationRegex === undefined || validationRegex.test(draftValue); useEffect(() => { + // TODO: improve logic here to not have setState in useEffect + // eslint-disable-next-line react-hooks/set-state-in-effect setDraftValue(value); }, [value, setDraftValue]); @@ -243,7 +244,13 @@ function LazyTextInput(props: LazyTextInputProps) { ); } -function useTagValues(client: TempoClient | undefined, tag: string, query: string, start?: number, end?: number) { +function useTagValues( + client: TempoClient | undefined, + tag: string, + query: string, + start?: number, + end?: number +): UseQueryResult { return useQuery({ queryKey: ['useTagValues', client, tag, query, start, end], enabled: !!client, diff --git a/tempo/src/components/complete.ts b/tempo/src/components/complete.ts index 20b9ac52..86666d0e 100644 --- a/tempo/src/components/complete.ts +++ b/tempo/src/components/complete.ts @@ -252,7 +252,7 @@ async function completeTagName( return response.scopes.flatMap((scope) => scope.tags).map((tag) => ({ label: tag })); } -function escapeString(input: string, quoteChar: string) { +function escapeString(input: string, quoteChar: string): string { // do not escape raw strings (when using backticks) if (quoteChar === '`') { return input; diff --git a/tempo/src/components/filter/filter.ts b/tempo/src/components/filter/filter.ts index ee7d47b5..6656fdbb 100644 --- a/tempo/src/components/filter/filter.ts +++ b/tempo/src/components/filter/filter.ts @@ -15,14 +15,14 @@ export interface DurationField { } /** split a string by whitespace, except when inside quotes */ -export function splitByUnquotedWhitespace(x: string) { +export function splitByUnquotedWhitespace(x: string): string[] { let quote = false; let from = 0; const chunks: string[] = []; for (let i = 0; i < x.length; i++) { - if (x[i] == '"' && x[i - 1] != '\\') { + if (x[i] === '"' && x[i - 1] !== '\\') { quote = !quote; - } else if (x[i] == ' ' && !quote) { + } else if (x[i] === ' ' && !quote) { chunks.push(x.slice(from, i)); from = i + 1; } diff --git a/tempo/src/components/filter/filter_to_traceql.ts b/tempo/src/components/filter/filter_to_traceql.ts index 1721fbbd..e93310fe 100644 --- a/tempo/src/components/filter/filter_to_traceql.ts +++ b/tempo/src/components/filter/filter_to_traceql.ts @@ -6,7 +6,7 @@ import { DurationField, Filter } from './filter'; * 2. Join all matchers with '&&' * 3. Return the full TraceQL query, for example '{ resource.service.name = "some_value" && name = "span_name" }' */ -export function filterToTraceQL(filter: Filter) { +export function filterToTraceQL(filter: Filter): string { const matchers: string[] = [ ...stringMatcher('resource.service.name', filter.serviceName), ...stringMatcher('name', filter.spanName), @@ -23,21 +23,21 @@ export function filterToTraceQL(filter: Filter) { return `{ ${matchers.join(' && ')} }`; } -function escape(q: string) { +function escape(q: string): string { return q.replaceAll('\\', '\\\\').replaceAll('"', '\\"'); } -function stringMatcher(attribute: string, values: string[]) { +function stringMatcher(attribute: string, values: string[]): string[] { const escapedValues = values.map(escape); if (escapedValues.length > 1) { return [`${attribute} =~ "${escapedValues.join('|')}"`]; - } else if (escapedValues.length == 1) { + } else if (escapedValues.length === 1) { return [`${attribute} = "${escapedValues[0]}"`]; } return []; } -function intrinsicMatcher(attribute: string, values: string[]) { +function intrinsicMatcher(attribute: string, values: string[]): string[] { const orConds = values.map((x) => `${attribute} = ${x}`); if (orConds.length > 1) { return ['(' + orConds.join(' || ') + ')']; @@ -48,7 +48,7 @@ function intrinsicMatcher(attribute: string, values: string[]) { } } -function durationMatcher(attribute: string, value: DurationField) { +function durationMatcher(attribute: string, value: DurationField): string[] { const matchers = []; if (value.min) { matchers.push(`${attribute} >= ${value.min}`); @@ -59,6 +59,6 @@ function durationMatcher(attribute: string, value: DurationField) { return matchers; } -function customMatcher(customMatchers: string[]) { +function customMatcher(customMatchers: string[]): string[] { return customMatchers; } diff --git a/tempo/src/components/filter/traceql_to_filter.ts b/tempo/src/components/filter/traceql_to_filter.ts index 6528c965..7183493f 100644 --- a/tempo/src/components/filter/traceql_to_filter.ts +++ b/tempo/src/components/filter/traceql_to_filter.ts @@ -28,7 +28,7 @@ export function traceQLToFilter(query: string): Filter { }; } -function parseQuery(query: string) { +function parseQuery(query: string): Record { const matchers: Record = {}; let attribute = ''; let operator = ''; @@ -64,46 +64,46 @@ function parseQuery(query: string) { return matchers; } -function unescape(q: string) { +function unescape(q: string): string { return q.replaceAll('\\"', '"').replaceAll('\\\\', '\\'); } -function reverseStringMatcher(matches?: Matcher[]) { +function reverseStringMatcher(matches?: Matcher[]): string[] { const values: string[] = []; for (const { operator, value } of matches ?? []) { const unescaped = unescape(value.slice(1, -1)); - if (operator == '=') { + if (operator === '=') { values.push(unescaped); - } else if (operator == '=~') { + } else if (operator === '=~') { values.push(...unescaped.split('|')); } } return values; } -function reverseIntrinsicMatcher(matches?: Matcher[]) { +function reverseIntrinsicMatcher(matches?: Matcher[]): string[] { const values: string[] = []; for (const { operator, value } of matches ?? []) { - if (operator == '=') { + if (operator === '=') { values.push(value); } } return values; } -function reverseDurationMatcher(matches?: Matcher[]) { +function reverseDurationMatcher(matches?: Matcher[]): DurationField { const duration: DurationField = {}; for (const { operator, value } of matches ?? []) { - if (operator == '>=') { + if (operator === '>=') { duration.min = value; - } else if (operator == '<=') { + } else if (operator === '<=') { duration.max = value; } } return duration; } -function reverseCustomMatcher(matchers: Record, skipAttrs: Set) { +function reverseCustomMatcher(matchers: Record, skipAttrs: Set): string[] { const customMatchers: string[] = []; for (const [attribute, matches] of Object.entries(matchers)) { if (skipAttrs.has(attribute)) { diff --git a/tempo/src/plugins/tempo-trace-query/TempoTraceQueryEditor.tsx b/tempo/src/plugins/tempo-trace-query/TempoTraceQueryEditor.tsx index 3280970e..c7c4afe1 100644 --- a/tempo/src/plugins/tempo-trace-query/TempoTraceQueryEditor.tsx +++ b/tempo/src/plugins/tempo-trace-query/TempoTraceQueryEditor.tsx @@ -21,17 +21,15 @@ import { } from '@perses-dev/plugin-system'; import { produce } from 'immer'; import { ReactElement, useCallback, useState } from 'react'; -import { TraceQLEditor } from '../../components'; -import { TempoClient } from '../../model/tempo-client'; +import { TraceQLEditor, filterToTraceQL, traceQLToFilter } from '../../components'; import { + TempoClient, DEFAULT_TEMPO, isDefaultTempoSelector, isTempoDatasourceSelector, TEMPO_DATASOURCE_KIND, -} from '../../model/tempo-selectors'; +} from '../../model'; import { AttributeFilters } from '../../components/AttributeFilters'; -import { filterToTraceQL } from '../../components/filter/filter_to_traceql'; -import { traceQLToFilter } from '../../components/filter/traceql_to_filter'; import { TraceQueryEditorProps, useQueryState } from './query-editor-model'; export function TempoTraceQueryEditor(props: TraceQueryEditorProps): ReactElement { @@ -67,7 +65,7 @@ export function TempoTraceQueryEditor(props: TraceQueryEditorProps): ReactElemen throw new Error('Got unexpected non-Tempo datasource selector'); }; - const runQuery = (newQuery: string) => { + const runQuery = (newQuery: string): void => { if (queryHandlerSettings?.watchQueryChanges) { queryHandlerSettings.watchQueryChanges(newQuery); } @@ -129,9 +127,9 @@ export function TempoTraceQueryEditor(props: TraceQueryEditorProps): ReactElemen ); } -function isSimpleTraceQLQuery(query: string) { +function isSimpleTraceQLQuery(query: string): boolean { // if a query can be transformed to a filter and back to the original query, we can show the attribute filter toolbar - return query == '' || filterToTraceQL(traceQLToFilter(query)) === query; + return query === '' || filterToTraceQL(traceQLToFilter(query)) === query; } const limitOptions = [20, 50, 100, 500, 1000, 5000]; @@ -141,7 +139,7 @@ interface LimitSelectProps { setValue: (x: number) => void; } -export function LimitSelect(props: LimitSelectProps) { +export function LimitSelect(props: LimitSelectProps): ReactElement { const { value, setValue } = props; // the outer is required, because has display: inline-flex, which doesn't work with the parent of the query editor diff --git a/tempo/src/plugins/tempo-trace-query/get-trace-data.ts b/tempo/src/plugins/tempo-trace-query/get-trace-data.ts index 7d167c59..1c6d0da2 100644 --- a/tempo/src/plugins/tempo-trace-query/get-trace-data.ts +++ b/tempo/src/plugins/tempo-trace-query/get-trace-data.ts @@ -100,21 +100,21 @@ function parseTraceResponse(response: QueryResponse): otlptracev1.TracesData { for (const resourceSpan of trace.resourceSpans) { for (const scopeSpan of resourceSpan.scopeSpans) { for (const span of scopeSpan.spans) { - if (span.traceId.length != 32) { + if (span.traceId.length !== 32) { span.traceId = base64ToHex(span.traceId); } - if (span.spanId.length != 16) { + if (span.spanId.length !== 16) { span.spanId = base64ToHex(span.spanId); } - if (span.parentSpanId && span.parentSpanId.length != 16) { + if (span.parentSpanId && span.parentSpanId.length !== 16) { span.parentSpanId = base64ToHex(span.parentSpanId); } for (const link of span.links ?? []) { - if (link.traceId.length != 32) { + if (link.traceId.length !== 32) { link.traceId = base64ToHex(link.traceId); } - if (link.spanId.length != 16) { + if (link.spanId.length !== 16) { link.spanId = base64ToHex(link.spanId); } } @@ -125,7 +125,7 @@ function parseTraceResponse(response: QueryResponse): otlptracev1.TracesData { return trace; } -function base64ToHex(str: string) { +function base64ToHex(str: string): string { try { return atob(str) .split('') diff --git a/timeserieschart/src/QuerySettingsEditor.tsx b/timeserieschart/src/QuerySettingsEditor.tsx index e4f1ba0e..c2739109 100644 --- a/timeserieschart/src/QuerySettingsEditor.tsx +++ b/timeserieschart/src/QuerySettingsEditor.tsx @@ -48,7 +48,7 @@ export function QuerySettingsEditor(props: TimeSeriesChartOptionsEditorProps): R const { onChange, value } = props; const querySettingsList = value.querySettings; - const handleQuerySettingsChange = (newQuerySettings: QuerySettingsOptions[]) => { + const handleQuerySettingsChange = (newQuerySettings: QuerySettingsOptions[]): void => { onChange( produce(value, (draft: TimeSeriesChartOptions) => { draft.querySettings = newQuerySettings; @@ -329,7 +329,7 @@ function QuerySettingsInput({ return options; }, [colorMode, lineStyle, areaOpacity, onAddColor, onAddLineStyle, onAddAreaOpacity]); - const handleAddMenuClick = (event: React.MouseEvent) => { + const handleAddMenuClick = (event: React.MouseEvent): void => { if (availableOptions.length === 1 && availableOptions[0]) { // If only one option left, add it directly availableOptions[0].action(); @@ -339,11 +339,11 @@ function QuerySettingsInput({ } }; - const handleMenuClose = () => { + const handleMenuClose = (): void => { setAnchorEl(null); }; - const handleMenuItemClick = (action: () => void) => { + const handleMenuItemClick = (action: () => void): void => { action(); handleMenuClose(); }; diff --git a/timeserieschart/src/TimeSeriesChartBase.tsx b/timeserieschart/src/TimeSeriesChartBase.tsx index 9468ae2f..06b5900a 100644 --- a/timeserieschart/src/TimeSeriesChartBase.tsx +++ b/timeserieschart/src/TimeSeriesChartBase.tsx @@ -62,6 +62,7 @@ import { } from '@perses-dev/components'; import { DatasetOption } from 'echarts/types/dist/shared'; +// eslint-disable-next-line react-hooks/rules-of-hooks use([ EChartsLineChart, EChartsBarChart, diff --git a/timeseriestable/src/components/EmbeddedPanel.tsx b/timeseriestable/src/components/EmbeddedPanel.tsx index 249b0a40..c29d98c4 100644 --- a/timeseriestable/src/components/EmbeddedPanel.tsx +++ b/timeseriestable/src/components/EmbeddedPanel.tsx @@ -16,6 +16,7 @@ import { QueryDataType, UnknownSpec } from '@perses-dev/core'; import { PanelPluginLoader } from '@perses-dev/dashboards'; import useResizeObserver from 'use-resize-observer'; import { ErrorAlert, ErrorBoundary } from '@perses-dev/components'; +import { ReactElement } from 'react'; interface EmbeddedPanelProps { kind: string; @@ -23,7 +24,7 @@ interface EmbeddedPanelProps { queryResults: Array>; } -export function EmbeddedPanel({ kind, spec, queryResults }: EmbeddedPanelProps) { +export function EmbeddedPanel({ kind, spec, queryResults }: EmbeddedPanelProps): ReactElement { const { ref, width = 1, height = 1 } = useResizeObserver(); return (
    diff --git a/tracetable/src/DataTable.tsx b/tracetable/src/DataTable.tsx index f018639b..7bf96108 100644 --- a/tracetable/src/DataTable.tsx +++ b/tracetable/src/DataTable.tsx @@ -113,7 +113,8 @@ export function DataTable(props: DataTableProps): ReactElement { flex: 2, minWidth: 145, display: 'flex', - valueGetter: (_, trace) => Object.values(trace.serviceStats).reduce((acc, val) => acc + val.spanCount, 0), + valueGetter: (_, trace): number => + Object.values(trace.serviceStats).reduce((acc, val) => acc + val.spanCount, 0), renderCell: ({ row }): ReactElement => { let totalSpanCount = 0; let totalErrorCount = 0; @@ -147,7 +148,7 @@ export function DataTable(props: DataTableProps): ReactElement { flex: 1, minWidth: 70, display: 'flex', - renderCell: ({ row }) => ( + renderCell: ({ row }): ReactElement => ( {row.durationMs < 1 ? '<1ms' : formatDuration(msToPrometheusDuration(row.durationMs))} @@ -162,7 +163,7 @@ export function DataTable(props: DataTableProps): ReactElement { flex: 3, minWidth: 240, display: 'flex', - renderCell: ({ row }) => ( + renderCell: ({ row }): ReactElement => ( {DATE_FORMATTER(new Date(row.startTimeUnixMs))} diff --git a/tracingganttchart/src/PanelActions.tsx b/tracingganttchart/src/PanelActions.tsx index dc90b2f2..1220ec97 100644 --- a/tracingganttchart/src/PanelActions.tsx +++ b/tracingganttchart/src/PanelActions.tsx @@ -14,11 +14,11 @@ import { InfoTooltip } from '@perses-dev/components'; import { otlptracev1 } from '@perses-dev/core'; import DownloadIcon from 'mdi-material-ui/DownloadOutline'; -import { useCallback } from 'react'; +import { ReactElement, useCallback } from 'react'; import { HeaderIconButton } from '@perses-dev/dashboards'; import { TracingGanttChartPanelProps } from './TracingGanttChartPanel'; -export function DownloadTraceAction(props: TracingGanttChartPanelProps) { +export function DownloadTraceAction(props: TracingGanttChartPanelProps): ReactElement | null { const { queryResults } = props; const trace = queryResults[0]?.data?.trace; @@ -47,7 +47,7 @@ export function DownloadTraceAction(props: TracingGanttChartPanelProps) { * A trace can only contain spans with the same trace id. Therefore, let's return the trace id of the first span. * Exported for tests only. */ -export function getFilename(trace: otlptracev1.TracesData) { +export function getFilename(trace: otlptracev1.TracesData): string { for (const resourceSpan of trace.resourceSpans) { for (const scopeSpan of resourceSpan.scopeSpans) { for (const span of scopeSpan.spans) { @@ -59,7 +59,7 @@ export function getFilename(trace: otlptracev1.TracesData) { return 'trace.json'; } -function downloadFile(filename: string, type: string, data: string) { +function downloadFile(filename: string, type: string, data: string): void { const url = URL.createObjectURL(new Blob([data], { type })); const link = document.createElement('a'); diff --git a/tracingganttchart/src/TracingGanttChart/DetailPane/Attributes.tsx b/tracingganttchart/src/TracingGanttChart/DetailPane/Attributes.tsx index 2cab98a7..6c8f2fb2 100644 --- a/tracingganttchart/src/TracingGanttChart/DetailPane/Attributes.tsx +++ b/tracingganttchart/src/TracingGanttChart/DetailPane/Attributes.tsx @@ -25,7 +25,7 @@ export interface TraceAttributesProps { span: Span; } -export function TraceAttributes(props: TraceAttributesProps) { +export function TraceAttributes(props: TraceAttributesProps): ReactElement { const { customLinks, trace, span } = props; return ( diff --git a/tracingganttchart/src/TracingGanttChart/DetailPane/SpanEvents.tsx b/tracingganttchart/src/TracingGanttChart/DetailPane/SpanEvents.tsx index 55ea9694..05b395dc 100644 --- a/tracingganttchart/src/TracingGanttChart/DetailPane/SpanEvents.tsx +++ b/tracingganttchart/src/TracingGanttChart/DetailPane/SpanEvents.tsx @@ -54,7 +54,7 @@ function SpanEventItem(props: SpanEventItemProps): ReactElement { const relativeTime = event.timeUnixMs - trace.startTimeUnixMs; const [open, setOpen] = useState(false); - const handleClick = () => { + const handleClick = (): void => { setOpen(!open); }; diff --git a/tracingganttchart/src/TracingGanttChart/GanttTable/SpanLinksButton.tsx b/tracingganttchart/src/TracingGanttChart/GanttTable/SpanLinksButton.tsx index 28d2b6ba..11b52c65 100644 --- a/tracingganttchart/src/TracingGanttChart/GanttTable/SpanLinksButton.tsx +++ b/tracingganttchart/src/TracingGanttChart/GanttTable/SpanLinksButton.tsx @@ -12,7 +12,7 @@ // limitations under the License. import { IconButton, Menu, MenuItem } from '@mui/material'; -import { MouseEvent, useState } from 'react'; +import { MouseEvent, ReactElement, useState } from 'react'; import LaunchIcon from 'mdi-material-ui/Launch'; import { InfoTooltip } from '@perses-dev/components'; import { replaceVariablesInString, useAllVariableValues, useRouterContext } from '@perses-dev/plugin-system'; @@ -24,7 +24,7 @@ export interface SpanLinksButtonProps { span: Span; } -export function SpanLinksButton(props: SpanLinksButtonProps) { +export function SpanLinksButton(props: SpanLinksButtonProps): ReactElement | null { const { customLinks, span } = props; const variableValues = useAllVariableValues(); const { RouterComponent } = useRouterContext(); @@ -36,7 +36,7 @@ export function SpanLinksButton(props: SpanLinksButtonProps) { } // if there is a single span link, render the button directly without a menu - if (span.links.length == 1 && span.links[0]) { + if (span.links.length === 1 && span.links[0]) { const link = span.links[0]; return ( @@ -55,14 +55,14 @@ export function SpanLinksButton(props: SpanLinksButtonProps) { ); } - const handleOpenMenu = (event: MouseEvent) => { + const handleOpenMenu = (event: MouseEvent): void => { // do not propagate onClick event to the table row (otherwise, the detail pane would open) event.stopPropagation(); setAnchorEl(event.currentTarget); }; - const handleClose = (event: MouseEvent) => { + const handleClose = (event: MouseEvent): void => { // Closing the menu, i.e. clicking on the fullscreen transparent MUI backdrop element, does trigger a click on the table row (which opens the detail pane). // Therefore, stop propagating this event event.stopPropagation();