Compare commits

..

3 Commits

Author SHA1 Message Date
joshhunt 978c989f3e Dashboards: Lazy-load scripted dashboards dependencies 2026-01-06 11:34:29 +00:00
Peter Nguyen 217427e072 Loki Language Provider: Add missing interpolation to fetchLabelsByLabelsEndpoint (#114608)
* Plugins: Implement bug fix for loki label selectors w/ variable interpolation

* Chore: Add test to ensure result is interpolated

---------

Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
2026-01-06 10:29:51 +00:00
grafana-pr-automation[bot] 585d24dafa I18n: Download translations from Crowdin (#115860)
New Crowdin translations by GitHub Action

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-06 10:01:38 +00:00
153 changed files with 909 additions and 803 deletions
-24
View File
@@ -192,30 +192,6 @@ 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:dev-preview-react19"
docker tag "${LOADED_IMAGE_NAME}" "${DOCKER_IMAGE}"
docker push "${DOCKER_IMAGE}"
run-e2e-tests:
needs:
- build-grafana
@@ -16,8 +16,8 @@
"@types/lodash": "4.17.7",
"@types/node": "24.9.2",
"@types/prismjs": "1.26.4",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@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": "19.2.0",
"react-dom": "19.2.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-router-dom": "^6.22.0",
"rxjs": "7.8.1",
"tslib": "2.6.3"
@@ -16,8 +16,8 @@
"@types/lodash": "4.17.7",
"@types/node": "24.9.2",
"@types/prismjs": "1.26.4",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@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": "19.2.0",
"react-dom": "19.2.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-router-dom": "^6.22.0",
"rxjs": "7.8.1",
"tslib": "2.6.3"
@@ -14,6 +14,9 @@ test.describe(
test('Graph panel is auto-migrated', async ({ gotoDashboardPage, page }) => {
await gotoDashboardPage({ uid: DASHBOARD_ID });
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
await expect(page.getByTestId(UPLOT_MAIN_DIV_SELECTOR).first()).toBeHidden();
await gotoDashboardPage({ uid: DASHBOARD_ID });
await expect(page.getByTestId(UPLOT_MAIN_DIV_SELECTOR).first()).toBeVisible();
});
@@ -21,6 +24,9 @@ test.describe(
test('Annotation markers exist for time regions', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_ID });
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
await expect(page.getByTestId(UPLOT_MAIN_DIV_SELECTOR).first()).toBeHidden();
await gotoDashboardPage({ uid: DASHBOARD_ID });
// Check Business Hours panel
const businessHoursPanel = dashboardPage.getByGrafanaSelector(
+6 -9
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": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@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.18.0",
"react-refresh": "0.14.0",
"react-select-event": "5.5.1",
"redux-mock-store": "1.5.5",
"rimraf": "6.0.1",
@@ -389,9 +389,9 @@
"pluralize": "^8.0.0",
"prismjs": "1.30.0",
"re-resizable": "6.11.2",
"react": "19.2.1",
"react": "18.3.1",
"react-diff-viewer-continued": "^3.4.0",
"react-dom": "19.2.1",
"react-dom": "18.3.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",
@@ -462,10 +462,7 @@
"js-yaml@npm:4.1.0": "^4.1.0",
"js-yaml@npm:=4.1.0": "^4.1.0",
"nodemailer": "7.0.11",
"@storybook/core@npm:8.6.2": "patch:@storybook/core@npm%3A8.6.2#~/.yarn/patches/@storybook-core-npm-8.6.2-8c752112c0.patch",
"pretty-format/react-is": "19.0.0",
"react": "19.2.1",
"react-dom": "19.2.1"
"@storybook/core@npm:8.6.2": "patch:@storybook/core@npm%3A8.6.2#~/.yarn/patches/@storybook-core-npm-8.6.2-8c752112c0.patch"
},
"workspaces": {
"packages": [
+4 -4
View File
@@ -70,13 +70,13 @@
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/lodash": "^4",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@types/tinycolor2": "^1",
"i18next": "^25.5.2",
"i18next-cli": "^1.24.22",
"react": "19.2.0",
"react-dom": "19.2.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-redux": "^9.2.0",
"rimraf": "^6.0.1",
"rollup": "^4.22.4",
+4 -4
View File
@@ -91,12 +91,12 @@
"@types/lodash": "4.17.20",
"@types/node": "24.10.1",
"@types/papaparse": "5.3.16",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@types/tinycolor2": "1.4.6",
"esbuild": "0.25.8",
"react": "19.2.0",
"react-dom": "19.2.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"rimraf": "6.0.1",
"rollup": "^4.22.4",
"rollup-plugin-esbuild": "6.2.1",
+2 -2
View File
@@ -57,7 +57,7 @@
"@leeoniya/ufuzzy": "1.0.19",
"d3": "^7.8.5",
"lodash": "4.17.21",
"react": "19.2.0",
"react": "18.3.1",
"react-use": "17.6.0",
"react-virtualized-auto-sizer": "1.0.26",
"tinycolor2": "1.6.0",
@@ -80,7 +80,7 @@
"@types/jest": "^29.5.4",
"@types/lodash": "4.17.20",
"@types/node": "24.10.1",
"@types/react": "19.2.7",
"@types/react": "18.3.18",
"@types/react-virtualized-auto-sizer": "1.0.8",
"@types/tinycolor2": "1.4.6",
"babel-jest": "29.7.0",
@@ -22,7 +22,7 @@ import { getBarColorByDiff, getBarColorByPackage, getBarColorByValue } from './c
import { CollapseConfig, CollapsedMap, FlameGraphDataContainer, LevelItem } from './dataTransform';
type RenderOptions = {
canvasRef: RefObject<HTMLCanvasElement | null>;
canvasRef: RefObject<HTMLCanvasElement>;
data: FlameGraphDataContainer;
root: LevelItem;
direction: 'children' | 'parents';
@@ -373,7 +373,7 @@ function useColorFunction(
);
}
function useSetupCanvas(canvasRef: RefObject<HTMLCanvasElement | null>, wrapperWidth: number, numberOfLevels: number) {
function useSetupCanvas(canvasRef: RefObject<HTMLCanvasElement>, wrapperWidth: number, numberOfLevels: number) {
const [ctx, setCtx] = useState<CanvasRenderingContext2D>();
useEffect(() => {
+1 -1
View File
@@ -63,7 +63,7 @@
"react-i18next": "^15.0.0"
},
"devDependencies": {
"@types/react": "19.2.7",
"@types/react": "18.3.18",
"rollup": "^4.22.4",
"rollup-plugin-copy": "3.5.0",
"typescript": "5.9.2"
@@ -36,10 +36,10 @@
"@testing-library/user-event": "14.6.1",
"@types/jest": "^29.5.4",
"@types/node": "24.10.1",
"@types/react": "19.2.7",
"@types/react": "18.3.18",
"@types/systemjs": "6.15.3",
"jest": "^29.6.4",
"react": "19.2.0",
"react": "18.3.1",
"ts-jest": "29.4.0",
"ts-node": "10.9.2",
"typescript": "5.9.2"
@@ -84,19 +84,3 @@ 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 });
},
},
};
});
+2 -2
View File
@@ -4,7 +4,7 @@
"private": true,
"version": "12.4.0-pre",
"dependencies": {
"react": "19.2.0",
"react": "18.3.1",
"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": "19.2.7",
"@types/react": "18.3.18",
"@types/webpack-bundle-analyzer": "^4.7.0",
"copy-webpack-plugin": "13.0.0",
"eslint": "9.32.0",
+4 -4
View File
@@ -64,8 +64,8 @@
"@reduxjs/toolkit": "2.10.1",
"@types/debounce-promise": "3.1.9",
"@types/lodash": "4.17.20",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@types/react-highlight-words": "0.20.0",
"@types/react-window": "1.8.8",
"@types/semver": "7.7.1",
@@ -101,8 +101,8 @@
"i18next-cli": "^1.24.22",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"react": "19.2.0",
"react-dom": "19.2.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-select-event": "5.5.1",
"rimraf": "6.0.1",
"rollup": "^4.22.4",
@@ -1,5 +1,4 @@
import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
import { flushSync } from 'react-dom';
import { useDebounce } from 'react-use';
import { TimeRange } from '@grafana/data';
@@ -226,10 +225,7 @@ export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: P
newSelectedMetric === '' ? undefined : selector
);
// TODO why?!
flushSync(() => {
setMetrics(fetchedMetrics);
});
setMetrics(fetchedMetrics);
setSelectedMetric(newSelectedMetric);
setLabelKeys(fetchedLabelKeys);
setIsLoadingLabelKeys(false);
@@ -99,7 +99,7 @@ describe('MetricsLabelsSection', () => {
onBlur: onBlur,
variableEditor: undefined,
}),
undefined
expect.anything()
);
});
@@ -124,7 +124,7 @@ describe('MetricsLabelsSection', () => {
labelsFilters: defaultQuery.labels,
variableEditor: undefined,
}),
undefined
expect.anything()
);
});
+4 -4
View File
@@ -78,12 +78,12 @@
"@types/history": "4.7.11",
"@types/jest": "29.5.14",
"@types/lodash": "4.17.20",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"esbuild": "0.25.8",
"lodash": "4.17.21",
"react": "19.2.0",
"react-dom": "19.2.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"rimraf": "6.0.1",
"rollup": "^4.22.4",
"rollup-plugin-esbuild": "6.2.1",
+4 -4
View File
@@ -25,8 +25,8 @@
"@react-awesome-query-builder/ui": "6.6.15",
"immutable": "5.1.4",
"lodash": "4.17.21",
"react": "19.2.0",
"react-dom": "19.2.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"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": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@types/react-virtualized-auto-sizer": "1.0.8",
"@types/systemjs": "6.15.3",
"@types/uuid": "10.0.0",
+5 -5
View File
@@ -75,7 +75,6 @@
"@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",
@@ -106,6 +105,7 @@
"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": "19.2.7",
"@types/react": "18.3.18",
"@types/react-color": "3.0.13",
"@types/react-dom": "19.2.3",
"@types/react-dom": "18.3.5",
"@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": "19.2.0",
"react-dom": "19.2.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-select-event": "^5.1.0",
"rimraf": "6.0.1",
"rollup": "^4.22.4",
@@ -1,5 +1,5 @@
import { css, cx } from '@emotion/css';
import RCCascader, { FieldNames } from '@rc-component/cascader';
import RCCascader, { FieldNames } from 'rc-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)}
popupClassName={cx(cascaderStyles.dropdown, styles.popup)}
dropdownClassName={cx(cascaderStyles.dropdown, styles.popup)}
{...rest}
expandIcon={null}
>
@@ -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}
popupClassName={styles.dropdown}
dropdownClassName={styles.dropdown}
>
<div className={disableDivFocus}>
<Input
@@ -1,4 +1,4 @@
import { BaseOptionType as RCCascaderOption, CascaderProps } from '@rc-component/cascader';
import { BaseOptionType as RCCascaderOption, CascaderProps } from 'rc-cascader';
import { CascaderOption } from './Cascader';
@@ -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 | null>;
scrollRef: React.RefObject<HTMLDivElement>;
getItemProps: UseComboboxPropGetters<ComboboxOption<T>>['getItemProps'];
enableAllOption?: boolean;
isMultiSelect?: boolean;
@@ -1,5 +1,5 @@
import { autoUpdate, autoPlacement, size, useFloating } from '@floating-ui/react';
import { CSSProperties, type RefObject, useMemo, useRef, useState } from 'react';
import { useMemo, useRef, useState } from 'react';
import { BOUNDARY_ELEMENT_ID } from '../../utils/floating';
import { measureText } from '../../utils/measureText';
@@ -21,21 +21,7 @@ const POPOVER_PADDING = 16;
const SCROLL_CONTAINER_PADDING = 8;
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 => {
export const useComboboxFloat = (items: Array<ComboboxOption<string | number>>, isOpen: boolean) => {
const inputRef = useRef<HTMLInputElement>(null);
const floatingRef = useRef<HTMLDivElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
@@ -10,7 +10,7 @@ import { useStyles2 } from '../../themes/ThemeContext';
import { List } from '../List/List';
interface DataLinkSuggestionsProps {
activeRef?: React.RefObject<HTMLDivElement | null>;
activeRef?: React.RefObject<HTMLDivElement>;
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 | null>;
activeRef?: React.RefObject<HTMLDivElement>;
}
const DataLinkSuggestionsList = React.memo(
@@ -13,7 +13,7 @@ describe('useListFocus', () => {
const testid = 'test';
const getListElement = (
ref: RefObject<HTMLUListElement | null>,
ref: RefObject<HTMLUListElement>,
handleKeys?: (event: KeyboardEvent) => void,
onClick?: () => void
) => (
@@ -7,7 +7,7 @@ const CAUGHT_KEYS = ['ArrowUp', 'ArrowDown', 'Home', 'End', 'Enter', 'Tab'];
/** @internal */
export interface UseListFocusProps {
localRef: RefObject<HTMLUListElement | null>;
localRef: RefObject<HTMLUListElement>;
options: TimeOption[];
}
@@ -1,6 +1,6 @@
import { RefObject, useRef } from 'react';
export function useFocus(): [RefObject<HTMLInputElement | null>, () => void] {
export function useFocus(): [RefObject<HTMLInputElement>, () => void] {
const ref = useRef<HTMLInputElement>(null);
const setFocus = () => {
ref.current && ref.current.focus();
@@ -13,7 +13,7 @@ describe('useMenuFocus', () => {
const testid = 'test';
const getMenuElement = (
ref: RefObject<HTMLDivElement | null>,
ref: RefObject<HTMLDivElement>,
handleKeys?: (event: KeyboardEvent) => void,
handleFocus?: () => void,
onClick?: () => void
@@ -6,7 +6,7 @@ const UNFOCUSED = -1;
/** @internal */
export interface UseMenuFocusProps {
localRef: RefObject<HTMLDivElement | null>;
localRef: RefObject<HTMLDivElement>;
isMenuOpen?: boolean;
close?: () => void;
onOpen?: (focusOnItem: (itemId: number) => void) => void;
@@ -1,10 +1,11 @@
import { createElement, type HTMLAttributes, type PropsWithChildren, type HTMLElementType, type JSX } from 'react';
import { HTMLAttributes, PropsWithChildren, type JSX } from 'react';
import * as React from 'react';
import { textUtil } from '@grafana/data';
export interface RenderUserContentAsHTMLProps<T = HTMLSpanElement>
extends Omit<HTMLAttributes<T>, 'dangerouslySetInnerHTML'> {
component?: HTMLElementType;
component?: keyof React.ReactHTML;
content: string;
}
@@ -18,7 +19,7 @@ export function RenderUserContentAsHTML<T>({
content,
...rest
}: PropsWithChildren<RenderUserContentAsHTMLProps<T>>): JSX.Element {
return createElement(component || 'span', {
return React.createElement(component || 'span', {
dangerouslySetInnerHTML: { __html: textUtil.sanitize(content) },
...rest,
});
@@ -20,7 +20,7 @@ export interface TableCellTooltipProps {
field: Field;
getActions: (field: Field, rowIdx: number) => ActionModel[];
getTextColorForBackground: (bgColor: string) => string;
gridRef: RefObject<DataGridHandle | null>;
gridRef: RefObject<DataGridHandle>;
height: number;
placement?: TableCellTooltipPlacement;
renderer: TableCellRenderer;
@@ -463,7 +463,7 @@ export function useColumnResize(
return dataGridResizeHandler;
}
export function useScrollbarWidth(ref: RefObject<DataGridHandle | null>, height: number) {
export function useScrollbarWidth(ref: RefObject<DataGridHandle>, height: number) {
const [scrollbarWidth, setScrollbarWidth] = useState(0);
useLayoutEffect(() => {
@@ -49,7 +49,7 @@ interface RowsListProps {
listHeight: number;
width: number;
cellHeight?: TableCellHeight;
listRef: React.RefObject<VariableSizeList | null>;
listRef: React.RefObject<VariableSizeList>;
tableState: TableState;
tableStyles: TableStyles;
nestedDataField?: Field;
@@ -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>(null);
const toggleAllRowsExpandedRef = useRef<(value?: boolean) => void>();
// Internal react table state reducer
const stateReducer = useTableStateReducer({
@@ -14,8 +14,8 @@ import { GrafanaTableState } from './types';
Select the scrollbar element from the VariableSizeList scope
*/
export function useFixScrollbarContainer(
variableSizeListScrollbarRef: React.RefObject<HTMLDivElement | null>,
tableDivRef: React.RefObject<HTMLDivElement | null>
variableSizeListScrollbarRef: React.RefObject<HTMLDivElement>,
tableDivRef: React.RefObject<HTMLDivElement>
) {
useEffect(() => {
if (variableSizeListScrollbarRef.current && tableDivRef.current) {
@@ -43,7 +43,7 @@ export function useFixScrollbarContainer(
*/
export function useResetVariableListSizeCache(
extendedState: GrafanaTableState,
listRef: React.RefObject<VariableSizeList | null>,
listRef: React.RefObject<VariableSizeList>,
data: DataFrame,
hasUniqueId: boolean
) {
@@ -19,7 +19,7 @@ interface EventsCanvasProps {
}
export function EventsCanvas({ id, events, renderEventMarker, mapEventToXYCoords, config }: EventsCanvasProps) {
const plotInstance = useRef<uPlot>(null);
const plotInstance = useRef<uPlot>();
// 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);
@@ -140,7 +140,7 @@ export const TooltipPlugin2 = ({
const [{ plot, isHovering, isPinned, contents, style, dismiss }, setState] = useReducer(mergeState, null, initState);
const sizeRef = useRef<TooltipContainerSize>(null);
const sizeRef = useRef<TooltipContainerSize>();
const styles = useStyles2(getStyles, maxWidth);
const renderRef = useRef(render);
@@ -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 | null>;
private plotInstance: React.RefObject<uPlot>;
private subscription = new Subscription();
@@ -53,7 +53,7 @@ export const TooltipPlugin = ({
renderTooltip,
...otherProps
}: TooltipPluginProps) => {
const plotInstance = useRef<uPlot>(null);
const plotInstance = useRef<uPlot>();
const theme = useTheme2();
const [focusedSeriesIdx, setFocusedSeriesIdx] = useState<number | null>(null);
const [focusedPointIdx, setFocusedPointIdx] = useState<number | null>(null);
@@ -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>(undefined);
const onStartTime = useRef<Date | undefined>();
useEffect(() => {
let timeout: ReturnType<typeof setTimeout> | undefined;
@@ -1,6 +1,5 @@
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';
@@ -35,10 +34,7 @@ export const ForgottenPassword = () => {
const sendEmail = async (formModel: EmailDTO) => {
const res = await getBackendSrv().post('/api/user/password/send-reset-email', formModel);
if (res) {
// TODO why?
flushSync(() => {
setEmailSent(true);
});
setEmailSent(true);
}
};
@@ -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 | null>;
private plotInstance: React.RefObject<uPlot>;
constructor(props: GraphNGProps) {
super(props);
@@ -1,4 +1,3 @@
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';
@@ -30,9 +29,7 @@ const selectComboboxOptionInTest = async (input: HTMLElement, optionOrOptions: s
};
const setup = async () => {
// 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 view = render(<SharedPreferences resourceUri="user" preferenceType="user" />);
const themeSelect = await screen.findByRole('combobox', { name: 'Interface theme' });
await waitFor(() => expect(themeSelect).not.toBeDisabled());
return view;
@@ -1,5 +1,4 @@
import { useState } from 'react';
import { flushSync } from 'react-dom';
import { useForm } from 'react-hook-form';
import { Trans, t } from '@grafana/i18n';
@@ -26,10 +25,7 @@ export const VerifyEmail = () => {
getBackendSrv()
.post('/api/user/signup', formModel)
.then(() => {
// TODO why?
flushSync(() => {
setEmailSent(true);
});
setEmailSent(true);
})
.catch((err) => {
const msg = err.data?.message || err;
@@ -1,4 +1,3 @@
import { act, fireEvent } from '@testing-library/react';
import { InitialEntry } from 'history';
import { last } from 'lodash';
import { Route, Routes } from 'react-router-dom-v5-compat';
@@ -145,11 +144,8 @@ const fillOutForm = async ({
};
const saveMuteTiming = async () => {
// 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));
const user = userEvent.setup();
await user.click(await screen.findByText(/save time interval/i));
};
setupMswServer();
@@ -2,7 +2,7 @@ import { HTMLAttributes } from 'react';
import { Button, IconSize } from '@grafana/ui';
interface Props extends Omit<HTMLAttributes<HTMLButtonElement>, 'onToggle'> {
interface Props extends HTMLAttributes<HTMLButtonElement> {
isCollapsed: boolean;
onToggle: (isCollapsed: boolean) => void;
// Todo: this should be made compulsory for a11y purposes
@@ -1,8 +1,7 @@
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, waitFor, waitForElementToBeRemoved } from 'test/test-utils';
import { render, userEvent, waitFor, waitForElementToBeRemoved } from 'test/test-utils';
import { byRole, byTestId, byText } from 'testing-library-selector';
import { mockExportApi, setupMswServer } from '../../mockApi';
@@ -73,15 +72,14 @@ 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');
// 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()));
await user.click(ui.exportButton.get());
const drawer = await ui.exportDrawer.dialog.find();
expect(drawer).toBeInTheDocument();
@@ -1,4 +1,3 @@
import { act, fireEvent } from '@testing-library/react';
import { render, testWithFeatureToggles, waitFor } from 'test/test-utils';
import { byLabelText, byRole } from 'testing-library-selector';
@@ -157,10 +156,7 @@ groups:
await user.click(await ui.dsImport.mimirDsOption.find());
// Click the import button
// 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()));
await user.click(ui.importButton.get());
// Verify confirmation dialog appears
expect(await ui.confirmationModal.find()).toBeInTheDocument();
@@ -1,4 +1,3 @@
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';
@@ -66,10 +65,7 @@ describe('new receiver', () => {
await user.type(email, 'tester@grafana.com');
// try to test the contact point
// 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()));
await user.click(await ui.testContactPointButton.find());
expect(await ui.testContactPointModal.find()).toBeInTheDocument();
@@ -1,4 +1,3 @@
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';
@@ -302,20 +301,15 @@ describe('AnnotationsField', function () {
})
);
// 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' },
],
}}
/>
)
render(
<FormWrapper
formValues={{
annotations: [
{ key: Annotation.dashboardUID, value: 'dash-test-uid' },
{ key: Annotation.panelID, value: '1' },
],
}}
/>
);
expect(await ui.dashboardAnnotation.find()).toBeInTheDocument();
@@ -479,7 +479,7 @@ describe('RuleViewer', () => {
expect.objectContaining({
ruleUid: 'test-rule-uid',
}),
undefined
expect.any(Object)
);
expect(screen.getByTestId('enrichment-section')).toBeInTheDocument();
});
@@ -500,7 +500,7 @@ describe('RuleViewer', () => {
expect.objectContaining({
ruleUid: 'test-rule-uid',
}),
undefined
expect.any(Object)
);
expect(screen.getByTestId('enrichment-section')).toBeInTheDocument();
});
@@ -1,4 +1,3 @@
import { act, fireEvent } from '@testing-library/react';
import { produce } from 'immer';
import { render } from 'test/test-utils';
import { byRole, byText } from 'testing-library-selector';
@@ -59,7 +58,7 @@ describe('Moving a Grafana managed rule', () => {
const ruleID = fromRulerRuleAndRuleGroupIdentifier(currentRuleGroupID, ruleToMove);
render(
const { user } = render(
<MoveRuleTestComponent
currentRuleGroupIdentifier={currentRuleGroupID}
targetRuleGroupIdentifier={targetRuleGroupID}
@@ -67,10 +66,7 @@ describe('Moving a Grafana managed rule', () => {
rule={ruleToMove}
/>
);
// 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()));
await user.click(byRole('button').get());
expect(await byText(/success/i).find()).toBeInTheDocument();
@@ -91,7 +87,7 @@ describe('Moving a Grafana managed rule', () => {
uid: 'does-not-exist',
};
render(
const { user } = render(
<MoveRuleTestComponent
currentRuleGroupIdentifier={currentRuleGroupID}
targetRuleGroupIdentifier={currentRuleGroupID}
@@ -99,10 +95,7 @@ describe('Moving a Grafana managed rule', () => {
rule={grafanaRulerRule}
/>
);
// 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()));
await user.click(byRole('button').get());
expect(await byText(/error/i).find()).toBeInTheDocument();
});
@@ -137,7 +130,7 @@ describe('Moving a Data source managed rule', () => {
draft.grafana_alert.title = 'updated rule title';
});
render(
const { user } = render(
<MoveRuleTestComponent
currentRuleGroupIdentifier={currentRuleGroupID}
targetRuleGroupIdentifier={targetRuleGroupID}
@@ -145,10 +138,7 @@ describe('Moving a Data source managed rule', () => {
rule={newRule}
/>
);
// 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()));
await user.click(byRole('button').get());
expect(await byText(/success/i).find()).toBeInTheDocument();
@@ -177,7 +167,7 @@ describe('Moving a Data source managed rule', () => {
const ruleID = fromRulerRuleAndRuleGroupIdentifier(currentRuleGroupID, ruleToMove);
render(
const { user } = render(
<MoveRuleTestComponent
currentRuleGroupIdentifier={currentRuleGroupID}
targetRuleGroupIdentifier={targetRuleGroupID}
@@ -185,10 +175,7 @@ describe('Moving a Data source managed rule', () => {
rule={ruleToMove}
/>
);
// 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()));
await user.click(byRole('button').get());
expect(await byText(/success/i).find()).toBeInTheDocument();
@@ -219,7 +206,7 @@ describe('Moving a Data source managed rule', () => {
const ruleID = fromRulerRuleAndRuleGroupIdentifier(currentRuleGroupID, ruleToMove);
render(
const { user } = render(
<MoveRuleTestComponent
currentRuleGroupIdentifier={currentRuleGroupID}
targetRuleGroupIdentifier={targetRuleGroupID}
@@ -227,10 +214,7 @@ describe('Moving a Data source managed rule', () => {
rule={ruleToMove}
/>
);
// 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()));
await user.click(byRole('button').get());
expect(await byText(/success/i).find()).toBeInTheDocument();
@@ -255,7 +239,7 @@ describe('Moving a Data source managed rule', () => {
draft.grafana_alert.title = 'updated rule title';
});
render(
const { user } = render(
<MoveRuleTestComponent
currentRuleGroupIdentifier={curentRuleGroupID}
targetRuleGroupIdentifier={curentRuleGroupID}
@@ -263,10 +247,7 @@ describe('Moving a Data source managed rule', () => {
rule={newRule}
/>
);
// 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()));
await user.click(byRole('button').get());
expect(await byText(/error/i).find()).toBeInTheDocument();
});
@@ -31,6 +31,7 @@ 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();
@@ -67,6 +68,7 @@ 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();
});
@@ -84,8 +84,8 @@ export function useAsync<Result, Args extends unknown[] = unknown[]>(
error: undefined,
result: initialValue,
});
const promiseRef = useRef<Promise<Result>>(undefined);
const argsRef = useRef<Args>(undefined);
const promiseRef = useRef<Promise<Result>>();
const argsRef = useRef<Args>();
const methods = useSyncedRef({
execute(...params: Args) {
@@ -155,8 +155,8 @@ export function useRuleGroupConsistencyCheck() {
const { isGroupInSync } = useRuleGroupIsInSync();
const [groupConsistent, setGroupConsistent] = useState<boolean | undefined>();
const apiCheckInterval = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const timeoutInterval = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const apiCheckInterval = useRef<ReturnType<typeof setTimeout> | undefined>();
const timeoutInterval = useRef<ReturnType<typeof setTimeout> | undefined>();
useEffect(() => {
return () => {
@@ -245,8 +245,8 @@ export function useRuleGroupConsistencyCheck() {
export function usePrometheusConsistencyCheck() {
const { matchingPromRuleExists } = useMatchingPromRuleExists();
const removalConsistencyInterval = useRef<number | undefined>(undefined);
const creationConsistencyInterval = useRef<number | undefined>(undefined);
const removalConsistencyInterval = useRef<number | undefined>();
const creationConsistencyInterval = useRef<number | undefined>();
useEffect(() => {
return () => {
@@ -1,4 +1,3 @@
import { act, fireEvent } from '@testing-library/react';
import { render, screen, waitFor, within } from 'test/test-utils';
import { byRole } from 'testing-library-selector';
@@ -59,10 +58,7 @@ describe('RuleList - GroupedView', () => {
});
it('should paginate through groups', async () => {
// 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 { user } = render(<GroupedView />);
const mimirSection = await ui.dsSection(/Mimir/).find();
@@ -77,10 +73,7 @@ describe('RuleList - GroupedView', () => {
expect(firstPageGroups[39]).toHaveTextContent('test-group-40');
const loadMoreButton = await within(mimirSection).findByRole('button', { name: /Show more/i });
// 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 user.click(loadMoreButton);
await waitFor(() => expect(loadMoreButton).toBeEnabled());
@@ -93,10 +86,7 @@ describe('RuleList - GroupedView', () => {
});
it('should disable next button when there is no more data', async () => {
// 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 { user } = render(<GroupedView />);
const prometheusSection = await ui.dsSection(/Prometheus/).find();
const promNamespace = await ui.namespace(/test-prometheus-namespace/).find(prometheusSection);
@@ -106,27 +96,17 @@ describe('RuleList - GroupedView', () => {
await ui.group('test-group-40').find(promNamespace);
// fetch page 2
// 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));
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
// 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-120
await ui.group('test-group-120').find(promNamespace);
// fetch page 4
// 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-130
await ui.group('test-group-130').find(promNamespace);
@@ -7,9 +7,6 @@ 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,
@@ -387,7 +387,7 @@ interface ListModeProps {
hasSearches: boolean;
canSave: boolean;
activeAction: ActiveAction;
saveButtonRef: React.RefObject<HTMLButtonElement | null>;
saveButtonRef: React.RefObject<HTMLButtonElement>;
isLoading: boolean;
onStartSave: () => void;
/** Callback to complete save. Throws ValidationError on validation failure. */
@@ -67,7 +67,7 @@ describe('StandardAnnotationQueryEditor', () => {
expect.objectContaining({
query: expect.objectContaining({ queryType: 'defaultAnnotationsQuery', refId: 'initialAnnotationRef' }),
}),
undefined
expect.anything()
);
});
@@ -85,7 +85,7 @@ describe('StandardAnnotationQueryEditor', () => {
expect.objectContaining({
query: expect.objectContaining({ refId: 'initialAnnotationRef' }),
}),
undefined
expect.anything()
);
});
@@ -204,7 +204,7 @@ describe('StandardAnnotationQueryEditor', () => {
refId: 'A',
}),
}),
undefined
expect.anything()
);
});
@@ -242,7 +242,7 @@ describe('StandardAnnotationQueryEditor', () => {
legendFormat: '{{method}} {{endpoint}}',
}),
}),
undefined
expect.anything()
);
});
@@ -284,7 +284,7 @@ describe('StandardAnnotationQueryEditor', () => {
refId: 'AnnoTarget',
}),
}),
undefined
expect.anything()
);
});
@@ -320,7 +320,7 @@ describe('StandardAnnotationQueryEditor', () => {
expr: 'up',
}),
}),
undefined
expect.anything()
);
});
@@ -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>(undefined);
const searchQueryRef = useRef<string>();
useEffect(() => {
if ((!parentId || parentId === 'scopes') && searchQuery && config.featureToggles.scopeSearchAllLevels) {
@@ -1,4 +1,4 @@
import { render, waitFor, screen, within, Matcher, getByRole, act, fireEvent } from '@testing-library/react';
import { render, waitFor, screen, within, Matcher, getByRole } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { merge, uniqueId } from 'lodash';
import { openMenu } from 'react-select-event';
@@ -300,9 +300,7 @@ describe('CorrelationsPage', () => {
await userEvent.click(screen.getByText('Regular expression'));
await userEvent.type(screen.getByLabelText(/expression/i), 'test expression');
// 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 userEvent.click(await screen.findByRole('button', { name: /add$/i }));
await waitFor(() => {
expect(mocks.reportInteraction).toHaveBeenCalledWith('grafana_correlations_added');
@@ -453,9 +451,7 @@ describe('CorrelationsPage', () => {
await userEvent.clear(screen.getByRole('textbox', { name: /results field/i }));
await userEvent.type(screen.getByRole('textbox', { name: /results field/i }), 'Line');
// 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 userEvent.click(screen.getByRole('button', { name: /add$/i }));
await waitFor(() => {
expect(mocks.reportInteraction).toHaveBeenCalledWith('grafana_correlations_added');
@@ -522,9 +518,7 @@ describe('CorrelationsPage', () => {
await userEvent.click(screen.getByRole('button', { name: /next$/i }));
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: /save$/i })));
await userEvent.click(screen.getByRole('button', { name: /save$/i }));
expect(await screen.findByRole('cell', { name: /edited label$/i }, { timeout: 5000 })).toBeInTheDocument();
@@ -542,9 +536,7 @@ describe('CorrelationsPage', () => {
const rowExpanderButton = within(tableRows[0]).getByRole('button', { name: /toggle row expanded/i });
await userEvent.click(rowExpanderButton);
// 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 }));
await userEvent.click(screen.getByRole('button', { name: /next$/i }));
// select Logfmt, be sure expression field is disabled
@@ -583,9 +575,7 @@ 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');
// 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 userEvent.click(screen.getByRole('button', { name: /save$/i }));
await waitFor(() => {
expect(mocks.reportInteraction).toHaveBeenCalledWith('grafana_correlations_edited');
});
@@ -736,9 +726,7 @@ describe('CorrelationsPage', () => {
expect(descriptionInput).toBeInTheDocument();
expect(descriptionInput).toHaveAttribute('readonly');
// 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 }));
await userEvent.click(screen.getByRole('button', { name: /next$/i }));
// expect the transformation to exist but be read only
@@ -58,7 +58,7 @@ jest.mock('app/features/plugins/extensions/getPluginExtensions', () => ({
createPluginExtensionsGetter: () => getPluginExtensionsMock,
}));
async function setup({ routeProps }: { routeProps?: Partial<GrafanaRouteComponentProps> } = {}) {
function setup({ routeProps }: { routeProps?: Partial<GrafanaRouteComponentProps> } = {}) {
const context = getGrafanaContextMock();
const defaultRouteProps = getRouteComponentProps();
const props: Props = {
@@ -66,29 +66,21 @@ async function setup({ routeProps }: { routeProps?: Partial<GrafanaRouteComponen
...routeProps,
};
// 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 {...props} />
</LocationServiceProvider>
</TestProvider>
)
const renderResult = render(
<TestProvider grafanaContext={context}>
<LocationServiceProvider service={locationService}>
<DashboardScenePage {...props} />
</LocationServiceProvider>
</TestProvider>
);
const rerender = async (newProps: Props) => {
await act(async () =>
renderResult.rerender(
<TestProvider grafanaContext={context}>
<LocationServiceProvider service={locationService}>
<DashboardScenePage {...newProps} />
</LocationServiceProvider>
</TestProvider>
)
const rerender = (newProps: Props) => {
renderResult.rerender(
<TestProvider grafanaContext={context}>
<LocationServiceProvider service={locationService}>
<DashboardScenePage {...newProps} />
</LocationServiceProvider>
</TestProvider>
);
};
@@ -160,8 +152,6 @@ 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
@@ -173,7 +163,7 @@ describe('DashboardScenePage', () => {
});
it('Can render dashboard', async () => {
await setup();
setup();
await waitForDashboardToRender();
@@ -185,7 +175,7 @@ describe('DashboardScenePage', () => {
});
it('routeReloadCounter should trigger reload', async () => {
const { rerender, props } = await setup();
const { rerender, props } = setup();
await waitForDashboardToRender();
@@ -200,13 +190,13 @@ describe('DashboardScenePage', () => {
props.location.state = { routeReloadCounter: 1 };
await rerender(props);
rerender(props);
expect(await screen.findByTitle('Updated title')).toBeInTheDocument();
});
it('Can inspect panel', async () => {
await setup();
setup();
await waitForDashboardToRender();
@@ -228,7 +218,7 @@ describe('DashboardScenePage', () => {
});
it('Can view panel in fullscreen', async () => {
await setup();
setup();
await waitForDashboardToRender();
@@ -248,7 +238,7 @@ describe('DashboardScenePage', () => {
interval: {} as SystemDateFormatsState['interval'],
useBrowserLocale: true,
});
await setup();
setup();
await waitForDashboardToRenderWithTimeRange({
from: '03/11/2025, 02:09:37 AM',
@@ -267,7 +257,7 @@ describe('DashboardScenePage', () => {
interval: {} as SystemDateFormatsState['interval'],
useBrowserLocale: true,
});
await setup();
setup();
await waitForDashboardToRenderWithTimeRange({
from: '11.03.2025, 02:09:37',
@@ -279,17 +269,17 @@ describe('DashboardScenePage', () => {
describe('empty state', () => {
it('Shows empty state when dashboard is empty', async () => {
loadDashboardMock.mockResolvedValue({ dashboard: { uid: 'my-dash-uid', panels: [] }, meta: {} });
await setup();
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 () => {
await setup();
setup();
await waitForDashboardToRender();
expect(screen.queryByText('Start your new dashboard by adding a visualization')).not.toBeInTheDocument();
expect(await 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();
@@ -299,7 +289,7 @@ describe('DashboardScenePage', () => {
act(() => {
dashboard.removePanel(panels[0]);
});
expect(screen.queryByText('Start your new dashboard by adding a visualization')).not.toBeInTheDocument();
expect(await screen.queryByText('Start your new dashboard by adding a visualization')).not.toBeInTheDocument();
act(() => {
dashboard.removePanel(panels[1]);
@@ -311,14 +301,14 @@ describe('DashboardScenePage', () => {
});
expect(await screen.findByTitle('Panel Added')).toBeInTheDocument();
expect(screen.queryByText('Start your new dashboard by adding a visualization')).not.toBeInTheDocument();
expect(await 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({});
await setup({
setup({
routeProps: {
route: {
...getRouteComponentProps().route,
@@ -341,7 +331,7 @@ describe('DashboardScenePage', () => {
loadDashboardMock.mockClear();
loadDashboardMock.mockResolvedValue({ dashboard: { uid: 'my-dash-uid', panels: [] }, meta: {} });
await setup();
setup();
await waitFor(() => expect(screen.queryByText('Refresh')).toBeInTheDocument());
await waitFor(() => expect(screen.queryByText('Last 6 hours')).toBeInTheDocument());
@@ -373,7 +363,7 @@ describe('DashboardScenePage', () => {
isHandled: true,
});
await setup();
setup();
expect(await screen.findByTestId(selectors.components.EntityNotFound.container)).toBeInTheDocument();
});
@@ -397,7 +387,7 @@ describe('DashboardScenePage', () => {
isHandled: true,
});
await setup();
setup();
expect(await screen.findByTestId('dashboard-page-error')).toBeInTheDocument();
expect(await screen.findByTestId('dashboard-page-error')).toHaveTextContent('Internal server error');
@@ -406,7 +396,7 @@ describe('DashboardScenePage', () => {
it('should render error alert for runtime errors', async () => {
setupLoadDashboardRuntimeErrorMock();
await setup();
setup();
expect(await screen.findByTestId('dashboard-page-error')).toBeInTheDocument();
expect(await screen.findByTestId('dashboard-page-error')).toHaveTextContent('Runtime error');
@@ -421,7 +411,7 @@ describe('DashboardScenePage', () => {
const manager = getDashboardScenePageStateManager();
manager.setActiveManager('v2');
const { unmount } = await setup();
const { unmount } = setup();
expect(manager['activeManager']).toBeInstanceOf(DashboardScenePageStateManagerV2);
unmount();
@@ -1,4 +1,4 @@
import { act, screen } from '@testing-library/react';
import { screen, waitForElementToBeRemoved } 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', () => ({
},
}));
async function setup(token = 'an-access-token') {
function setup(token = 'an-access-token') {
const pubdashProps: PublicDashboardSceneProps = {
...getRouteComponentProps({
route: {
@@ -37,19 +37,11 @@ async function setup(token = 'an-access-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}`] } }
)
return render(
<Routes>
<Route path="/public-dashboards/:accessToken" element={<PublicDashboardScenePage {...pubdashProps} />} />
</Routes>,
{ historyOptions: { initialEntries: [`/public-dashboards/${token}`] } }
);
}
@@ -123,6 +115,7 @@ const publicDashboardSceneSelector = e2eSelectors.pages.PublicDashboardScene;
describe('PublicDashboardScenePage', () => {
beforeEach(() => {
config.publicDashboardAccessToken = 'an-access-token';
getDashboardScenePageStateManager().clearDashboardCache();
setupLoadDashboardMock({ dashboard: simpleDashboard, meta: {} });
@@ -132,9 +125,7 @@ describe('PublicDashboardScenePage', () => {
});
it('can render public dashboard', async () => {
const accessToken = 'an-access-token';
config.publicDashboardAccessToken = accessToken;
await setup(accessToken);
setup();
await waitForDashboardGridToRender();
@@ -148,9 +139,7 @@ describe('PublicDashboardScenePage', () => {
});
it('cannot see menu panel', async () => {
const accessToken = 'cannot-see-menu-panel';
config.publicDashboardAccessToken = accessToken;
await setup(accessToken);
setup();
await waitForDashboardGridToRender();
@@ -159,9 +148,7 @@ describe('PublicDashboardScenePage', () => {
});
it('shows time controls when it is not hidden', async () => {
const accessToken = 'shows-time-controls';
config.publicDashboardAccessToken = accessToken;
await setup(accessToken);
setup();
await waitForDashboardGridToRender();
@@ -171,9 +158,7 @@ describe('PublicDashboardScenePage', () => {
});
it('does not render paused or deleted screen', async () => {
const accessToken = 'does-not-render-paused-or-deleted-screen';
config.publicDashboardAccessToken = accessToken;
await setup(accessToken);
setup();
await waitForDashboardGridToRender();
@@ -187,7 +172,7 @@ describe('PublicDashboardScenePage', () => {
dashboard: { ...simpleDashboard, timepicker: { hidden: true } },
meta: {},
});
await setup(accessToken);
setup(accessToken);
await waitForDashboardGridToRender();
@@ -222,7 +207,9 @@ describe('given unavailable public dashboard', () => {
},
});
await setup(accessToken);
setup(accessToken);
await waitForElementToBeRemoved(screen.getByTestId(publicDashboardSceneSelector.loadingPage));
expect(screen.queryByTestId(publicDashboardSceneSelector.page)).not.toBeInTheDocument();
expect(screen.getByTestId(publicDashboardSelector.NotAvailable.title)).toBeInTheDocument();
@@ -252,7 +239,9 @@ describe('given unavailable public dashboard', () => {
},
});
await setup(accessToken);
setup(accessToken);
await waitForElementToBeRemoved(screen.getByTestId(publicDashboardSceneSelector.loadingPage));
expect(screen.queryByTestId(publicDashboardSelector.page)).not.toBeInTheDocument();
expect(screen.queryByTestId(publicDashboardSelector.NotAvailable.pausedDescription)).not.toBeInTheDocument();
@@ -49,7 +49,7 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) {
const [contentSent, setContentSent] = useState<{ title?: string; folderUid?: string }>({});
const validationTimeoutRef = useRef<NodeJS.Timeout>(null);
const validationTimeoutRef = useRef<NodeJS.Timeout>();
// Validate title on form mount to catch invalid default values
useEffect(() => {
@@ -59,18 +59,14 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) {
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (validationTimeoutRef.current) {
clearTimeout(validationTimeoutRef.current);
}
clearTimeout(validationTimeoutRef.current);
};
}, []);
const handleTitleChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setValue('title', e.target.value, { shouldDirty: true });
if (validationTimeoutRef.current) {
clearTimeout(validationTimeoutRef.current);
}
clearTimeout(validationTimeoutRef.current);
validationTimeoutRef.current = setTimeout(() => {
trigger('title');
}, 400);
@@ -79,9 +75,7 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) {
);
const onSave = async (overwrite: boolean) => {
if (validationTimeoutRef.current) {
clearTimeout(validationTimeoutRef.current);
}
clearTimeout(validationTimeoutRef.current);
const isTitleValid = await trigger('title');
@@ -15,7 +15,7 @@ import { DashboardGridItem, RepeatDirection } from './DashboardGridItem';
interface PanelWrapperProps {
panel: VizPanel;
isLazy: boolean;
containerRef?: RefObject<HTMLDivElement | null>;
containerRef?: RefObject<HTMLDivElement>;
}
function PanelWrapper({ panel, isLazy, containerRef }: PanelWrapperProps) {
@@ -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 | null>) {
export function scrollCanvasElementIntoView(sceneObject: SceneObject, ref: React.RefObject<HTMLElement>) {
if (ref.current?.isConnected) {
scrollIntoView(ref.current);
return;
@@ -33,7 +33,7 @@ global.ResizeObserver = jest.fn().mockImplementation((callback) => {
});
// Helper function to assign a mock div to a ref
function assignMockDivToRef(ref: React.RefObject<HTMLDivElement | null>, mockDiv: HTMLDivElement) {
function assignMockDivToRef(ref: React.RefObject<HTMLDivElement>, mockDiv: HTMLDivElement) {
// Use type assertion to bypass readonly restriction in tests
(ref as { current: HTMLDivElement | null }).current = mockDiv;
}
@@ -8,7 +8,7 @@ import grafanaTextLogoDarkSvg from 'img/grafana_text_logo_dark.svg';
import grafanaTextLogoLightSvg from 'img/grafana_text_logo_light.svg';
interface SoloPanelPageLogoProps {
containerRef: React.RefObject<HTMLDivElement | null>;
containerRef: React.RefObject<HTMLDivElement>;
isHovered: boolean;
hideLogo?: UrlQueryValue;
}
@@ -1,4 +1,4 @@
import { act, fireEvent, screen, waitFor, within } from '@testing-library/react';
import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from 'test/test-utils';
@@ -154,9 +154,7 @@ 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 });
// TODO investigate why we need act/fireEvent
// see https://github.com/testing-library/react-testing-library/issues/1375
act(() => fireEvent.click(showMoreButton));
await user.click(showMoreButton);
expect(historySrv.getHistoryList).toBeCalledTimes(2);
expect(screen.getByText(/Fetching more entries/i)).toBeInTheDocument();
@@ -21,7 +21,7 @@ export const usePanelLatestData = (
options: GetDataOptions,
checkSchema?: boolean
): UsePanelLatestData => {
const querySubscription = useRef<Unsubscribable>(null);
const querySubscription = useRef<Unsubscribable>();
const [latestData, setLatestData] = useState<PanelData>();
useEffect(() => {
@@ -61,7 +61,7 @@ interface State {
class UnThemedTransformationsEditor extends React.PureComponent<TransformationsEditorProps, State> {
subscription?: Unsubscribable;
ref: RefObject<HTMLDivElement | null>;
ref: RefObject<HTMLDivElement>;
constructor(props: TransformationsEditorProps) {
super(props);
@@ -1,4 +1,4 @@
import { act, screen, waitFor } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import { Routes, Route } from 'react-router-dom-v5-compat';
import { render } from 'test/test-utils';
@@ -62,9 +62,7 @@ describe('PublicDashboardPageProxy', () => {
describe('when scene feature enabled', () => {
it('should render PublicDashboardScenePage if publicDashboardsScene is enabled', async () => {
config.featureToggles.publicDashboardsScene = true;
// TODO investigate why we need act
// see https://github.com/testing-library/react-testing-library/issues/1375
await act(() => setup({}));
setup({});
await waitFor(() => {
expect(screen.queryByTestId(PublicDashboardScene.page)).toBeInTheDocument();
@@ -1,13 +1,10 @@
import $ from 'jquery';
import _, { isFunction } from 'lodash'; // eslint-disable-line lodash/import-scope
import moment from 'moment'; // eslint-disable-line no-restricted-imports
import { isFunction } from 'lodash'; // eslint-disable-line lodash/import-scope
import { AppEvents, dateMath, UrlQueryMap, UrlQueryValue } from '@grafana/data';
import { getBackendSrv, isFetchError, locationService } from '@grafana/runtime';
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { backendSrv } from 'app/core/services/backend_srv';
import impressionSrv from 'app/core/services/impression_srv';
import kbn from 'app/core/utils/kbn';
import { getDashboardScenePageStateManager } from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { DashboardDTO } from 'app/types/dashboard';
@@ -68,7 +65,15 @@ abstract class DashboardLoaderSrvBase<T> implements DashboardLoaderSrvLike<T> {
);
}
private executeScript(result: any) {
private async executeScript(result: any) {
// Async-load dependencies used only in scripted dashboards to avoid them being in the main bundle, if not needed
const [jQuery, moment, lodash, kbn] = await Promise.all([
import('jquery'),
import('moment'),
import('lodash'),
import('app/core/utils/kbn'),
]);
const services = {
dashboardSrv: getDashboardSrv(),
datasourceSrv: getDatasourceSrv(),
@@ -90,12 +95,12 @@ abstract class DashboardLoaderSrvBase<T> implements DashboardLoaderSrvLike<T> {
locationService.getSearchObject(),
kbn,
dateMath,
_,
lodash,
moment,
window,
document,
$,
$,
jQuery,
jQuery,
services
);
+1 -1
View File
@@ -64,7 +64,7 @@ export function useDatasource(dataSource: string | DataSourceRef | DataSourceIns
export interface KeybaordNavigatableListProps {
keyboardEvents?: Observable<React.KeyboardEvent>;
containerRef: React.RefObject<HTMLElement | null>;
containerRef: React.RefObject<HTMLElement>;
}
/**
@@ -25,7 +25,7 @@ interface State {
}
export class ThresholdsEditor extends PureComponent<Props, State> {
private latestThresholdInputRef: React.RefObject<HTMLInputElement | null>;
private latestThresholdInputRef: React.RefObject<HTMLInputElement>;
constructor(props: Props) {
super(props);
@@ -46,9 +46,6 @@ 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);
@@ -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>>(null);
const paneRef = useRef<ReturnType<typeof paneSelector>>();
paneRef.current = useSelector(paneSelector);
useEffect(() => {
@@ -44,7 +44,7 @@ export type QueryLibraryContextType = {
onUpdateSuccess?: () => void,
onSelectQuery?: (query: DataQuery) => void,
datasourceFilters?: string[],
parentRef?: React.RefObject<HTMLDivElement | null>
parentRef?: React.RefObject<HTMLDivElement>
) => ReactNode;
/**
@@ -63,7 +63,7 @@ type Props = {
scrollElementClass?: string;
traceProp: Trace;
datasource: DataSourceApi<DataQuery, DataSourceJsonData, {}> | undefined;
topOfViewRef?: RefObject<HTMLDivElement | null>;
topOfViewRef?: RefObject<HTMLDivElement>;
createSpanLink?: SpanLinkFunc;
focusedSpanId?: string;
createFocusSpanLink?: (traceId: string, spanId: string) => LinkModel<Field>;
@@ -27,8 +27,7 @@ const timeRange = {
to: new Date(1000),
} as unknown as TimeRange;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getContent(result: React.ReactElement<any>) {
function getContent(result: React.ReactElement) {
return result.props.children.props.children[0];
}
@@ -561,7 +561,7 @@ const CardsContainer = ({
mainContainerRef,
}: {
listOfContentCards: React.ReactNode[];
mainContainerRef?: React.RefObject<HTMLDivElement | null>;
mainContainerRef?: React.RefObject<HTMLDivElement>;
}) => {
const styles = useStyles2(getStyles);
@@ -102,7 +102,7 @@ type TVirtualizedTraceViewOwnProps = {
focusedSpanIdForSearch: string;
showSpanFilterMatchesOnly: boolean;
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
topOfViewRef?: RefObject<HTMLDivElement | null>;
topOfViewRef?: RefObject<HTMLDivElement>;
datasourceType: string;
datasourceUid: string;
headerHeight: number;
@@ -104,7 +104,7 @@ export type TProps = {
focusedSpanIdForSearch: string;
showSpanFilterMatchesOnly: boolean;
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
topOfViewRef?: RefObject<HTMLDivElement | null>;
topOfViewRef?: RefObject<HTMLDivElement>;
headerHeight: number;
criticalPath: CriticalPathSection[];
traceFlameGraphs: TraceFlameGraphs;
@@ -197,10 +197,8 @@ export class UnthemedTraceTimelineViewer extends PureComponent<TProps, State> {
return (
<div
className={styles.TraceTimelineViewer}
ref={(ref) => {
if (ref) {
this.setState({ height: ref.getBoundingClientRect().height });
}
ref={(ref: HTMLDivElement | null) => {
ref && this.setState({ height: ref.getBoundingClientRect().height });
}}
>
<TimelineHeaderRow
@@ -1,4 +1,4 @@
import { act, fireEvent, screen, waitFor, within } from '@testing-library/react';
import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from 'test/test-utils';
@@ -103,9 +103,7 @@ describe('SignupInvitedPage', () => {
get: { email: '', invitedBy: '', name: '', username: '', orgName: '' },
});
// 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 userEvent.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();
@@ -43,7 +43,7 @@ export const LibraryPanelsView = ({
}
);
const asyncDispatch = useMemo(() => asyncDispatcher(dispatch), [dispatch]);
const abortControllerRef = useRef<AbortController>(null);
const abortControllerRef = useRef<AbortController>();
useDebounce(
() => {
@@ -12,7 +12,7 @@ export interface Props {}
export const LiveConnectionWarning = memo(function LiveConnectionWarning() {
const [show, setShow] = useState<boolean | undefined>(undefined);
const subscriptionRef = useRef<Unsubscribable>(null);
const subscriptionRef = useRef<Unsubscribable>();
const styles = useStyles2(getStyle);
useEffect(() => {
@@ -1,6 +1,5 @@
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';
@@ -32,10 +31,7 @@ export const LogLineDetailsTrace = ({ timeRange, timeZone, traceRef }: Props) =>
.get(traceRef.dsUID)
.then((dataSource) => {
if (dataSource) {
// TODO why?
flushSync(() => {
setDataSource(dataSource);
});
setDataSource(dataSource);
} else {
setDataFrames(null);
}
@@ -60,7 +60,12 @@ export function VisualizationSuggestionCard({
content = (
<button {...commonButtonProps}>
<div style={innerStyles} className={cx(styles.renderContainer, isSelected && styles.selectedSuggestion)} inert>
{/* to use inert in React 18, we have to do this hacky object spread thing. https://stackoverflow.com/questions/72720469/error-when-using-inert-attribute-with-typescript */}
<div
style={innerStyles}
className={cx(styles.renderContainer, isSelected && styles.selectedSuggestion)}
{...{ inert: '' }}
>
<PanelRenderer
title=""
data={data}
@@ -1,7 +1,7 @@
import { QueryStatus } from '@reduxjs/toolkit/query';
import { fireEvent, screen, waitFor } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import { UserEvent } from '@testing-library/user-event';
import { act, type JSX } from 'react';
import type { JSX } from 'react';
import { render } from 'test/test-utils';
import {
@@ -245,9 +245,7 @@ describe('ProvisioningWizard', () => {
path: '/',
});
// 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 user.click(screen.getByRole('button', { name: /Choose what to synchronize/i }));
await waitFor(() => {
expect(screen.getByRole('heading', { name: /2\. Choose what to synchronize/i })).toBeInTheDocument();
@@ -255,9 +253,7 @@ describe('ProvisioningWizard', () => {
expect(mockUseCreateOrUpdateRepository).toHaveBeenCalled();
// 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 user.click(screen.getByRole('button', { name: /Synchronize with external storage/i }));
await waitFor(() => {
expect(screen.getByRole('heading', { name: /3\. Synchronize with external storage/i })).toBeInTheDocument();
@@ -293,9 +289,7 @@ describe('ProvisioningWizard', () => {
path: '/',
});
// 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 user.click(screen.getByRole('button', { name: /Choose what to synchronize/i }));
await waitFor(() => {
expect(screen.getByRole('heading', { name: /2\. Choose what to synchronize/i })).toBeInTheDocument();
@@ -353,9 +347,7 @@ describe('ProvisioningWizard', () => {
path: '/',
});
// 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 user.click(screen.getByRole('button', { name: /Choose what to synchronize/i }));
await waitFor(() => {
expect(screen.getByText('Branch "invalid-branch" not found')).toBeInTheDocument();
@@ -389,9 +381,7 @@ describe('ProvisioningWizard', () => {
path: '/',
});
// 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 user.click(screen.getByRole('button', { name: /Choose what to synchronize/i }));
await waitFor(() => {
expect(screen.getByRole('alert')).toBeInTheDocument();
@@ -432,9 +422,7 @@ describe('ProvisioningWizard', () => {
path: '/',
});
// 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 user.click(screen.getByRole('button', { name: /Choose what to synchronize/i }));
await waitFor(() => {
expect(screen.getByRole('alert')).toBeInTheDocument();
@@ -462,17 +450,13 @@ describe('ProvisioningWizard', () => {
path: '/',
});
// 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 user.click(screen.getByRole('button', { name: /Choose what to synchronize/i }));
await waitFor(() => {
expect(screen.getByRole('heading', { name: /2\. Choose what to synchronize/i })).toBeInTheDocument();
});
// 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 user.click(screen.getByRole('button', { name: /Previous/i }));
await waitFor(() => {
expect(screen.getByRole('heading', { name: /1\. Connect to external storage/i })).toBeInTheDocument();
@@ -509,9 +493,7 @@ describe('ProvisioningWizard', () => {
path: '/',
});
// 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 user.click(screen.getByRole('button', { name: /Choose what to synchronize/i }));
expect(screen.getByRole('button', { name: /Submitting.../i })).toBeDisabled();
});
@@ -548,9 +530,7 @@ describe('ProvisioningWizard', () => {
path: '/',
});
// 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 user.click(screen.getByRole('button', { name: /Choose what to synchronize/i }));
await waitFor(() => {
expect(screen.getByRole('button', { name: /Synchronize with external storage/i })).toBeInTheDocument();
@@ -1,5 +1,4 @@
import { useCallback, useState } from 'react';
import { flushSync } from 'react-dom';
import { FormProvider, useForm } from 'react-hook-form';
import { AppEvents } from '@grafana/data';
@@ -44,10 +43,7 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions
const workflow = watch('workflow');
const handleSubmitForm = async (data: BulkActionFormData) => {
// TODO why?
flushSync(() => {
setHasSubmitted(true);
});
setHasSubmitted(true);
const resources = collectSelectedItems(selectedItems);
@@ -613,7 +613,7 @@ function SavedQueryButtons(props: {
onUpdateSuccess?: () => void;
onSelectQuery: (query: DataQuery) => void;
datasourceFilters: string[];
parentRef: React.RefObject<HTMLDivElement | null>;
parentRef: React.RefObject<HTMLDivElement>;
}) {
const { renderSavedQueryButtons } = useQueryLibraryContext();
return renderSavedQueryButtons(
@@ -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>(null);
const urlsRef = useRef<Field>();
// Clear selection when the search results change
useEffect(() => {
@@ -77,7 +77,7 @@ export const SuggestionsInput = ({
const theme = useTheme2();
const styles = getStyles(theme, inputHeight);
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null);
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>();
useEffect(() => {
scrollRef.current?.scrollTo(0, scrollTop);
@@ -11,7 +11,7 @@ import checkboxWhitePng from 'img/checkbox_white.png';
import { ALL_VARIABLE_VALUE } from '../../constants';
export interface Props extends Omit<React.HTMLProps<HTMLUListElement>, 'onToggle'>, Themeable2 {
export interface Props extends React.HTMLProps<HTMLUListElement>, Themeable2 {
multi: boolean;
values: VariableOption[];
selectedValues: VariableOption[];
@@ -18,8 +18,8 @@
"lodash": "4.17.21",
"monaco-editor": "0.34.1",
"prismjs": "1.30.0",
"react": "19.2.0",
"react-dom": "19.2.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"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": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"i18next-cli": "^1.24.22",
"jest": "29.7.0",
"react-select-event": "5.5.1",
@@ -295,18 +295,14 @@ describe('VisualMetricQueryEditor', () => {
const defaultQuery = { ...query, ...defaultTimeSeriesList(datasource), filters: query.filters };
const range = getDefaultTimeRange();
// TODO investigate why we need act
// see https://github.com/testing-library/react-testing-library/issues/1375
await act(async () =>
render(
<VisualMetricQueryEditor
{...defaultProps}
onChange={onChange}
datasource={datasource}
query={query}
range={range}
/>
)
render(
<VisualMetricQueryEditor
{...defaultProps}
onChange={onChange}
datasource={datasource}
query={query}
range={range}
/>
);
expect(document.body).toHaveTextContent('metric.test_label');
expect(await screen.findByText('Delta')).toBeInTheDocument();
@@ -41,7 +41,7 @@ exports[`VariableQueryEditor renders correctly 1`] = `
<div
class="css-8nwx1l-singleValue css-0"
>
Projects
Loading...
</div>
<div
class="css-1eu65zc"
@@ -18,8 +18,8 @@
"lodash": "4.17.21",
"monaco-editor": "0.34.1",
"prismjs": "1.30.0",
"react": "19.2.0",
"react-dom": "19.2.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"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": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"jest": "29.7.0",
"react-select-event": "5.5.1",
"ts-node": "10.9.2",
@@ -20,8 +20,8 @@ interface CodeEditorProps {
export const LogsQLCodeEditor = (props: CodeEditorProps) => {
const { query, datasource, onChange } = props;
const monacoRef = useRef<Monaco>(null);
const disposalRef = useRef<monacoType.IDisposable>(undefined);
const monacoRef = useRef<Monaco>();
const disposalRef = useRef<monacoType.IDisposable>();
const onFocus = useCallback(async () => {
disposalRef.current = await reRegisterCompletionProvider(

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