Jelajahi Sumber

Fixed custom dates for react timepicker

* added jest tests for timepicker component
David Kaltschmidt 7 tahun lalu
induk
melakukan
23c9da6162

+ 74 - 0
public/app/containers/Explore/TimePicker.jest.tsx

@@ -0,0 +1,74 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import sinon from 'sinon';
+
+import * as rangeUtil from 'app/core/utils/rangeutil';
+import TimePicker, { DEFAULT_RANGE, parseTime } from './TimePicker';
+
+describe('<TimePicker />', () => {
+  it('renders closed with default values', () => {
+    const rangeString = rangeUtil.describeTimeRange(DEFAULT_RANGE);
+    const wrapper = shallow(<TimePicker />);
+    expect(wrapper.find('.timepicker-rangestring').text()).toBe(rangeString);
+    expect(wrapper.find('.gf-timepicker-dropdown').exists()).toBe(false);
+  });
+
+  it('renders with relative range', () => {
+    const range = {
+      from: 'now-7h',
+      to: 'now',
+    };
+    const rangeString = rangeUtil.describeTimeRange(range);
+    const wrapper = shallow(<TimePicker range={range} isOpen />);
+    expect(wrapper.find('.timepicker-rangestring').text()).toBe(rangeString);
+    expect(wrapper.state('fromRaw')).toBe(range.from);
+    expect(wrapper.state('toRaw')).toBe(range.to);
+    expect(wrapper.find('.timepicker-from').props().value).toBe(range.from);
+    expect(wrapper.find('.timepicker-to').props().value).toBe(range.to);
+  });
+
+  it('renders with epoch (millies) range converted to ISO-ish', () => {
+    const range = {
+      from: '1',
+      to: '1000',
+    };
+    const rangeString = rangeUtil.describeTimeRange({
+      from: parseTime(range.from),
+      to: parseTime(range.to),
+    });
+    const wrapper = shallow(<TimePicker range={range} isUtc isOpen />);
+    expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:00');
+    expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:01');
+    expect(wrapper.find('.timepicker-rangestring').text()).toBe(rangeString);
+    expect(wrapper.find('.timepicker-from').props().value).toBe('1970-01-01 00:00:00');
+    expect(wrapper.find('.timepicker-to').props().value).toBe('1970-01-01 00:00:01');
+  });
+
+  it('moves ranges forward and backward by half the range on arrow click', () => {
+    const range = {
+      from: '2000',
+      to: '4000',
+    };
+    const rangeString = rangeUtil.describeTimeRange({
+      from: parseTime(range.from),
+      to: parseTime(range.to),
+    });
+
+    const onChangeTime = sinon.spy();
+    const wrapper = shallow(<TimePicker range={range} isUtc isOpen onChangeTime={onChangeTime} />);
+    expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:02');
+    expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:04');
+    expect(wrapper.find('.timepicker-rangestring').text()).toBe(rangeString);
+    expect(wrapper.find('.timepicker-from').props().value).toBe('1970-01-01 00:00:02');
+    expect(wrapper.find('.timepicker-to').props().value).toBe('1970-01-01 00:00:04');
+
+    wrapper.find('.timepicker-left').simulate('click');
+    expect(onChangeTime.calledOnce).toBe(true);
+    expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:01');
+    expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:03');
+
+    wrapper.find('.timepicker-right').simulate('click');
+    expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:02');
+    expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:04');
+  });
+});

+ 69 - 16
public/app/containers/Explore/TimePicker.tsx

@@ -4,22 +4,43 @@ import moment from 'moment';
 import * as dateMath from 'app/core/utils/datemath';
 import * as rangeUtil from 'app/core/utils/rangeutil';
 
+const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
+
 export const DEFAULT_RANGE = {
   from: 'now-6h',
   to: 'now',
 };
 
