Compare commits

...

63 Commits

Author SHA1 Message Date
Ashley Harrison
4752b45f81 Merge branch 'main' into ash/react-19-again 2025-12-17 09:29:19 +00:00
Ashley Harrison
43ce2acafc "fix" loki decoupled plugin tests 2025-12-16 13:16:37 +00:00
Ashley Harrison
f2cb70b1e0 add MessageChannel mock to grafana-plugin-configs 2025-12-16 12:04:11 +00:00
Ashley Harrison
7a1938c23b Merge branch 'main' into ash/react-19-again 2025-12-16 11:48:17 +00:00
Ashley Harrison
03de5f59e6 fix @grafana/ui type errors 2025-12-16 11:26:27 +00:00
Ashley Harrison
494a663449 fix @grafana/flamegraph types 2025-12-16 11:07:30 +00:00
Ashley Harrison
d9059ca7b2 ignore react-use errors for now 2025-12-16 10:56:40 +00:00
Ashley Harrison
c254cf1387 remove unused vars 2025-12-16 10:19:25 +00:00
Ashley Harrison
ebf6ba442b add temporary publish to dockerhub step 2025-12-16 10:17:35 +00:00
Ashley Harrison
462b6354d0 fix pluginExtensions tests 2025-12-15 16:43:12 +00:00
Ashley Harrison
7f1f3c6ba6 kick CI again 2025-12-15 16:31:28 +00:00
Ashley Harrison
de6d2700b7 kick CI 2025-12-15 16:28:30 +00:00
Ashley Harrison
481dc3e630 type fixes in grafana-ui 2025-12-15 15:50:45 +00:00
Ashley Harrison
0f46f38b77 mock out testing libraries in prod builds 2025-12-15 12:56:29 +00:00
Ashley Harrison
2f3a4d0358 maybe better way to attach meta? 2025-12-12 17:05:43 +00:00
Ashley Harrison
d0aec88ca8 "fix" PublicDashboardPageProxy tests 2025-12-12 15:56:57 +00:00
Ashley Harrison
4a0e9204b3 fix explore query tests 2025-12-12 15:55:19 +00:00
Ashley Harrison
12c6e9615f "fix" NewReceiverView tests 2025-12-12 15:45:53 +00:00
Ashley Harrison
7a0d7c5dec "fix" GroupedView tests 2025-12-12 15:41:56 +00:00
Ashley Harrison
b035732a85 kick CI 2025-12-12 13:56:58 +00:00
Ashley Harrison
05ef468b41 Merge branch 'main' into ash/react-19-again 2025-12-12 11:45:59 +00:00
Ashley Harrison
730f10597a "fix" useMoveRuleFromRuleGroup tests 2025-12-12 11:38:50 +00:00
Ashley Harrison
caff0e2d1e "fix" MuteTimings tests 2025-12-12 11:34:28 +00:00
Ashley Harrison
f10a494369 "fix" ImportToGMARules tests 2025-12-12 11:30:59 +00:00
Ashley Harrison
8d42d4a079 "fix" SignupInvited tests 2025-12-12 11:26:04 +00:00
Ashley Harrison
720f038981 "fix" VersionsSettings tests 2025-12-12 11:24:00 +00:00
Ashley Harrison
b380ce2bfd fix CloneRuleEditor tests 2025-12-12 11:19:53 +00:00
Ashley Harrison
68a83b73c9 "fix" provisioningwizard tests 2025-12-12 11:16:05 +00:00
Ashley Harrison
3808ddf948 "fix" some CorrelationsPage tests 2025-12-12 11:08:04 +00:00
Ashley Harrison
f1d654d2e3 fix some DashboardScenePage tests 2025-12-12 10:44:59 +00:00
Ashley Harrison
b0798f24c5 "fix" GrafanaModifyExport test 2025-12-12 10:30:06 +00:00
Ashley Harrison
4c90d10281 fix AnnotationsStep changes 2025-12-10 10:24:32 +00:00
Ashley Harrison
96614c4eca fix QueryEditor test 2025-12-10 10:18:38 +00:00
Ashley Harrison
fd4a97e49e fix split test 2025-12-10 09:46:28 +00:00
Ashley Harrison
68e0ed782c fix type error in geomap panel 2025-12-09 10:39:26 +00:00
Ashley Harrison
5fbbf2ac4a fix typings on LogRowMenuCell 2025-12-09 10:05:29 +00:00
Ashley Harrison
e97d48d86b add resolution for react + react-dom 2025-12-09 09:49:20 +00:00
Ashley Harrison
74c656713a "fix" SendResetMailPage tests 2025-12-08 17:07:46 +00:00
Ashley Harrison
470cd869f3 "fix" VerifyEmailPage tests 2025-12-08 17:06:18 +00:00
Ashley Harrison
3ef28b727f fix useStatelessReducer tests 2025-12-08 16:59:10 +00:00
Ashley Harrison
afe54f6739 fix ElasticsearchQueryContext test 2025-12-08 16:58:08 +00:00
Ashley Harrison
f7d8fd4986 "fix" loglinedetailstrace tests 2025-12-08 16:53:57 +00:00
Ashley Harrison
c73db56467 fix usePauseAlertRule tests 2025-12-08 16:02:25 +00:00
Ashley Harrison
37bd5ded3a fix LoginPage tests 2025-12-08 15:56:36 +00:00
Ashley Harrison
418c1a4d5a "fix" BulkDeleteProvisionedResource tests 2025-12-08 15:42:24 +00:00
Ashley Harrison
d3e807d6e2 Merge branch 'main' into ash/react-19-again 2025-12-08 15:11:45 +00:00
Ashley Harrison
03a044a9a0 fix SharedPreferences tests 2025-12-08 13:56:10 +00:00
Ashley Harrison
e861318c2d almost fix PublicDashboardScenePage 2025-12-08 13:52:42 +00:00
Ashley Harrison
35633b756d temporary flushSync to fix some tests 2025-12-08 12:26:02 +00:00
Ashley Harrison
eb77bf89df maybe fix some RTL tests 2025-12-05 14:57:25 +00:00
Ashley Harrison
636c62862d Merge branch 'main' into ash/react-19-again 2025-12-04 16:38:41 +00:00
Ashley Harrison
737ee7c7bd Merge branch 'main' into ash/react-19-again 2025-12-04 16:21:30 +00:00
Ashley Harrison
13b5c3f974 add resolution for react-is 2025-12-04 13:19:04 +00:00
Ashley Harrison
1915a92eb2 upgrade rc-cascader 2025-12-04 12:02:24 +00:00
Ashley Harrison
94cad60654 fix a few more src test errors 2025-12-02 17:40:33 +00:00
Ashley Harrison
9d9085075b undo mock changes and handle in component 2025-12-02 17:32:04 +00:00
Ashley Harrison
efeac25952 fix some unit tests 2025-12-02 16:51:42 +00:00
Ashley Harrison
0da94b11ee fix lots of type errors 2025-12-02 15:04:28 +00:00
Ashley Harrison
9aa86eb056 some more type fixes 2025-11-28 13:28:31 +00:00
Ashley Harrison
f6107150e0 Merge branch 'main' into ash/react-19-again 2025-11-28 11:30:45 +00:00
Ashley Harrison
cb90eddf84 fix ref type errors 2025-11-27 15:51:01 +00:00
Ashley Harrison
141ed7bdbf add MessageChannel mock 2025-11-27 11:20:26 +00:00
Ashley Harrison
d2bf550499 bump to latest react versions 2025-11-27 11:00:22 +00:00
123 changed files with 773 additions and 597 deletions

View File

@@ -192,6 +192,30 @@ jobs:
-f "output[summary]=${IMAGE}" \
-f "output[text]=${IMAGE}"
# TODO remove this when delivering
# This will push the temporary docker image to dockerhub
push-docker-image-to-dockerhub:
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
permissions:
contents: read
id-token: write
runs-on: ubuntu-latest
needs:
- build-grafana
steps:
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
with:
name: grafana-docker-tar-gz
path: .
- uses: grafana/shared-workflows/actions/dockerhub-login@dockerhub-login/v1.0.2
- name: Load & Push Docker image
run: |
set -euo pipefail
LOADED_IMAGE_NAME=$(docker load -i grafana.docker.tar.gz | sed 's/Loaded image: //g')
DOCKER_IMAGE="grafana/grafana:12.4.0-react19"
docker tag "${LOADED_IMAGE_NAME}" "${DOCKER_IMAGE}"
docker push "${DOCKER_IMAGE}"
run-e2e-tests:
needs:
- build-grafana

View File

@@ -16,8 +16,8 @@
"@types/lodash": "4.17.7",
"@types/node": "24.9.2",
"@types/prismjs": "1.26.4",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/semver": "7.5.8",
"@types/uuid": "9.0.8",
"glob": "10.5.0",
@@ -37,8 +37,8 @@
"@grafana/runtime": "workspace:*",
"@grafana/schema": "workspace:*",
"@grafana/ui": "workspace:*",
"react": "18.3.1",
"react-dom": "18.3.1",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-router-dom": "^6.22.0",
"rxjs": "7.8.1",
"tslib": "2.6.3"

View File

@@ -16,8 +16,8 @@
"@types/lodash": "4.17.7",
"@types/node": "24.9.2",
"@types/prismjs": "1.26.4",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/semver": "7.5.8",
"@types/uuid": "9.0.8",
"glob": "10.4.1",
@@ -37,8 +37,8 @@
"@grafana/runtime": "workspace:*",
"@grafana/schema": "workspace:*",
"@grafana/ui": "workspace:*",
"react": "18.3.1",
"react-dom": "18.3.1",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-router-dom": "^6.22.0",
"rxjs": "7.8.1",
"tslib": "2.6.3"

View File

