Chore: convert some class components to functional (#112661)

* convert BigValue to functional component

* convert AbstractList

* Revert "convert AbstractList"

This reverts commit 673c22a81d.

* 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
This commit is contained in:
Ashley Harrison
2025-10-21 17:33:38 +01:00
committed by GitHub
parent 5d82502535
commit 7678fc9de1
23 changed files with 471 additions and 592 deletions
-100
View File
@@ -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
@@ -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<Props> {
interface StoryProps extends Props {
numeric: number;
title: string;
color: string;
@@ -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<HTMLElement>;
onClick?: MouseEventHandler<HTMLElement>;
/** 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<Props> {
static defaultProps: Partial<Props> = {
justifyMode: BigValueJustifyMode.Auto,
};
export const BigValue = memo<Props>((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 (
<div className={className} style={panelStyles} title={tooltip}>
<div style={valueAndTitleContainerStyles}>
{textValues.title && <div style={titleStyles}>{textValues.title}</div>}
<FormattedValueDisplay value={textValues} style={valueStyles} />
{showPercentChange && (
<PercentChange
percentChange={percentChange}
styles={layout.getPercentChangeStyles(percentChange, percentChangeColorMode, valueStyles)}
/>
)}
</div>
{layout.renderChart()}
</div>
);
}
// When there is an outer data link this tooltip will override the outer native tooltip
const tooltip = hasLinks ? undefined : textValues.tooltip;
if (!onClick) {
return (
<button
type="button"
className={cx(clearButtonStyles(theme), className)}
style={panelStyles}
onClick={onClick}
title={tooltip}
>
<div className={className} style={panelStyles} title={tooltip}>
<div style={valueAndTitleContainerStyles}>
{textValues.title && <div style={titleStyles}>{textValues.title}</div>}
<FormattedValueDisplay value={textValues} style={valueStyles} />
{showPercentChange && (
<PercentChange
percentChange={percentChange}
styles={layout.getPercentChangeStyles(percentChange, percentChangeColorMode, valueStyles)}
/>
)}
</div>
{layout.renderChart()}
</button>
</div>
);
}
}
return (
<button
type="button"
className={cx(clearButtonStyles(theme), className)}
style={panelStyles}
onClick={onClick}
title={tooltip}
>
<div style={valueAndTitleContainerStyles}>
{textValues.title && <div style={titleStyles}>{textValues.title}</div>}
<FormattedValueDisplay value={textValues} style={valueStyles} />
</div>
{layout.renderChart()}
</button>
);
});
BigValue.displayName = 'BigValue';
@@ -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<T> extends PureComponent<AbstractListProps<T>> {
constructor(props: AbstractListProps<T>) {
super(props);
@@ -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<T> extends PureComponent<ListProps<T>> {
render() {
return <AbstractList inline {...this.props} />;
@@ -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<T> extends PureComponent<ListProps<T>> {
render() {
return <AbstractList {...this.props} />;
@@ -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<CanvasElementProps> {
render() {
const { config } = this.props;
return (
<div>
<Trans
i18nKey="canvas.not-found-display.not-found"
components={{ config: <pre>{JSON.stringify(config, null, 2)}</pre> }}
>
<h3>Not found: </h3>
{'<config />'}
</Trans>
</div>
);
}
}
const NotFoundDisplay = memo(({ config }: CanvasElementProps) => {
return (
<div>
<Trans
i18nKey="canvas.not-found-display.not-found"
components={{ config: <pre>{JSON.stringify(config, null, 2)}</pre> }}
>
<h3>Not found: </h3>
{'<config />'}
</Trans>
</div>
);
});
NotFoundDisplay.displayName = 'NotFoundDisplay';
export const notFoundItem: CanvasElementItem = {
id: 'not-found',
@@ -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<CanvasElementProps<TextConfig, TextData>> {
render() {
const { data } = this.props;
const styles = getStyles(config.theme2, data);
const RectangleDisplay = memo<CanvasElementProps<TextConfig, TextData>>(({ data }) => {
const styles = useStyles2(getStyles, data);
return (
<div className={styles.container}>
<span className={styles.span}>{data?.text}</span>
</div>
);
}
}
return (
<div className={styles.container}>
<span className={styles.span}>{data?.text}</span>
</div>
);
});
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<TextConfig, TextData> = {
id: 'rectangle',
@@ -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 };
});
@@ -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<DashboardPanelProps> {
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 };
});
@@ -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<ThresholdsConfig, ThresholdsFieldConfigSettings>;
export class ThresholdsValueEditor extends PureComponent<Props> {
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 <ThresholdsEditor thresholds={thresholdsValue} onChange={onChange} />;
});
return <ThresholdsEditor thresholds={value} onChange={onChange} />;
}
}
ThresholdsValueEditor.displayName = 'ThresholdsValueEditor';
@@ -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<SpanGraphProps> {
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<SpanGraphProps> {
<ViewingLayer
viewRange={viewRange}
numTicks={TIMELINE_TICK_INTERVAL}
height={height || DEFAULT_HEIGHT}
height={height}
updateViewRangeTime={updateViewRangeTime}
updateNextViewRangeTime={updateNextViewRangeTime}
/>
@@ -95,4 +90,8 @@ export default class SpanGraph extends React.PureComponent<SpanGraphProps> {
</div>
);
}
}
);
SpanGraph.displayName = 'SpanGraph';
export default SpanGraph;
+26 -27
View File
@@ -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<typeof connector>;
class InviteeRow extends PureComponent<Props> {
render() {
const { invitee, revokeInvite } = this.props;
return (
<tr>
<td>{invitee.email}</td>
<td>{invitee.name}</td>
<td className="text-right">
<ClipboardButton icon="copy" variant="secondary" size="sm" getText={() => invitee.url}>
Copy Invite
</ClipboardButton>
&nbsp;
</td>
<td>
<Button
variant="destructive"
size="sm"
icon="times"
onClick={() => revokeInvite(invitee.code)}
aria-label={t('invites.invitee-row.aria-label-revoke-invite', 'Revoke invite')}
/>
</td>
</tr>
);
}
}
const InviteeRow = memo<Props>(({ invitee, revokeInvite }) => {
return (
<tr>
<td>{invitee.email}</td>
<td>{invitee.name}</td>
<td className="text-right">
<ClipboardButton icon="copy" variant="secondary" size="sm" getText={() => invitee.url}>
Copy Invite
</ClipboardButton>
&nbsp;
</td>
<td>
<Button
variant="destructive"
size="sm"
icon="times"
onClick={() => revokeInvite(invitee.code)}
aria-label={t('invites.invitee-row.aria-label-revoke-invite', 'Revoke invite')}
/>
</td>
</tr>
);
});
InviteeRow.displayName = 'InviteeRow';
export default connector(InviteeRow);
+27 -27
View File
@@ -1,4 +1,4 @@
import { PureComponent } from 'react';
import { memo } from 'react';
import { Trans } from '@grafana/i18n';
import { Invitee } from 'app/types/user';
@@ -9,30 +9,30 @@ export interface Props {
invitees: Invitee[];
}
export default class InviteesTable extends PureComponent<Props> {
render() {
const { invitees } = this.props;
const InviteesTable = memo(({ invitees }: Props) => {
return (
<table className="filter-table form-inline">
<thead>
<tr>
<th>
<Trans i18nKey="invites.invitees-table.email">Email</Trans>
</th>
<th>
<Trans i18nKey="invites.invitees-table.name">Name</Trans>
</th>
<th />
<th style={{ width: '34px' }} />
</tr>
</thead>
<tbody data-testid="InviteesTable-body">
{invitees.map((invitee, index) => {
return <InviteeRow key={`${invitee.id}-${index}`} invitee={invitee} />;
})}
</tbody>
</table>
);
});
return (
<table className="filter-table form-inline">
<thead>
<tr>
<th>
<Trans i18nKey="invites.invitees-table.email">Email</Trans>
</th>
<th>
<Trans i18nKey="invites.invitees-table.name">Name</Trans>
</th>
<th />
<th style={{ width: '34px' }} />
</tr>
</thead>
<tbody data-testid="InviteesTable-body">
{invitees.map((invitee, index) => {
return <InviteeRow key={`${invitee.id}-${index}`} invitee={invitee} />;
})}
</tbody>
</table>
);
}
}
InviteesTable.displayName = 'InviteesTable';
export default InviteesTable;
@@ -1,5 +1,5 @@
// Libraries
import { PureComponent, ReactNode } from 'react';
import { memo, ReactNode } from 'react';
// Types
import { PanelProps, PanelPlugin, PluginType, PanelPluginMeta } from '@grafana/data';
@@ -13,44 +13,41 @@ interface Props {
text?: ReactNode;
}
class PanelPluginError extends PureComponent<Props> {
constructor(props: Props) {
super(props);
}
const PanelPluginError = memo<Props>(({ title, text }) => {
const style = {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
};
render() {
const style = {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
};
return (
<div style={style}>
<Alert severity={AppNotificationSeverity.Error} title={title}>
{text}
</Alert>
</div>
);
});
return (
<div style={style}>
<Alert severity={AppNotificationSeverity.Error} {...this.props} />
</div>
);
}
}
PanelPluginError.displayName = 'PanelPluginError';
export function getPanelPluginLoadError(meta: PanelPluginMeta, err: unknown): PanelPlugin {
const LoadError = class LoadError extends PureComponent<PanelProps> {
render() {
const text = (
<Trans i18nKey="panel.panel-plugin-error.text-load-error">
Check the server startup logs for more information. <br />
If this plugin was loaded from Git, then make sure it was compiled.
</Trans>
);
return (
<PanelPluginError
title={t('panel.panel-plugin-error.title-load-error', 'Error loading: {{panelId}}', { panelId: meta.id })}
text={text}
/>
);
}
};
const LoadError = memo<PanelProps>(() => {
const text = (
<Trans i18nKey="panel.panel-plugin-error.text-load-error">
Check the server startup logs for more information. <br />
If this plugin was loaded from Git, then make sure it was compiled.
</Trans>
);
return (
<PanelPluginError
title={t('panel.panel-plugin-error.title-load-error', 'Error loading: {{panelId}}', { panelId: meta.id })}
text={text}
/>
);
});
LoadError.displayName = 'LoadError';
const plugin = new PanelPlugin(LoadError);
plugin.meta = meta;
plugin.loadError = true;
@@ -58,15 +55,14 @@ export function getPanelPluginLoadError(meta: PanelPluginMeta, err: unknown): Pa
}
export function getPanelPluginNotFound(id: string, silent?: boolean): PanelPlugin {
const NotFound = class NotFound extends PureComponent<PanelProps> {
render() {
return (
<PanelPluginError
title={t('panel.panel-plugin-error.title-not-found', 'Panel plugin not found: {{id}}', { id })}
/>
);
}
};
const NotFound = memo<PanelProps>(() => {
return (
<PanelPluginError
title={t('panel.panel-plugin-error.title-not-found', 'Panel plugin not found: {{id}}', { id })}
/>
);
});
NotFound.displayName = 'NotFound';
const plugin = new PanelPlugin(silent ? () => null : NotFound);
@@ -1,4 +1,4 @@
import { PureComponent } from 'react';
import { memo } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
@@ -12,73 +12,71 @@ export interface Props {
setUserOrg: (org: UserOrg) => void;
}
export class UserOrganizations extends PureComponent<Props> {
render() {
const { isLoading, orgs, user } = this.props;
if (isLoading) {
return (
<LoadingPlaceholder
text={t('profile.user-organizations.text-loading-organizations', 'Loading organizations...')}
/>
);
}
if (orgs.length === 0) {
return null;
}
export const UserOrganizations = memo<Props>(({ isLoading, orgs, user, setUserOrg }) => {
if (isLoading) {
return (
<div>
<h3 className="page-sub-heading">
<Trans i18nKey="user-orgs.title">Organizations</Trans>
</h3>
<ScrollContainer overflowY="visible" overflowX="auto" width="100%">
<table className="filter-table form-inline" data-testid={selectors.components.UserProfile.orgsTable}>
<thead>
<tr>
<th>
<Trans i18nKey="user-orgs.name-column">Name</Trans>
</th>
<th>
<Trans i18nKey="user-orgs.role-column">Role</Trans>
</th>
<th />
</tr>
</thead>
<tbody>
{orgs.map((org: UserOrg, index) => {
return (
<tr key={index}>
<td>{org.name}</td>
<td>{org.role}</td>
<td className="text-right">
{org.orgId === user?.orgId ? (
<Button variant="secondary" size="sm" disabled>
<Trans i18nKey="user-orgs.current-org-button">Current</Trans>
</Button>
) : (
<Button
variant="secondary"
size="sm"
onClick={() => {
this.props.setUserOrg(org);
}}
>
<Trans i18nKey="user-orgs.select-org-button">Select organisation</Trans>
</Button>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</ScrollContainer>
</div>
<LoadingPlaceholder
text={t('profile.user-organizations.text-loading-organizations', 'Loading organizations...')}
/>
);
}
}
if (orgs.length === 0) {
return null;
}
return (
<div>
<h3 className="page-sub-heading">
<Trans i18nKey="user-orgs.title">Organizations</Trans>
</h3>
<ScrollContainer overflowY="visible" overflowX="auto" width="100%">
<table className="filter-table form-inline" data-testid={selectors.components.UserProfile.orgsTable}>
<thead>
<tr>
<th>
<Trans i18nKey="user-orgs.name-column">Name</Trans>
</th>
<th>
<Trans i18nKey="user-orgs.role-column">Role</Trans>
</th>
<th />
</tr>
</thead>
<tbody>
{orgs.map((org: UserOrg, index) => {
return (
<tr key={index}>
<td>{org.name}</td>
<td>{org.role}</td>
<td className="text-right">
{org.orgId === user?.orgId ? (
<Button variant="secondary" size="sm" disabled>
<Trans i18nKey="user-orgs.current-org-button">Current</Trans>
</Button>
) : (
<Button
variant="secondary"
size="sm"
onClick={() => {
setUserOrg(org);
}}
>
<Trans i18nKey="user-orgs.select-org-button">Select organisation</Trans>
</Button>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</ScrollContainer>
</div>
);
});
UserOrganizations.displayName = 'UserOrganizations';
export default UserOrganizations;
+83 -84
View File
@@ -1,9 +1,9 @@
import { css } from '@emotion/css';
import { PureComponent } from 'react';
import { memo } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { Button, Icon, LoadingPlaceholder, ScrollContainer } from '@grafana/ui';
import { Button, Icon, LoadingPlaceholder, ScrollContainer, useStyles2 } from '@grafana/ui';
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
import { formatDate } from 'app/core/internationalization/dates';
import { UserSession } from 'app/types/user';
@@ -14,90 +14,89 @@ interface Props {
revokeUserSession: (tokenId: number) => void;
}
class UserSessions extends PureComponent<Props> {
render() {
const { isLoading, sessions, revokeUserSession } = this.props;
const styles = getStyles();
const UserSessions = memo<Props>(({ isLoading, sessions, revokeUserSession }) => {
const styles = useStyles2(getStyles);
if (isLoading) {
return <LoadingPlaceholder text={<Trans i18nKey="user-sessions.loading">Loading sessions...</Trans>} />;
}
return (
<div className={styles.wrapper}>
{sessions.length > 0 && (
<>
<h3 className="page-sub-heading">
<Trans i18nKey="profile.user-sessions.sessions">Sessions</Trans>
</h3>
<ScrollContainer overflowY="visible" overflowX="auto" width="100%">
<table className="filter-table form-inline" data-testid={selectors.components.UserProfile.sessionsTable}>
<thead>
<tr>
<th>
<Trans i18nKey="user-session.seen-at-column">Last seen</Trans>
</th>
<th>
<Trans i18nKey="user-session.created-at-column">Logged on</Trans>
</th>
<th>
<Trans i18nKey="user-session.ip-column">IP address</Trans>
</th>
<th>
<Trans i18nKey="user-session.browser-column">Browser & OS</Trans>
</th>
<th>
<Trans i18nKey="user-session.identity-provider-column">Identity Provider</Trans>
</th>
<th></th>
</tr>
</thead>
<tbody>
{sessions.map((session: UserSession, index) => (
<tr key={index}>
{session.isActive ? (
<td>
<Trans i18nKey="profile.user-sessions.now">Now</Trans>
</td>
) : (
<td>{session.seenAt}</td>
)}
<td>{formatDate(session.createdAt, { dateStyle: 'long' })}</td>
<td>{session.clientIp}</td>
<td>
<Trans
i18nKey="profile.user-sessions.browser-details"
values={{ browser: session.browser, os: session.os, osVersion: session.osVersion }}
>
{'{{browser}}'} on {'{{os}}'} {'{{osVersion}}'}
</Trans>
</td>
<td>
{session.authModule && <TagBadge label={session.authModule} removeIcon={false} count={0} />}
</td>
<td>
<Button
size="sm"
variant="destructive"
tooltip={t('user-session.revoke', 'Revoke user session')}
onClick={() => revokeUserSession(session.id)}
aria-label={t('user-session.revoke', 'Revoke user session')}
>
<Icon name="power" />
</Button>
</td>
</tr>
))}
</tbody>
</table>
</ScrollContainer>
</>
)}
</div>
);
if (isLoading) {
return <LoadingPlaceholder text={<Trans i18nKey="user-sessions.loading">Loading sessions...</Trans>} />;
}
}
return (
<div className={styles.wrapper}>
{sessions.length > 0 && (
<>
<h3 className="page-sub-heading">
<Trans i18nKey="profile.user-sessions.sessions">Sessions</Trans>
</h3>
<ScrollContainer overflowY="visible" overflowX="auto" width="100%">
<table className="filter-table form-inline" data-testid={selectors.components.UserProfile.sessionsTable}>
<thead>
<tr>
<th>
<Trans i18nKey="user-session.seen-at-column">Last seen</Trans>
</th>
<th>
<Trans i18nKey="user-session.created-at-column">Logged on</Trans>
</th>
<th>
<Trans i18nKey="user-session.ip-column">IP address</Trans>
</th>
<th>
<Trans i18nKey="user-session.browser-column">Browser & OS</Trans>
</th>
<th>
<Trans i18nKey="user-session.identity-provider-column">Identity Provider</Trans>
</th>
<th></th>
</tr>
</thead>
<tbody>
{sessions.map((session: UserSession, index) => (
<tr key={index}>
{session.isActive ? (
<td>
<Trans i18nKey="profile.user-sessions.now">Now</Trans>
</td>
) : (
<td>{session.seenAt}</td>
)}
<td>{formatDate(session.createdAt, { dateStyle: 'long' })}</td>
<td>{session.clientIp}</td>
<td>
<Trans
i18nKey="profile.user-sessions.browser-details"
values={{ browser: session.browser, os: session.os, osVersion: session.osVersion }}
>
{'{{browser}}'} on {'{{os}}'} {'{{osVersion}}'}
</Trans>
</td>
<td>
{session.authModule && <TagBadge label={session.authModule} removeIcon={false} count={0} />}
</td>
<td>
<Button
size="sm"
variant="destructive"
tooltip={t('user-session.revoke', 'Revoke user session')}
onClick={() => revokeUserSession(session.id)}
aria-label={t('user-session.revoke', 'Revoke user session')}
>
<Icon name="power" />
</Button>
</td>
</tr>
))}
</tbody>
</table>
</ScrollContainer>
</>
)}
</div>
);
});
UserSessions.displayName = 'UserSessions';
const getStyles = () => ({
wrapper: css({
+54 -56
View File
@@ -1,4 +1,4 @@
import { PureComponent } from 'react';
import { memo } from 'react';
import { Trans, t } from '@grafana/i18n';
import { LoadingPlaceholder, ScrollContainer } from '@grafana/ui';
@@ -9,61 +9,59 @@ export interface Props {
isLoading: boolean;
}
export class UserTeams extends PureComponent<Props> {
render() {
const { isLoading, teams } = this.props;
if (isLoading) {
return <LoadingPlaceholder text={t('profile.user-teams.text-loading-teams', 'Loading teams...')} />;
}
if (teams.length === 0) {
return null;
}
return (
<div>
<h3 className="page-sub-heading">
<Trans i18nKey="profile.user-teams.teams">Teams</Trans>
</h3>
<ScrollContainer overflowY="visible" overflowX="auto" width="100%">
<table
className="filter-table form-inline"
aria-label={t('profile.user-teams.aria-label-user-teams-table', 'User teams table')}
>
<thead>
<tr>
<th />
<th>
<Trans i18nKey="profile.user-teams.name">Name</Trans>
</th>
<th>
<Trans i18nKey="profile.user-teams.email">Email</Trans>
</th>
<th>
<Trans i18nKey="profile.user-teams.members">Members</Trans>
</th>
</tr>
</thead>
<tbody>
{teams.map((team: Team, index) => {
return (
<tr key={index}>
<td className="width-4 text-center">
<img className="filter-table__avatar" src={team.avatarUrl} alt="" />
</td>
<td>{team.name}</td>
<td>{team.email}</td>
<td>{team.memberCount}</td>
</tr>
);
})}
</tbody>
</table>
</ScrollContainer>
</div>
);
export const UserTeams = memo<Props>(({ isLoading, teams }) => {
if (isLoading) {
return <LoadingPlaceholder text={t('profile.user-teams.text-loading-teams', 'Loading teams...')} />;
}
}
if (teams.length === 0) {
return null;
}
return (
<div>
<h3 className="page-sub-heading">
<Trans i18nKey="profile.user-teams.teams">Teams</Trans>
</h3>
<ScrollContainer overflowY="visible" overflowX="auto" width="100%">
<table
className="filter-table form-inline"
aria-label={t('profile.user-teams.aria-label-user-teams-table', 'User teams table')}
>
<thead>
<tr>
<th />
<th>
<Trans i18nKey="profile.user-teams.name">Name</Trans>
</th>
<th>
<Trans i18nKey="profile.user-teams.email">Email</Trans>
</th>
<th>
<Trans i18nKey="profile.user-teams.members">Members</Trans>
</th>
</tr>
</thead>
<tbody>
{teams.map((team: Team, index) => {
return (
<tr key={index}>
<td className="width-4 text-center">
<img className="filter-table__avatar" src={team.avatarUrl} alt="" />
</td>
<td>{team.name}</td>
<td>{team.email}</td>
<td>{team.memberCount}</td>
</tr>
);
})}
</tbody>
</table>
</ScrollContainer>
</div>
);
});
UserTeams.displayName = 'UserTeams';
export default UserTeams;
@@ -1,15 +1,12 @@
import { css } from '@emotion/css';
import { PureComponent } from 'react';
import { memo } from 'react';
import { QueryEditorHelpProps } from '@grafana/data';
import { CloudMonitoringQuery } from '../types/query';
export default class CloudMonitoringCheatSheet extends PureComponent<
QueryEditorHelpProps<CloudMonitoringQuery>,
{ userExamples: string[] }
> {
render() {
const CloudMonitoringCheatSheet = memo(
({ query, datasource, onClickExample }: QueryEditorHelpProps<CloudMonitoringQuery>) => {
return (
<div>
<h2>Cloud Monitoring alias patterns</h2>
@@ -75,4 +72,8 @@ export default class CloudMonitoringCheatSheet extends PureComponent<
</div>
);
}
}
);
CloudMonitoringCheatSheet.displayName = 'CloudMonitoringCheatSheet';
export default CloudMonitoringCheatSheet;
@@ -1,5 +1,5 @@
// Libraries
import { PureComponent } from 'react';
import { memo } from 'react';
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
@@ -8,8 +8,8 @@ type Props = DataSourcePluginOptionsEditorProps;
/**
* Empty Config Editor -- settings to save
*/
export class ConfigEditor extends PureComponent<Props> {
render() {
return <div />;
}
}
export const ConfigEditor = memo<Props>(() => {
return <div />;
});
ConfigEditor.displayName = 'ConfigEditor';
@@ -1,5 +1,5 @@
// Libraries
import { PureComponent } from 'react';
import { memo } from 'react';
import { PluginConfigPageProps, DataSourcePluginMeta, DataSourceJsonData } from '@grafana/data';
import { LinkButton } from '@grafana/ui';
@@ -8,27 +8,23 @@ import { LinkButton } from '@grafana/ui';
interface Props extends PluginConfigPageProps<DataSourcePluginMeta<DataSourceJsonData>> {}
export class TestInfoTab extends PureComponent<Props> {
constructor(props: Props) {
super(props);
}
export const TestInfoTab = memo<Props>(() => {
return (
<div>
See github for more information about setting up a reproducible test environment.
<br />
<br />
<LinkButton
variant="secondary"
href="https://github.com/grafana/grafana/tree/main/devenv"
target="_blank"
rel="noopener noreferrer"
>
GitHub
</LinkButton>
<br />
</div>
);
});
render() {
return (
<div>
See github for more information about setting up a reproducible test environment.
<br />
<br />
<LinkButton
variant="secondary"
href="https://github.com/grafana/grafana/tree/main/devenv"
target="_blank"
rel="noopener noreferrer"
>
GitHub
</LinkButton>
<br />
</div>
);
}
}
TestInfoTab.displayName = 'TestInfoTab';
@@ -9,6 +9,7 @@ import { TimeRangeUpdatedEvent } from '@grafana/runtime';
import {
Alert,
BigValue,
BigValueColorMode,
BigValueGraphMode,
BigValueJustifyMode,
BigValueTextMode,
@@ -226,6 +227,7 @@ function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
<section>
{props.options.viewMode === ViewMode.Stat && (
<BigValue
colorMode={BigValueColorMode.None}
width={props.width}
height={props.height}
graphMode={BigValueGraphMode.None}
+13 -17
View File
@@ -1,5 +1,3 @@
import { Component } from 'react';
import { PanelProps } from '@grafana/data';
import { CursorView } from './CursorView';
@@ -10,21 +8,19 @@ import { Options, DebugMode } from './panelcfg.gen';
type Props = PanelProps<Options>;
export class DebugPanel extends Component<Props> {
render() {
const { options } = this.props;
export function DebugPanel(props: Props) {
const { options } = props;
switch (options.mode) {
case DebugMode.Events:
return <EventBusLoggerPanel eventBus={this.props.eventBus} />;
case DebugMode.Cursor:
return <CursorView eventBus={this.props.eventBus} />;
case DebugMode.State:
return <StateView {...this.props} />;
case DebugMode.ThrowError:
throw new Error('I failed you and for that i am deeply sorry');
default:
return <RenderInfoViewer {...this.props} />;
}
switch (options.mode) {
case DebugMode.Events:
return <EventBusLoggerPanel eventBus={props.eventBus} />;
case DebugMode.Cursor:
return <CursorView eventBus={props.eventBus} />;
case DebugMode.State:
return <StateView {...props} />;
case DebugMode.ThrowError:
throw new Error('I failed you and for that i am deeply sorry');
default:
return <RenderInfoViewer {...props} />;
}
}