TimePicker.tsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. import React, { PureComponent } from 'react';
  2. import moment from 'moment';
  3. import * as dateMath from 'app/core/utils/datemath';
  4. import * as rangeUtil from 'app/core/utils/rangeutil';
  5. import { RawTimeRange, TimeRange } from '@grafana/ui';
  6. const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
  7. export const DEFAULT_RANGE = {
  8. from: 'now-6h',
  9. to: 'now',
  10. };
  11. /**
  12. * Return a human-editable string of either relative (inludes "now") or absolute local time (in the shape of DATE_FORMAT).
  13. * @param value Epoch or relative time
  14. */
  15. export function parseTime(value: string | moment.Moment, isUtc = false, ensureString = false): string | moment.Moment {
  16. if (moment.isMoment(value)) {
  17. if (ensureString) {
  18. return value.format(DATE_FORMAT);
  19. }
  20. return value;
  21. }
  22. if ((value as string).indexOf('now') !== -1) {
  23. return value;
  24. }
  25. let time: any = value;
  26. // Possible epoch
  27. if (!isNaN(time)) {
  28. time = parseInt(time, 10);
  29. }
  30. time = isUtc ? moment.utc(time) : moment(time);
  31. return time.format(DATE_FORMAT);
  32. }
  33. interface TimePickerProps {
  34. isOpen?: boolean;
  35. isUtc?: boolean;
  36. range?: RawTimeRange;
  37. onChangeTime?: (range: RawTimeRange, scanning?: boolean) => void;
  38. iconOnly?: boolean;
  39. }
  40. interface TimePickerState {
  41. isOpen: boolean;
  42. isUtc: boolean;
  43. rangeString: string;
  44. refreshInterval?: string;
  45. initialRange?: RawTimeRange;
  46. // Input-controlled text, keep these in a shape that is human-editable
  47. fromRaw: string;
  48. toRaw: string;
  49. }
  50. /**
  51. * TimePicker with dropdown menu for relative dates.
  52. *
  53. * Initialize with a range that is either based on relative time strings,
  54. * or on Moment objects.
  55. * Internally the component needs to keep a string representation in `fromRaw`
  56. * and `toRaw` for the controlled inputs.
  57. * When a time is picked, `onChangeTime` is called with the new range that
  58. * is again based on relative time strings or Moment objects.
  59. */
  60. export default class TimePicker extends PureComponent<TimePickerProps, TimePickerState> {
  61. dropdownEl: any;
  62. constructor(props) {
  63. super(props);
  64. this.state = {
  65. isOpen: props.isOpen,
  66. isUtc: props.isUtc,
  67. rangeString: '',
  68. fromRaw: '',
  69. toRaw: '',
  70. initialRange: DEFAULT_RANGE,
  71. refreshInterval: '',
  72. };
  73. } //Temp solution... How do detect if ds supports table format?
  74. static getDerivedStateFromProps(props, state) {
  75. if (state.initialRange && state.initialRange === props.range) {
  76. return state;
  77. }
  78. const from = props.range ? props.range.from : DEFAULT_RANGE.from;
  79. const to = props.range ? props.range.to : DEFAULT_RANGE.to;
  80. // Ensure internal string format
  81. const fromRaw = parseTime(from, props.isUtc, true);
  82. const toRaw = parseTime(to, props.isUtc, true);
  83. const range = {
  84. from: fromRaw,
  85. to: toRaw,
  86. };
  87. return {
  88. ...state,
  89. fromRaw,
  90. toRaw,
  91. initialRange: props.range,
  92. rangeString: rangeUtil.describeTimeRange(range),
  93. };
  94. }
  95. move(direction: number, scanning?: boolean): RawTimeRange {
  96. const { onChangeTime } = this.props;
  97. const { fromRaw, toRaw } = this.state;
  98. const from = dateMath.parse(fromRaw, false);
  99. const to = dateMath.parse(toRaw, true);
  100. const step = scanning ? 1 : 2;
  101. const timespan = (to.valueOf() - from.valueOf()) / step;
  102. let nextTo, nextFrom;
  103. if (direction === -1) {
  104. nextTo = to.valueOf() - timespan;
  105. nextFrom = from.valueOf() - timespan;
  106. } else if (direction === 1) {
  107. nextTo = to.valueOf() + timespan;
  108. nextFrom = from.valueOf() + timespan;
  109. if (nextTo > Date.now() && to < Date.now()) {
  110. nextTo = Date.now();
  111. nextFrom = from.valueOf();
  112. }
  113. } else {
  114. nextTo = to.valueOf();
  115. nextFrom = from.valueOf();
  116. }
  117. const nextRange = {
  118. from: moment(nextFrom),
  119. to: moment(nextTo),
  120. };
  121. const nextTimeRange: TimeRange = {
  122. raw: nextRange,
  123. from: nextRange.from,
  124. to: nextRange.to,
  125. };
  126. this.setState(
  127. {
  128. rangeString: rangeUtil.describeTimeRange(nextRange),
  129. fromRaw: nextRange.from.format(DATE_FORMAT),
  130. toRaw: nextRange.to.format(DATE_FORMAT),
  131. },
  132. () => {
  133. onChangeTime(nextTimeRange, scanning);
  134. }
  135. );
  136. return nextRange;
  137. }
  138. handleChangeFrom = e => {
  139. this.setState({
  140. fromRaw: e.target.value,
  141. });
  142. };
  143. handleChangeTo = e => {
  144. this.setState({
  145. toRaw: e.target.value,
  146. });
  147. };
  148. handleClickApply = () => {
  149. const { onChangeTime } = this.props;
  150. let range;
  151. this.setState(
  152. state => {
  153. const { toRaw, fromRaw } = this.state;
  154. range = {
  155. from: dateMath.parse(fromRaw, false),
  156. to: dateMath.parse(toRaw, true),
  157. };
  158. const rangeString = rangeUtil.describeTimeRange(range);
  159. return {
  160. isOpen: false,
  161. rangeString,
  162. };
  163. },
  164. () => {
  165. if (onChangeTime) {
  166. onChangeTime(range);
  167. }
  168. }
  169. );
  170. };
  171. handleClickLeft = () => this.move(-1);
  172. handleClickPicker = () => {
  173. this.setState(state => ({
  174. isOpen: !state.isOpen,
  175. }));
  176. };
  177. handleClickRight = () => this.move(1);
  178. handleClickRefresh = () => {};
  179. handleClickRelativeOption = range => {
  180. const { onChangeTime } = this.props;
  181. const rangeString = rangeUtil.describeTimeRange(range);
  182. this.setState(
  183. {
  184. toRaw: range.to,
  185. fromRaw: range.from,
  186. isOpen: false,
  187. rangeString,
  188. },
  189. () => {
  190. if (onChangeTime) {
  191. onChangeTime(range);
  192. }
  193. }
  194. );
  195. };
  196. getTimeOptions() {
  197. return rangeUtil.getRelativeTimesList({}, this.state.rangeString);
  198. }
  199. dropdownRef = el => {
  200. this.dropdownEl = el;
  201. };
  202. renderDropdown() {
  203. const { fromRaw, isOpen, toRaw } = this.state;
  204. if (!isOpen) {
  205. return null;
  206. }
  207. const timeOptions = this.getTimeOptions();
  208. return (
  209. <div ref={this.dropdownRef} className="gf-timepicker-dropdown">
  210. <div className="popover-box">
  211. <div className="popover-box__header">
  212. <span className="popover-box__title">Quick ranges</span>
  213. </div>
  214. <div className="popover-box__body gf-timepicker-relative-section">
  215. {Object.keys(timeOptions).map(section => {
  216. const group = timeOptions[section];
  217. return (
  218. <ul key={section}>
  219. {group.map(option => (
  220. <li className={option.active ? 'active' : ''} key={option.display}>
  221. <a onClick={() => this.handleClickRelativeOption(option)}>{option.display}</a>
  222. </li>
  223. ))}
  224. </ul>
  225. );
  226. })}
  227. </div>
  228. </div>
  229. <div className="popover-box">
  230. <div className="popover-box__header">
  231. <span className="popover-box__title">Custom range</span>
  232. </div>
  233. <div className="popover-box__body gf-timepicker-absolute-section">
  234. <label className="small">From:</label>
  235. <div className="gf-form-inline">
  236. <div className="gf-form max-width-28">
  237. <input
  238. type="text"
  239. className="gf-form-input input-large timepicker-from"
  240. value={fromRaw}
  241. onChange={this.handleChangeFrom}
  242. />
  243. </div>
  244. </div>
  245. <label className="small">To:</label>
  246. <div className="gf-form-inline">
  247. <div className="gf-form max-width-28">
  248. <input
  249. type="text"
  250. className="gf-form-input input-large timepicker-to"
  251. value={toRaw}
  252. onChange={this.handleChangeTo}
  253. />
  254. </div>
  255. </div>
  256. <div className="gf-form">
  257. <button className="btn gf-form-btn btn-secondary" onClick={this.handleClickApply}>
  258. Apply
  259. </button>
  260. </div>
  261. </div>
  262. </div>
  263. </div>
  264. );
  265. }
  266. render() {
  267. const { iconOnly } = this.props;
  268. const { isUtc, rangeString, refreshInterval } = this.state;
  269. return (
  270. <div className="timepicker">
  271. <div className="navbar-buttons">
  272. <button className="btn navbar-button navbar-button--tight timepicker-left" onClick={this.handleClickLeft}>
  273. <i className="fa fa-chevron-left" />
  274. </button>
  275. {iconOnly ? (
  276. <button className="btn navbar-button gf-timepicker-nav-btn" onClick={this.handleClickPicker}>
  277. <i className="fa fa-clock-o" />
  278. </button>
  279. ) : (
  280. <button className="btn navbar-button gf-timepicker-nav-btn" onClick={this.handleClickPicker}>
  281. <i className="fa fa-clock-o" />
  282. <span className="timepicker-rangestring">{rangeString}</span>
  283. {isUtc ? <span className="gf-timepicker-utc">UTC</span> : null}
  284. {refreshInterval ? <span className="text-warning">&nbsp; Refresh every {refreshInterval}</span> : null}
  285. </button>
  286. )}
  287. <button className="btn navbar-button navbar-button--tight timepicker-right" onClick={this.handleClickRight}>
  288. <i className="fa fa-chevron-right" />
  289. </button>
  290. </div>
  291. {this.renderDropdown()}
  292. </div>
  293. );
  294. }
  295. }