From 1e74037eae6627f4891899ad2cbc8bfcad1e8f56 Mon Sep 17 00:00:00 2001 From: Boyko Date: Mon, 18 May 2020 19:03:29 +0300 Subject: [PATCH] React: refactor away from unsafe lifecycle methods (#21291) * refactor aliasBy and PopoverCtrl components * refactor Aggregations component * refactor MetricSelect component * refactor UserProvider * popoverCtr: remove redundant logic * simplified the MetricsSelect a bit. * skipping testing of internal component logic. * changed to componentWillMount. * changed elapsed time to a functional component. * rewrote the tests. * fixed missing test title. * fixed a tiny issue with elapsed time. * rename of field. Co-authored-by: Marcus Andersson --- .../components/Tooltip/PopoverController.tsx | 52 +------ .../components/Select/MetricSelect.test.tsx | 48 ++++++ .../core/components/Select/MetricSelect.tsx | 110 ++++++-------- public/app/features/explore/ElapsedTime.tsx | 78 ++-------- public/app/features/explore/LiveLogs.tsx | 2 +- public/app/features/explore/QueryStatus.tsx | 2 +- public/app/features/explore/Time.tsx | 35 +++++ .../components/Aggregations.test.tsx | 55 ++++--- .../stackdriver/components/Aggregations.tsx | 138 ++++++++---------- 9 files changed, 242 insertions(+), 278 deletions(-) create mode 100644 public/app/core/components/Select/MetricSelect.test.tsx create mode 100644 public/app/features/explore/Time.tsx diff --git a/packages/grafana-ui/src/components/Tooltip/PopoverController.tsx b/packages/grafana-ui/src/components/Tooltip/PopoverController.tsx index 4adcde79bde..8d3ee22a412 100644 --- a/packages/grafana-ui/src/components/Tooltip/PopoverController.tsx +++ b/packages/grafana-ui/src/components/Tooltip/PopoverController.tsx @@ -48,63 +48,27 @@ interface Props { } interface State { - placement: PopperJS.Placement; show: boolean; } class PopoverController extends React.Component { private hideTimeout: any; - - constructor(props: Props) { - super(props); - - this.state = { - placement: this.props.placement || 'auto', - show: false, - }; - } - - UNSAFE_componentWillReceiveProps(nextProps: Props) { - if (nextProps.placement && nextProps.placement !== this.state.placement) { - this.setState((prevState: State) => { - return { - ...prevState, - placement: nextProps.placement || 'auto', - }; - }); - } - } + state = { show: false }; showPopper = () => { - if (this.hideTimeout) { - clearTimeout(this.hideTimeout); - } - - this.setState(prevState => ({ - ...prevState, - show: true, - })); + clearTimeout(this.hideTimeout); + this.setState({ show: true }); }; hidePopper = () => { - if (this.props.hideAfter !== 0) { - this.hideTimeout = setTimeout(() => { - this.setState(prevState => ({ - ...prevState, - show: false, - })); - }, this.props.hideAfter); - return; - } - this.setState(prevState => ({ - ...prevState, - show: false, - })); + this.hideTimeout = setTimeout(() => { + this.setState({ show: false }); + }, this.props.hideAfter); }; render() { - const { children, content } = this.props; - const { show, placement } = this.state; + const { children, content, placement = 'auto' } = this.props; + const { show } = this.state; return children(this.showPopper, this.hidePopper, { show, diff --git a/public/app/core/components/Select/MetricSelect.test.tsx b/public/app/core/components/Select/MetricSelect.test.tsx new file mode 100644 index 00000000000..9a5165a2f53 --- /dev/null +++ b/public/app/core/components/Select/MetricSelect.test.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { MetricSelect } from './MetricSelect'; +import { LegacyForms } from '@grafana/ui'; +const { Select } = LegacyForms; + +describe('MetricSelect', () => { + describe('When receive props', () => { + it('should pass correct set of props to Select component', () => { + const props: any = { + placeholder: 'Select Reducer', + className: 'width-15', + options: [], + variables: [], + }; + const wrapper = shallow(); + expect(wrapper.find(Select).props()).toMatchObject({ + className: 'width-15', + isMulti: false, + isClearable: false, + backspaceRemovesValue: false, + isSearchable: true, + maxMenuHeight: 500, + placeholder: 'Select Reducer', + }); + }); + it('should pass callbacks correctly to the Select component', () => { + const spyOnChange = jest.fn(); + const props: any = { + onChange: spyOnChange, + options: [], + variables: [], + }; + const wrapper = shallow(); + wrapper + .find(Select) + .props() + .onChange({ value: 'foo' }); + expect( + wrapper + .find(Select) + .props() + .noOptionsMessage() + ).toEqual('No options found'); + expect(spyOnChange).toHaveBeenCalledWith('foo'); + }); + }); +}); diff --git a/public/app/core/components/Select/MetricSelect.tsx b/public/app/core/components/Select/MetricSelect.tsx index a726e470ac0..37bee34db3b 100644 --- a/public/app/core/components/Select/MetricSelect.tsx +++ b/public/app/core/components/Select/MetricSelect.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo, useCallback, FC } from 'react'; import _ from 'lodash'; import { LegacyForms } from '@grafana/ui'; @@ -16,75 +16,51 @@ export interface Props { variables?: Variable[]; } -interface State { - options: Array>; -} +export const MetricSelect: FC = props => { + const { value, placeholder, className, isSearchable, onChange } = props; + const options = useSelectOptions(props); + const selected = useSelectedOption(options, value); + const onChangeValue = useCallback((selectable: SelectableValue) => onChange(selectable.value), [onChange]); -export class MetricSelect extends React.Component { - static defaultProps: Partial = { - variables: [], - options: [], - isSearchable: true, - }; + return ( + onChange(item.value ?? '')} - options={options} - isSearchable={isSearchable} - maxMenuHeight={500} - placeholder={placeholder} - noOptionsMessage={() => 'No options found'} - value={selectedOption} - /> - ); - } -} + return allOptions.find(option => option.value === value); + }, [options, value]); +}; diff --git a/public/app/features/explore/ElapsedTime.tsx b/public/app/features/explore/ElapsedTime.tsx index ab50cc12472..c358a0048bf 100644 --- a/public/app/features/explore/ElapsedTime.tsx +++ b/public/app/features/explore/ElapsedTime.tsx @@ -1,76 +1,22 @@ -import React, { PureComponent } from 'react'; -import { toDuration } from '@grafana/data'; +import React, { FC, useState, useEffect } from 'react'; +import { useInterval } from 'react-use'; +import { Time, TimeProps } from './Time'; const INTERVAL = 150; -export interface Props { - time?: number; +export interface ElapsedTimeProps extends Omit { // Use this to reset the timer. Any value is allowed just need to be !== from the previous. // Keep in mind things like [] !== [] or {} !== {}. resetKey?: any; - className?: string; - humanize?: boolean; } -export interface State { - elapsed: number; -} +export const ElapsedTime: FC = ({ resetKey, humanize, className }) => { + const [elapsed, setElapsed] = useState(0); // the current value of elapsed -/** - * Shows an incremental time ticker of elapsed time from some event. - */ -export default class ElapsedTime extends PureComponent { - offset: number; - timer: number; + // hook that will schedule a interval and then update the elapsed value on every tick. + useInterval(() => setElapsed(elapsed + INTERVAL), INTERVAL); + // this effect will only be run when resetKey changes. This will reset the elapsed to 0. + useEffect(() => setElapsed(0), [resetKey]); - state = { - elapsed: 0, - }; - - start() { - this.offset = Date.now(); - this.timer = window.setInterval(this.tick, INTERVAL); - } - - tick = () => { - const jetzt = Date.now(); - const elapsed = jetzt - this.offset; - this.setState({ elapsed }); - }; - - UNSAFE_componentWillReceiveProps(nextProps: Props) { - if (nextProps.time) { - clearInterval(this.timer); - } else if (this.props.time) { - this.start(); - } - - if (nextProps.resetKey !== this.props.resetKey) { - clearInterval(this.timer); - this.start(); - } - } - - componentDidMount() { - this.start(); - } - - componentWillUnmount() { - clearInterval(this.timer); - } - - render() { - const { elapsed } = this.state; - const { className, time, humanize } = this.props; - const value = (time || elapsed) / 1000; - let displayValue = `${value.toFixed(1)}s`; - if (humanize) { - const duration = toDuration(elapsed); - const hours = duration.hours(); - const minutes = duration.minutes(); - const seconds = duration.seconds(); - displayValue = hours ? `${hours}h ${minutes}m ${seconds}s` : minutes ? ` ${minutes}m ${seconds}s` : `${seconds}s`; - } - return {displayValue}; - } -} + return