diff --git a/superset-frontend/spec/javascripts/sqllab/Timer_spec.jsx b/superset-frontend/spec/javascripts/sqllab/Timer_spec.jsx index 19481679e3ea5..49efac73d89cc 100644 --- a/superset-frontend/spec/javascripts/sqllab/Timer_spec.jsx +++ b/superset-frontend/spec/javascripts/sqllab/Timer_spec.jsx @@ -18,6 +18,8 @@ */ import React from 'react'; import { styledMount as mount } from 'spec/helpers/theming'; +import sinon from 'sinon'; + import Timer from 'src/components/Timer'; import { now } from 'src/modules/dates'; @@ -38,10 +40,28 @@ describe('Timer', () => { expect(React.isValidElement()).toBe(true); }); - it('useEffect starts timer after 30ms and sets state of clockStr', async () => { - expect(wrapper.find('span').text()).toBe(''); + it('componentWillMount starts timer after 30ms and sets state.clockStr', async () => { + expect(wrapper.state().clockStr).toBe(''); await new Promise(r => setTimeout(r, 35)); - expect(wrapper.find('span').text()).not.toBe(''); + expect(wrapper.state().clockStr).not.toBe(''); + }); + + it('calls startTimer on mount', () => { + // Timer is already mounted in beforeEach + wrapper.unmount(); + const startTimerSpy = sinon.spy(Timer.prototype, 'startTimer'); + wrapper.mount(); + // Timer is started once in willUnmount and a second timer in render + // TODO: Questionable whether this is necessary. + expect(startTimerSpy.callCount).toBe(2); + startTimerSpy.restore(); + }); + + it('calls stopTimer on unmount', () => { + const stopTimerSpy = sinon.spy(Timer.prototype, 'stopTimer'); + wrapper.unmount(); + expect(stopTimerSpy.callCount).toBe(1); + stopTimerSpy.restore(); }); it('renders a span with the correct class', () => { diff --git a/superset-frontend/src/components/Timer.jsx b/superset-frontend/src/components/Timer.jsx new file mode 100644 index 0000000000000..5cddef3073d87 --- /dev/null +++ b/superset-frontend/src/components/Timer.jsx @@ -0,0 +1,95 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import PropTypes from 'prop-types'; + +import Label from 'src/components/Label'; +import { now, fDuration } from '../modules/dates'; + +const propTypes = { + endTime: PropTypes.number, + isRunning: PropTypes.bool.isRequired, + startTime: PropTypes.number, + status: PropTypes.string, +}; + +const defaultProps = { + endTime: null, + startTime: null, + status: 'success', +}; + +export default class Timer extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + clockStr: '', + }; + this.stopwatch = this.stopwatch.bind(this); + } + + UNSAFE_componentWillMount() { + this.startTimer(); + } + + componentWillUnmount() { + this.stopTimer(); + } + + startTimer() { + if (!this.timer) { + this.timer = setInterval(this.stopwatch, 30); + } + } + + stopTimer() { + this.timer = clearInterval(this.timer); + } + + stopwatch() { + if (this.props && this.props.startTime) { + const endDttm = this.props.endTime || now(); + if (this.props.startTime < endDttm) { + const clockStr = fDuration(this.props.startTime, endDttm); + this.setState({ clockStr }); + } + if (!this.props.isRunning) { + this.stopTimer(); + } + } + } + + render() { + if (this.props && this.props.isRunning) { + this.startTimer(); + } + let timerSpan = null; + if (this.props) { + timerSpan = ( + + ); + } + return timerSpan; + } +} + +Timer.propTypes = propTypes; +Timer.defaultProps = defaultProps; diff --git a/superset-frontend/src/components/Timer.tsx b/superset-frontend/src/components/Timer.tsx deleted file mode 100644 index 7daedef31b8a6..0000000000000 --- a/superset-frontend/src/components/Timer.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import React, { useEffect, useState } from 'react'; -import Label from 'src/components/Label'; - -import { now, fDuration } from '../modules/dates'; - -interface TimerProps { - endTime?: number; - isRunning: boolean; - startTime?: number; - status?: string; -} - -export default function Timer({ - endTime, - isRunning, - startTime, - status = 'success', -}: TimerProps) { - const [clockStr, setClockStr] = useState(''); - const [timer, setTimer] = useState(); - - const stopTimer = () => { - if (timer) { - clearInterval(timer); - setTimer(undefined); - } - }; - - const stopwatch = () => { - if (startTime) { - const endDttm = endTime || now(); - if (startTime < endDttm) { - setClockStr(fDuration(startTime, endDttm)); - } - if (!isRunning) { - stopTimer(); - } - } - }; - - const startTimer = () => { - setTimer(setInterval(stopwatch, 30)); - }; - - useEffect(() => { - if (isRunning) { - startTimer(); - } - }, [isRunning]); - - useEffect(() => { - return () => { - stopTimer(); - }; - }); - - return ( - - ); -}