From 8b31ec5040c4b15fca4f3ae9fe28780ccebb3e9a Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 30 Oct 2025 09:23:21 +0000 Subject: [PATCH] Chore: Convert to functional components (#112951) * refactor ErrorBoundary so it doesn't trigger the lint rule * refactor ErrorBoundaryAlert to functional component * convert StatPanel to a functional component * convert ServiceAccountPicker to a functional component * convert UserPicker to a functional component * don't need displayName when not memoized * convert TimelineChart to a functional component * convert UserLdapSyncInfo to a functional component * convert UserOrgs to functional component * convert OrgRow to a functional component * convert UserSessions to a functional component * convert TimePickerSettings to a functional component * convert DataSourcePluginSettings to a functional component * convert ExploreTimeControls to a functional component * convert SearchBarInput to a functional component * convert LiveConnectionWarning to a functional component * convert ConcatenateTransformerEditor * convert ConstantVariableEditor a functional component * convert VariableInput to a functional component * convert ConfigEditor to a functional component * convert CSVWavesEditor to a functional component --- eslint-suppressions.json | 86 +---- .../src/components/ErrorBoundary.tsx | 10 +- .../ErrorBoundary/ErrorBoundary.tsx | 23 +- .../Select/ServiceAccountPicker.tsx | 110 +++---- .../app/core/components/Select/UserPicker.tsx | 104 +++--- .../TimelineChart/TimelineChart.tsx | 121 +++---- .../app/features/admin/UserLdapSyncInfo.tsx | 138 ++++---- public/app/features/admin/UserOrgs.tsx | 300 ++++++++---------- public/app/features/admin/UserSessions.tsx | 195 ++++++------ .../DashboardSettings/TimePickerSettings.tsx | 113 ++++--- .../components/DataSourcePluginSettings.tsx | 51 ++- .../features/explore/ExploreTimeControls.tsx | 88 +++-- .../components/common/SearchBarInput.test.tsx | 5 +- .../components/common/SearchBarInput.tsx | 65 ++-- .../features/live/LiveConnectionWarning.tsx | 97 +++--- .../editors/ConcatenateTransformerEditor.tsx | 116 +++---- .../constant/ConstantVariableEditor.tsx | 15 +- .../pickers/shared/VariableInput.tsx | 57 ++-- .../components/ConfigEditor/ConfigEditor.tsx | 62 ++-- .../components/CSVWaveEditor.tsx | 63 ++-- public/app/plugins/panel/stat/StatPanel.tsx | 206 ++++++------ public/locales/en-US/grafana.json | 3 + 22 files changed, 924 insertions(+), 1104 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 2d84e04dadf..e068ca60eff 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -539,11 +539,6 @@ "count": 2 } }, - "packages/grafana-sql/src/components/ErrorBoundary.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "packages/grafana-sql/src/components/configuration/ConnectionLimits.tsx": { "no-restricted-syntax": { "count": 2 @@ -666,11 +661,6 @@ "count": 3 } }, - "packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "packages/grafana-ui/src/components/FormField/FormField.tsx": { "no-restricted-syntax": { "count": 1 @@ -1278,21 +1268,11 @@ "count": 1 } }, - "public/app/core/components/Select/ServiceAccountPicker.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/core/components/Select/TeamPicker.tsx": { "react-prefer-function-component/react-prefer-function-component": { "count": 1 } }, - "public/app/core/components/Select/UserPicker.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/core/components/SharedPreferences/SharedPreferences.tsx": { "no-restricted-syntax": { "count": 8 @@ -1342,11 +1322,6 @@ "count": 2 } }, - "public/app/core/components/TimelineChart/TimelineChart.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/core/config.ts": { "no-barrel-files/no-barrel-files": { "count": 2 @@ -1483,25 +1458,17 @@ "public/app/features/admin/UserLdapSyncInfo.tsx": { "no-restricted-syntax": { "count": 3 - }, - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 } }, "public/app/features/admin/UserOrgs.tsx": { "no-restricted-syntax": { "count": 2 }, - "react-prefer-function-component/react-prefer-function-component": { - "count": 3 - } - }, - "public/app/features/admin/UserProfile.tsx": { "react-prefer-function-component/react-prefer-function-component": { "count": 1 } }, - "public/app/features/admin/UserSessions.tsx": { + "public/app/features/admin/UserProfile.tsx": { "react-prefer-function-component/react-prefer-function-component": { "count": 1 } @@ -2399,9 +2366,6 @@ "public/app/features/dashboard/components/DashboardSettings/TimePickerSettings.tsx": { "no-restricted-syntax": { "count": 5 - }, - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 } }, "public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx": { @@ -2752,11 +2716,6 @@ "count": 1 } }, - "public/app/features/datasources/components/DataSourcePluginSettings.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/datasources/components/DataSourcePluginState.tsx": { "no-restricted-syntax": { "count": 3 @@ -2872,11 +2831,6 @@ "count": 1 } }, - "public/app/features/explore/ExploreTimeControls.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/explore/Logs/LiveLogs.tsx": { "react-prefer-function-component/react-prefer-function-component": { "count": 1 @@ -2977,11 +2931,6 @@ "count": 1 } }, - "public/app/features/explore/TraceView/components/common/SearchBarInput.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/explore/TraceView/components/demo/trace-generators.ts": { "@typescript-eslint/consistent-type-assertions": { "count": 1 @@ -3118,11 +3067,6 @@ "count": 2 } }, - "public/app/features/live/LiveConnectionWarning.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/live/centrifuge/LiveDataStream.ts": { "@typescript-eslint/consistent-type-assertions": { "count": 1 @@ -3578,11 +3522,6 @@ "count": 1 } }, - "public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx": { "@typescript-eslint/consistent-type-assertions": { "count": 1 @@ -3700,11 +3639,6 @@ "count": 2 } }, - "public/app/features/variables/constant/ConstantVariableEditor.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/variables/constant/reducer.ts": { "@typescript-eslint/consistent-type-assertions": { "count": 1 @@ -3787,9 +3721,6 @@ "public/app/features/variables/pickers/shared/VariableInput.tsx": { "no-restricted-syntax": { "count": 1 - }, - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 } }, "public/app/features/variables/pickers/shared/VariableOptions.tsx": { @@ -4004,11 +3935,6 @@ "count": 1 } }, - "public/app/plugins/datasource/cloud-monitoring/components/ConfigEditor/ConfigEditor.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/plugins/datasource/cloud-monitoring/components/Fields.tsx": { "no-restricted-syntax": { "count": 1 @@ -4207,11 +4133,6 @@ "count": 1 } }, - "public/app/plugins/datasource/grafana-testdata-datasource/components/CSVWaveEditor.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/plugins/datasource/grafana-testdata-datasource/components/RandomWalkEditor.tsx": { "@typescript-eslint/consistent-type-assertions": { "count": 2 @@ -4920,11 +4841,6 @@ "count": 1 } }, - "public/app/plugins/panel/stat/StatPanel.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/plugins/panel/state-timeline/migrations.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 diff --git a/packages/grafana-sql/src/components/ErrorBoundary.tsx b/packages/grafana-sql/src/components/ErrorBoundary.tsx index f7991415795..8500fb68fdf 100644 --- a/packages/grafana-sql/src/components/ErrorBoundary.tsx +++ b/packages/grafana-sql/src/components/ErrorBoundary.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import { Component, ErrorInfo, PropsWithChildren } from 'react'; import { Trans } from '@grafana/i18n'; @@ -6,14 +6,14 @@ type Props = { fallBackComponent?: React.ReactNode; }; -export class ErrorBoundary extends React.Component, { hasError: boolean }> { - constructor(props: React.PropsWithChildren) { +export class ErrorBoundary extends Component, { hasError: boolean }> { + constructor(props: PropsWithChildren) { super(props); this.state = { hasError: false }; } - static getDerivedStateFromError() { - return { hasError: true }; + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + this.setState({ hasError: true }); } render() { diff --git a/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx b/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx index dd49f34b985..818e919710c 100644 --- a/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx +++ b/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -1,6 +1,7 @@ -import { PureComponent, ReactNode, ComponentType, ErrorInfo } from 'react'; +import { PureComponent, ReactNode, ComponentType, ErrorInfo, memo } from 'react'; import { faro } from '@grafana/faro-web-sdk'; +import { t } from '@grafana/i18n'; import { Alert } from '../Alert/Alert'; @@ -116,15 +117,9 @@ export interface ErrorBoundaryAlertProps { errorLogger?: (error: Error) => void; } -export class ErrorBoundaryAlert extends PureComponent { - static defaultProps: Partial = { - title: 'An unexpected error happened', - style: 'alertbox', - }; - - render() { - const { title, children, style, dependencies, errorLogger, boundaryName } = this.props; - +export const ErrorBoundaryAlert = memo( + ({ title, children, style = 'alertbox', dependencies, errorLogger, boundaryName }: ErrorBoundaryAlertProps) => { + const alertTitle = title ?? t('grafana-ui.error-boundary.title', 'An unexpected error happened'); return ( {({ error, errorInfo }) => { @@ -134,7 +129,7 @@ export class ErrorBoundaryAlert extends PureComponent { if (style === 'alertbox') { return ( - +
{error && error.toString()}
@@ -144,12 +139,14 @@ export class ErrorBoundaryAlert extends PureComponent { ); } - return ; + return ; }} ); } -} +); + +ErrorBoundaryAlert.displayName = 'ErrorBoundaryAlert'; /** * HOC for wrapping a component in an error boundary. diff --git a/public/app/core/components/Select/ServiceAccountPicker.tsx b/public/app/core/components/Select/ServiceAccountPicker.tsx index 037404dcb2b..e9b200a4cb0 100644 --- a/public/app/core/components/Select/ServiceAccountPicker.tsx +++ b/public/app/core/components/Select/ServiceAccountPicker.tsx @@ -1,6 +1,6 @@ import debounce from 'debounce-promise'; import { isNil } from 'lodash'; -import { Component } from 'react'; +import { useMemo, useState } from 'react'; import { SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; @@ -14,66 +14,58 @@ export interface Props { inputId?: string; } -export interface State { - isLoading: boolean; -} +export const ServiceAccountPicker = ({ className, onSelected, inputId }: Props) => { + const [isLoading, setIsLoading] = useState(false); -export class ServiceAccountPicker extends Component { - constructor(props: Props) { - super(props); - this.state = { isLoading: false }; - } + const search = useMemo( + () => + debounce( + async (query?: string) => { + setIsLoading(true); - search = debounce( - async (query?: string) => { - this.setState({ isLoading: true }); + if (isNil(query)) { + query = ''; + } - if (isNil(query)) { - query = ''; - } - - return getBackendSrv() - .get(`/api/serviceaccounts/search?query=${query}&perpage=100`) - .then((result: ServiceAccountsState) => { - return result.serviceAccounts.map((sa) => ({ - id: sa.id, - uid: sa.uid, - value: sa, - label: sa.login, - imgUrl: sa.avatarUrl, - login: sa.login, - })); - }) - .finally(() => { - this.setState({ isLoading: false }); - }); - }, - 300, - { leading: true } + return getBackendSrv() + .get(`/api/serviceaccounts/search?query=${query}&perpage=100`) + .then((result: ServiceAccountsState) => { + return result.serviceAccounts.map((sa) => ({ + id: sa.id, + uid: sa.uid, + value: sa, + label: sa.login, + imgUrl: sa.avatarUrl, + login: sa.login, + })); + }) + .finally(() => { + setIsLoading(false); + }); + }, + 300, + { leading: true } + ), + [] ); - render() { - const { className, onSelected, inputId } = this.props; - const { isLoading } = this.state; - - return ( -
- -
- ); - } -} + return ( +
+ +
+ ); +}; diff --git a/public/app/core/components/Select/UserPicker.tsx b/public/app/core/components/Select/UserPicker.tsx index 5284cbeca5f..e7d4110d09b 100644 --- a/public/app/core/components/Select/UserPicker.tsx +++ b/public/app/core/components/Select/UserPicker.tsx @@ -1,6 +1,6 @@ import debounce from 'debounce-promise'; import { isNil } from 'lodash'; -import { Component } from 'react'; +import { useMemo, useState } from 'react'; import { SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; @@ -14,63 +14,55 @@ export interface Props { inputId?: string; } -export interface State { - isLoading: boolean; -} +export const UserPicker = ({ className, onSelected, inputId }: Props) => { + const [isLoading, setIsLoading] = useState(false); -export class UserPicker extends Component { - constructor(props: Props) { - super(props); - this.state = { isLoading: false }; - } + const search = useMemo( + () => + debounce( + async (query?: string) => { + setIsLoading(true); - search = debounce( - async (query?: string) => { - this.setState({ isLoading: true }); + if (isNil(query)) { + query = ''; + } - if (isNil(query)) { - query = ''; - } - - return getBackendSrv() - .get(`/api/org/users/lookup?query=${query}&limit=100`) - .then((result: OrgUser[]) => { - return result.map((user) => ({ - id: user.userId, - uid: user.uid, - value: user, - label: user.login, - imgUrl: user.avatarUrl, - login: user.login, - })); - }) - .finally(() => { - this.setState({ isLoading: false }); - }); - }, - 300, - { leading: true } + return getBackendSrv() + .get(`/api/org/users/lookup?query=${query}&limit=100`) + .then((result: OrgUser[]) => { + return result.map((user) => ({ + id: user.userId, + uid: user.uid, + value: user, + label: user.login, + imgUrl: user.avatarUrl, + login: user.login, + })); + }) + .finally(() => { + setIsLoading(false); + }); + }, + 300, + { leading: true } + ), + [] ); - render() { - const { className, onSelected, inputId } = this.props; - const { isLoading } = this.state; - - return ( -
- -
- ); - } -} + return ( +
+ +
+ ); +}; diff --git a/public/app/core/components/TimelineChart/TimelineChart.tsx b/public/app/core/components/TimelineChart/TimelineChart.tsx index f4317a127b7..ce6e4a1f4c1 100644 --- a/public/app/core/components/TimelineChart/TimelineChart.tsx +++ b/public/app/core/components/TimelineChart/TimelineChart.tsx @@ -1,4 +1,4 @@ -import { Component } from 'react'; +import { useCallback } from 'react'; import { DataFrame, FALLBACK_COLOR, FieldType, TimeRange } from '@grafana/data'; import { VisibilityMode, TimelineValueAlignment, TooltipDisplayMode, VizTooltipOptions } from '@grafana/schema'; @@ -25,69 +25,76 @@ export interface TimelineProps extends Omit { - getValueColor = (frameIdx: number, fieldIdx: number, value: unknown) => { - const field = this.props.frames[frameIdx]?.fields[fieldIdx]; +export const TimelineChart = (props: TimelineProps) => { + const { frames, timeZone, rowHeight, tooltip, legend, legendItems } = props; - if (field?.display) { - const disp = field.display(value); // will apply color modes - if (disp.color) { - return disp.color; + const getValueColor = useCallback( + (frameIdx: number, fieldIdx: number, value: unknown) => { + const field = frames[frameIdx]?.fields[fieldIdx]; + + if (field?.display) { + const disp = field.display(value); // will apply color modes + if (disp.color) { + return disp.color; + } } - } - return FALLBACK_COLOR; - }; + return FALLBACK_COLOR; + }, + [frames] + ); - prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => { - return preparePlotConfigBuilder({ - frame: alignedFrame, - getTimeRange, - allFrames: this.props.frames, - ...this.props, + const prepConfig = useCallback( + (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => { + return preparePlotConfigBuilder({ + frame: alignedFrame, + getTimeRange, + allFrames: frames, + ...props, - // Ensure timezones is passed as an array - timeZones: Array.isArray(this.props.timeZone) ? this.props.timeZone : [this.props.timeZone], + // Ensure timezones is passed as an array + timeZones: Array.isArray(timeZone) ? timeZone : [timeZone], - // When there is only one row, use the full space - rowHeight: alignedFrame.fields.length > 2 ? this.props.rowHeight : 1, - getValueColor: this.getValueColor, + // When there is only one row, use the full space + rowHeight: alignedFrame.fields.length > 2 ? rowHeight : 1, + getValueColor: getValueColor, - hoverMulti: this.props.tooltip?.mode === TooltipDisplayMode.Multi, - }); - }; + hoverMulti: tooltip?.mode === TooltipDisplayMode.Multi, + }); + }, + [frames, props, timeZone, rowHeight, getValueColor, tooltip] + ); - renderLegend = (config: UPlotConfigBuilder) => { - const { legend, legendItems } = this.props; + const renderLegend = useCallback( + (config: UPlotConfigBuilder) => { + if (!config || !legendItems || !legend || legend.showLegend === false) { + return null; + } - if (!config || !legendItems || !legend || legend.showLegend === false) { - return null; - } + return ( + + + + ); + }, + [legend, legendItems] + ); - return ( - - - - ); - }; - - render() { - return ( - f.type === FieldType.time, - y: (f) => - f.type === FieldType.number || - f.type === FieldType.boolean || - f.type === FieldType.string || - f.type === FieldType.enum, - }} - prepConfig={this.prepConfig} - propsToDiff={propsToDiff} - renderLegend={this.renderLegend} - omitHideFromViz={true} - /> - ); - } -} + return ( + f.type === FieldType.time, + y: (f) => + f.type === FieldType.number || + f.type === FieldType.boolean || + f.type === FieldType.string || + f.type === FieldType.enum, + }} + prepConfig={prepConfig} + propsToDiff={propsToDiff} + renderLegend={renderLegend} + omitHideFromViz={true} + /> + ); +}; diff --git a/public/app/features/admin/UserLdapSyncInfo.tsx b/public/app/features/admin/UserLdapSyncInfo.tsx index 1baf9e219a2..4277fa4c814 100644 --- a/public/app/features/admin/UserLdapSyncInfo.tsx +++ b/public/app/features/admin/UserLdapSyncInfo.tsx @@ -1,4 +1,4 @@ -import { PureComponent } from 'react'; +import { memo } from 'react'; import { dateTimeFormat } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; @@ -16,80 +16,72 @@ interface Props { onUserSync: () => void; } -interface State {} - const format = 'dddd YYYY-MM-DD HH:mm zz'; const debugLDAPMappingBaseURL = '/admin/authentication/ldap'; -export class UserLdapSyncInfo extends PureComponent { - onUserSync = () => { - this.props.onUserSync(); - }; +export const UserLdapSyncInfo = memo(({ ldapSyncInfo, user, onUserSync }: Props) => { + const nextSyncSuccessful = ldapSyncInfo && ldapSyncInfo.nextSync; + const nextSyncTime = nextSyncSuccessful ? dateTimeFormat(ldapSyncInfo.nextSync, { format }) : ''; + const debugLDAPMappingURL = `${debugLDAPMappingBaseURL}?username=${user && user.login}`; + const canReadLDAPUser = contextSrv.hasPermission(AccessControlAction.LDAPUsersRead); + const canSyncLDAPUser = contextSrv.hasPermission(AccessControlAction.LDAPUsersSync); - render() { - const { ldapSyncInfo, user } = this.props; - const nextSyncSuccessful = ldapSyncInfo && ldapSyncInfo.nextSync; - const nextSyncTime = nextSyncSuccessful ? dateTimeFormat(ldapSyncInfo.nextSync, { format }) : ''; - const debugLDAPMappingURL = `${debugLDAPMappingBaseURL}?username=${user && user.login}`; - const canReadLDAPUser = contextSrv.hasPermission(AccessControlAction.LDAPUsersRead); - const canSyncLDAPUser = contextSrv.hasPermission(AccessControlAction.LDAPUsersSync); - - return ( - <> -

- LDAP Synchronisation -

-
-
- - - - - - - - - - - - -
- External sync - - - User synced via LDAP. Some changes must be done in LDAP or mappings. - - - -
- Next scheduled synchronization - - {ldapSyncInfo.enabled ? ( - nextSyncTime - ) : ( - Not enabled - )} -
-
-
- {canSyncLDAPUser && ( - - )} - {canReadLDAPUser && ( - - Debug LDAP Mapping - - )} -
+ return ( + <> +

+ LDAP Synchronisation +

+
+
+ + + + + + + + + + + + +
+ External sync + + + User synced via LDAP. Some changes must be done in LDAP or mappings. + + + +
+ Next scheduled synchronization + + {ldapSyncInfo.enabled ? ( + nextSyncTime + ) : ( + Not enabled + )} +
- - ); - } -} +
+ {canSyncLDAPUser && ( + + )} + {canReadLDAPUser && ( + + Debug LDAP Mapping + + )} +
+
+ + ); +}); +UserLdapSyncInfo.displayName = 'UserLdapSyncInfo'; diff --git a/public/app/features/admin/UserOrgs.tsx b/public/app/features/admin/UserOrgs.tsx index b7083bf7051..a0169a1022a 100644 --- a/public/app/features/admin/UserOrgs.tsx +++ b/public/app/features/admin/UserOrgs.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { createRef, PureComponent, ReactElement } from 'react'; +import { memo, PureComponent, ReactElement, useEffect, useRef, useState } from 'react'; import { GrafanaTheme2, OrgRole } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; @@ -10,10 +10,8 @@ import { Icon, Modal, stylesFactory, - Themeable2, Tooltip, useStyles2, - withTheme2, Stack, TextLink, } from '@grafana/ui'; @@ -37,73 +35,63 @@ interface Props { onOrgAdd: (orgId: number, role: OrgRole) => void; } -interface State { - showAddOrgModal: boolean; -} +export const UserOrgs = memo(({ user, orgs, isExternalUser, onOrgRoleChange, onOrgRemove, onOrgAdd }: Props) => { + const [showAddOrgModal, setShowAddOrgModal] = useState(false); + const addToOrgButtonRef = useRef(null); -export class UserOrgs extends PureComponent { - addToOrgButtonRef = createRef(); - state = { - showAddOrgModal: false, + const showOrgAddModal = () => { + setShowAddOrgModal(true); }; - showOrgAddModal = () => { - this.setState({ showAddOrgModal: true }); + const dismissOrgAddModal = () => { + setShowAddOrgModal(false); + addToOrgButtonRef.current?.focus(); }; - dismissOrgAddModal = () => { - this.setState({ showAddOrgModal: false }, () => { - this.addToOrgButtonRef.current?.focus(); - }); - }; + const canAddToOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersAdd) && !isExternalUser; - render() { - const { user, orgs, isExternalUser, onOrgRoleChange, onOrgRemove, onOrgAdd } = this.props; - const { showAddOrgModal } = this.state; + return ( +
+

+ Organizations +

+ + + + {orgs.map((org, index) => ( + + ))} + +
- const canAddToOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersAdd) && !isExternalUser; - return ( -
-

- Organizations -

- - - - {orgs.map((org, index) => ( - - ))} - -
+
+ {canAddToOrg && ( + + )} +
+ +
+
+ ); +}); +UserOrgs.displayName = 'UserOrgs'; -
- {canAddToOrg && ( - - )} -
- -
-
- ); - } -} - -const getOrgRowStyles = stylesFactory((theme: GrafanaTheme2) => { +const getOrgRowStyles = (theme: GrafanaTheme2) => { return { removeButton: css({ marginRight: '0.6rem', @@ -130,9 +118,9 @@ const getOrgRowStyles = stylesFactory((theme: GrafanaTheme2) => { marginRight: theme.spacing(1), }), }; -}); +}; -interface OrgRowProps extends Themeable2 { +interface OrgRowProps { user?: UserDTO; org: UserOrg; isExternalUser?: boolean; @@ -140,124 +128,116 @@ interface OrgRowProps extends Themeable2 { onOrgRoleChange: (orgId: number, newRole: OrgRole) => void; } -class UnThemedOrgRow extends PureComponent { - state = { - currentRole: this.props.org.role, - isChangingRole: false, - roleOptions: [], - }; +const OrgRow = memo(({ user, org, isExternalUser, onOrgRemove, onOrgRoleChange }: OrgRowProps) => { + const [currentRole, setCurrentRole] = useState(org.role); + const [isChangingRole, setIsChangingRole] = useState(false); + const [roleOptions, setRoleOptions] = useState([]); + const styles = useStyles2(getOrgRowStyles); - componentDidMount() { + useEffect(() => { if (contextSrv.licensedAccessControlEnabled()) { if (contextSrv.hasPermission(AccessControlAction.ActionRolesList)) { - fetchRoleOptions(this.props.org.orgId) - .then((roles) => this.setState({ roleOptions: roles })) + fetchRoleOptions(org.orgId) + .then((roles) => setRoleOptions(roles)) .catch((e) => console.error(e)); } } - } + }, [org.orgId]); - onOrgRemove = async () => { - const { org } = this.props; - this.props.onOrgRemove(org.orgId); + const handleOrgRemove = async () => { + onOrgRemove(org.orgId); }; - onChangeRoleClick = () => { - const { org } = this.props; - this.setState({ isChangingRole: true, currentRole: org.role }); + const handleChangeRoleClick = () => { + setIsChangingRole(true); + setCurrentRole(org.role); }; - onOrgRoleChange = (newRole: OrgRole) => { - this.setState({ currentRole: newRole }); + const handleOrgRoleChange = (newRole: OrgRole) => { + setCurrentRole(newRole); }; - onOrgRoleSave = () => { - this.props.onOrgRoleChange(this.props.org.orgId, this.state.currentRole); + const handleOrgRoleSave = () => { + onOrgRoleChange(org.orgId, currentRole); }; - onCancelClick = () => { - this.setState({ isChangingRole: false }); + const handleCancelClick = () => { + setIsChangingRole(false); }; - onBasicRoleChange = (newRole: OrgRole) => { - this.props.onOrgRoleChange(this.props.org.orgId, newRole); + const handleBasicRoleChange = (newRole: OrgRole) => { + onOrgRoleChange(org.orgId, newRole); }; - render() { - const { user, org, isExternalUser, theme } = this.props; - const authSource = user?.authLabels?.length && user?.authLabels[0]; - const lockMessage = authSource ? `Synced via ${authSource}` : ''; - const { currentRole, isChangingRole } = this.state; - const styles = getOrgRowStyles(theme); - const labelClass = cx('width-16', styles.label); - const canChangeRole = contextSrv.hasPermission(AccessControlAction.OrgUsersWrite); - const canRemoveFromOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersRemove) && !isExternalUser; - const rolePickerDisabled = isExternalUser || !canChangeRole; + const authSource = user?.authLabels?.length && user?.authLabels[0]; + const lockMessage = authSource ? `Synced via ${authSource}` : ''; + const labelClass = cx('width-16', styles.label); + const canChangeRole = contextSrv.hasPermission(AccessControlAction.OrgUsersWrite); + const canRemoveFromOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersRemove) && !isExternalUser; + const rolePickerDisabled = isExternalUser || !canChangeRole; - const inputId = `${org.name}-input`; - return ( - - - - - {contextSrv.licensedAccessControlEnabled() ? ( - -
-
- -
- {isExternalUser && } + const inputId = `${org.name}-input`; + return ( + + + + + {contextSrv.licensedAccessControlEnabled() ? ( + +
+
+
- - ) : ( - <> - {isChangingRole ? ( - - - - ) : ( - {org.role} - )} - - {canChangeRole && ( - - )} - - - )} - - {canRemoveFromOrg && ( - - {t('admin.user-orgs.remove-button', 'Remove from organization')} - - )} + {isExternalUser && } +
- - ); - } -} - -const OrgRow = withTheme2(UnThemedOrgRow); + ) : ( + <> + {isChangingRole ? ( + + + + ) : ( + {org.role} + )} + + {canChangeRole && ( + + )} + + + )} + + {canRemoveFromOrg && ( + + {t('admin.user-orgs.remove-button', 'Remove from organization')} + + )} + + + ); +}); +OrgRow.displayName = 'OrgRow'; const getAddToOrgModalStyles = stylesFactory(() => ({ modal: css({ diff --git a/public/app/features/admin/UserSessions.tsx b/public/app/features/admin/UserSessions.tsx index f05278d7884..16fadaa8313 100644 --- a/public/app/features/admin/UserSessions.tsx +++ b/public/app/features/admin/UserSessions.tsx @@ -1,4 +1,4 @@ -import { createRef, PureComponent } from 'react'; +import { memo, useRef, useState } from 'react'; import { Trans, t } from '@grafana/i18n'; import { ConfirmButton, ConfirmModal, Button, Stack } from '@grafana/ui'; @@ -15,120 +15,107 @@ interface Props { onAllSessionsRevoke: () => void; } -interface State { - showLogoutModal: boolean; -} +export const UserSessions = memo(({ sessions, onSessionRevoke, onAllSessionsRevoke }: Props) => { + const [showLogoutModal, setShowLogoutModal] = useState(false); + const forceAllLogoutButton = useRef(null); -class BaseUserSessions extends PureComponent { - forceAllLogoutButton = createRef(); - state: State = { - showLogoutModal: false, + const showLogoutConfirmationModal = () => { + setShowLogoutModal(true); }; - showLogoutConfirmationModal = () => { - this.setState({ showLogoutModal: true }); + const dismissLogoutConfirmationModal = () => { + setShowLogoutModal(false); + forceAllLogoutButton.current?.focus(); }; - dismissLogoutConfirmationModal = () => { - this.setState({ showLogoutModal: false }, () => { - this.forceAllLogoutButton.current?.focus(); - }); - }; - - onSessionRevoke = (id: number) => { + const handleSessionRevoke = (id: number) => { return () => { - this.props.onSessionRevoke(id); + onSessionRevoke(id); }; }; - onAllSessionsRevoke = () => { - this.setState({ showLogoutModal: false }); - this.props.onAllSessionsRevoke(); + const handleAllSessionsRevoke = () => { + setShowLogoutModal(false); + onAllSessionsRevoke(); }; - render() { - const { sessions } = this.props; - const { showLogoutModal } = this.state; + const canLogout = contextSrv.hasPermission(AccessControlAction.UsersLogout); - const canLogout = contextSrv.hasPermission(AccessControlAction.UsersLogout); + return ( +
+

+ Sessions +

+ +
+ + + + + + + + + + + + {sessions && + sessions.map((session, index) => ( + + + + + + + + + ))} + +
+ Last seen + + Logged on + + IP address + + Browser and OS + + Identity Provider +
{session.isActive ? t('admin.user-sessions.now', 'Now') : session.seenAt}{formatDate(session.createdAt, { dateStyle: 'long' })}{session.clientIp}{`${session.browser} on ${session.os} ${session.osVersion}`} + {session.authModule && } + + {canLogout && ( + + {t('admin.user-sessions.force-logout-button', 'Force logout')} + + )} +
+
- return ( -
-

- Sessions -

- -
- - - - - - - - - - - - {sessions && - sessions.map((session, index) => ( - - - - - - - - - ))} - -
- Last seen - - Logged on - - IP address - - Browser and OS - - Identity Provider -
{session.isActive ? t('admin.user-sessions.now', 'Now') : session.seenAt}{formatDate(session.createdAt, { dateStyle: 'long' })}{session.clientIp}{`${session.browser} on ${session.os} ${session.osVersion}`} - {session.authModule && } - - {canLogout && ( - - {t('admin.user-sessions.force-logout-button', 'Force logout')} - - )} -
-
- -
- {canLogout && sessions.length > 0 && ( - +
+ {canLogout && sessions.length > 0 && ( + + )} + -
- -
- ); - } -} - -export const UserSessions = BaseUserSessions; + confirmText={t('admin.base-user-sessions.confirmText-force-logout', 'Force logout')} + onConfirm={handleAllSessionsRevoke} + onDismiss={dismissLogoutConfirmationModal} + /> +
+
+
+ ); +}); +UserSessions.displayName = 'UserSessions'; diff --git a/public/app/features/dashboard/components/DashboardSettings/TimePickerSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/TimePickerSettings.tsx index bfcd9a5e66d..ba88db1e1bf 100644 --- a/public/app/features/dashboard/components/DashboardSettings/TimePickerSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/TimePickerSettings.tsx @@ -1,6 +1,5 @@ import { isEmpty } from 'lodash'; -import { PureComponent } from 'react'; -import * as React from 'react'; +import { FormEvent, memo, useState } from 'react'; import { rangeUtil, TimeZone } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; @@ -24,49 +23,58 @@ interface Props { liveNow?: boolean; } -interface State { - isNowDelayValid: boolean; -} +export const TimePickerSettings = memo( + ({ + onWeekStartChange, + onTimeZoneChange, + onRefreshIntervalChange, + onNowDelayChange, + onHideTimePickerChange, + onLiveNowChange, + refreshIntervals, + timePickerHidden, + nowDelay, + timezone, + weekStart, + liveNow, + }: Props) => { + const [isNowDelayValid, setIsNowDelayValid] = useState(true); -export class TimePickerSettings extends PureComponent { - state: State = { isNowDelayValid: true }; + const handleNowDelayChange = (event: FormEvent) => { + const value = event.currentTarget.value; - onNowDelayChange = (event: React.FormEvent) => { - const value = event.currentTarget.value; + if (isEmpty(value)) { + setIsNowDelayValid(true); + return onNowDelayChange(value); + } - if (isEmpty(value)) { - this.setState({ isNowDelayValid: true }); - return this.props.onNowDelayChange(value); - } + if (rangeUtil.isValidTimeSpan(value)) { + setIsNowDelayValid(true); + return onNowDelayChange(value); + } - if (rangeUtil.isValidTimeSpan(value)) { - this.setState({ isNowDelayValid: true }); - return this.props.onNowDelayChange(value); - } + setIsNowDelayValid(false); + }; - this.setState({ isNowDelayValid: false }); - }; + const handleHideTimePickerChange = () => { + onHideTimePickerChange(!timePickerHidden); + }; - onHideTimePickerChange = () => { - this.props.onHideTimePickerChange(!this.props.timePickerHidden); - }; + const handleLiveNowChange = () => { + onLiveNowChange(!liveNow); + }; - onLiveNowChange = () => { - this.props.onLiveNowChange(!this.props.liveNow); - }; + const handleTimeZoneChange = (timeZone?: string) => { + if (typeof timeZone !== 'string') { + return; + } + onTimeZoneChange(timeZone); + }; - onTimeZoneChange = (timeZone?: string) => { - if (typeof timeZone !== 'string') { - return; - } - this.props.onTimeZoneChange(timeZone); - }; + const handleWeekStartChange = (weekStart?: WeekStart) => { + onWeekStartChange(weekStart); + }; - onWeekStartChange = (weekStart?: WeekStart) => { - this.props.onWeekStartChange(weekStart); - }; - - render() { return ( { @@ -85,17 +93,9 @@ export class TimePickerSettings extends PureComponent { label={t('dashboard-settings.time-picker.week-start-label', 'Week start')} data-testid={selectors.components.WeekStartPicker.containerV2} > - + - + { > - + { 'Continuously update panels when the time range includes the current time' )} > - + ); } -} +); +TimePickerSettings.displayName = 'TimePickerSettings'; diff --git a/public/app/features/datasources/components/DataSourcePluginSettings.tsx b/public/app/features/datasources/components/DataSourcePluginSettings.tsx index f920837275b..556896147fe 100644 --- a/public/app/features/datasources/components/DataSourcePluginSettings.tsx +++ b/public/app/features/datasources/components/DataSourcePluginSettings.tsx @@ -1,4 +1,4 @@ -import { createElement, PureComponent } from 'react'; +import { createElement, memo } from 'react'; import { DataSourcePluginMeta, DataSourceSettings } from '@grafana/data'; import { writableProxy } from 'app/features/plugins/extensions/utils'; @@ -12,36 +12,23 @@ export interface Props { onModelChange: (dataSource: DataSourceSettings) => void; } -export class DataSourcePluginSettings extends PureComponent { - constructor(props: Props) { - super(props); - - this.onModelChanged = this.onModelChanged.bind(this); +export const DataSourcePluginSettings = memo(({ plugin, dataSource, onModelChange }: Props) => { + if (!plugin) { + return null; } - onModelChanged = (dataSource: DataSourceSettings) => { - this.props.onModelChange(dataSource); - }; - - render() { - const { plugin, dataSource } = this.props; - - if (!plugin) { - return null; - } - - return ( -
- {plugin.components.ConfigEditor && - createElement(plugin.components.ConfigEditor, { - options: writableProxy(dataSource, { - source: 'datasource', - pluginId: plugin.meta?.id, - pluginVersion: plugin.meta?.info?.version, - }), - onOptionsChange: this.onModelChanged, - })} -
- ); - } -} + return ( +
+ {plugin.components.ConfigEditor && + createElement(plugin.components.ConfigEditor, { + options: writableProxy(dataSource, { + source: 'datasource', + pluginId: plugin.meta?.id, + pluginVersion: plugin.meta?.info?.version, + }), + onOptionsChange: onModelChange, + })} +
+ ); +}); +DataSourcePluginSettings.displayName = 'DataSourcePluginSettings'; diff --git a/public/app/features/explore/ExploreTimeControls.tsx b/public/app/features/explore/ExploreTimeControls.tsx index 07c92e8fba5..ef616131b6e 100644 --- a/public/app/features/explore/ExploreTimeControls.tsx +++ b/public/app/features/explore/ExploreTimeControls.tsx @@ -1,5 +1,3 @@ -import { Component } from 'react'; - import { TimeRange, RawTimeRange, dateTimeForTimeZone, dateMath } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { TimeZone } from '@grafana/schema'; @@ -22,9 +20,19 @@ export interface Props { onChangeFiscalYearStartMonth: (fiscalYearStartMonth: number) => void; } -export class ExploreTimeControls extends Component { - onMoveTimePicker = (direction: number) => { - const { range, onChangeTime, timeZone } = this.props; +export const ExploreTimeControls = ({ + range, + timeZone, + fiscalYearStartMonth, + splitted, + syncedTimes, + onChangeTimeSync, + hideText, + onChangeTimeZone, + onChangeFiscalYearStartMonth, + onChangeTime, +}: Props) => { + const onMoveTimePicker = (direction: number) => { const { from, to } = getShiftedTimeRange(direction, range); const nextTimeRange = { from: dateTimeForTimeZone(timeZone, from), @@ -34,14 +42,14 @@ export class ExploreTimeControls extends Component { onChangeTime(nextTimeRange); }; - onMoveForward = () => this.onMoveTimePicker(1); - onMoveBack = () => this.onMoveTimePicker(-1); + const onMoveForward = () => onMoveTimePicker(1); + const onMoveBack = () => onMoveTimePicker(-1); - onChangeTimePicker = (timeRange: TimeRange) => { + const onChangeTimePicker = (timeRange: TimeRange) => { const adjustedFrom = dateMath.isMathString(timeRange.raw.from) ? timeRange.raw.from : timeRange.from; const adjustedTo = dateMath.isMathString(timeRange.raw.to) ? timeRange.raw.to : timeRange.to; - this.props.onChangeTime({ + onChangeTime({ from: adjustedFrom, to: adjustedTo, }); @@ -52,8 +60,7 @@ export class ExploreTimeControls extends Component { }); }; - onZoom = () => { - const { range, onChangeTime, timeZone } = this.props; + const onZoom = () => { const { from, to } = getZoomedTimeRange(range, 2); const nextTimeRange = { from: dateTimeForTimeZone(timeZone, from), @@ -63,40 +70,27 @@ export class ExploreTimeControls extends Component { onChangeTime(nextTimeRange); }; - render() { - const { - range, - timeZone, - fiscalYearStartMonth, - splitted, - syncedTimes, - onChangeTimeSync, - hideText, - onChangeTimeZone, - onChangeFiscalYearStartMonth, - } = this.props; - const timeSyncButton = splitted ? : undefined; - const timePickerCommonProps = { - value: range, - timeZone, - fiscalYearStartMonth, - onMoveBackward: this.onMoveBack, - onMoveForward: this.onMoveForward, - onZoom: this.onZoom, - hideText, - }; + const timeSyncButton = splitted ? : undefined; + const timePickerCommonProps = { + value: range, + timeZone, + fiscalYearStartMonth, + onMoveBackward: onMoveBack, + onMoveForward: onMoveForward, + onZoom: onZoom, + hideText, + }; - return ( - - ); - } -} + return ( + + ); +}; diff --git a/public/app/features/explore/TraceView/components/common/SearchBarInput.test.tsx b/public/app/features/explore/TraceView/components/common/SearchBarInput.test.tsx index e613c07fcfc..6e7b590a016 100644 --- a/public/app/features/explore/TraceView/components/common/SearchBarInput.test.tsx +++ b/public/app/features/explore/TraceView/components/common/SearchBarInput.test.tsx @@ -18,15 +18,16 @@ import SearchBarInput from './SearchBarInput'; describe('SearchBarInput', () => { describe('rendering', () => { + const onChange = jest.fn(); it('renders as expected with no value', () => { - render(); + render(); const searchBarInput = screen.queryByPlaceholderText('Find...'); expect(searchBarInput).toBeInTheDocument(); expect(searchBarInput?.getAttribute('value')).toEqual(''); }); it('renders as expected with value', () => { - render(); + render(); const searchBarInput = screen.queryByPlaceholderText('Find...'); expect(searchBarInput).toBeInTheDocument(); expect(searchBarInput?.getAttribute('value')).toEqual('value'); diff --git a/public/app/features/explore/TraceView/components/common/SearchBarInput.tsx b/public/app/features/explore/TraceView/components/common/SearchBarInput.tsx index 58db71fd862..a6527ca1c0e 100644 --- a/public/app/features/explore/TraceView/components/common/SearchBarInput.tsx +++ b/public/app/features/explore/TraceView/components/common/SearchBarInput.tsx @@ -12,49 +12,44 @@ // See the License for the specific language governing permissions and // limitations under the License. -import * as React from 'react'; +import { memo } from 'react'; import { t } from '@grafana/i18n'; import { IconButton, Input } from '@grafana/ui'; type Props = { - value: string | undefined; + value?: string; onChange: (value: string) => void; }; -export default class SearchBarInput extends React.PureComponent { - static defaultProps: Partial = { - value: undefined, +const SearchBarInput = memo(({ value, onChange }: Props) => { + const clearUiFind = () => { + onChange(''); }; - clearUiFind = () => { - this.props.onChange(''); - }; - - render() { - const { value } = this.props; - - const suffix = ( - <> - {value && value.length && ( - - )} - - ); - - return ( -
- this.props.onChange(e.currentTarget.value)} - suffix={suffix} - value={value} + const suffix = ( + <> + {value && value.length && ( + -
- ); - } -} + )} + + ); + + return ( +
+ onChange(e.currentTarget.value)} + suffix={suffix} + value={value} + /> +
+ ); +}); +SearchBarInput.displayName = 'SearchBarInput'; + +export default SearchBarInput; diff --git a/public/app/features/live/LiveConnectionWarning.tsx b/public/app/features/live/LiveConnectionWarning.tsx index 27595f0b908..2f78ebd22db 100644 --- a/public/app/features/live/LiveConnectionWarning.tsx +++ b/public/app/features/live/LiveConnectionWarning.tsx @@ -1,72 +1,63 @@ import { css } from '@emotion/css'; -import { PureComponent } from 'react'; +import { memo, useEffect, useRef, useState } from 'react'; import { Unsubscribable } from 'rxjs'; import { GrafanaTheme2, OrgRole } from '@grafana/data'; import { t } from '@grafana/i18n'; import { config, getGrafanaLiveSrv } from '@grafana/runtime'; -import { Alert, stylesFactory } from '@grafana/ui'; +import { Alert, useStyles2 } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; export interface Props {} -export interface State { - show?: boolean; -} +export const LiveConnectionWarning = memo(function LiveConnectionWarning() { + const [show, setShow] = useState(undefined); + const subscriptionRef = useRef(); + const styles = useStyles2(getStyle); -export class LiveConnectionWarning extends PureComponent { - subscription?: Unsubscribable; - styles = getStyle(config.theme2); - state: State = {}; - - componentDidMount() { + useEffect(() => { // Only show the error in development mode if (process.env.NODE_ENV === 'development') { // Wait a second to listen for server errors - setTimeout(this.initListener, 1500); + const timer = setTimeout(() => { + const live = getGrafanaLiveSrv(); + if (live) { + subscriptionRef.current = live.getConnectionState().subscribe({ + next: (v) => { + setShow(!v); + }, + }); + } + }, 1500); + + return () => { + clearTimeout(timer); + if (subscriptionRef.current) { + subscriptionRef.current.unsubscribe(); + } + }; } + + return undefined; + }, []); + + if (show) { + if (!contextSrv.isSignedIn || !config.liveEnabled || contextSrv.user.orgRole === OrgRole.None) { + return null; // do not show the warning for anonymous users or ones with no org (and /login page etc) + } + + return ( + + ); } + return null; +}); - initListener = () => { - const live = getGrafanaLiveSrv(); - if (live) { - this.subscription = live.getConnectionState().subscribe({ - next: (v) => { - this.setState({ show: !v }); - }, - }); - } - }; - - componentWillUnmount() { - if (this.subscription) { - this.subscription.unsubscribe(); - } - } - - render() { - const { show } = this.state; - if (show) { - if (!contextSrv.isSignedIn || !config.liveEnabled || contextSrv.user.orgRole === OrgRole.None) { - return null; // do not show the warning for anonymous users or ones with no org (and /login page etc) - } - - return ( - - ); - } - return null; - } -} - -const getStyle = stylesFactory((theme: GrafanaTheme2) => ({ +const getStyle = (theme: GrafanaTheme2) => ({ warn: css({ position: 'fixed', bottom: 0, @@ -76,4 +67,4 @@ const getStyle = stylesFactory((theme: GrafanaTheme2) => ({ zIndex: theme.zIndex.portal, cursor: 'wait', }), -})); +}); diff --git a/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx b/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx index e31ea83a565..91bcb2c1542 100644 --- a/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx +++ b/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx @@ -1,4 +1,4 @@ -import { PureComponent, ChangeEvent } from 'react'; +import { memo, ChangeEvent } from 'react'; import { DataTransformerID, @@ -18,13 +18,8 @@ import lightImage from '../images/light/concatenate.svg'; interface ConcatenateTransformerEditorProps extends TransformerUIProps {} -export class ConcatenateTransformerEditor extends PureComponent { - constructor(props: ConcatenateTransformerEditorProps) { - super(props); - } - - onModeChanged = (value: SelectableValue) => { - const { options, onChange } = this.props; +export const ConcatenateTransformerEditor = memo(({ options, onChange }: ConcatenateTransformerEditorProps) => { + const onModeChanged = (value: SelectableValue) => { const frameNameMode = value.value ?? ConcatenateFrameNameMode.FieldName; onChange({ ...options, @@ -32,74 +27,63 @@ export class ConcatenateTransformerEditor extends PureComponent) => { - const { options } = this.props; - this.props.onChange({ + const onLabelChanged = (evt: ChangeEvent) => { + onChange({ ...options, frameNameLabel: evt.target.value, }); }; - //--------------------------------------------------------- - // Render - //--------------------------------------------------------- + const nameModes: Array> = [ + { + value: ConcatenateFrameNameMode.FieldName, + label: t( + 'transformers.concatenate-transformer-editor.name-modes.label.copy-frame-name-to-field', + 'Copy frame name to field name' + ), + }, + { + value: ConcatenateFrameNameMode.Label, + label: t( + 'transformers.concatenate-transformer-editor.name-modes.label.label-frame', + 'Add a label with the frame name' + ), + }, + { + value: ConcatenateFrameNameMode.Drop, + label: t( + 'transformers.concatenate-transformer-editor.name-modes.label.ignore-the-frame-name', + 'Ignore the frame name' + ), + }, + ]; - render() { - const { options } = this.props; - const nameModes: Array> = [ - { - value: ConcatenateFrameNameMode.FieldName, - label: t( - 'transformers.concatenate-transformer-editor.name-modes.label.copy-frame-name-to-field', - 'Copy frame name to field name' - ), - }, - { - value: ConcatenateFrameNameMode.Label, - label: t( - 'transformers.concatenate-transformer-editor.name-modes.label.label-frame', - 'Add a label with the frame name' - ), - }, - { - value: ConcatenateFrameNameMode.Drop, - label: t( - 'transformers.concatenate-transformer-editor.name-modes.label.ignore-the-frame-name', - 'Ignore the frame name' - ), - }, - ]; + const frameNameMode = options.frameNameMode ?? ConcatenateFrameNameMode.FieldName; - const frameNameMode = options.frameNameMode ?? ConcatenateFrameNameMode.FieldName; - - return ( -
- - v.value === frameNameMode)} + onChange={onModeChanged} + /> + + {frameNameMode === ConcatenateFrameNameMode.Label && ( + + v.value === frameNameMode)} - onChange={this.onModeChanged} + value={options.frameNameLabel ?? ''} + placeholder={t('transformers.concatenate-transformer-editor.placeholder-frame', 'Frame')} + onChange={onLabelChanged} /> - {frameNameMode === ConcatenateFrameNameMode.Label && ( - - - - )} -
- ); - } -} + )} +
+ ); +}); +ConcatenateTransformerEditor.displayName = 'ConcatenateTransformerEditor'; export const getConcatenateTransformRegistryItem: () => TransformerRegistryItem = () => ({ diff --git a/public/app/features/variables/constant/ConstantVariableEditor.tsx b/public/app/features/variables/constant/ConstantVariableEditor.tsx index 6325ef2890c..5eb7a1f352d 100644 --- a/public/app/features/variables/constant/ConstantVariableEditor.tsx +++ b/public/app/features/variables/constant/ConstantVariableEditor.tsx @@ -1,4 +1,4 @@ -import { FormEvent, PureComponent } from 'react'; +import { FormEvent, memo } from 'react'; import { ConstantVariableModel } from '@grafana/data'; import { ConstantVariableForm } from 'app/features/dashboard-scene/settings/variables/components/ConstantVariableForm'; @@ -7,16 +7,15 @@ import { VariableEditorProps } from '../editor/types'; export interface Props extends VariableEditorProps {} -export class ConstantVariableEditor extends PureComponent { - onChange = (event: FormEvent) => { - this.props.onPropChange({ +export const ConstantVariableEditor = memo(({ variable, onPropChange }: Props) => { + const onChange = (event: FormEvent) => { + onPropChange({ propName: 'query', propValue: event.currentTarget.value, updateOptions: true, }); }; - render() { - return ; - } -} + return ; +}); +ConstantVariableEditor.displayName = 'ConstantVariableEditor'; diff --git a/public/app/features/variables/pickers/shared/VariableInput.tsx b/public/app/features/variables/pickers/shared/VariableInput.tsx index 7ff43d04657..8f577ac5da5 100644 --- a/public/app/features/variables/pickers/shared/VariableInput.tsx +++ b/public/app/features/variables/pickers/shared/VariableInput.tsx @@ -1,48 +1,45 @@ -import { PureComponent } from 'react'; -import * as React from 'react'; +import { memo, type KeyboardEvent, type HTMLProps } from 'react'; import { t } from '@grafana/i18n'; import { NavigationKey } from '../types'; -export interface Props extends Omit, 'onChange' | 'value'> { +export interface Props extends Omit, 'onChange' | 'value'> { onChange: (value: string) => void; onNavigate: (key: NavigationKey, clearOthers: boolean) => void; value: string | null; } -export class VariableInput extends PureComponent { - onKeyDown = (event: React.KeyboardEvent) => { +export const VariableInput = memo(({ value, id, onNavigate, onChange, ...restProps }: Props) => { + const onKeyDown = (event: KeyboardEvent) => { if (NavigationKey[event.keyCode] && event.keyCode !== NavigationKey.select) { const clearOthers = event.ctrlKey || event.metaKey || event.shiftKey; - this.props.onNavigate(event.keyCode, clearOthers); + onNavigate(event.keyCode, clearOthers); event.preventDefault(); } }; - onChange = (event: React.ChangeEvent) => { - this.props.onChange(event.target.value); + const handleChange = (event: React.ChangeEvent) => { + onChange(event.target.value); }; - render() { - const { value, id, onNavigate, ...restProps } = this.props; - return ( - { - if (instance) { - instance.focus(); - instance.setAttribute('style', `width:${Math.max(instance.width, 150)}px`); - } - }} - id={id} - type="text" - className="gf-form-input" - value={value ?? ''} - onChange={this.onChange} - onKeyDown={this.onKeyDown} - placeholder={t('variable.dropdown.placeholder', 'Enter variable value')} - /> - ); - } -} + return ( + { + if (instance) { + instance.focus(); + instance.setAttribute('style', `width:${Math.max(instance.width, 150)}px`); + } + }} + id={id} + type="text" + className="gf-form-input" + value={value ?? ''} + onChange={handleChange} + onKeyDown={onKeyDown} + placeholder={t('variable.dropdown.placeholder', 'Enter variable value')} + /> + ); +}); +VariableInput.displayName = 'VariableInput'; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/ConfigEditor/ConfigEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/ConfigEditor/ConfigEditor.tsx index 9889693efae..d866ad49185 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/ConfigEditor/ConfigEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/ConfigEditor/ConfigEditor.tsx @@ -1,4 +1,4 @@ -import { PureComponent } from 'react'; +import { memo } from 'react'; import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; import { ConnectionConfig } from '@grafana/google-sdk'; @@ -10,8 +10,8 @@ import { CloudMonitoringOptions, CloudMonitoringSecureJsonData } from '../../typ export type Props = DataSourcePluginOptionsEditorProps; -export class ConfigEditor extends PureComponent { - handleOnOptionsChange = (options: Props['options']) => { +export const ConfigEditor = memo(({ options, onOptionsChange }: Props) => { + const handleOnOptionsChange = (options: Props['options']) => { if (options.jsonData.privateKeyPath || options.secureJsonFields['privateKey']) { reportInteraction('grafana_cloud_monitoring_config_changed', { authenticationType: 'JWT', @@ -19,34 +19,32 @@ export class ConfigEditor extends PureComponent { privateKeyPath: !!options.jsonData.privateKeyPath, }); } - this.props.onOptionsChange(options); + onOptionsChange(options); }; - render() { - const { options, onOptionsChange } = this.props; - return ( - <> - - - - {config.secureSocksDSProxyEnabled && ( - <> - - - - - - )} - - ); - } -} + return ( + <> + + + + {config.secureSocksDSProxyEnabled && ( + <> + + + + + + )} + + ); +}); +ConfigEditor.displayName = 'ConfigEditor'; diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/components/CSVWaveEditor.tsx b/public/app/plugins/datasource/grafana-testdata-datasource/components/CSVWaveEditor.tsx index ac2a9a2a779..1643cc0e9bd 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/components/CSVWaveEditor.tsx +++ b/public/app/plugins/datasource/grafana-testdata-datasource/components/CSVWaveEditor.tsx @@ -1,4 +1,4 @@ -import { PureComponent, useState } from 'react'; +import { memo, useState } from 'react'; import * as React from 'react'; import { Button, InlineField, InlineFieldRow, Input } from '@grafana/ui'; @@ -93,43 +93,42 @@ const CSVWaveEditor = (props: WaveProps) => { ); }; -export class CSVWavesEditor extends PureComponent { - onChange = (index: number, wave?: CSVWave) => { - let waves = [...(this.props.waves ?? defaultCSVWaveQuery)]; +export const CSVWavesEditor = memo(({ waves, onChange }: WavesProps) => { + const handleChange = (index: number, wave?: CSVWave) => { + let wavesArray = [...(waves ?? defaultCSVWaveQuery)]; if (wave) { - waves[index] = { ...wave }; + wavesArray[index] = { ...wave }; } else { // remove the element - waves.splice(index, 1); + wavesArray.splice(index, 1); } - this.props.onChange(waves); + onChange(wavesArray); }; - onAdd = () => { - const waves = [...(this.props.waves ?? defaultCSVWaveQuery)]; - waves.push({ ...defaultCSVWaveQuery[0] }); - this.props.onChange(waves); + const onAdd = () => { + const wavesArray = [...(waves ?? defaultCSVWaveQuery)]; + wavesArray.push({ ...defaultCSVWaveQuery[0] }); + onChange(wavesArray); }; - render() { - let waves = this.props.waves ?? defaultCSVWaveQuery; - if (!waves.length) { - waves = defaultCSVWaveQuery; - } - - return ( - <> - {waves.map((wave, index) => ( - - ))} - - ); + let wavesArray = waves ?? defaultCSVWaveQuery; + if (!wavesArray.length) { + wavesArray = defaultCSVWaveQuery; } -} + + return ( + <> + {wavesArray.map((wave, index) => ( + + ))} + + ); +}); +CSVWavesEditor.displayName = 'CSVWavesEditor'; diff --git a/public/app/plugins/panel/stat/StatPanel.tsx b/public/app/plugins/panel/stat/StatPanel.tsx index 6d1c35cc63a..055fc4adc93 100644 --- a/public/app/plugins/panel/stat/StatPanel.tsx +++ b/public/app/plugins/panel/stat/StatPanel.tsx @@ -1,5 +1,5 @@ import { isNumber } from 'lodash'; -import { PureComponent } from 'react'; +import { memo, useCallback } from 'react'; import { DisplayValueAlignmentFactors, @@ -12,120 +12,131 @@ import { } from '@grafana/data'; import { findNumericFieldMinMax } from '@grafana/data/internal'; import { BigValueTextMode, BigValueGraphMode } from '@grafana/schema'; -import { BigValue, DataLinksContextMenu, VizRepeater, VizRepeaterRenderValueProps } from '@grafana/ui'; +import { BigValue, DataLinksContextMenu, useTheme2, VizRepeater, VizRepeaterRenderValueProps } from '@grafana/ui'; import { DataLinksContextMenuApi } from '@grafana/ui/internal'; -import { config } from 'app/core/config'; import { Options } from './panelcfg.gen'; -export class StatPanel extends PureComponent> { - renderComponent = ( - valueProps: VizRepeaterRenderValueProps, - menuProps: DataLinksContextMenuApi - ): JSX.Element => { - const { timeRange, options } = this.props; - const { value, alignmentFactors, width, height, count } = valueProps; - const { openMenu, targetClassName } = menuProps; - let sparkline = value.sparkline; - if (sparkline) { - sparkline.timeRange = timeRange; - } +export const StatPanel = memo( + ({ + timeRange, + options, + fieldConfig, + title, + data, + replaceVariables, + timeZone, + height, + width, + renderCounter, + }: PanelProps) => { + const theme = useTheme2(); - return ( - + const getTextMode = useCallback(() => { + // If we have manually set displayName or panel title switch text mode to value and name + if (options.textMode === BigValueTextMode.Auto && (fieldConfig.defaults.displayName || !title)) { + return BigValueTextMode.ValueAndName; + } + + return options.textMode; + }, [options.textMode, fieldConfig.defaults.displayName, title]); + + const renderComponent = useCallback( + ( + valueProps: VizRepeaterRenderValueProps, + menuProps: DataLinksContextMenuApi + ): JSX.Element => { + const { value, alignmentFactors, width, height, count } = valueProps; + const { openMenu, targetClassName } = menuProps; + let sparkline = value.sparkline; + if (sparkline) { + sparkline.timeRange = timeRange; + } + + return ( + + ); + }, + [theme, timeRange, options, getTextMode] ); - }; - getTextMode() { - const { options, fieldConfig, title } = this.props; + const renderValue = useCallback( + (valueProps: VizRepeaterRenderValueProps): JSX.Element => { + const { value } = valueProps; + const { getLinks, hasLinks } = value; - // If we have manually set displayName or panel title switch text mode to value and name - if (options.textMode === BigValueTextMode.Auto && (fieldConfig.defaults.displayName || !title)) { - return BigValueTextMode.ValueAndName; - } + if (hasLinks && getLinks) { + return ( + + {(api) => { + return renderComponent(valueProps, api); + }} + + ); + } - return options.textMode; - } + return renderComponent(valueProps, {}); + }, + [renderComponent] + ); - renderValue = (valueProps: VizRepeaterRenderValueProps): JSX.Element => { - const { value } = valueProps; - const { getLinks, hasLinks } = value; + const getValues = useCallback((): FieldDisplay[] => { + let globalRange: NumericRange | undefined = undefined; - if (hasLinks && getLinks) { - return ( - - {(api) => { - return this.renderComponent(valueProps, api); - }} - - ); - } - - return this.renderComponent(valueProps, {}); - }; - - getValues = (): FieldDisplay[] => { - const { data, options, replaceVariables, fieldConfig, timeZone } = this.props; - - let globalRange: NumericRange | undefined = undefined; - - for (let frame of data.series) { - for (let field of frame.fields) { - let { config } = field; - // mostly copied from fieldOverrides, since they are skipped during streaming - // Set the Min/Max value automatically - if (field.type === FieldType.number) { - if (field.state?.range) { - continue; + for (let frame of data.series) { + for (let field of frame.fields) { + let { config } = field; + // mostly copied from fieldOverrides, since they are skipped during streaming + // Set the Min/Max value automatically + if (field.type === FieldType.number) { + if (field.state?.range) { + continue; + } + if (!globalRange && (!isNumber(config.min) || !isNumber(config.max))) { + globalRange = findNumericFieldMinMax(data.series); + } + const min = config.min ?? globalRange!.min; + const max = config.max ?? globalRange!.max; + field.state = field.state ?? {}; + field.state.range = { min, max, delta: max! - min! }; } - if (!globalRange && (!isNumber(config.min) || !isNumber(config.max))) { - globalRange = findNumericFieldMinMax(data.series); - } - const min = config.min ?? globalRange!.min; - const max = config.max ?? globalRange!.max; - field.state = field.state ?? {}; - field.state.range = { min, max, delta: max! - min! }; } } - } - return getFieldDisplayValues({ - fieldConfig, - reduceOptions: options.reduceOptions, - replaceVariables, - theme: config.theme2, - data: data.series, - sparkline: options.graphMode !== BigValueGraphMode.None, - percentChange: options.showPercentChange, - timeZone, - }); - }; - - render() { - const { height, options, width, data, renderCounter } = this.props; + return getFieldDisplayValues({ + fieldConfig, + reduceOptions: options.reduceOptions, + replaceVariables, + theme, + data: data.series, + sparkline: options.graphMode !== BigValueGraphMode.None, + percentChange: options.showPercentChange, + timeZone, + }); + }, [data, fieldConfig, theme, options, replaceVariables, timeZone]); return ( > { /> ); } -} +); +StatPanel.displayName = 'StatPanel'; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index dcbf60af7b5..2d5bd426837 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -8782,6 +8782,9 @@ "drawer": { "close": "Close" }, + "error-boundary": { + "title": "An unexpected error happened" + }, "feature-badge": { "experimental": "Experimental", "new": "New!",