@@ -140,8 +140,8 @@
"@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.3.0",
"@types/pluralize": "^0.0.33",
"@types/prismjs": "1.26.5",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/react-grid-layout": "1.3.5",
"@types/react-highlight-words": "0.20.0",
"@types/react-resizable": "3.0.8",
@@ -239,7 +239,7 @@
"prettier": "3.6.2",
"prom-client": "^15.1.3",
"publint": "^0.3.12",
"react-refresh": "0.14.0",
"react-refresh": "0.18.0",
"react-select-event": "5.5.1",
"redux-mock-store": "1.5.5",
"rimraf": "6.0.1",
@@ -388,9 +388,9 @@
"pluralize": "^8.0.0",
"prismjs": "1.30.0",
"re-resizable": "6.11.2",
"react": "18.3.1",
"react": "19.2.1",
"react-diff-viewer-continued": "^3.4.0",
"react-dom": "18.3.1",
"react-dom": "19.2.1",
"react-draggable": "4.5.0",
"react-dropzone": "^14.2.3",
"react-grid-layout": "patch:react-grid-layout@npm%3A1.4.4#~/.yarn/patches/react-grid-layout-npm-1.4.4-4024c5395b.patch",
@@ -460,7 +460,10 @@
"tmp@npm:^0.0.33": "~0.2.1",
"js-yaml@npm:4.1.0": "^4.1.0",
"js-yaml@npm:=4.1.0": "^4.1.0",
"nodemailer": "7.0.7"
"nodemailer": "7.0.7",
"pretty-format/react-is": "19.0.0",
"react": "19.2.1",
"react-dom": "19.2.1"
},
"workspaces": {
"packages": [

View File

@@ -68,13 +68,13 @@
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/lodash": "^4",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/tinycolor2": "^1",
"i18next": "^25.5.2",
"i18next-cli": "^1.24.22",
"react": "18.3.1",
"react-dom": "18.3.1",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-redux": "^9.2.0",
"rimraf": "^6.0.1",
"rollup": "^4.22.4",

View File

@@ -92,12 +92,12 @@
"@types/lodash": "4.17.20",
"@types/node": "24.10.1",
"@types/papaparse": "5.3.16",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/tinycolor2": "1.4.6",
"esbuild": "0.25.8",
"react": "18.3.1",
"react-dom": "18.3.1",
"react": "19.2.0",
"react-dom": "19.2.0",
"rimraf": "6.0.1",
"rollup": "^4.22.4",
"rollup-plugin-esbuild": "6.2.1",

View File

@@ -50,7 +50,7 @@
"@leeoniya/ufuzzy": "1.0.19",
"d3": "^7.8.5",
"lodash": "4.17.21",
"react": "18.3.1",
"react": "19.2.0",
"react-use": "17.6.0",
"react-virtualized-auto-sizer": "1.0.26",
"tinycolor2": "1.6.0",
@@ -73,7 +73,7 @@
"@types/jest": "^29.5.4",
"@types/lodash": "4.17.20",
"@types/node": "24.10.1",
"@types/react": "18.3.18",
"@types/react": "19.2.7",
"@types/react-virtualized-auto-sizer": "1.0.8",
"@types/tinycolor2": "1.4.6",
"babel-jest": "29.7.0",

View File

@@ -22,7 +22,7 @@ import { getBarColorByDiff, getBarColorByPackage, getBarColorByValue } from './c
import { CollapseConfig, CollapsedMap, FlameGraphDataContainer, LevelItem } from './dataTransform';
type RenderOptions = {
canvasRef: RefObject<HTMLCanvasElement>;
canvasRef: RefObject<HTMLCanvasElement | null>;
data: FlameGraphDataContainer;
root: LevelItem;
direction: 'children' | 'parents';
@@ -373,7 +373,7 @@ function useColorFunction(
);
}
function useSetupCanvas(canvasRef: RefObject<HTMLCanvasElement>, wrapperWidth: number, numberOfLevels: number) {
function useSetupCanvas(canvasRef: RefObject<HTMLCanvasElement | null>, wrapperWidth: number, numberOfLevels: number) {
const [ctx, setCtx] = useState<CanvasRenderingContext2D>();
useEffect(() => {

View File

@@ -64,7 +64,7 @@
"react-i18next": "^15.0.0"
},
"devDependencies": {
"@types/react": "18.3.18",
"@types/react": "19.2.7",
"rollup": "^4.22.4",
"rollup-plugin-copy": "3.5.0",
"typescript": "5.9.2"

View File

@@ -36,10 +36,10 @@
"@testing-library/user-event": "14.6.1",
"@types/jest": "^29.5.4",
"@types/node": "24.10.1",
"@types/react": "18.3.18",
"@types/react": "19.2.7",
"@types/systemjs": "6.15.3",
"jest": "^29.6.4",
"react": "18.3.1",
"react": "19.2.0",
"ts-jest": "29.4.0",
"ts-node": "10.9.2",
"typescript": "5.9.2"

View File

@@ -84,3 +84,19 @@ global.ResizeObserver = class ResizeObserver {
this.#isObserving = false;
}
};
global.MessageChannel = jest.fn().mockImplementation(() => {
let onmessage;
return {
port1: {
set onmessage(cb) {
onmessage = cb;
},
},
port2: {
postMessage: (data) => {
onmessage?.({ data });
},
},
};
});

View File

@@ -4,7 +4,7 @@
"private": true,
"version": "12.4.0-pre",
"dependencies": {
"react": "18.3.1",
"react": "19.2.0",
"terser-webpack-plugin": "5.3.14",
"tslib": "2.8.1"
},
@@ -15,7 +15,7 @@
"@swc/helpers": "^0.5.0",
"@swc/jest": "^0.2.26",
"@types/eslint": "9.6.1",
"@types/react": "18.3.18",
"@types/react": "19.2.7",
"@types/webpack-bundle-analyzer": "^4.7.0",
"copy-webpack-plugin": "13.0.0",
"eslint": "9.32.0",

View File

@@ -57,8 +57,8 @@
"@reduxjs/toolkit": "2.10.1",
"@types/debounce-promise": "3.1.9",
"@types/lodash": "4.17.20",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/react-highlight-words": "0.20.0",
"@types/react-window": "1.8.8",
"@types/semver": "7.7.1",
@@ -94,8 +94,8 @@
"i18next-cli": "^1.24.22",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-select-event": "5.5.1",
"rimraf": "6.0.1",
"rollup": "^4.22.4",

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
import { flushSync } from 'react-dom';
import { useDebounce } from 'react-use';
import { TimeRange } from '@grafana/data';
@@ -225,7 +226,10 @@ export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: P
newSelectedMetric === '' ? undefined : selector
);
setMetrics(fetchedMetrics);
// TODO why?!
flushSync(() => {
setMetrics(fetchedMetrics);
});
setSelectedMetric(newSelectedMetric);
setLabelKeys(fetchedLabelKeys);
setIsLoadingLabelKeys(false);

View File

@@ -99,7 +99,7 @@ describe('MetricsLabelsSection', () => {
onBlur: onBlur,
variableEditor: undefined,
}),
expect.anything()
undefined
);
});
@@ -124,7 +124,7 @@ describe('MetricsLabelsSection', () => {
labelsFilters: defaultQuery.labels,
variableEditor: undefined,
}),
expect.anything()
undefined
);
});

View File

@@ -78,12 +78,12 @@
"@types/history": "4.7.11",
"@types/jest": "29.5.14",
"@types/lodash": "4.17.20",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"esbuild": "0.25.8",
"lodash": "4.17.21",
"react": "18.3.1",
"react-dom": "18.3.1",
"react": "19.2.0",
"react-dom": "19.2.0",
"rimraf": "6.0.1",
"rollup": "^4.22.4",
"rollup-plugin-esbuild": "6.2.1",

View File

@@ -25,8 +25,8 @@
"@react-awesome-query-builder/ui": "6.6.15",
"immutable": "5.1.4",
"lodash": "4.17.21",
"react": "18.3.1",
"react-dom": "18.3.1",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-select": "5.10.2",
"react-use": "17.6.0",
"react-virtualized-auto-sizer": "1.0.26",
@@ -43,8 +43,8 @@
"@types/jest": "^29.5.4",
"@types/lodash": "4.17.20",
"@types/node": "24.10.1",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/react-virtualized-auto-sizer": "1.0.8",
"@types/systemjs": "6.15.3",
"@types/uuid": "10.0.0",

View File

@@ -75,6 +75,7 @@
"@hello-pangea/dnd": "18.0.1",
"@monaco-editor/react": "4.7.0",
"@popperjs/core": "2.11.8",
"@rc-component/cascader": "1.9.0",
"@rc-component/drawer": "1.3.0",
"@rc-component/picker": "1.7.1",
"@rc-component/slider": "1.0.1",
@@ -105,7 +106,6 @@
"monaco-editor": "0.34.1",
"ol": "10.7.0",
"prismjs": "1.30.0",
"rc-cascader": "3.34.0",
"react-calendar": "^6.0.0",
"react-colorful": "5.6.1",
"react-custom-scrollbars-2": "4.5.0",
@@ -167,9 +167,9 @@
"@types/mock-raf": "1.0.6",
"@types/node": "24.10.1",
"@types/prismjs": "1.26.5",
"@types/react": "18.3.18",
"@types/react": "19.2.7",
"@types/react-color": "3.0.13",
"@types/react-dom": "18.3.5",
"@types/react-dom": "19.2.3",
"@types/react-highlight-words": "0.20.0",
"@types/react-transition-group": "4.4.12",
"@types/react-window": "1.8.8",
@@ -190,8 +190,8 @@
"msw": "^2.10.2",
"msw-storybook-addon": "^2.0.5",
"process": "^0.11.10",
"react": "18.3.1",
"react-dom": "18.3.1",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-select-event": "^5.1.0",
"rimraf": "6.0.1",
"rollup": "^4.22.4",

View File

