React 19: non-breaking type and test changes (#114760)

non-breaking type and test changes needed for react 19
This commit is contained in:
Ashley Harrison
2025-12-04 16:19:25 +00:00
committed by GitHub
parent 030c7099cb
commit d7d0c104ab
35 changed files with 82 additions and 83 deletions
@@ -19,7 +19,7 @@ export interface Props<T = string> extends Omit<FieldProps, 'children'> {
/** Custom error message to display on saving */
saveErrorMessage?: string;
/** Input that will save its value on change */
children: (onChange: (newValue: T) => void) => React.ReactElement;
children: (onChange: (newValue: T) => void) => React.ReactElement<Record<string, unknown>>;
}
/**
@@ -22,7 +22,7 @@ type BaseProps = {
size?: ComponentSize;
variant?: ButtonVariant;
fill?: ButtonFill;
icon?: IconName | React.ReactElement;
icon?: IconName | React.ReactElement<IconElementProps>;
className?: string;
fullWidth?: boolean;
type?: string;
@@ -207,8 +207,13 @@ export const LinkButton = React.forwardRef<HTMLAnchorElement, ButtonLinkProps>(
LinkButton.displayName = 'LinkButton';
type IconElementProps = {
className?: string;
size?: IconSize;
};
interface IconRendererProps {
icon?: IconName | React.ReactElement<{ className?: string; size?: IconSize }>;
icon?: IconName | React.ReactElement<IconElementProps>;
size?: IconSize;
className?: string;
iconType?: IconType;
@@ -316,7 +316,9 @@ const BaseActions = ({ children, disabled, variant, className }: ActionsProps) =
return (
<div className={cx(css, className)}>
{React.Children.map(children, (child) => {
return React.isValidElement(child) ? cloneElement(child, { disabled: isDisabled, ...child.props }) : null;
return React.isValidElement<Record<string, unknown>>(child)
? cloneElement(child, child.type !== React.Fragment ? { disabled: isDisabled, ...child.props } : undefined)
: null;
})}
</div>
);
@@ -530,8 +530,8 @@ describe('Combobox', () => {
const input = screen.getByRole('combobox');
await user.click(input);
await user.type(input, 'fir');
await act(async () => {
await user.type(input, 'fir');
jest.advanceTimersByTime(500); // Custom value while typing
});
@@ -604,8 +604,8 @@ describe('Combobox', () => {
const input = screen.getByRole('combobox');
await user.click(input);
await user.type(input, 'Opt');
await act(async () => {
await user.type(input, 'Opt');
jest.advanceTimersByTime(500); // Custom value while typing
});
@@ -12,7 +12,7 @@ import { Button, ButtonVariant } from '../Button/Button';
export interface Props {
/** Confirm action callback */
onConfirm(): void;
children: string | ReactElement;
children: string | ReactElement<Record<string, unknown>>;
/** Custom button styles */
className?: string;
/** Button size */
@@ -24,7 +24,7 @@ import { TooltipPlacement } from '../Tooltip/types';
export interface Props {
overlay: React.ReactElement | (() => React.ReactElement);
placement?: TooltipPlacement;
children: React.ReactElement;
children: React.ReactElement<Record<string, unknown>>;
root?: HTMLElement;
/** Amount in pixels to nudge the dropdown vertically and horizontally, respectively. */
offset?: [number, number];
@@ -12,7 +12,7 @@ import { Label } from './Label';
export interface FieldProps extends HTMLAttributes<HTMLDivElement> {
/** Form input element, i.e Input or Switch */
children: React.ReactElement;
children: React.ReactElement<Record<string, unknown>>;
/** Label for the field */
label?: React.ReactNode;
/** Description of the field */
@@ -85,7 +85,7 @@ export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
<div className={cx(styles.field, horizontal && styles.fieldHorizontal, className)} {...otherProps}>
{labelElement}
<div>
<div ref={ref}>{React.cloneElement(children, childProps)}</div>
<div ref={ref}>{React.cloneElement(children, children.type !== React.Fragment ? childProps : undefined)}</div>
{invalid && error && !horizontal && (
<div
className={cx(styles.fieldValidationWrapper, {
@@ -212,14 +212,13 @@ export class UnThemedQueryField extends PureComponent<QueryFieldProps, QueryFiel
<div className="slate-query-field" data-testid={selectors.components.QueryField.container}>
<Editor
ref={(editor) => {
this.editor = editor!;
this.editor = editor;
}}
schema={SCHEMA}
autoCorrect={false}
readOnly={this.props.disabled}
onBlur={this.handleBlur}
onClick={this.props.onClick}
// onKeyDown={this.onKeyDown}
onChange={(change: { value: Value }) => {
this.onChange(change.value, false);
}}
@@ -197,7 +197,7 @@ export const VirtualizedSelectMenu = ({
// check if a child has array children (and is therefore a react-select group)
// we need to flatten these so the correct count and elements are passed to the virtualized list
const hasArrayChildren = (child: React.ReactNode) => {
return React.isValidElement(child) && Array.isArray(child.props.children);
return React.isValidElement<Record<string, unknown>>(child) && Array.isArray(child.props.children);
};
VirtualizedSelectMenu.displayName = 'VirtualizedSelectMenu';
@@ -1,5 +1,6 @@
import { css } from '@emotion/css';
import { createElement, CSSProperties } from 'react';
import { CSSObject } from '@emotion/serialize';
import { createElement } from 'react';
import * as React from 'react';
import { GrafanaTheme2, ThemeTypographyVariantTypes } from '@grafana/data';
@@ -25,7 +26,7 @@ export interface TextProps extends Omit<React.HTMLAttributes<HTMLElement>, 'clas
/** If true, numbers will have fixed width, useful for displaying tabular data. False by default */
tabular?: boolean;
/** Whether to align the text to left, center or right */
textAlignment?: CSSProperties['textAlign'];
textAlignment?: CSSObject['textAlign'];
children: NonNullable<React.ReactNode>;
}
@@ -8,7 +8,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../themes/ThemeContext';
type Props = {
children: React.ReactElement;
children: React.ReactElement<Record<string, unknown>>;
visible: boolean;
duration?: number;
};
@@ -8,7 +8,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../themes/ThemeContext';
type Props = {
children: React.ReactElement;
children: React.ReactElement<Record<string, unknown>>;
visible: boolean;
size: number;
@@ -19,7 +19,7 @@ import {
import { VizLegendOptions } from '@grafana/schema';
import { PanelContext, PanelContextRoot } from '../../components/PanelChrome/PanelContext';
import { VizLayout } from '../../components/VizLayout/VizLayout';
import { VizLayout, VizLayoutLegendProps } from '../../components/VizLayout/VizLayout';
import { UPlotChart } from '../../components/uPlot/Plot';
import { AxisProps } from '../../components/uPlot/config/UPlotAxisBuilder';
import { Renderers, UPlotConfigBuilder } from '../../components/uPlot/config/UPlotConfigBuilder';
@@ -54,7 +54,7 @@ export interface GraphNGProps extends Themeable2 {
prepConfig: (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => UPlotConfigBuilder;
propsToDiff?: Array<string | PropDiffFn>;
preparePlotFrame?: (frames: DataFrame[], dimFields: XYFieldMatchers) => DataFrame | null;
renderLegend: (config: UPlotConfigBuilder) => React.ReactElement | null;
renderLegend: (config: UPlotConfigBuilder) => React.ReactElement<VizLayoutLegendProps> | null;
/**
* needed for propsToDiff to re-init the plot & config
+1 -1
View File
@@ -2,7 +2,7 @@ import { ReactElement } from 'react';
import * as React from 'react';
/** Returns the ID value of the first, and only, child element */
export function getChildId(children: ReactElement): string | undefined {
export function getChildId(children: ReactElement<Record<string, unknown>>): string | undefined {
let inputId: unknown;
// Get the first, and only, child to retrieve form input's id
@@ -1,4 +1,4 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { SendResetMailPage } from './SendResetMailPage';
@@ -38,7 +38,7 @@ describe('VerifyEmail Page', () => {
it('should pass validation checks for email field', async () => {
render(<SendResetMailPage />);
fireEvent.click(screen.getByRole('button', { name: 'Send reset email' }));
await userEvent.click(screen.getByRole('button', { name: 'Send reset email' }));
expect(await screen.findByText('Email or username is required')).toBeInTheDocument();
await userEvent.type(screen.getByRole('textbox', { name: /User Enter your information/i }), 'test@gmail.com');
@@ -49,7 +49,7 @@ describe('VerifyEmail Page', () => {
render(<SendResetMailPage />);
await userEvent.type(screen.getByRole('textbox', { name: /User Enter your information/i }), 'test@gmail.com');
fireEvent.click(screen.getByRole('button', { name: 'Send reset email' }));
await userEvent.click(screen.getByRole('button', { name: 'Send reset email' }));
await waitFor(() =>
expect(postMock).toHaveBeenCalledWith('/api/user/password/send-reset-email', {
userOrEmail: 'test@gmail.com',
@@ -15,7 +15,7 @@ import {
TimeZone,
} from '@grafana/data';
import { DashboardCursorSync, VizLegendOptions } from '@grafana/schema';
import { Themeable2, VizLayout } from '@grafana/ui';
import { Themeable2, VizLayout, VizLayoutLegendProps } from '@grafana/ui';
import { AxisProps, pluginLog, Renderers, ScaleProps, UPlotChart, UPlotConfigBuilder } from '@grafana/ui/internal';
import { GraphNGLegendEvent, XYFieldMatchers } from './types';
@@ -48,7 +48,7 @@ export interface GraphNGProps extends Themeable2 {
) => UPlotConfigBuilder;
propsToDiff?: Array<string | PropDiffFn>;
preparePlotFrame?: (frames: DataFrame[], dimFields: XYFieldMatchers) => DataFrame | null;
renderLegend: (config: UPlotConfigBuilder) => React.ReactElement | null;
renderLegend: (config: UPlotConfigBuilder) => React.ReactElement<VizLayoutLegendProps> | null;
replaceVariables: InterpolateFunction;
dataLinkPostProcessor?: DataLinkPostProcessor;
cursorSync?: DashboardCursorSync;
@@ -7,7 +7,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { Popover as GrafanaPopover, PopoverController, Stack, useStyles2 } from '@grafana/ui';
export interface PopupCardProps {
children: ReactElement;
children: ReactElement<Record<string, unknown>>;
header?: ReactNode;
content: ReactElement;
footer?: ReactNode;
@@ -140,7 +140,7 @@ export const NotificationPreview = ({
<div className={styles.firstAlertManagerLine} />
<div className={styles.alertManagerName}>
<Trans i18nKey="alerting.notification-preview.alertmanager">Alertmanager:</Trans>
<img src={alertManagerSource.imgUrl} alt="" className={styles.img} />
<img src={alertManagerSource.imgUrl || undefined} alt="" className={styles.img} />
{alertManagerSource.name}
</div>
<div className={styles.secondAlertManagerLine} />
@@ -19,7 +19,7 @@ interface RenderParams<T = ActionImpl | string> {
interface KBarResultsProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
items: any[];
onRender: (params: RenderParams) => React.ReactElement;
onRender: (params: RenderParams) => React.ReactElement<Record<string, unknown>>;
maxHeight?: number;
}
@@ -38,7 +38,7 @@ describe('ShareDrawer', () => {
expect(locationService.getSearch().get('shareView')).toBe('link');
expect(await screen.findByText('Share externally')).toBeInTheDocument();
const closeButton = await screen.findByTestId(selectors.components.Drawer.General.close);
await act(() => userEvent.click(closeButton));
await userEvent.click(closeButton);
expect(locationService.getSearch().get('shareView')).toBe(null);
});
@@ -16,7 +16,7 @@ export interface OptionsPaneItemInfo {
value?: any;
description?: string;
popularRank?: number;
render: (descriptor: OptionsPaneItemDescriptor) => React.ReactElement;
render: (descriptor: OptionsPaneItemDescriptor) => React.ReactElement<Record<string, unknown>>;
skipField?: boolean;
showIf?: () => boolean;
/** Hook for controlling visibility */
@@ -1,4 +1,4 @@
import { act, screen, waitFor } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import { useParams } from 'react-router-dom-v5-compat';
import { Props } from 'react-virtualized-auto-sizer';
import { render } from 'test/test-utils';
@@ -120,10 +120,8 @@ describe('DashboardPageProxy', () => {
it('home dashboard', async () => {
getDashboardScenePageStateManager().setDashboardCache(HOME_DASHBOARD_CACHE_KEY, dashMock);
act(() => {
setup({
route: { routeName: DashboardRoutes.Home, component: () => null, path: '/' },
});
setup({
route: { routeName: DashboardRoutes.Home, component: () => null, path: '/' },
});
await waitFor(() => {
@@ -134,11 +132,9 @@ describe('DashboardPageProxy', () => {
it('uid dashboard', async () => {
getDashboardScenePageStateManager().setDashboardCache('abc-def', dashMock);
act(() => {
setup({
route: { routeName: DashboardRoutes.Normal, component: () => null, path: '/' },
uid: 'abc-def',
});
setup({
route: { routeName: DashboardRoutes.Normal, component: () => null, path: '/' },
uid: 'abc-def',
});
await waitFor(() => {
@@ -156,11 +152,9 @@ describe('DashboardPageProxy', () => {
describe('when user can edit a dashboard ', () => {
it('should not render DashboardScenePage if route is Home', async () => {
getDashboardScenePageStateManager().setDashboardCache(HOME_DASHBOARD_CACHE_KEY, homeMockEditable);
act(() => {
setup({
route: { routeName: DashboardRoutes.Home, component: () => null, path: '/' },
uid: '',
});
setup({
route: { routeName: DashboardRoutes.Home, component: () => null, path: '/' },
uid: '',
});
await waitFor(() => {
@@ -170,11 +164,9 @@ describe('DashboardPageProxy', () => {
it('should not render DashboardScenePage if route is Normal and has uid', async () => {
getDashboardScenePageStateManager().setDashboardCache('abc-def', dashMockEditable);
act(() => {
setup({
route: { routeName: DashboardRoutes.Normal, component: () => null, path: '/' },
uid: 'abc-def',
});
setup({
route: { routeName: DashboardRoutes.Normal, component: () => null, path: '/' },
uid: 'abc-def',
});
await waitFor(() => {
expect(screen.queryAllByTestId('dashboard-scene-page')).toHaveLength(0);
@@ -185,11 +177,9 @@ describe('DashboardPageProxy', () => {
describe('when user can only view a dashboard ', () => {
it('should render DashboardScenePage if route is Home', async () => {
getDashboardScenePageStateManager().setDashboardCache(HOME_DASHBOARD_CACHE_KEY, homeMock);
act(() => {
setup({
route: { routeName: DashboardRoutes.Home, component: () => null, path: '/' },
uid: '',
});
setup({
route: { routeName: DashboardRoutes.Home, component: () => null, path: '/' },
uid: '',
});
await waitFor(() => {
@@ -199,11 +189,9 @@ describe('DashboardPageProxy', () => {
it('should render DashboardScenePage if route is Normal and has uid', async () => {
getDashboardScenePageStateManager().setDashboardCache('uid', dashMock);
act(() => {
setup({
route: { routeName: DashboardRoutes.Normal, component: () => null, path: '/' },
uid: 'uid',
});
setup({
route: { routeName: DashboardRoutes.Normal, component: () => null, path: '/' },
uid: 'uid',
});
await waitFor(() => {
expect(screen.queryAllByTestId('dashboard-scene-page')).toHaveLength(1);
@@ -212,11 +200,9 @@ describe('DashboardPageProxy', () => {
it('should render not DashboardScenePage if dashboard UID does not match route UID', async () => {
getDashboardScenePageStateManager().setDashboardCache('uid', dashMock);
act(() => {
setup({
route: { routeName: DashboardRoutes.Normal, component: () => null, path: '/' },
uid: 'wrongUID',
});
setup({
route: { routeName: DashboardRoutes.Normal, component: () => null, path: '/' },
uid: 'wrongUID',
});
await waitFor(() => {
expect(screen.queryAllByTestId('dashboard-scene-page')).toHaveLength(0);
@@ -5,7 +5,7 @@ import { render } from 'test/test-utils';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { config, locationService } from '@grafana/runtime';
import { backendSrv } from 'app/core/services/backend_srv';
import { DashboardRoutes } from 'app/types/dashboard';
import { DashboardDTO, DashboardRoutes } from 'app/types/dashboard';
import PublicDashboardPageProxy, { PublicDashboardPageProxyProps } from './PublicDashboardPageProxy';
@@ -56,8 +56,7 @@ describe('PublicDashboardPageProxy', () => {
// Mock the dashboard UID response so we don't get any refused connection errors
// from this test (as the fetch polyfill means this logic would actually try and call the API)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
jest.spyOn(backendSrv, 'getPublicDashboardByUid').mockResolvedValue({ dashboard: {}, meta: {} } as any);
jest.spyOn(backendSrv, 'getPublicDashboardByUid').mockResolvedValue({ dashboard: {}, meta: {} } as DashboardDTO);
});
describe('when scene feature enabled', () => {
@@ -54,7 +54,7 @@ export function DataSourceCard({
</div>
</Card.Heading>
<Card.Figure className={styles.logo}>
<img src={ds.meta.info.logos.small} alt={`${ds.meta.name} Logo`} />
<img src={ds.meta.info.logos.small || undefined} alt={`${ds.meta.name} Logo`} />
</Card.Figure>
</Card>
);
@@ -20,7 +20,7 @@ export function DataSourceLogo(props: DataSourceLogoProps) {
<img
className={styles.pickerDSLogo}
alt={`${dataSource.meta.name} logo`}
src={dataSource.meta.info.logos.small}
src={dataSource.meta.info.logos.small || undefined}
></img>
);
}
@@ -3,7 +3,7 @@ import { cloneElement, ReactElement, useRef } from 'react';
import { Popover as GrafanaPopover, PopoverController } from '@grafana/ui';
export type PopoverProps = {
children: ReactElement;
children: ReactElement<Record<string, unknown>>;
content: ReactElement;
overlayClassName?: string;
};
@@ -22,7 +22,7 @@ type Props = {
onChange: (value: string) => void;
};
const SearchBarInput = memo(({ value, onChange }: Props) => {
const SearchBarInput = memo(({ value = '', onChange }: Props) => {
const clearUiFind = () => {
onChange('');
};
@@ -15,7 +15,7 @@ const EXPRESSION_ICON_MAP = {
} as const satisfies Record<ExpressionQueryType, string>;
interface ExpressionTypeDropdownProps {
children: ReactElement;
children: ReactElement<Record<string, unknown>>;
handleOnSelect: (value: ExpressionQueryType) => void;
}
@@ -55,7 +55,11 @@ const PanelTypeCardComponent = ({
isCurrent ? t('panel.panel-type-card.title-click-to-close', 'Click again to close this section') : plugin.name
}
>
<img className={cx(styles.img, { [styles.disabled]: isDisabled })} src={plugin.info.logos.small} alt="" />
<img
className={cx(styles.img, { [styles.disabled]: isDisabled })}
src={plugin.info.logos.small || undefined}
alt=""
/>
<div className={cx(styles.itemContent, { [styles.disabled]: isDisabled })}>
<div className={styles.name}>{title}</div>
@@ -25,7 +25,6 @@ export const ExtensionErrorBoundary = ({
log.error(`Extension "${pluginId}/${extensionTitle}" failed to load.`, {
message: error.message,
componentStack: errorInfo.componentStack ?? '',
digest: errorInfo.digest ?? '',
});
}}
fallback={() => {
@@ -785,7 +785,6 @@ describe('Plugin Extensions / Utils', () => {
expect(log.error).toHaveBeenCalledWith(`Extension "${pluginId}/${extensionTitle}" failed to load.`, {
message: 'Test error',
componentStack: expect.any(String),
digest: expect.any(String),
});
expect(screen.getByText(`Extension failed to load: "${pluginId}/${extensionTitle}"`)).toBeVisible();
@@ -818,7 +817,6 @@ describe('Plugin Extensions / Utils', () => {
expect(log.error).toHaveBeenCalledWith(`Extension "${pluginId}/${extensionTitle}" failed to load.`, {
message: 'Test error',
componentStack: expect.any(String),
digest: expect.any(String),
});
expect(screen.getByText(`Extension failed to load: "${pluginId}/${extensionTitle}"`)).toBeVisible();
@@ -965,7 +963,6 @@ describe('Plugin Extensions / Utils', () => {
expect(log.error).toHaveBeenCalledWith(`Extension "${pluginId}/${extensionTitle}" failed to load.`, {
message: 'Test error',
componentStack: expect.any(String),
digest: expect.any(String),
});
});
@@ -995,7 +992,6 @@ describe('Plugin Extensions / Utils', () => {
expect(log.error).toHaveBeenCalledWith(`Extension "${pluginId}/${extensionTitle}" failed to load.`, {
message: 'Test error',
componentStack: expect.any(String),
digest: expect.any(String),
});
});
});
@@ -120,7 +120,15 @@ export const RegressionTransformerEditor = ({
);
};
const RegressionField = ({ label, tooltip, children }: { label: string; tooltip?: string; children: ReactElement }) => (
const RegressionField = ({
label,
tooltip,
children,
}: {
label: string;
tooltip?: string;
children: ReactElement<Record<string, unknown>>;
}) => (
<InlineField labelWidth={LABEL_WIDTH} label={label} tooltip={tooltip}>
{children}
</InlineField>
@@ -39,7 +39,7 @@ export const DefaultSubscription = (props: Props) => {
let canceled = false;
getSubscriptions().then((result) => {
if (!canceled) {
updateSubscriptions(result, loadSubscriptionsClicked);
updateSubscriptions(result, Boolean(loadSubscriptionsClicked));
}
});
return () => {
@@ -7,7 +7,7 @@ import { Field, Icon, PopoverContent, ReactUtils, Tooltip, useStyles2 } from '@g
interface EditorFieldProps extends ComponentProps<typeof Field> {
label: string;
children: React.ReactElement;
children: React.ReactElement<Record<string, unknown>>;
width?: number | string;
optional?: boolean;
tooltip?: PopoverContent;
@@ -5,7 +5,7 @@ import { InlineFieldRow, InlineField } from '@grafana/ui';
interface Props {
label: string;
tooltip?: string;
children: React.ReactElement;
children: React.ReactElement<Record<string, unknown>>;
}
const SearchField = ({ label, tooltip, children }: Props) => {
return (