From 7678fc9de1757af1faeb95cfedbec5f55d7de8f0 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 21 Oct 2025 17:33:38 +0100 Subject: [PATCH] Chore: convert some class components to functional (#112661) * convert BigValue to functional component * convert AbstractList * Revert "convert AbstractList" This reverts commit 673c22a81db6b59935f83bb27b8afbb0b6805fe3. * ignore and deprecate remaining list components * convert NotFoundDisplay * convert rectangle * fix some test components * convert thresholds.tsx * convert SpanGraph to functional component * convert InviteeRow to functional component * converted InviteesTable to functional component * convert PanelPluginError to functional components * convert UserOrganizations * convert UserSessions to a functional component * convert UserTeams to a functional component * convert CloudMonitoringCheatSheet to a functional component * convert ConfigEditor a react functional component * convert TestInfoTab to a functional component * convert DebugPanel to a functional component * fix types + BigValue story --- eslint-suppressions.json | 100 ----------- .../components/BigValue/BigValue.story.tsx | 31 ++-- .../src/components/BigValue/BigValue.tsx | 90 +++++----- .../src/components/List/AbstractList.tsx | 3 + .../src/components/List/InlineList.tsx | 2 + .../grafana-ui/src/components/List/List.tsx | 2 + .../app/features/canvas/elements/notFound.tsx | 33 ++-- .../features/canvas/elements/rectangle.tsx | 30 ++-- .../containers/DashboardPage.test.tsx | 9 +- .../containers/SoloPanelPage.test.tsx | 11 +- .../editors/ThresholdsEditor/thresholds.tsx | 32 ++-- .../TracePageHeader/SpanGraph/index.tsx | 19 +- public/app/features/invites/InviteeRow.tsx | 53 +++--- public/app/features/invites/InviteesTable.tsx | 54 +++--- .../panel/components/PanelPluginError.tsx | 84 +++++---- .../features/profile/UserOrganizations.tsx | 130 +++++++------- public/app/features/profile/UserSessions.tsx | 167 +++++++++--------- public/app/features/profile/UserTeams.tsx | 110 ++++++------ .../components/CloudMonitoringCheatSheet.tsx | 15 +- .../ConfigEditor.tsx | 12 +- .../TestInfoTab.tsx | 44 +++-- .../panel/alertlist/UnifiedAlertList.tsx | 2 + public/app/plugins/panel/debug/DebugPanel.tsx | 30 ++-- 23 files changed, 471 insertions(+), 592 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 120a475bac4..ec71ebba007 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -574,11 +574,6 @@ "count": 1 } }, - "packages/grafana-ui/src/components/BigValue/BigValue.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "packages/grafana-ui/src/components/Cascader/Cascader.tsx": { "react-prefer-function-component/react-prefer-function-component": { "count": 1 @@ -780,21 +775,6 @@ "count": 2 } }, - "packages/grafana-ui/src/components/List/AbstractList.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, - "packages/grafana-ui/src/components/List/InlineList.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, - "packages/grafana-ui/src/components/List/List.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "packages/grafana-ui/src/components/MatchersUI/FieldValueMatcher.tsx": { "@typescript-eslint/consistent-type-assertions": { "count": 1 @@ -2028,16 +2008,6 @@ "count": 1 } }, - "public/app/features/canvas/elements/notFound.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, - "public/app/features/canvas/elements/rectangle.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/canvas/runtime/element.tsx": { "react-prefer-function-component/react-prefer-function-component": { "count": 1 @@ -2748,11 +2718,6 @@ "count": 1 } }, - "public/app/features/dashboard/containers/DashboardPage.test.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/dashboard/containers/DashboardPage.tsx": { "@grafana/no-aria-label-selectors": { "count": 1 @@ -2777,11 +2742,6 @@ "count": 1 } }, - "public/app/features/dashboard/containers/SoloPanelPage.test.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/dashboard/dashgrid/DashboardGrid.tsx": { "react-prefer-function-component/react-prefer-function-component": { "count": 1 @@ -2992,11 +2952,6 @@ "count": 1 } }, - "public/app/features/dimensions/editors/ThresholdsEditor/thresholds.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/dimensions/editors/URLPickerTab.tsx": { "no-restricted-syntax": { "count": 2 @@ -3082,11 +3037,6 @@ "count": 1 } }, - "public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/index.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx": { "react-hooks/rules-of-hooks": { "count": 1 @@ -3298,16 +3248,6 @@ "count": 1 } }, - "public/app/features/invites/InviteeRow.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, - "public/app/features/invites/InviteesTable.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/invites/SignupInvited.tsx": { "no-restricted-syntax": { "count": 4 @@ -3458,11 +3398,6 @@ "count": 1 } }, - "public/app/features/panel/components/PanelPluginError.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 3 - } - }, "public/app/features/panel/components/VizTypePicker/PanelTypeCard.tsx": { "@grafana/no-aria-label-selectors": { "count": 1 @@ -3587,26 +3522,11 @@ "count": 3 } }, - "public/app/features/profile/UserOrganizations.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/profile/UserProfileEditForm.tsx": { "no-restricted-syntax": { "count": 3 } }, - "public/app/features/profile/UserSessions.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, - "public/app/features/profile/UserTeams.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/provisioning/Config/ConfigFormGithubCollapse.tsx": { "no-restricted-syntax": { "count": 2 @@ -4287,11 +4207,6 @@ "count": 1 } }, - "public/app/plugins/datasource/cloud-monitoring/components/CloudMonitoringCheatSheet.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/plugins/datasource/cloud-monitoring/components/ConfigEditor/ConfigEditor.tsx": { "react-prefer-function-component/react-prefer-function-component": { "count": 1 @@ -4494,11 +4409,6 @@ "count": 1 } }, - "public/app/plugins/datasource/grafana-testdata-datasource/ConfigEditor.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/plugins/datasource/grafana-testdata-datasource/QueryEditor.tsx": { "@typescript-eslint/consistent-type-assertions": { "count": 3 @@ -4510,11 +4420,6 @@ "count": 1 } }, - "public/app/plugins/datasource/grafana-testdata-datasource/TestInfoTab.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/plugins/datasource/grafana-testdata-datasource/components/CSVWaveEditor.tsx": { "react-prefer-function-component/react-prefer-function-component": { "count": 1 @@ -5031,11 +4936,6 @@ "count": 1 } }, - "public/app/plugins/panel/debug/DebugPanel.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/plugins/panel/debug/EventBusLogger.tsx": { "@typescript-eslint/no-explicit-any": { "count": 2 diff --git a/packages/grafana-ui/src/components/BigValue/BigValue.story.tsx b/packages/grafana-ui/src/components/BigValue/BigValue.story.tsx index 48c45f9843c..f112a86efd1 100644 --- a/packages/grafana-ui/src/components/BigValue/BigValue.story.tsx +++ b/packages/grafana-ui/src/components/BigValue/BigValue.story.tsx @@ -29,27 +29,36 @@ const meta: Meta = { width: { control: { type: 'range', min: 200, max: 800 } }, height: { control: { type: 'range', min: 200, max: 800 } }, colorMode: { - control: { type: 'select', options: [BigValueColorMode.Value, BigValueColorMode.Background] }, + control: { + type: 'select', + }, + options: [BigValueColorMode.Value, BigValueColorMode.Background], + }, + graphMode: { + control: { type: 'select' }, + options: [BigValueGraphMode.Area, BigValueGraphMode.None], + }, + justifyMode: { + control: { type: 'select' }, + options: [BigValueJustifyMode.Auto, BigValueJustifyMode.Center], }, - graphMode: { control: { type: 'select', options: [BigValueGraphMode.Area, BigValueGraphMode.None] } }, - justifyMode: { control: { type: 'select', options: [BigValueJustifyMode.Auto, BigValueJustifyMode.Center] } }, textMode: { control: { type: 'radio', - options: [ - BigValueTextMode.Auto, - BigValueTextMode.Name, - BigValueTextMode.ValueAndName, - BigValueTextMode.None, - BigValueTextMode.Value, - ], }, + options: [ + BigValueTextMode.Auto, + BigValueTextMode.Name, + BigValueTextMode.ValueAndName, + BigValueTextMode.None, + BigValueTextMode.Value, + ], }, color: { control: 'color' }, }, }; -interface StoryProps extends Partial { +interface StoryProps extends Props { numeric: number; title: string; color: string; diff --git a/packages/grafana-ui/src/components/BigValue/BigValue.tsx b/packages/grafana-ui/src/components/BigValue/BigValue.tsx index 6aceeb3660b..08bacf3e2af 100644 --- a/packages/grafana-ui/src/components/BigValue/BigValue.tsx +++ b/packages/grafana-ui/src/components/BigValue/BigValue.tsx @@ -1,6 +1,5 @@ import { cx } from '@emotion/css'; -import { PureComponent } from 'react'; -import * as React from 'react'; +import { memo, type MouseEventHandler } from 'react'; import { DisplayValue, DisplayValueAlignmentFactors, FieldSparkline } from '@grafana/data'; import { PercentChangeColorMode, VizTextDisplayOptions } from '@grafana/schema'; @@ -51,7 +50,7 @@ export interface Props extends Themeable2 { /** Sparkline values for showing a graph under/behind the value */ sparkline?: FieldSparkline; /** onClick handler for the value */ - onClick?: React.MouseEventHandler; + onClick?: MouseEventHandler; /** Custom styling */ className?: string; /** Color mode for coloring the value or the background */ @@ -83,58 +82,55 @@ export interface Props extends Themeable2 { disableWideLayout?: boolean; } -export class BigValue extends PureComponent { - static defaultProps: Partial = { - justifyMode: BigValueJustifyMode.Auto, - }; +export const BigValue = memo((props) => { + const { onClick, className, hasLinks, theme, justifyMode = BigValueJustifyMode.Auto } = props; - render() { - const { onClick, className, hasLinks, theme } = this.props; - const layout = buildLayout(this.props); - const panelStyles = layout.getPanelStyles(); - const valueAndTitleContainerStyles = layout.getValueAndTitleContainerStyles(); - const valueStyles = layout.getValueStyles(); - const titleStyles = layout.getTitleStyles(); - const textValues = layout.textValues; - const percentChange = this.props.value.percentChange; - const percentChangeColorMode = this.props.percentChangeColorMode; - const showPercentChange = percentChange != null && !Number.isNaN(percentChange); + const layout = buildLayout({ ...props, justifyMode }); + const panelStyles = layout.getPanelStyles(); + const valueAndTitleContainerStyles = layout.getValueAndTitleContainerStyles(); + const valueStyles = layout.getValueStyles(); + const titleStyles = layout.getTitleStyles(); + const textValues = layout.textValues; + const percentChange = props.value.percentChange; + const percentChangeColorMode = props.percentChangeColorMode; + const showPercentChange = percentChange != null && !Number.isNaN(percentChange); - // When there is an outer data link this tooltip will override the outer native tooltip - const tooltip = hasLinks ? undefined : textValues.tooltip; - - if (!onClick) { - return ( -
-
- {textValues.title &&
{textValues.title}
} - - {showPercentChange && ( - - )} -
- {layout.renderChart()} -
- ); - } + // When there is an outer data link this tooltip will override the outer native tooltip + const tooltip = hasLinks ? undefined : textValues.tooltip; + if (!onClick) { return ( - + ); } -} + + return ( + + ); +}); + +BigValue.displayName = 'BigValue'; diff --git a/packages/grafana-ui/src/components/List/AbstractList.tsx b/packages/grafana-ui/src/components/List/AbstractList.tsx index bc4f6260a9e..920b6cf0b5b 100644 --- a/packages/grafana-ui/src/components/List/AbstractList.tsx +++ b/packages/grafana-ui/src/components/List/AbstractList.tsx @@ -26,6 +26,9 @@ const getStyles = stylesFactory((inlineList = false) => ({ }), })); +/** @deprecated Use ul/li/arr.map directly instead */ +// no point converting, this is deprecated +// eslint-disable-next-line react-prefer-function-component/react-prefer-function-component export class AbstractList extends PureComponent> { constructor(props: AbstractListProps) { super(props); diff --git a/packages/grafana-ui/src/components/List/InlineList.tsx b/packages/grafana-ui/src/components/List/InlineList.tsx index 8ea88dc18f7..c27df3f9084 100644 --- a/packages/grafana-ui/src/components/List/InlineList.tsx +++ b/packages/grafana-ui/src/components/List/InlineList.tsx @@ -3,6 +3,8 @@ import { PureComponent } from 'react'; import { ListProps, AbstractList } from './AbstractList'; /** @deprecated Use ul/li/arr.map directly instead */ +// no point converting, this is deprecated +// eslint-disable-next-line react-prefer-function-component/react-prefer-function-component export class InlineList extends PureComponent> { render() { return ; diff --git a/packages/grafana-ui/src/components/List/List.tsx b/packages/grafana-ui/src/components/List/List.tsx index 9f7788dedf9..a58bb3c5d4e 100644 --- a/packages/grafana-ui/src/components/List/List.tsx +++ b/packages/grafana-ui/src/components/List/List.tsx @@ -3,6 +3,8 @@ import { PureComponent } from 'react'; import { ListProps, AbstractList } from './AbstractList'; /** @deprecated Use ul/li/arr.map directly instead */ +// no point converting, this is deprecated +// eslint-disable-next-line react-prefer-function-component/react-prefer-function-component export class List extends PureComponent> { render() { return ; diff --git a/public/app/features/canvas/elements/notFound.tsx b/public/app/features/canvas/elements/notFound.tsx index a8840a635c3..540d9728753 100644 --- a/public/app/features/canvas/elements/notFound.tsx +++ b/public/app/features/canvas/elements/notFound.tsx @@ -1,25 +1,24 @@ -import { PureComponent } from 'react'; +import { memo } from 'react'; import { Trans } from '@grafana/i18n'; import { CanvasElementItem, CanvasElementProps } from '../element'; -class NotFoundDisplay extends PureComponent { - render() { - const { config } = this.props; - return ( -
- {JSON.stringify(config, null, 2)} }} - > -