+export function parseTime(value, isUtc = false, asString = false) {
+  if (value.indexOf('now') !== -1) {
+    return value;
+  }
+  if (!isNaN(value)) {
+    const epoch = parseInt(value);
+    const m = isUtc ? moment.utc(epoch) : moment(epoch);
+    return asString ? m.format(DATE_FORMAT) : m;
+  }
+  return undefined;
+}
+
 export default class TimePicker extends PureComponent<any, any> {
   dropdownEl: any;
   constructor(props) {
     super(props);
+
+    const fromRaw = props.range ? props.range.from : DEFAULT_RANGE.from;
+    const toRaw = props.range ? props.range.to : DEFAULT_RANGE.to;
+    const range = {
+      from: parseTime(fromRaw),
+      to: parseTime(toRaw),
+    };
     this.state = {
-      fromRaw: props.range ? props.range.from : DEFAULT_RANGE.from,
-      isOpen: false,
-      isUtc: false,
-      rangeString: rangeUtil.describeTimeRange(props.range || DEFAULT_RANGE),
+      fromRaw: parseTime(fromRaw, props.isUtc, true),
+      isOpen: props.isOpen,
+      isUtc: props.isUtc,
+      rangeString: rangeUtil.describeTimeRange(range),
       refreshInterval: '',
-      toRaw: props.range ? props.range.to : DEFAULT_RANGE.to,
+      toRaw: parseTime(toRaw, props.isUtc, true),
     };
   }
 
@@ -49,14 +70,15 @@ export default class TimePicker extends PureComponent<any, any> {
     }
 
     const rangeString = rangeUtil.describeTimeRange(range);
-    to = moment.utc(to);
-    from = moment.utc(from);
+    // No need to convert to UTC again
+    to = moment(to);
+    from = moment(from);
 
     this.setState(
       {
         rangeString,
-        fromRaw: from,
-        toRaw: to,
+        fromRaw: from.format(DATE_FORMAT),
+        toRaw: to.format(DATE_FORMAT),
       },
       () => {
         onChangeTime({ to, from });
@@ -76,6 +98,27 @@ export default class TimePicker extends PureComponent<any, any> {
     });
   };
 
+  handleClickApply = () => {
+    const { onChangeTime } = this.props;
+    const { toRaw, fromRaw } = this.state;
+    const range = {
+      from: dateMath.parse(fromRaw, false),
+      to: dateMath.parse(toRaw, true),
+    };
+    const rangeString = rangeUtil.describeTimeRange(range);
+    this.setState(
+      {
+        isOpen: false,
+        rangeString,
+      },
+      () => {
+        if (onChangeTime) {
+          onChangeTime(range);
+        }
+      }
+    );
+  };
+
   handleClickLeft = () => this.move(-1);
   handleClickPicker = () => {
     this.setState(state => ({
@@ -118,7 +161,7 @@ export default class TimePicker extends PureComponent<any, any> {
     const timeOptions = this.getTimeOptions();
     return (
       <div ref={this.dropdownRef} className="gf-timepicker-dropdown">
-        <form name="timeForm" className="gf-timepicker-absolute-section">
+        <div className="gf-timepicker-absolute-section">
           <h3 className="section-heading">Custom range</h3>
 
           <label className="small">From:</label>
@@ -126,7 +169,7 @@ export default class TimePicker extends PureComponent<any, any> {
             <div className="gf-form max-width-28">
               <input
                 type="text"
-                className="gf-form-input input-large"
+                className="gf-form-input input-large timepicker-from"
                 value={fromRaw}
                 onChange={this.handleChangeFrom}
               />
@@ -136,7 +179,12 @@ export default class TimePicker extends PureComponent<any, any> {
           <label className="small">To:</label>
           <div className="gf-form-inline">
             <div className="gf-form max-width-28">
-              <input type="text" className="gf-form-input input-large" value={toRaw} onChange={this.handleChangeTo} />
+              <input
+                type="text"
+                className="gf-form-input input-large timepicker-to"
+                value={toRaw}
+                onChange={this.handleChangeTo}
+              />
             </div>
           </div>
 
@@ -146,7 +194,12 @@ export default class TimePicker extends PureComponent<any, any> {
               <select className="gf-form-input input-medium" ng-options="f.value as f.text for f in ctrl.refresh.options"></select>
             </div>
           </div> */}
-        </form>
+          <div className="gf-form">
+            <button className="btn gf-form-btn btn-secondary" onClick={this.handleClickApply}>
+              Apply
+            </button>
+          </div>
+        </div>
 
         <div className="gf-timepicker-relative-section">
           <h3 className="section-heading">Quick ranges</h3>
@@ -172,16 +225,16 @@ export default class TimePicker extends PureComponent<any, any> {
     return (
       <div className="timepicker">
         <div className="navbar-buttons">
-          <button className="btn navbar-button navbar-button--tight" onClick={this.handleClickLeft}>
+          <button className="btn navbar-button navbar-button--tight timepicker-left" onClick={this.handleClickLeft}>
             <i className="fa fa-chevron-left" />
           </button>
           <button className="btn navbar-button gf-timepicker-nav-btn" onClick={this.handleClickPicker}>
             <i className="fa fa-clock-o" />
-            <span> {rangeString}</span>
+            <span className="timepicker-rangestring">{rangeString}</span>
             {isUtc ? <span className="gf-timepicker-utc">UTC</span> : null}
             {refreshInterval ? <span className="text-warning">&nbsp; Refresh every {refreshInterval}</span> : null}
           </button>
-          <button className="btn navbar-button navbar-button--tight" onClick={this.handleClickRight}>
+          <button className="btn navbar-button navbar-button--tight timepicker-right" onClick={this.handleClickRight}>
             <i className="fa fa-chevron-right" />
           </button>
         </div>

+ 4 - 0
public/sass/pages/_explore.scss

@@ -36,6 +36,10 @@
 
   .timepicker {
     display: flex;
+
+    &-rangestring {
+      margin-left: 0.5em;
+    }
   }
 
   .run-icon {