@@ -1,5 +1,5 @@
import { css, cx } from '@emotion/css';
import RCCascader, { FieldNames } from 'rc-cascader';
import RCCascader, { FieldNames } from '@rc-component/cascader';
import * as React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
@@ -47,7 +47,7 @@ export const ButtonCascader = (props: ButtonCascaderProps) => {
<RCCascader
onChange={onChangeCascader(onChange)}
loadData={onLoadDataCascader(loadData)}
dropdownClassName={cx(cascaderStyles.dropdown, styles.popup)}
popupClassName={cx(cascaderStyles.dropdown, styles.popup)}
{...rest}
expandIcon={null}
>

View File

@@ -1,6 +1,6 @@
import { css } from '@emotion/css';
import RCCascader from '@rc-component/cascader';
import memoize from 'micro-memoize';
import RCCascader from 'rc-cascader';
import { PureComponent } from 'react';
import * as React from 'react';
@@ -279,7 +279,7 @@ class UnthemedCascader extends PureComponent<CascaderProps, CascaderState> {
expandIcon={null}
open={this.props.alwaysOpen}
disabled={disabled}
dropdownClassName={styles.dropdown}
popupClassName={styles.dropdown}
>
<div className={disableDivFocus}>
<Input

View File

@@ -1,4 +1,4 @@
import { BaseOptionType as RCCascaderOption, CascaderProps } from 'rc-cascader';
import { BaseOptionType as RCCascaderOption, CascaderProps } from '@rc-component/cascader';
import { CascaderOption } from './Cascader';

View File

@@ -18,7 +18,7 @@ interface ComboboxListProps<T extends string | number> {
options: Array<ComboboxOption<T>>;
highlightedIndex: number | null;
selectedItems?: Array<ComboboxOption<T>>;
scrollRef: React.RefObject<HTMLDivElement>;
scrollRef: React.RefObject<HTMLDivElement | null>;
getItemProps: UseComboboxPropGetters<ComboboxOption<T>>['getItemProps'];
enableAllOption?: boolean;
isMultiSelect?: boolean;

View File

@@ -1,5 +1,5 @@
import { autoUpdate, autoPlacement, size, useFloating } from '@floating-ui/react';
import { useMemo, useRef, useState } from 'react';
import { CSSProperties, type RefObject, useMemo, useRef, useState } from 'react';
import { BOUNDARY_ELEMENT_ID } from '../../utils/floating';
import { measureText } from '../../utils/measureText';
@@ -21,7 +21,21 @@ const POPOVER_PADDING = 16;
const SCROLL_CONTAINER_PADDING = 8;
export const useComboboxFloat = (items: Array<ComboboxOption<string | number>>, isOpen: boolean) => {
interface UseComboboxFloatReturn {
inputRef: RefObject<HTMLInputElement | null>;
floatingRef: RefObject<HTMLDivElement | null>;
scrollRef: RefObject<HTMLDivElement | null>;
floatStyles: CSSProperties & {
width: number;
maxWidth: number;
maxHeight: number;
};
}
export const useComboboxFloat = (
items: Array<ComboboxOption<string | number>>,
isOpen: boolean
): UseComboboxFloatReturn => {
const inputRef = useRef<HTMLInputElement>(null);
const floatingRef = useRef<HTMLDivElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);

View File

@@ -10,7 +10,7 @@ import { useStyles2 } from '../../themes/ThemeContext';
import { List } from '../List/List';
interface DataLinkSuggestionsProps {
activeRef?: React.RefObject<HTMLDivElement>;
activeRef?: React.RefObject<HTMLDivElement | null>;
suggestions: VariableSuggestion[];
activeIndex: number;
onSuggestionSelect: (suggestion: VariableSuggestion) => void;
@@ -103,7 +103,7 @@ DataLinkSuggestions.displayName = 'DataLinkSuggestions';
interface DataLinkSuggestionsListProps extends DataLinkSuggestionsProps {
label: string;
activeIndexOffset: number;
activeRef?: React.RefObject<HTMLDivElement>;
activeRef?: React.RefObject<HTMLDivElement | null>;
}
const DataLinkSuggestionsList = React.memo(

View File

@@ -13,7 +13,7 @@ describe('useListFocus', () => {
const testid = 'test';
const getListElement = (
ref: RefObject<HTMLUListElement>,
ref: RefObject<HTMLUListElement | null>,
handleKeys?: (event: KeyboardEvent) => void,
onClick?: () => void
) => (

View File

@@ -7,7 +7,7 @@ const CAUGHT_KEYS = ['ArrowUp', 'ArrowDown', 'Home', 'End', 'Enter', 'Tab'];
/** @internal */
export interface UseListFocusProps {
localRef: RefObject<HTMLUListElement>;
localRef: RefObject<HTMLUListElement | null>;
options: TimeOption[];
}

View File

@@ -1,6 +1,6 @@
import { RefObject, useRef } from 'react';
export function useFocus(): [RefObject<HTMLInputElement>, () => void] {
export function useFocus(): [RefObject<HTMLInputElement | null>, () => void] {
const ref = useRef<HTMLInputElement>(null);
const setFocus = () => {
ref.current && ref.current.focus();

View File

@@ -13,7 +13,7 @@ describe('useMenuFocus', () => {
const testid = 'test';
const getMenuElement = (
ref: RefObject<HTMLDivElement>,
ref: RefObject<HTMLDivElement | null>,
handleKeys?: (event: KeyboardEvent) => void,
handleFocus?: () => void,
onClick?: () => void

View File

@@ -6,7 +6,7 @@ const UNFOCUSED = -1;
/** @internal */
export interface UseMenuFocusProps {
localRef: RefObject<HTMLDivElement>;
localRef: RefObject<HTMLDivElement | null>;
isMenuOpen?: boolean;
close?: () => void;
onOpen?: (focusOnItem: (itemId: number) => void) => void;

View File

@@ -1,11 +1,10 @@
import { HTMLAttributes, PropsWithChildren, type JSX } from 'react';
import * as React from 'react';
import { createElement, type HTMLAttributes, type PropsWithChildren, type HTMLElementType, type JSX } from 'react';
import { textUtil } from '@grafana/data';
export interface RenderUserContentAsHTMLProps<T = HTMLSpanElement>
extends Omit<HTMLAttributes<T>, 'dangerouslySetInnerHTML'> {
component?: keyof React.ReactHTML;
component?: HTMLElementType;
content: string;
}
@@ -19,7 +18,7 @@ export function RenderUserContentAsHTML<T>({
content,
...rest
}: PropsWithChildren<RenderUserContentAsHTMLProps<T>>): JSX.Element {
return React.createElement(component || 'span', {
return createElement(component || 'span', {
dangerouslySetInnerHTML: { __html: textUtil.sanitize(content) },
...rest,
});

View File

@@ -20,7 +20,7 @@ export interface TableCellTooltipProps {
field: Field;
getActions: (field: Field, rowIdx: number) => ActionModel[];
getTextColorForBackground: (bgColor: string) => string;
gridRef: RefObject<DataGridHandle>;
gridRef: RefObject<DataGridHandle | null>;
height: number;
placement?: TableCellTooltipPlacement;
renderer: TableCellRenderer;

View File

@@ -463,7 +463,7 @@ export function useColumnResize(
return dataGridResizeHandler;
}
export function useScrollbarWidth(ref: RefObject<DataGridHandle>, height: number) {
export function useScrollbarWidth(ref: RefObject<DataGridHandle | null>, height: number) {
const [scrollbarWidth, setScrollbarWidth] = useState(0);
useLayoutEffect(() => {

View File

@@ -49,7 +49,7 @@ interface RowsListProps {
listHeight: number;
width: number;
cellHeight?: TableCellHeight;
listRef: React.RefObject<VariableSizeList>;
listRef: React.RefObject<VariableSizeList | null>;
tableState: TableState;
tableStyles: TableStyles;
nestedDataField?: Field;

View File

@@ -135,7 +135,7 @@ export const Table = memo((props: Props) => {
// `useTableStateReducer`, which is needed to construct options for `useTable` (the hook that returns
// `toggleAllRowsExpanded`), and if we used a variable, that variable would be undefined at the time
// we initialize `useTableStateReducer`.
const toggleAllRowsExpandedRef = useRef<(value?: boolean) => void>();
const toggleAllRowsExpandedRef = useRef<(value?: boolean) => void>(null);
// Internal react table state reducer
const stateReducer = useTableStateReducer({

View File

@@ -14,8 +14,8 @@ import { GrafanaTableState } from './types';
Select the scrollbar element from the VariableSizeList scope
*/
export function useFixScrollbarContainer(
variableSizeListScrollbarRef: React.RefObject<HTMLDivElement>,
tableDivRef: React.RefObject<HTMLDivElement>
variableSizeListScrollbarRef: React.RefObject<HTMLDivElement | null>,
tableDivRef: React.RefObject<HTMLDivElement | null>
) {
useEffect(() => {
if (variableSizeListScrollbarRef.current && tableDivRef.current) {
@@ -43,7 +43,7 @@ export function useFixScrollbarContainer(
*/
export function useResetVariableListSizeCache(
extendedState: GrafanaTableState,
listRef: React.RefObject<VariableSizeList>,
listRef: React.RefObject<VariableSizeList | null>,
data: DataFrame,
hasUniqueId: boolean
) {

View File

@@ -19,7 +19,7 @@ interface EventsCanvasProps {
}
export function EventsCanvas({ id, events, renderEventMarker, mapEventToXYCoords, config }: EventsCanvasProps) {
const plotInstance = useRef<uPlot>();
const plotInstance = useRef<uPlot>(null);
// render token required to re-render annotation markers. Rendering lines happens in uPlot and the props do not change
// so we need to force the re-render when the draw hook was performed by uPlot
const [renderToken, setRenderToken] = useState(0);

View File

@@ -140,7 +140,7 @@ export const TooltipPlugin2 = ({
const [{ plot, isHovering, isPinned, contents, style, dismiss }, setState] = useReducer(mergeState, null, initState);
const sizeRef = useRef<TooltipContainerSize>();
const sizeRef = useRef<TooltipContainerSize>(null);
const styles = useStyles2(getStyles, maxWidth);
const renderRef = useRef(render);

View File

@@ -96,7 +96,7 @@ export interface GraphNGState {
export class GraphNG extends Component<GraphNGProps, GraphNGState> {
static contextType = PanelContextRoot;
panelContext: PanelContext = {} as PanelContext;
private plotInstance: React.RefObject<uPlot>;
private plotInstance: React.RefObject<uPlot | null>;
private subscription = new Subscription();

View File

@@ -53,7 +53,7 @@ export const TooltipPlugin = ({
renderTooltip,
...otherProps
}: TooltipPluginProps) => {
const plotInstance = useRef<uPlot>();
const plotInstance = useRef<uPlot>(null);
const theme = useTheme2();
const [focusedSeriesIdx, setFocusedSeriesIdx] = useState<number | null>(null);
const [focusedPointIdx, setFocusedPointIdx] = useState<number | null>(null);

View File

@@ -19,7 +19,7 @@ export function useDelayedSwitch(value: boolean, options: DelayOptions = {}): bo
const { duration = 250, delay = 250 } = options;
const [delayedValue, setDelayedValue] = useState(value);
const onStartTime = useRef<Date | undefined>();
const onStartTime = useRef<Date | undefined>(undefined);
useEffect(() => {
let timeout: ReturnType<typeof setTimeout> | undefined;

View File

@@ -1,5 +1,6 @@
import { css } from '@emotion/css';
import { useState } from 'react';
import { flushSync } from 'react-dom';
import { useForm } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data';
@@ -34,7 +35,10 @@ export const ForgottenPassword = () => {
const sendEmail = async (formModel: EmailDTO) => {
const res = await getBackendSrv().post('/api/user/password/send-reset-email', formModel);
if (res) {
setEmailSent(true);
// TODO why?
flushSync(() => {
setEmailSent(true);
});
}
};

View File

@@ -109,7 +109,7 @@ const defaultMatchers = {
* "Time as X" core component, expects ascending x
*/
export class GraphNG extends Component<GraphNGProps, GraphNGState> {
private plotInstance: React.RefObject<uPlot>;
private plotInstance: React.RefObject<uPlot | null>;
constructor(props: GraphNGProps) {
super(props);

View File

@@ -1,3 +1,4 @@
import { act } from '@testing-library/react';
import { comboboxTestSetup } from 'test/helpers/comboboxTestSetup';
import { getSelectParent, selectOptionInTest } from 'test/helpers/selectOptionInTest';
import { render, screen, userEvent, waitFor, within } from 'test/test-utils';
@@ -29,7 +30,9 @@ const selectComboboxOptionInTest = async (input: HTMLElement, optionOrOptions: s
};
const setup = async () => {
const view = render(<SharedPreferences resourceUri="user" preferenceType="user" />);
// TODO investigate why we need act
// see https://github.com/testing-library/react-testing-library/issues/1375
const view = await act(() => render(<SharedPreferences resourceUri="user" preferenceType="user" />));
const themeSelect = await screen.findByRole('combobox', { name: 'Interface theme' });
await waitFor(() => expect(themeSelect).not.toBeDisabled());
return view;

View File

@@ -1,4 +1,5 @@
import { useState } from 'react';
import { flushSync } from 'react-dom';
import { useForm } from 'react-hook-form';
import { Trans, t } from '@grafana/i18n';
@@ -25,7 +26,10 @@ export const VerifyEmail = () => {
getBackendSrv()
.post('/api/user/signup', formModel)
.then(() => {
setEmailSent(true);
// TODO why?
flushSync(() => {
setEmailSent(true);
});
})
.catch((err) => {
const msg = err.data?.message || err;

View File

@@ -1,3 +1,4 @@
import { act, fireEvent } from '@testing-library/react';
import { InitialEntry } from 'history';
import { last } from 'lodash';
import { Route, Routes } from 'react-router-dom-v5-compat';
@@ -144,8 +145,11 @@ const fillOutForm = async ({
};
const saveMuteTiming = async () => {
const user = userEvent.setup();
await user.click(await screen.findByText(/save time interval/i));
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
const button = await screen.findByText(/save time interval/i);
// eslint-disable-next-line testing-library/no-unnecessary-act, testing-library/prefer-user-event
await act(() => fireEvent.click(button));
};
setupMswServer();

View File

@@ -2,7 +2,7 @@ import { HTMLAttributes } from 'react';
import { Button, IconSize } from '@grafana/ui';
interface Props extends HTMLAttributes<HTMLButtonElement> {
interface Props extends Omit<HTMLAttributes<HTMLButtonElement>, 'onToggle'> {
isCollapsed: boolean;
onToggle: (isCollapsed: boolean) => void;
// Todo: this should be made compulsory for a11y purposes

View File

@@ -1,7 +1,8 @@
import { act, fireEvent } from '@testing-library/react';
import * as React from 'react';
import { Route, Routes } from 'react-router-dom-v5-compat';
import { Props } from 'react-virtualized-auto-sizer';
import { render, userEvent, waitFor, waitForElementToBeRemoved } from 'test/test-utils';
import { render, waitFor, waitForElementToBeRemoved } from 'test/test-utils';
import { byRole, byTestId, byText } from 'testing-library-selector';
import { mockExportApi, setupMswServer } from '../../mockApi';
@@ -72,14 +73,15 @@ describe('GrafanaModifyExport', () => {
json: 'Json Export Content',
});
const user = userEvent.setup();
renderModifyExport(grafanaRulerRule.grafana_alert.uid);
await waitForElementToBeRemoved(() => ui.loading.get());
expect(await ui.form.nameInput.find()).toHaveValue('Grafana-rule');
await user.click(ui.exportButton.get());
// TODO investigate why we need act
// see https://github.com/testing-library/react-testing-library/issues/1375
// eslint-disable-next-line testing-library/no-unnecessary-act, testing-library/prefer-user-event
await act(() => fireEvent.click(ui.exportButton.get()));
const drawer = await ui.exportDrawer.dialog.find();
expect(drawer).toBeInTheDocument();

View File

@@ -1,3 +1,4 @@
import { act, fireEvent } from '@testing-library/react';
import { render, testWithFeatureToggles, waitFor } from 'test/test-utils';
import { byLabelText, byRole } from 'testing-library-selector';
@@ -156,7 +157,10 @@ groups:
await user.click(await ui.dsImport.mimirDsOption.find());
// Click the import button
await user.click(ui.importButton.get());
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
// eslint-disable-next-line testing-library/no-unnecessary-act, testing-library/prefer-user-event
await act(() => fireEvent.click(ui.importButton.get()));
// Verify confirmation dialog appears
expect(await ui.confirmationModal.find()).toBeInTheDocument();

View File

@@ -1,3 +1,4 @@
import { act, fireEvent } from '@testing-library/react';
import { Route, Routes } from 'react-router-dom-v5-compat';
import { render, screen } from 'test/test-utils';
import { byPlaceholderText, byRole, byTestId } from 'testing-library-selector';
@@ -65,7 +66,10 @@ describe('new receiver', () => {
await user.type(email, 'tester@grafana.com');
// try to test the contact point
await user.click(await ui.testContactPointButton.find());
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
// eslint-disable-next-line testing-library/no-unnecessary-act, testing-library/prefer-user-event
await act(async () => fireEvent.click(await ui.testContactPointButton.find()));
expect(await ui.testContactPointModal.find()).toBeInTheDocument();

View File

@@ -1,3 +1,4 @@
import { act } from '@testing-library/react';
import type { JSX } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { render, screen, within } from 'test/test-utils';
@@ -301,15 +302,20 @@ describe('AnnotationsField', function () {
})
);
render(
<FormWrapper
formValues={{
annotations: [
{ key: Annotation.dashboardUID, value: 'dash-test-uid' },
{ key: Annotation.panelID, value: '1' },
],
}}
/>
// TODO investigate why we need act
// see https://github.com/testing-library/react-testing-library/issues/1375
// eslint-disable-next-line testing-library/no-unnecessary-act
await act(() =>
render(
<FormWrapper
formValues={{
annotations: [
{ key: Annotation.dashboardUID, value: 'dash-test-uid' },
{ key: Annotation.panelID, value: '1' },
],
}}
/>
)
);
expect(await ui.dashboardAnnotation.find()).toBeInTheDocument();

View File

@@ -479,7 +479,7 @@ describe('RuleViewer', () => {
expect.objectContaining({
ruleUid: 'test-rule-uid',
}),
expect.any(Object)
undefined
);
expect(screen.getByTestId('enrichment-section')).toBeInTheDocument();
});
@@ -500,7 +500,7 @@ describe('RuleViewer', () => {
expect.objectContaining({
ruleUid: 'test-rule-uid',
}),
expect.any(Object)
undefined
);
expect(screen.getByTestId('enrichment-section')).toBeInTheDocument();
});

View File

@@ -1,3 +1,4 @@
import { act, fireEvent } from '@testing-library/react';
import { produce } from 'immer';
import { render } from 'test/test-utils';
import { byRole, byText } from 'testing-library-selector';
@@ -58,7 +59,7 @@ describe('Moving a Grafana managed rule', () => {
const ruleID = fromRulerRuleAndRuleGroupIdentifier(currentRuleGroupID, ruleToMove);
const { user } = render(
render(
<MoveRuleTestComponent
currentRuleGroupIdentifier={currentRuleGroupID}
targetRuleGroupIdentifier={targetRuleGroupID}
@@ -66,7 +67,10 @@ describe('Moving a Grafana managed rule', () => {
rule={ruleToMove}
/>
);
await user.click(byRole('button').get());
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
// eslint-disable-next-line testing-library/no-unnecessary-act, testing-library/prefer-user-event
await act(() => fireEvent.click(byRole('button').get()));
expect(await byText(/success/i).find()).toBeInTheDocument();
@@ -87,7 +91,7 @@ describe('Moving a Grafana managed rule', () => {
uid: 'does-not-exist',
};
const { user } = render(
render(
<MoveRuleTestComponent
currentRuleGroupIdentifier={currentRuleGroupID}
targetRuleGroupIdentifier={currentRuleGroupID}
@@ -95,7 +99,10 @@ describe('Moving a Grafana managed rule', () => {
rule={grafanaRulerRule}
/>
);
await user.click(byRole('button').get());
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
// eslint-disable-next-line testing-library/no-unnecessary-act, testing-library/prefer-user-event
await act(() => fireEvent.click(byRole('button').get()));
expect(await byText(/error/i).find()).toBeInTheDocument();
});
@@ -130,7 +137,7 @@ describe('Moving a Data source managed rule', () => {
draft.grafana_alert.title = 'updated rule title';
});
const { user } = render(
render(
<MoveRuleTestComponent
currentRuleGroupIdentifier={currentRuleGroupID}
targetRuleGroupIdentifier={targetRuleGroupID}
@@ -138,7 +145,10 @@ describe('Moving a Data source managed rule', () => {
rule={newRule}
/>
);
await user.click(byRole('button').get());
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
// eslint-disable-next-line testing-library/no-unnecessary-act, testing-library/prefer-user-event
await act(() => fireEvent.click(byRole('button').get()));
expect(await byText(/success/i).find()).toBeInTheDocument();
@@ -167,7 +177,7 @@ describe('Moving a Data source managed rule', () => {
const ruleID = fromRulerRuleAndRuleGroupIdentifier(currentRuleGroupID, ruleToMove);
const { user } = render(
render(
<MoveRuleTestComponent
currentRuleGroupIdentifier={currentRuleGroupID}
targetRuleGroupIdentifier={targetRuleGroupID}
@@ -175,7 +185,10 @@ describe('Moving a Data source managed rule', () => {
rule={ruleToMove}
/>
);
await user.click(byRole('button').get());
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
// eslint-disable-next-line testing-library/no-unnecessary-act, testing-library/prefer-user-event
await act(() => fireEvent.click(byRole('button').get()));
expect(await byText(/success/i).find()).toBeInTheDocument();
@@ -206,7 +219,7 @@ describe('Moving a Data source managed rule', () => {
const ruleID = fromRulerRuleAndRuleGroupIdentifier(currentRuleGroupID, ruleToMove);
const { user } = render(
render(
<MoveRuleTestComponent
currentRuleGroupIdentifier={currentRuleGroupID}
targetRuleGroupIdentifier={targetRuleGroupID}
@@ -214,7 +227,10 @@ describe('Moving a Data source managed rule', () => {
rule={ruleToMove}
/>
);
await user.click(byRole('button').get());
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
// eslint-disable-next-line testing-library/no-unnecessary-act, testing-library/prefer-user-event
await act(() => fireEvent.click(byRole('button').get()));
expect(await byText(/success/i).find()).toBeInTheDocument();
@@ -239,7 +255,7 @@ describe('Moving a Data source managed rule', () => {
draft.grafana_alert.title = 'updated rule title';
});
const { user } = render(
render(
<MoveRuleTestComponent
currentRuleGroupIdentifier={curentRuleGroupID}
targetRuleGroupIdentifier={curentRuleGroupID}
@@ -247,7 +263,10 @@ describe('Moving a Data source managed rule', () => {
rule={newRule}
/>
);
await user.click(byRole('button').get());
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
// eslint-disable-next-line testing-library/no-unnecessary-act, testing-library/prefer-user-event
await act(() => fireEvent.click(byRole('button').get()));
expect(await byText(/error/i).find()).toBeInTheDocument();
});

View File

@@ -31,7 +31,6 @@ describe('pause rule', () => {
expect(byText(/uninitialized/i).get()).toBeInTheDocument();
await userEvent.click(byRole('button').get());
expect(await byText(/loading/i).find()).toBeInTheDocument();
expect(await byText(/success/i).find()).toBeInTheDocument();
expect(await byText(/result/i).find()).toBeInTheDocument();
@@ -68,7 +67,6 @@ describe('pause rule', () => {
expect(await byText(/uninitialized/i).find()).toBeInTheDocument();
await userEvent.click(byRole('button').get());
expect(await byText(/loading/i).find()).toBeInTheDocument();
expect(byText(/success/i).query()).not.toBeInTheDocument();
expect(await byText(/error:(.+)oops/i).find()).toBeInTheDocument();
});

View File

@@ -84,8 +84,8 @@ export function useAsync<Result, Args extends unknown[] = unknown[]>(
error: undefined,
result: initialValue,
});
const promiseRef = useRef<Promise<Result>>();
const argsRef = useRef<Args>();
const promiseRef = useRef<Promise<Result>>(undefined);
const argsRef = useRef<Args>(undefined);
const methods = useSyncedRef({
execute(...params: Args) {

View File

@@ -155,8 +155,8 @@ export function useRuleGroupConsistencyCheck() {
const { isGroupInSync } = useRuleGroupIsInSync();
const [groupConsistent, setGroupConsistent] = useState<boolean | undefined>();
const apiCheckInterval = useRef<ReturnType<typeof setTimeout> | undefined>();
const timeoutInterval = useRef<ReturnType<typeof setTimeout> | undefined>();
const apiCheckInterval = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const timeoutInterval = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
useEffect(() => {
return () => {
@@ -245,8 +245,8 @@ export function useRuleGroupConsistencyCheck() {
export function usePrometheusConsistencyCheck() {
const { matchingPromRuleExists } = useMatchingPromRuleExists();
const removalConsistencyInterval = useRef<number | undefined>();
const creationConsistencyInterval = useRef<number | undefined>();
const removalConsistencyInterval = useRef<number | undefined>(undefined);
const creationConsistencyInterval = useRef<number | undefined>(undefined);
useEffect(() => {
return () => {

View File

@@ -1,3 +1,4 @@
import { act, fireEvent } from '@testing-library/react';
import { render, screen, waitFor, within } from 'test/test-utils';
import { byRole } from 'testing-library-selector';
@@ -58,7 +59,10 @@ describe('RuleList - GroupedView', () => {
});
it('should paginate through groups', async () => {
const { user } = render(<GroupedView />);
// TODO investigate why we need act
// see https://github.com/testing-library/react-testing-library/issues/1375
// eslint-disable-next-line testing-library/no-unnecessary-act
await act(() => render(<GroupedView />));
const mimirSection = await ui.dsSection(/Mimir/).find();
@@ -73,7 +77,10 @@ describe('RuleList - GroupedView', () => {
expect(firstPageGroups[39]).toHaveTextContent('test-group-40');
const loadMoreButton = await within(mimirSection).findByRole('button', { name: /Show more/i });
await user.click(loadMoreButton);
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
// eslint-disable-next-line testing-library/no-unnecessary-act, testing-library/prefer-user-event
await act(() => fireEvent.click(loadMoreButton));
await waitFor(() => expect(loadMoreButton).toBeEnabled());
@@ -86,7 +93,10 @@ describe('RuleList - GroupedView', () => {
});
it('should disable next button when there is no more data', async () => {
const { user } = render(<GroupedView />);
// TODO investigate why we need act
// see https://github.com/testing-library/react-testing-library/issues/1375
// eslint-disable-next-line testing-library/no-unnecessary-act
await act(() => render(<GroupedView />));
const prometheusSection = await ui.dsSection(/Prometheus/).find();
const promNamespace = await ui.namespace(/test-prometheus-namespace/).find(prometheusSection);
@@ -96,17 +106,27 @@ describe('RuleList - GroupedView', () => {
await ui.group('test-group-40').find(promNamespace);
// fetch page 2
await user.click(await loadMoreButton.find(prometheusSection));
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
// eslint-disable-next-line testing-library/no-unnecessary-act, testing-library/prefer-user-event
await act(async () => fireEvent.click(await loadMoreButton.find(prometheusSection)));
// await user.click(await loadMoreButton.find(prometheusSection));
// we should now have all groups 1-80
await ui.group('test-group-80').find(promNamespace);
// fetch page 3
await user.click(await loadMoreButton.find(prometheusSection));
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
// eslint-disable-next-line testing-library/no-unnecessary-act, testing-library/prefer-user-event
await act(async () => fireEvent.click(await loadMoreButton.find(prometheusSection)));
// we should now have all groups 1-120
await ui.group('test-group-120').find(promNamespace);
// fetch page 4
await user.click(await loadMoreButton.find(prometheusSection));
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
// eslint-disable-next-line testing-library/no-unnecessary-act, testing-library/prefer-user-event
await act(async () => fireEvent.click(await loadMoreButton.find(prometheusSection)));
// we should now have all groups 1-130
await ui.group('test-group-130').find(promNamespace);

View File

@@ -7,6 +7,9 @@ type Props = {
function LoadMoreHelper({ handleLoad }: Props) {
const intersectionRef = useRef<HTMLDivElement>(null);
// TODO remove when react-use is fixed
// see https://github.com/streamich/react-use/issues/2612
// @ts-expect-error
const intersection = useIntersection(intersectionRef, {
root: null,
threshold: 1,

View File

@@ -67,7 +67,7 @@ describe('StandardAnnotationQueryEditor', () => {
expect.objectContaining({
query: expect.objectContaining({ queryType: 'defaultAnnotationsQuery', refId: 'initialAnnotationRef' }),
}),
expect.anything()
undefined
);
});
@@ -85,7 +85,7 @@ describe('StandardAnnotationQueryEditor', () => {
expect.objectContaining({
query: expect.objectContaining({ refId: 'initialAnnotationRef' }),
}),
expect.anything()
undefined
);
});
@@ -204,7 +204,7 @@ describe('StandardAnnotationQueryEditor', () => {
refId: 'A',
}),
}),
expect.anything()
undefined
);
});
@@ -242,7 +242,7 @@ describe('StandardAnnotationQueryEditor', () => {
legendFormat: '{{method}} {{endpoint}}',
}),
}),
expect.anything()
undefined
);
});
@@ -284,7 +284,7 @@ describe('StandardAnnotationQueryEditor', () => {
refId: 'AnnoTarget',
}),
}),
expect.anything()
undefined
);
});
@@ -320,7 +320,7 @@ describe('StandardAnnotationQueryEditor', () => {
expr: 'up',
}),
}),
expect.anything()
undefined
);
});

View File

@@ -141,7 +141,7 @@ function useScopesRow(onApply: () => void) {
function useGlobalScopesSearch(searchQuery: string, parentId?: string | null) {
const { selectScope, searchAllNodes, getScopeNodes } = useScopeServicesState();
const [actions, setActions] = useState<CommandPaletteAction[] | undefined>(undefined);
const searchQueryRef = useRef<string>();
const searchQueryRef = useRef<string>(undefined);
useEffect(() => {
if ((!parentId || parentId === 'scopes') && searchQuery && config.featureToggles.scopeSearchAllLevels) {

View File

@@ -1,4 +1,4 @@
import { render, waitFor, screen, within, Matcher, getByRole } from '@testing-library/react';
import { render, waitFor, screen, within, Matcher, getByRole, act, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { merge, uniqueId } from 'lodash';
import { openMenu } from 'react-select-event';
@@ -300,7 +300,9 @@ describe('CorrelationsPage', () => {
await userEvent.click(screen.getByText('Regular expression'));
await userEvent.type(screen.getByLabelText(/expression/i), 'test expression');
await userEvent.click(await screen.findByRole('button', { name: /add$/i }));
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
await act(() => fireEvent.click(screen.getByRole('button', { name: /add$/i })));
await waitFor(() => {
expect(mocks.reportInteraction).toHaveBeenCalledWith('grafana_correlations_added');
@@ -451,7 +453,9 @@ describe('CorrelationsPage', () => {
await userEvent.clear(screen.getByRole('textbox', { name: /results field/i }));
await userEvent.type(screen.getByRole('textbox', { name: /results field/i }), 'Line');
await userEvent.click(screen.getByRole('button', { name: /add$/i }));
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
await act(() => fireEvent.click(screen.getByRole('button', { name: /add$/i })));
await waitFor(() => {
expect(mocks.reportInteraction).toHaveBeenCalledWith('grafana_correlations_added');
@@ -518,7 +522,9 @@ describe('CorrelationsPage', () => {
await userEvent.click(screen.getByRole('button', { name: /next$/i }));
await userEvent.click(screen.getByRole('button', { name: /next$/i }));
await userEvent.click(screen.getByRole('button', { name: /save$/i }));
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
await act(() => fireEvent.click(screen.getByRole('button', { name: /save$/i })));
expect(await screen.findByRole('cell', { name: /edited label$/i }, { timeout: 5000 })).toBeInTheDocument();
@@ -536,7 +542,9 @@ describe('CorrelationsPage', () => {
const rowExpanderButton = within(tableRows[0]).getByRole('button', { name: /toggle row expanded/i });
await userEvent.click(rowExpanderButton);
await userEvent.click(screen.getByRole('button', { name: /next$/i }));
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
await act(() => fireEvent.click(screen.getByRole('button', { name: /next$/i })));
await userEvent.click(screen.getByRole('button', { name: /next$/i }));
// select Logfmt, be sure expression field is disabled
@@ -575,7 +583,9 @@ describe('CorrelationsPage', () => {
await userEvent.click(screen.getByRole('button', { name: /save$/i }));
expect(screen.getByText('Please define an expression')).toBeInTheDocument();
await userEvent.type(screen.getByLabelText(/expression/i), 'test expression');
await userEvent.click(screen.getByRole('button', { name: /save$/i }));
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
await act(() => fireEvent.click(screen.getByRole('button', { name: /save$/i })));
await waitFor(() => {
expect(mocks.reportInteraction).toHaveBeenCalledWith('grafana_correlations_edited');
});
@@ -726,7 +736,9 @@ describe('CorrelationsPage', () => {
expect(descriptionInput).toBeInTheDocument();
expect(descriptionInput).toHaveAttribute('readonly');
await userEvent.click(screen.getByRole('button', { name: /next$/i }));
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
await act(() => fireEvent.click(screen.getByRole('button', { name: /next$/i })));
await userEvent.click(screen.getByRole('button', { name: /next$/i }));
// expect the transformation to exist but be read only

View File

@@ -58,7 +58,7 @@ jest.mock('app/features/plugins/extensions/getPluginExtensions', () => ({
createPluginExtensionsGetter: () => getPluginExtensionsMock,
}));
function setup({ routeProps }: { routeProps?: Partial<GrafanaRouteComponentProps> } = {}) {
async function setup({ routeProps }: { routeProps?: Partial<GrafanaRouteComponentProps> } = {}) {
const context = getGrafanaContextMock();
const defaultRouteProps = getRouteComponentProps();
const props: Props = {
@@ -66,21 +66,29 @@ function setup({ routeProps }: { routeProps?: Partial<GrafanaRouteComponentProps
...routeProps,
};
const renderResult = render(
<TestProvider grafanaContext={context}>
<LocationServiceProvider service={locationService}>
<DashboardScenePage {...props} />
</LocationServiceProvider>
</TestProvider>
);
const rerender = (newProps: Props) => {
renderResult.rerender(
// react 19 changed how suspense rendering works
// RTL hasn't caught up yet
// see https://github.com/testing-library/react-testing-library/issues/1375
// TODO remove this hack when RTL is updated. probably `render` itself will become async
const renderResult = await act(async () =>
render(
<TestProvider grafanaContext={context}>
<LocationServiceProvider service={locationService}>
<DashboardScenePage {...newProps} />
<DashboardScenePage {...props} />
</LocationServiceProvider>
</TestProvider>
)
);
const rerender = async (newProps: Props) => {
await act(async () =>
renderResult.rerender(
<TestProvider grafanaContext={context}>
<LocationServiceProvider service={locationService}>
<DashboardScenePage {...newProps} />
</LocationServiceProvider>
</TestProvider>
)
);
};
@@ -152,6 +160,8 @@ describe('DashboardScenePage', () => {
beforeEach(() => {
locationService.push('/d/my-dash-uid');
getDashboardScenePageStateManager().clearDashboardCache();
getDashboardScenePageStateManager().clearSceneCache();
getDashboardScenePageStateManager().clearState();
loadDashboardMock.mockClear();
loadDashboardMock.mockResolvedValue({ dashboard: simpleDashboard, meta: { slug: '123' } });
// hacky way because mocking autosizer does not work
@@ -163,7 +173,7 @@ describe('DashboardScenePage', () => {
});
it('Can render dashboard', async () => {
setup();
await setup();
await waitForDashboardToRender();
@@ -175,7 +185,7 @@ describe('DashboardScenePage', () => {
});
it('routeReloadCounter should trigger reload', async () => {
const { rerender, props } = setup();
const { rerender, props } = await setup();
await waitForDashboardToRender();
@@ -190,13 +200,13 @@ describe('DashboardScenePage', () => {
props.location.state = { routeReloadCounter: 1 };
rerender(props);
await rerender(props);
expect(await screen.findByTitle('Updated title')).toBeInTheDocument();
});
it('Can inspect panel', async () => {
setup();
await setup();
await waitForDashboardToRender();
@@ -218,7 +228,7 @@ describe('DashboardScenePage', () => {
});
it('Can view panel in fullscreen', async () => {
setup();
await setup();
await waitForDashboardToRender();
@@ -238,7 +248,7 @@ describe('DashboardScenePage', () => {
interval: {} as SystemDateFormatsState['interval'],
useBrowserLocale: true,
});
setup();
await setup();
await waitForDashboardToRenderWithTimeRange({
from: '03/11/2025, 02:09:37 AM',
@@ -257,7 +267,7 @@ describe('DashboardScenePage', () => {
interval: {} as SystemDateFormatsState['interval'],
useBrowserLocale: true,
});
setup();
await setup();
await waitForDashboardToRenderWithTimeRange({
from: '11.03.2025, 02:09:37',
@@ -269,17 +279,17 @@ describe('DashboardScenePage', () => {
describe('empty state', () => {
it('Shows empty state when dashboard is empty', async () => {
loadDashboardMock.mockResolvedValue({ dashboard: { uid: 'my-dash-uid', panels: [] }, meta: {} });
setup();
await setup();
expect(await screen.findByText('Start your new dashboard by adding a visualization')).toBeInTheDocument();
});
it('shows and hides empty state when panels are added and removed', async () => {
setup();
await setup();
await waitForDashboardToRender();
expect(await screen.queryByText('Start your new dashboard by adding a visualization')).not.toBeInTheDocument();
expect(screen.queryByText('Start your new dashboard by adding a visualization')).not.toBeInTheDocument();
// Hacking a bit, accessing private cache property to get access to the underlying DashboardScene object
const dashboardScenesCache = getDashboardScenePageStateManager().getCache();
@@ -289,7 +299,7 @@ describe('DashboardScenePage', () => {
act(() => {
dashboard.removePanel(panels[0]);
});
expect(await screen.queryByText('Start your new dashboard by adding a visualization')).not.toBeInTheDocument();
expect(screen.queryByText('Start your new dashboard by adding a visualization')).not.toBeInTheDocument();
act(() => {
dashboard.removePanel(panels[1]);
@@ -301,14 +311,14 @@ describe('DashboardScenePage', () => {
});
expect(await screen.findByTitle('Panel Added')).toBeInTheDocument();
expect(await screen.queryByText('Start your new dashboard by adding a visualization')).not.toBeInTheDocument();
expect(screen.queryByText('Start your new dashboard by adding a visualization')).not.toBeInTheDocument();
});
});
describe('home page', () => {
it('should render the dashboard when the route is home', async () => {
(useParams as jest.Mock).mockReturnValue({});
setup({
await setup({
routeProps: {
route: {
...getRouteComponentProps().route,
@@ -331,7 +341,7 @@ describe('DashboardScenePage', () => {
loadDashboardMock.mockClear();
loadDashboardMock.mockResolvedValue({ dashboard: { uid: 'my-dash-uid', panels: [] }, meta: {} });
setup();
await setup();
await waitFor(() => expect(screen.queryByText('Refresh')).toBeInTheDocument());
await waitFor(() => expect(screen.queryByText('Last 6 hours')).toBeInTheDocument());
@@ -363,7 +373,7 @@ describe('DashboardScenePage', () => {
isHandled: true,
});
setup();
await setup();
expect(await screen.findByTestId(selectors.components.EntityNotFound.container)).toBeInTheDocument();
});
@@ -387,7 +397,7 @@ describe('DashboardScenePage', () => {
isHandled: true,
});
setup();
await setup();
expect(await screen.findByTestId('dashboard-page-error')).toBeInTheDocument();
expect(await screen.findByTestId('dashboard-page-error')).toHaveTextContent('Internal server error');
@@ -396,7 +406,7 @@ describe('DashboardScenePage', () => {
it('should render error alert for runtime errors', async () => {
setupLoadDashboardRuntimeErrorMock();
setup();
await setup();
expect(await screen.findByTestId('dashboard-page-error')).toBeInTheDocument();
expect(await screen.findByTestId('dashboard-page-error')).toHaveTextContent('Runtime error');
@@ -411,7 +421,7 @@ describe('DashboardScenePage', () => {
const manager = getDashboardScenePageStateManager();
manager.setActiveManager('v2');
const { unmount } = setup();
const { unmount } = await setup();
expect(manager['activeManager']).toBeInstanceOf(DashboardScenePageStateManagerV2);
unmount();

View File

@@ -1,4 +1,4 @@
import { screen, waitForElementToBeRemoved } from '@testing-library/react';
import { act, screen } from '@testing-library/react';
import { Route, Routes } from 'react-router-dom-v5-compat';
import { of } from 'rxjs';
import { render } from 'test/test-utils';
@@ -26,7 +26,7 @@ jest.mock('@grafana/runtime', () => ({
},
}));
function setup(token = 'an-access-token') {
async function setup(token = 'an-access-token') {
const pubdashProps: PublicDashboardSceneProps = {
...getRouteComponentProps({
route: {
@@ -37,11 +37,19 @@ function setup(token = 'an-access-token') {
}),
};
return render(
<Routes>
<Route path="/public-dashboards/:accessToken" element={<PublicDashboardScenePage {...pubdashProps} />} />
</Routes>,
{ historyOptions: { initialEntries: [`/public-dashboards/${token}`] } }
// TODO investigate why act is needed here
// see https://github.com/testing-library/react-testing-library/issues/1375
return await act(() =>
render(
<Routes>
<Route
path="/public-dashboards/:accessToken"
element={<PublicDashboardScenePage {...pubdashProps} />}
key={token}
/>
</Routes>,
{ historyOptions: { initialEntries: [`/public-dashboards/${token}`] } }
)
);
}
@@ -115,7 +123,6 @@ const publicDashboardSceneSelector = e2eSelectors.pages.PublicDashboardScene;
describe('PublicDashboardScenePage', () => {
beforeEach(() => {
config.publicDashboardAccessToken = 'an-access-token';
getDashboardScenePageStateManager().clearDashboardCache();
setupLoadDashboardMock({ dashboard: simpleDashboard, meta: {} });
@@ -125,7 +132,9 @@ describe('PublicDashboardScenePage', () => {
});
it('can render public dashboard', async () => {
setup();
const accessToken = 'an-access-token';
config.publicDashboardAccessToken = accessToken;
await setup(accessToken);
await waitForDashboardGridToRender();
@@ -139,7 +148,9 @@ describe('PublicDashboardScenePage', () => {
});
it('cannot see menu panel', async () => {
setup();
const accessToken = 'cannot-see-menu-panel';
config.publicDashboardAccessToken = accessToken;
await setup(accessToken);
await waitForDashboardGridToRender();
@@ -148,7 +159,9 @@ describe('PublicDashboardScenePage', () => {
});
it('shows time controls when it is not hidden', async () => {
setup();
const accessToken = 'shows-time-controls';
config.publicDashboardAccessToken = accessToken;
await setup(accessToken);
await waitForDashboardGridToRender();
@@ -158,7 +171,9 @@ describe('PublicDashboardScenePage', () => {
});
it('does not render paused or deleted screen', async () => {
setup();
const accessToken = 'does-not-render-paused-or-deleted-screen';
config.publicDashboardAccessToken = accessToken;
await setup(accessToken);
await waitForDashboardGridToRender();
@@ -172,7 +187,7 @@ describe('PublicDashboardScenePage', () => {
dashboard: { ...simpleDashboard, timepicker: { hidden: true } },
meta: {},
});
setup(accessToken);
await setup(accessToken);
await waitForDashboardGridToRender();
@@ -207,9 +222,7 @@ describe('given unavailable public dashboard', () => {
},
});
setup(accessToken);
await waitForElementToBeRemoved(screen.getByTestId(publicDashboardSceneSelector.loadingPage));
await setup(accessToken);
expect(screen.queryByTestId(publicDashboardSceneSelector.page)).not.toBeInTheDocument();
expect(screen.getByTestId(publicDashboardSelector.NotAvailable.title)).toBeInTheDocument();
@@ -239,9 +252,7 @@ describe('given unavailable public dashboard', () => {
},
});
setup(accessToken);
await waitForElementToBeRemoved(screen.getByTestId(publicDashboardSceneSelector.loadingPage));
await setup(accessToken);
expect(screen.queryByTestId(publicDashboardSelector.page)).not.toBeInTheDocument();
expect(screen.queryByTestId(publicDashboardSelector.NotAvailable.pausedDescription)).not.toBeInTheDocument();

View File

@@ -49,7 +49,7 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) {
const [contentSent, setContentSent] = useState<{ title?: string; folderUid?: string }>({});
const validationTimeoutRef = useRef<NodeJS.Timeout>();
const validationTimeoutRef = useRef<NodeJS.Timeout>(null);
// Validate title on form mount to catch invalid default values
useEffect(() => {
@@ -59,14 +59,18 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) {
// Cleanup timeout on unmount
useEffect(() => {
return () => {
clearTimeout(validationTimeoutRef.current);
if (validationTimeoutRef.current) {
clearTimeout(validationTimeoutRef.current);
}
};
}, []);
const handleTitleChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setValue('title', e.target.value, { shouldDirty: true });
clearTimeout(validationTimeoutRef.current);
if (validationTimeoutRef.current) {
clearTimeout(validationTimeoutRef.current);
}
validationTimeoutRef.current = setTimeout(() => {
trigger('title');
}, 400);
@@ -75,7 +79,9 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) {
);
const onSave = async (overwrite: boolean) => {
clearTimeout(validationTimeoutRef.current);
if (validationTimeoutRef.current) {
clearTimeout(validationTimeoutRef.current);
}
const isTitleValid = await trigger('title');

View File

@@ -8,7 +8,7 @@ import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
* Will scroll element into view. If element is not connected yet, it will try to expand rows
* and switch tabs to make it visible.
*/
export function scrollCanvasElementIntoView(sceneObject: SceneObject, ref: React.RefObject<HTMLElement>) {
export function scrollCanvasElementIntoView(sceneObject: SceneObject, ref: React.RefObject<HTMLElement | null>) {
if (ref.current?.isConnected) {
scrollIntoView(ref.current);
return;

View File

@@ -1,4 +1,4 @@
import { screen, waitFor, within } from '@testing-library/react';
import { act, fireEvent, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from 'test/test-utils';
@@ -154,7 +154,9 @@ describe('VersionSettings', () => {
expect(within(screen.getAllByRole('rowgroup')[1]).getAllByRole('row').length).toBe(VERSIONS_FETCH_LIMIT);
const showMoreButton = screen.getByRole('button', { name: /show more versions/i });
await user.click(showMoreButton);
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
act(() => fireEvent.click(showMoreButton));
expect(historySrv.getHistoryList).toBeCalledTimes(2);
expect(screen.getByText(/Fetching more entries/i)).toBeInTheDocument();

View File

@@ -21,7 +21,7 @@ export const usePanelLatestData = (
options: GetDataOptions,
checkSchema?: boolean
): UsePanelLatestData => {
const querySubscription = useRef<Unsubscribable>();
const querySubscription = useRef<Unsubscribable>(null);
const [latestData, setLatestData] = useState<PanelData>();
useEffect(() => {

View File

@@ -61,7 +61,7 @@ interface State {
class UnThemedTransformationsEditor extends React.PureComponent<TransformationsEditorProps, State> {
subscription?: Unsubscribable;
ref: RefObject<HTMLDivElement>;
ref: RefObject<HTMLDivElement | null>;
constructor(props: TransformationsEditorProps) {
super(props);

View File

@@ -1,4 +1,4 @@
import { screen, waitFor } from '@testing-library/react';
import { act, screen, waitFor } from '@testing-library/react';
import { Routes, Route } from 'react-router-dom-v5-compat';
import { render } from 'test/test-utils';
@@ -62,7 +62,9 @@ describe('PublicDashboardPageProxy', () => {
describe('when scene feature enabled', () => {
it('should render PublicDashboardScenePage if publicDashboardsScene is enabled', async () => {
config.featureToggles.publicDashboardsScene = true;
setup({});
// TODO investigate why we need act
// see https://github.com/testing-library/react-testing-library/issues/1375
await act(() => setup({}));
await waitFor(() => {
expect(screen.queryByTestId(PublicDashboardScene.page)).toBeInTheDocument();

View File

@@ -64,7 +64,7 @@ export function useDatasource(dataSource: string | DataSourceRef | DataSourceIns
export interface KeybaordNavigatableListProps {
keyboardEvents?: Observable<React.KeyboardEvent>;
containerRef: React.RefObject<HTMLElement>;
containerRef: React.RefObject<HTMLElement | null>;
}
/**

View File

@@ -25,7 +25,7 @@ interface State {
}
export class ThresholdsEditor extends PureComponent<Props, State> {
private latestThresholdInputRef: React.RefObject<HTMLInputElement>;
private latestThresholdInputRef: React.RefObject<HTMLInputElement | null>;
constructor(props: Props) {
super(props);

View File

@@ -46,6 +46,9 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement |
);
const styles = useStyles2(getStyles, contentOutlineExpanded);
const scrollerRef = useRef(scroller || null);
// TODO remove when react-use is fixed
// see https://github.com/streamich/react-use/issues/2612
// @ts-expect-error
const { y: verticalScroll } = useScroll(scrollerRef);
const { outlineItems } = useContentOutlineContext() ?? { outlineItems: [] };
const [activeSectionId, setActiveSectionId] = useState(outlineItems[0]?.id);

View File

@@ -74,7 +74,7 @@ export const ExplorePaneContainer = connector(ExplorePaneContainerUnconnected);
function useStopQueries(exploreId: string) {
const paneSelector = useMemo(() => getExploreItemSelector(exploreId), [exploreId]);
const paneRef = useRef<ReturnType<typeof paneSelector>>();
const paneRef = useRef<ReturnType<typeof paneSelector>>(null);
paneRef.current = useSelector(paneSelector);
useEffect(() => {

View File

@@ -63,7 +63,7 @@ type Props = {
scrollElementClass?: string;
traceProp: Trace;
datasource: DataSourceApi<DataQuery, DataSourceJsonData, {}> | undefined;
topOfViewRef?: RefObject<HTMLDivElement>;
topOfViewRef?: RefObject<HTMLDivElement | null>;
createSpanLink?: SpanLinkFunc;
focusedSpanId?: string;
createFocusSpanLink?: (traceId: string, spanId: string) => LinkModel<Field>;

View File

@@ -57,7 +57,7 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
const [focusedSpanIndexForSearch, setFocusedSpanIndexForSearch] = useState(-1);
const [tagKeys, setTagKeys] = useState<Array<SelectableValue<string>>>();
const [tagValues, setTagValues] = useState<{ [key: string]: Array<SelectableValue<string>> }>({});
const prevTraceIdRef = useRef<string>();
const prevTraceIdRef = useRef<string>(null);
const durationRegex = /^\d+(?:\.\d)?\d*(?:ns|us|µs|ms|s|m|h)$/;

View File

@@ -27,7 +27,8 @@ const timeRange = {
to: new Date(1000),
} as unknown as TimeRange;
function getContent(result: React.ReactElement) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getContent(result: React.ReactElement<any>) {
return result.props.children.props.children[0];
}

View File

@@ -561,7 +561,7 @@ const CardsContainer = ({
mainContainerRef,
}: {
listOfContentCards: React.ReactNode[];
mainContainerRef?: React.RefObject<HTMLDivElement>;
mainContainerRef?: React.RefObject<HTMLDivElement | null>;
}) => {
const styles = useStyles2(getStyles);

View File

@@ -102,7 +102,7 @@ type TVirtualizedTraceViewOwnProps = {
focusedSpanIdForSearch: string;
showSpanFilterMatchesOnly: boolean;
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
topOfViewRef?: RefObject<HTMLDivElement>;
topOfViewRef?: RefObject<HTMLDivElement | null>;
datasourceType: string;
datasourceUid: string;
headerHeight: number;

View File

@@ -104,7 +104,7 @@ export type TProps = {
focusedSpanIdForSearch: string;
showSpanFilterMatchesOnly: boolean;
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
topOfViewRef?: RefObject<HTMLDivElement>;
topOfViewRef?: RefObject<HTMLDivElement | null>;
headerHeight: number;
criticalPath: CriticalPathSection[];
traceFlameGraphs: TraceFlameGraphs;
@@ -197,8 +197,10 @@ export class UnthemedTraceTimelineViewer extends PureComponent<TProps, State> {
return (
<div
className={styles.TraceTimelineViewer}
ref={(ref: HTMLDivElement | null) => {
ref && this.setState({ height: ref.getBoundingClientRect().height });
ref={(ref) => {
if (ref) {
this.setState({ height: ref.getBoundingClientRect().height });
}
}}
>
<TimelineHeaderRow

View File

@@ -1,4 +1,4 @@
import { screen, waitFor, within } from '@testing-library/react';
import { act, fireEvent, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from 'test/test-utils';
@@ -103,7 +103,9 @@ describe('SignupInvitedPage', () => {
get: { email: '', invitedBy: '', name: '', username: '', orgName: '' },
});
await userEvent.click(screen.getByRole('button', { name: /sign up/i }));
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
await act(() => fireEvent.click(screen.getByRole('button', { name: /sign up/i })));
await waitFor(() => expect(screen.getByText(/email is required/i)).toBeInTheDocument());
expect(screen.getByText(/username is required/i)).toBeInTheDocument();

View File

@@ -43,7 +43,7 @@ export const LibraryPanelsView = ({
}
);
const asyncDispatch = useMemo(() => asyncDispatcher(dispatch), [dispatch]);
const abortControllerRef = useRef<AbortController>();
const abortControllerRef = useRef<AbortController>(null);
useDebounce(
() => {

View File

@@ -12,7 +12,7 @@ export interface Props {}
export const LiveConnectionWarning = memo(function LiveConnectionWarning() {
const [show, setShow] = useState<boolean | undefined>(undefined);
const subscriptionRef = useRef<Unsubscribable>();
const subscriptionRef = useRef<Unsubscribable>(null);
const styles = useStyles2(getStyle);
useEffect(() => {

View File

@@ -1,5 +1,6 @@
import { css } from '@emotion/css';
import { useEffect, useMemo, useState } from 'react';
import { flushSync } from 'react-dom';
import { isObservable, lastValueFrom } from 'rxjs';
import { DataFrame, DataQueryRequest, DataSourceApi, GrafanaTheme2, TimeRange } from '@grafana/data';
@@ -31,7 +32,10 @@ export const LogLineDetailsTrace = ({ timeRange, timeZone, traceRef }: Props) =>
.get(traceRef.dsUID)
.then((dataSource) => {
if (dataSource) {
setDataSource(dataSource);
// TODO why?
flushSync(() => {
setDataSource(dataSource);
});
} else {
setDataFrames(null);
}

View File

@@ -1,7 +1,7 @@
import { QueryStatus } from '@reduxjs/toolkit/query';
import { screen, waitFor } from '@testing-library/react';
import { fireEvent, screen, waitFor } from '@testing-library/react';
import { UserEvent } from '@testing-library/user-event';
import type { JSX } from 'react';
import { act, type JSX } from 'react';
import { render } from 'test/test-utils';
import {
@@ -237,7 +237,9 @@ describe('ProvisioningWizard', () => {
path: '/',
});
await user.click(screen.getByRole('button', { name: /Choose what to synchronize/i }));
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
await act(() => fireEvent.click(screen.getByRole('button', { name: /Choose what to synchronize/i })));
await waitFor(() => {
expect(screen.getByRole('heading', { name: /2\. Choose what to synchronize/i })).toBeInTheDocument();
@@ -245,7 +247,9 @@ describe('ProvisioningWizard', () => {
expect(mockUseCreateOrUpdateRepository).toHaveBeenCalled();
await user.click(screen.getByRole('button', { name: /Synchronize with external storage/i }));
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
await act(() => fireEvent.click(screen.getByRole('button', { name: /Synchronize with external storage/i })));
await waitFor(() => {
expect(screen.getByRole('heading', { name: /3\. Synchronize with external storage/i })).toBeInTheDocument();
@@ -281,7 +285,9 @@ describe('ProvisioningWizard', () => {
path: '/',
});
await user.click(screen.getByRole('button', { name: /Choose what to synchronize/i }));
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
await act(() => fireEvent.click(screen.getByRole('button', { name: /Choose what to synchronize/i })));
await waitFor(() => {
expect(screen.getByRole('heading', { name: /2\. Choose what to synchronize/i })).toBeInTheDocument();
@@ -339,7 +345,9 @@ describe('ProvisioningWizard', () => {
path: '/',
});
await user.click(screen.getByRole('button', { name: /Choose what to synchronize/i }));
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
await act(() => fireEvent.click(screen.getByRole('button', { name: /Choose what to synchronize/i })));
await waitFor(() => {
expect(screen.getByText('Branch "invalid-branch" not found')).toBeInTheDocument();
@@ -373,7 +381,9 @@ describe('ProvisioningWizard', () => {
path: '/',
});
await user.click(screen.getByRole('button', { name: /Choose what to synchronize/i }));
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
await act(() => fireEvent.click(screen.getByRole('button', { name: /Choose what to synchronize/i })));
await waitFor(() => {
expect(screen.getByRole('alert')).toBeInTheDocument();
@@ -414,7 +424,9 @@ describe('ProvisioningWizard', () => {
path: '/',
});
await user.click(screen.getByRole('button', { name: /Choose what to synchronize/i }));
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
await act(() => fireEvent.click(screen.getByRole('button', { name: /Choose what to synchronize/i })));
await waitFor(() => {
expect(screen.getByRole('alert')).toBeInTheDocument();
@@ -442,13 +454,17 @@ describe('ProvisioningWizard', () => {
path: '/',
});
await user.click(screen.getByRole('button', { name: /Choose what to synchronize/i }));
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
await act(() => fireEvent.click(screen.getByRole('button', { name: /Choose what to synchronize/i })));
await waitFor(() => {
expect(screen.getByRole('heading', { name: /2\. Choose what to synchronize/i })).toBeInTheDocument();
});
await user.click(screen.getByRole('button', { name: /Previous/i }));
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
await act(() => fireEvent.click(screen.getByRole('button', { name: /Previous/i })));
await waitFor(() => {
expect(screen.getByRole('heading', { name: /1\. Connect to external storage/i })).toBeInTheDocument();
@@ -485,7 +501,9 @@ describe('ProvisioningWizard', () => {
path: '/',
});
await user.click(screen.getByRole('button', { name: /Choose what to synchronize/i }));
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
await act(() => fireEvent.click(screen.getByRole('button', { name: /Choose what to synchronize/i })));
expect(screen.getByRole('button', { name: /Submitting.../i })).toBeDisabled();
});
@@ -522,7 +540,9 @@ describe('ProvisioningWizard', () => {
path: '/',
});
await user.click(screen.getByRole('button', { name: /Choose what to synchronize/i }));
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
await act(async () => fireEvent.click(screen.getByRole('button', { name: /Choose what to synchronize/i })));
await waitFor(() => {
expect(screen.getByRole('button', { name: /Synchronize with external storage/i })).toBeInTheDocument();

View File

@@ -1,4 +1,5 @@
import { useCallback, useState } from 'react';
import { flushSync } from 'react-dom';
import { FormProvider, useForm } from 'react-hook-form';
import { AppEvents } from '@grafana/data';
@@ -43,7 +44,10 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions
const workflow = watch('workflow');
const handleSubmitForm = async (data: BulkActionFormData) => {
setHasSubmitted(true);
// TODO why?
flushSync(() => {
setHasSubmitted(true);
});
const resources = collectSelectedItems(selectedItems);

View File

@@ -38,7 +38,7 @@ export function useSearchKeyboardNavigation(
): ItemSelection {
const highlightIndexRef = useRef<ItemSelection>({ x: 0, y: -1 });
const [highlightIndex, setHighlightIndex] = useState<ItemSelection>({ x: 0, y: -1 });
const urlsRef = useRef<Field>();
const urlsRef = useRef<Field>(null);
// Clear selection when the search results change
useEffect(() => {

View File

@@ -77,7 +77,7 @@ export const SuggestionsInput = ({
const theme = useTheme2();
const styles = getStyles(theme, inputHeight);
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>();
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null);
useEffect(() => {
scrollRef.current?.scrollTo(0, scrollTop);

View File

@@ -11,7 +11,7 @@ import checkboxWhitePng from 'img/checkbox_white.png';
import { ALL_VARIABLE_VALUE } from '../../constants';
export interface Props extends React.HTMLProps<HTMLUListElement>, Themeable2 {
export interface Props extends Omit<React.HTMLProps<HTMLUListElement>, 'onToggle'>, Themeable2 {
multi: boolean;
values: VariableOption[];
selectedValues: VariableOption[];

View File

@@ -18,8 +18,8 @@
"lodash": "4.17.21",
"monaco-editor": "0.34.1",
"prismjs": "1.30.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-select": "5.10.2",
"react-use": "17.6.0",
"rxjs": "7.8.2",
@@ -36,8 +36,8 @@
"@types/lodash": "4.17.20",
"@types/node": "24.10.1",
"@types/prismjs": "1.26.5",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"i18next-cli": "^1.24.22",
"jest": "29.7.0",
"react-select-event": "5.5.1",

View File

@@ -18,8 +18,8 @@
"lodash": "4.17.21",
"monaco-editor": "0.34.1",
"prismjs": "1.30.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-select": "5.10.2",
"react-use": "17.6.0",
"rxjs": "7.8.2",
@@ -37,8 +37,8 @@
"@types/lodash": "4.17.20",
"@types/node": "24.10.1",
"@types/prismjs": "1.26.5",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"jest": "29.7.0",
"react-select-event": "5.5.1",
"ts-node": "10.9.2",

View File

@@ -20,8 +20,8 @@ interface CodeEditorProps {
export const LogsQLCodeEditor = (props: CodeEditorProps) => {
const { query, datasource, onChange } = props;
const monacoRef = useRef<Monaco>();
const disposalRef = useRef<monacoType.IDisposable>();
const monacoRef = useRef<Monaco>(null);
const disposalRef = useRef<monacoType.IDisposable>(undefined);
const onFocus = useCallback(async () => {
disposalRef.current = await reRegisterCompletionProvider(

View File

@@ -42,8 +42,8 @@ interface LogsCodeEditorProps {
export const PPLQueryEditor = (props: LogsCodeEditorProps) => {
const { query, datasource, onChange } = props;
const monacoRef = useRef<Monaco>();
const disposalRef = useRef<monacoType.IDisposable>();
const monacoRef = useRef<Monaco>(null);
const disposalRef = useRef<monacoType.IDisposable>(undefined);
const onFocus = useCallback(async () => {
disposalRef.current = await reRegisterCompletionProvider(

View File

@@ -20,8 +20,8 @@ interface SQLCodeEditorProps {
export const SQLQueryEditor = (props: SQLCodeEditorProps) => {
const { query, datasource, onChange } = props;
const monacoRef = useRef<Monaco>();
const disposalRef = useRef<monacoType.IDisposable>();
const monacoRef = useRef<Monaco>(null);
const disposalRef = useRef<monacoType.IDisposable>(undefined);
const onFocus = useCallback(async () => {
disposalRef.current = await reRegisterCompletionProvider(

View File

@@ -1,4 +1,4 @@
import { render, screen, waitFor } from '@testing-library/react';
import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { selectOptionInTest } from 'test/helpers/selectOptionInTest';
@@ -347,7 +347,9 @@ describe('QueryEditor should render right editor', () => {
it('should have an account selector when the feature is enabled', async () => {
config.featureToggles.cloudWatchCrossAccountQuerying = true;
props.datasource.resources.getAccounts = jest.fn().mockResolvedValue(['account123']);
render(<QueryEditor {...props} query={validMetricQueryBuilderQuery} />);
// TODO investigate why we need act
// see https://github.com/testing-library/react-testing-library/issues/1375
await act(async () => render(<QueryEditor {...props} query={validMetricQueryBuilderQuery} />));
await screen.findByText('Metric Insights');
expect(await screen.findByText('Account')).toBeInTheDocument();
});

View File

@@ -46,11 +46,9 @@ describe('ElasticsearchQueryContext', () => {
// the following applies to all hooks in ElasticsearchQueryContext as they all share the same code.
describe('useQuery Hook', () => {
it('Should throw when used outside of ElasticsearchQueryContext', () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
expect(() => {
renderHook(() => useQuery());
}).toThrow();
expect(console.error).toHaveBeenCalled();
});
it('Should return the current query object', () => {

View File

@@ -33,11 +33,9 @@ describe('useStatelessReducer Hook', () => {
describe('useDispatch Hook', () => {
it('Should throw when used outside of DispatchContext', () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
expect(() => {
renderHook(() => useDispatch());
}).toThrow();
expect(console.error).toHaveBeenCalled();
});
it('Should return a dispatch function', () => {

View File

@@ -11,7 +11,7 @@
"@grafana/sql": "12.4.0-pre",
"@grafana/ui": "12.4.0-pre",
"lodash": "4.17.21",
"react": "18.3.1",
"react": "19.2.0",
"rxjs": "7.8.2",
"tslib": "2.8.1"
},
@@ -24,7 +24,7 @@
"@types/jest": "29.5.14",
"@types/lodash": "4.17.20",
"@types/node": "24.10.1",
"@types/react": "18.3.18",
"@types/react": "19.2.7",
"jest": "29.7.0",
"ts-node": "10.9.2",
"typescript": "5.9.2",

View File

@@ -94,7 +94,7 @@ const EDITOR_HEIGHT_OFFSET = 2;
* Hook that returns function that will set up monaco autocomplete for the label selector
*/
function useAutocomplete(getLabelValues: (label: string) => Promise<string[]>, labels?: string[]) {
const providerRef = useRef<CompletionProvider>();
const providerRef = useRef<CompletionProvider>(null);
if (providerRef.current === undefined) {
providerRef.current = new CompletionProvider();
}

View File

@@ -13,8 +13,8 @@
"lodash": "4.17.21",
"monaco-editor": "0.34.1",
"prismjs": "1.30.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-use": "17.6.0",
"rxjs": "7.8.2",
"tslib": "2.8.1"
@@ -29,8 +29,8 @@
"@types/lodash": "4.17.20",
"@types/node": "24.10.1",
"@types/prismjs": "1.26.5",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"css-loader": "7.1.2",
"jest": "29.7.0",
"style-loader": "4.0.0",

View File

@@ -12,8 +12,8 @@
"d3-random": "^3.0.1",
"lodash": "4.17.21",
"micro-memoize": "^4.1.2",
"react": "18.3.1",
"react-dom": "18.3.1",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-select": "5.10.2",
"react-use": "17.6.0",
"rxjs": "7.8.2",
@@ -31,8 +31,8 @@
"@types/jest": "29.5.14",
"@types/lodash": "4.17.20",
"@types/node": "24.10.1",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/uuid": "10.0.0",
"jest": "29.7.0",
"ts-node": "10.9.2",

View File

@@ -13,8 +13,8 @@
"@reduxjs/toolkit": "2.10.1",
"lodash": "4.17.21",
"moment": "2.30.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-use": "17.6.0",
"redux": "5.0.1",
"rxjs": "7.8.2",
@@ -32,8 +32,8 @@
"@types/jest": "29.5.14",
"@types/lodash": "4.17.20",
"@types/node": "24.10.1",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/semver": "7.7.1",
"@types/uuid": "10.0.0",
"jest": "29.7.0",

Some files were not shown because too many files have changed in this diff Show More