Not found:

- {''} -
-
- ); - } -} +const NotFoundDisplay = memo(({ config }: CanvasElementProps) => { + return ( +
+ {JSON.stringify(config, null, 2)} }} + > +

Not found:

+ {''} +
+
+ ); +}); + +NotFoundDisplay.displayName = 'NotFoundDisplay'; export const notFoundItem: CanvasElementItem = { id: 'not-found', diff --git a/public/app/features/canvas/elements/rectangle.tsx b/public/app/features/canvas/elements/rectangle.tsx index 19037a83740..a5ee6aa9d1f 100644 --- a/public/app/features/canvas/elements/rectangle.tsx +++ b/public/app/features/canvas/elements/rectangle.tsx @@ -1,10 +1,9 @@ import { css } from '@emotion/css'; -import { PureComponent } from 'react'; +import { memo } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { stylesFactory } from '@grafana/ui'; -import { config } from 'app/core/config'; +import { useStyles2 } from '@grafana/ui'; import { DimensionContext } from 'app/features/dimensions/context'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; @@ -18,20 +17,19 @@ import { } from '../element'; import { Align, TextConfig, TextData, VAlign } from '../types'; -class RectangleDisplay extends PureComponent> { - render() { - const { data } = this.props; - const styles = getStyles(config.theme2, data); +const RectangleDisplay = memo>(({ data }) => { + const styles = useStyles2(getStyles, data); - return ( -
- {data?.text} -
- ); - } -} + return ( +
+ {data?.text} +
+ ); +}); -const getStyles = stylesFactory((theme: GrafanaTheme2, data) => ({ +RectangleDisplay.displayName = 'RectangleDisplay'; + +const getStyles = (theme: GrafanaTheme2, data: TextData | undefined) => ({ container: css({ position: 'absolute', height: '100%', @@ -45,7 +43,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, data) => ({ fontSize: `${data?.size}px`, color: data?.color, }), -})); +}); export const rectangleItem: CanvasElementItem = { id: 'rectangle', diff --git a/public/app/features/dashboard/containers/DashboardPage.test.tsx b/public/app/features/dashboard/containers/DashboardPage.test.tsx index da7454a6d02..aee5d4417d6 100644 --- a/public/app/features/dashboard/containers/DashboardPage.test.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.test.tsx @@ -1,6 +1,5 @@ import { screen, waitFor } from '@testing-library/react'; import { KBarProvider } from 'kbar'; -import { Component } from 'react'; import { useEffectOnce } from 'react-use'; import { mockToolkitActionCreator } from 'test/core/redux/mocks'; import { render } from 'test/test-utils'; @@ -34,11 +33,9 @@ jest.mock('app/features/dashboard/dashgrid/LazyLoader', () => { }); jest.mock('app/features/dashboard/components/DashboardSettings/GeneralSettings', () => { - class GeneralSettings extends Component<{}, {}> { - render() { - return <>general settings; - } - } + const GeneralSettings = () => { + return <>general settings; + }; return { GeneralSettings }; }); diff --git a/public/app/features/dashboard/containers/SoloPanelPage.test.tsx b/public/app/features/dashboard/containers/SoloPanelPage.test.tsx index 786a3e165e2..536b1b47ea4 100644 --- a/public/app/features/dashboard/containers/SoloPanelPage.test.tsx +++ b/public/app/features/dashboard/containers/SoloPanelPage.test.tsx @@ -1,5 +1,4 @@ import { render, screen } from '@testing-library/react'; -import { Component } from 'react'; import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; import { Dashboard } from '@grafana/schema'; @@ -16,12 +15,10 @@ import { Props, SoloPanelPage } from './SoloPanelPage'; jest.mock('app/features/dashboard/components/DashboardSettings/GeneralSettings', () => ({})); jest.mock('app/features/dashboard/dashgrid/DashboardPanel', () => { - class DashboardPanel extends Component { - render() { - // In this test we only check whether a new panel has arrived in the props - return <>{this.props.panel?.title}; - } - } + const DashboardPanel = (props: DashboardPanelProps) => { + // In this test we only check whether a new panel has arrived in the props + return <>{props.panel?.title}; + }; return { DashboardPanel }; }); diff --git a/public/app/features/dimensions/editors/ThresholdsEditor/thresholds.tsx b/public/app/features/dimensions/editors/ThresholdsEditor/thresholds.tsx index 335300534f4..3e7c714ca0e 100644 --- a/public/app/features/dimensions/editors/ThresholdsEditor/thresholds.tsx +++ b/public/app/features/dimensions/editors/ThresholdsEditor/thresholds.tsx @@ -1,4 +1,4 @@ -import { PureComponent } from 'react'; +import { memo } from 'react'; import { StandardEditorProps, ThresholdsConfig, ThresholdsMode, ThresholdsFieldConfigSettings } from '@grafana/data'; @@ -6,25 +6,17 @@ import { ThresholdsEditor } from './ThresholdsEditor'; type Props = StandardEditorProps; -export class ThresholdsValueEditor extends PureComponent { - constructor(props: Props) { - super(props); - } +export const ThresholdsValueEditor = memo(({ value, onChange }: Props) => { + const thresholdsValue = value ?? { + mode: ThresholdsMode.Percentage, - render() { - const { onChange } = this.props; - let value = this.props.value; - if (!value) { - value = { - mode: ThresholdsMode.Percentage, + // Must be sorted by 'value', first value is always -Infinity + steps: [ + // anything? + ], + }; - // Must be sorted by 'value', first value is always -Infinity - steps: [ - // anything? - ], - }; - } + return ; +}); - return ; - } -} +ThresholdsValueEditor.displayName = 'ThresholdsValueEditor'; diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/index.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/index.tsx index 78a4c151f88..d0a45548490 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/index.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/SpanGraph/index.tsx @@ -14,7 +14,7 @@ import { css } from '@emotion/css'; import memoizeOne from 'memoize-one'; -import * as React from 'react'; +import { memo } from 'react'; import { ViewRange, TUpdateViewRangeTimeFunction, ViewRangeTimeUpdate } from '../../TraceTimelineViewer/types'; import { Trace, TraceSpan } from '../../types/trace'; @@ -65,13 +65,8 @@ function getItems(trace: Trace): SpanItem[] { const memoizedGetitems = memoizeOne(getItems); -export default class SpanGraph extends React.PureComponent { - static defaultProps = { - height: DEFAULT_HEIGHT, - }; - - render() { - const { height, trace, viewRange, updateNextViewRangeTime, updateViewRangeTime } = this.props; +const SpanGraph = memo( + ({ height = DEFAULT_HEIGHT, trace, viewRange, updateNextViewRangeTime, updateViewRangeTime }: SpanGraphProps) => { const styles = getStyles(); if (!trace) { @@ -87,7 +82,7 @@ export default class SpanGraph extends React.PureComponent { @@ -95,4 +90,8 @@ export default class SpanGraph extends React.PureComponent { ); } -} +); + +SpanGraph.displayName = 'SpanGraph'; + +export default SpanGraph; diff --git a/public/app/features/invites/InviteeRow.tsx b/public/app/features/invites/InviteeRow.tsx index d5fd484e083..15449a38c26 100644 --- a/public/app/features/invites/InviteeRow.tsx +++ b/public/app/features/invites/InviteeRow.tsx @@ -1,4 +1,4 @@ -import { PureComponent } from 'react'; +import { memo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { t } from '@grafana/i18n'; @@ -19,31 +19,30 @@ interface OwnProps { export type Props = OwnProps & ConnectedProps; -class InviteeRow extends PureComponent { - render() { - const { invitee, revokeInvite } = this.props; - return ( - - {invitee.email} - {invitee.name} - - invitee.url}> - Copy Invite - -   - - -