From 644fd636116385fae770e498658be461914d573e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Mon, 25 Feb 2019 16:55:57 +0100 Subject: [PATCH] [EuiSuperDatePicker] Add `onRefresh` handler (#1577) --- CHANGELOG.md | 1 + .../views/date_picker/super_date_picker.js | 9 ++ .../super_date_picker.test.js.snap | 1 + .../super_date_picker/async_interval.js | 22 +++++ .../super_date_picker/async_interval.test.js | 95 +++++++++++++++++++ .../super_date_picker/super_date_picker.js | 45 ++++++++- 6 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 src/components/date_picker/super_date_picker/async_interval.js create mode 100644 src/components/date_picker/super_date_picker/async_interval.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index a283838570e..04439eec6bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## [`master`](https://github.com/elastic/eui/tree/master) +- Added `onRefresh` option for `EuiSuperDatePicker` ([#1577](https://github.com/elastic/eui/pull/1577)) - Converted `EuiToggle` to TypeScript ([#1570](https://github.com/elastic/eui/pull/1570)) - Added type definitions for `EuiButtonGroup`,`EuiButtonToggle`, `EuiFilterButton`, `EuiFilterGroup`, and `EuiFilterSelectItem` ([#1570](https://github.com/elastic/eui/pull/1570)) - Added `displayOnly` prop to EuiFormRow ([#1582](https://github.com/elastic/eui/pull/1582)) diff --git a/src-docs/src/views/date_picker/super_date_picker.js b/src-docs/src/views/date_picker/super_date_picker.js index 3cbf018c4df..791c88b2ce4 100644 --- a/src-docs/src/views/date_picker/super_date_picker.js +++ b/src-docs/src/views/date_picker/super_date_picker.js @@ -51,6 +51,14 @@ export default class extends Component { }, this.startLoading); } + onRefresh = ({ start, end, refreshInterval }) => { + return new Promise((resolve) => { + setTimeout(resolve, 100); + }).then(() => { + console.log(start, end, refreshInterval); + }); + } + onStartInputChange = e => { this.setState({ start: e.target.value, @@ -168,6 +176,7 @@ export default class extends Component { start={this.state.start} end={this.state.end} onTimeChange={this.onTimeChange} + onRefresh={this.onRefresh} isPaused={this.state.isPaused} refreshInterval={this.state.refreshInterval} onRefreshChange={this.onRefreshChange} diff --git a/src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.js.snap b/src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.js.snap index 19e03cf7e6f..481b5a7612f 100644 --- a/src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.js.snap +++ b/src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.js.snap @@ -21,6 +21,7 @@ exports[`EuiSuperDatePicker is rendered 1`] = ` isLoading={false} prepend={ { + if (!this.isStopped) { + this.timeoutId = window.setTimeout(async () => { + this.__pendingFn = await fn(); + this.setAsyncInterval(fn, ms); + }, ms); + } + }; + + stop = () => { + this.isStopped = true; + window.clearTimeout(this.timeoutId); + }; +} diff --git a/src/components/date_picker/super_date_picker/async_interval.test.js b/src/components/date_picker/super_date_picker/async_interval.test.js new file mode 100644 index 00000000000..7b3eab69b22 --- /dev/null +++ b/src/components/date_picker/super_date_picker/async_interval.test.js @@ -0,0 +1,95 @@ +import { AsyncInterval } from './async_interval'; +import { times } from 'lodash'; + +describe('AsyncInterval', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + // Advances time and awaits any pending promises after every 100ms + // This helper makes it easier to advance time without worrying + // whether tasks are still lingering on the event loop + async function andvanceTimerAndAwaitFn(instance, ms) { + const iterations = times(Math.floor(ms / 100)); + const remainder = ms % 100; + // eslint-disable-next-line no-unused-vars + for (const item of iterations) { + await instance.__pendingFn; + jest.advanceTimersByTime(100); + await instance.__pendingFn; + } + jest.advanceTimersByTime(remainder); + await instance.__pendingFn; + } + + describe('when creating a 1000ms interval', async () => { + let instance; + let spy; + beforeEach(() => { + spy = jest.fn(); + instance = new AsyncInterval(spy, 1000); + }); + + it('should not call fn immediately', async () => { + await andvanceTimerAndAwaitFn(instance, 0); + expect(spy).toHaveBeenCalledTimes(0); + }); + + it('should have called fn once after 1000ms', async () => { + await andvanceTimerAndAwaitFn(instance, 1000); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should have called fn twice after 2000ms', async () => { + await andvanceTimerAndAwaitFn(instance, 2000); + expect(spy).toHaveBeenCalledTimes(2); + }); + + it('should have called fn three times after 3000ms', async () => { + await andvanceTimerAndAwaitFn(instance, 3000); + expect(spy).toHaveBeenCalledTimes(3); + }); + + it('should not call fn after stop has been invoked', async () => { + await andvanceTimerAndAwaitFn(instance, 1000); + expect(spy).toHaveBeenCalledTimes(1); + instance.stop(); + await andvanceTimerAndAwaitFn(instance, 1000); + expect(spy).toHaveBeenCalledTimes(1); + }); + }); + + describe('when creating a 1000ms interval that calls a fn that takes 2000ms to complete', async () => { + let instance; + let spy; + beforeEach(() => { + const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + spy = jest.fn(async () => await sleep(2000)); + instance = new AsyncInterval(spy, 1000); + }); + + it('should not call fn immediately', async () => { + await andvanceTimerAndAwaitFn(instance, 0); + expect(spy).toHaveBeenCalledTimes(0); + }); + + it('should have called fn once after 1000ms', async () => { + await andvanceTimerAndAwaitFn(instance, 1000); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should have called fn twice after 4000ms', async () => { + await andvanceTimerAndAwaitFn(instance, 4000); + expect(spy).toHaveBeenCalledTimes(2); + }); + + it('should have called fn tree times after 7000ms', async () => { + await andvanceTimerAndAwaitFn(instance, 7000); + expect(spy).toHaveBeenCalledTimes(3); + }); + }); +}); diff --git a/src/components/date_picker/super_date_picker/super_date_picker.js b/src/components/date_picker/super_date_picker/super_date_picker.js index 22cdc670e7f..99bdbd709b7 100644 --- a/src/components/date_picker/super_date_picker/super_date_picker.js +++ b/src/components/date_picker/super_date_picker/super_date_picker.js @@ -15,6 +15,7 @@ import { EuiDatePopoverButton } from './date_popover/date_popover_button'; import { EuiDatePickerRange } from '../date_picker_range'; import { EuiFormControlLayout } from '../../form'; import { EuiFlexGroup, EuiFlexItem } from '../../flex'; +import { AsyncInterval } from './async_interval'; function isRangeInvalid(start, end) { if (start === 'now' && end === 'now') { @@ -62,6 +63,14 @@ export class EuiSuperDatePicker extends Component { */ onRefreshChange: PropTypes.func, + /** + * Callback for when the refresh interval is fired. Called with { start, end, refreshInterval } + * EuiSuperDatePicker will only manage a refresh interval timer when onRefresh callback is supplied + * If a promise is returned, the next refresh interval will not start until the promise has resolved. + * If the promise rejects the refresh interval will stop and the error thrown + */ + onRefresh: PropTypes.func, + /** * 'start' and 'end' must be string as either datemath (e.g.: now, now-15m, now-15m/m) or * absolute date in the format 'YYYY-MM-DDTHH:mm:ss.sssZ' @@ -168,6 +177,16 @@ export class EuiSuperDatePicker extends Component { } } + componentDidMount = () => { + if(!this.props.isPaused) { + this.startInterval(this.props.refreshInterval); + } + } + + componentWillUnmount = () => { + this.stopInterval(); + } + setStart = (start) => { this.setTime({ start, end: this.state.end }); } @@ -221,6 +240,30 @@ export class EuiSuperDatePicker extends Component { this.setState({ isEndDatePopoverOpen: false }); } + onRefreshChange = ({ refreshInterval, isPaused }) => { + this.stopInterval(); + if(!isPaused) { + this.startInterval(refreshInterval); + } + if(this.props.onRefreshChange) { + this.props.onRefreshChange({ refreshInterval, isPaused }); + } + } + + stopInterval = () => { + if(this.asyncInterval) { + this.asyncInterval.stop(); + } + } + + startInterval = (refreshInterval) => { + const { start, end, onRefresh } = this.props; + if(onRefresh) { + const handler = () => onRefresh({ start, end, refreshInterval }); + this.asyncInterval = new AsyncInterval(handler, refreshInterval); + } + } + renderDatePickerRange = () => { const { start, @@ -329,7 +372,7 @@ export class EuiSuperDatePicker extends Component { applyTime={this.applyQuickTime} start={this.props.start} end={this.props.end} - applyRefreshInterval={this.props.onRefreshChange} + applyRefreshInterval={this.props.onRefreshChange ? this.onRefreshChange : null} isPaused={this.props.isPaused} refreshInterval={this.props.refreshInterval} commonlyUsedRanges={this.props.commonlyUsedRanges}