diff --git a/CHANGELOG.md b/CHANGELOG.md index 31cb1690edd..b178de3ef5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Features 1. [11954](https://github.com/influxdata/influxdb/pull/11954): Add the ability to run a task manually from tasks page +1. [11990](https://github.com/influxdata/influxdb/pull/11990): Add the ability to select a custom time range in explorer and dashboard ### Bug Fixes diff --git a/ui/package-lock.json b/ui/package-lock.json index b970ba8cb08..8b354cb598f 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -777,7 +777,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz", "integrity": "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==", - "dev": true, "requires": { "regenerator-runtime": "^0.12.0" }, @@ -785,8 +784,7 @@ "regenerator-runtime": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", - "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==", - "dev": true + "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==" } } }, @@ -3696,6 +3694,15 @@ "object-assign": "^4.1.1" } }, + "create-react-context": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.2.2.tgz", + "integrity": "sha512-KkpaLARMhsTsgp0d2NA/R94F/eDLbhXERdIq3LvX2biCAXcDvHYoOqHfWCHf1+OLj+HKBotLG3KqaOOf+C1C+A==", + "requires": { + "fbjs": "^0.8.0", + "gud": "^1.0.0" + } + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -6589,6 +6596,11 @@ "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", "dev": true }, + "gud": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", + "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" + }, "handlebars": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.0.tgz", @@ -11336,6 +11348,11 @@ "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", "dev": true }, + "popper.js": { + "version": "1.14.7", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.7.tgz", + "integrity": "sha512-4q1hNvoUre/8srWsH7hnoSJ5xVmIL4qgz+s4qf2TnJIMyZFUFMGH+9vE7mXynAlHSZ/NdTmmow86muD0myUkVQ==" + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -12488,6 +12505,25 @@ "prop-types": "^15.5.8" } }, + "react-datepicker": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-2.1.0.tgz", + "integrity": "sha512-zsPqierShVc0NN+JCyJO18jMFDTbGNSgmekQm+Zr5JYH/aZShsjOBGQmjNiQmIw7nJNQDRzh1oQUND3TY/9Swg==", + "requires": { + "classnames": "^2.2.5", + "date-fns": "^2.0.0-alpha.23", + "prop-types": "^15.6.0", + "react-onclickoutside": "^6.7.1", + "react-popper": "^1.0.2" + }, + "dependencies": { + "date-fns": { + "version": "2.0.0-alpha.27", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.0.0-alpha.27.tgz", + "integrity": "sha512-cqfVLS+346P/Mpj2RpDrBv0P4p2zZhWWvfY5fuWrXNR/K38HaAGEkeOwb47hIpQP9Jr/TIxjZ2/sNMQwdXuGMg==" + } + } + }, "react-dimensions": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/react-dimensions/-/react-dimensions-1.3.1.tgz", @@ -12574,6 +12610,34 @@ "xtend": "^4.0.1" } }, + "react-onclickoutside": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.7.1.tgz", + "integrity": "sha512-p84kBqGaMoa7VYT0vZ/aOYRfJB+gw34yjpda1Z5KeLflg70HipZOT+MXQenEhdkPAABuE2Astq4zEPdMqUQxcg==" + }, + "react-popper": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.3.tgz", + "integrity": "sha512-ynMZBPkXONPc5K4P5yFWgZx5JGAUIP3pGGLNs58cfAPgK67olx7fmLp+AdpZ0+GoQ+ieFDa/z4cdV6u7sioH6w==", + "requires": { + "@babel/runtime": "^7.1.2", + "create-react-context": "<=0.2.2", + "popper.js": "^1.14.4", + "prop-types": "^15.6.1", + "typed-styles": "^0.0.7", + "warning": "^4.0.2" + }, + "dependencies": { + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + } + } + }, "react-redux": { "version": "5.0.7", "resolved": "http://registry.npmjs.org/react-redux/-/react-redux-5.0.7.tgz", @@ -14630,204 +14694,6 @@ "loader-utils": "^1.0.2", "micromatch": "^3.1.4", "semver": "^5.0.1" - }, - "dependencies": { - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - } - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - } } }, "tslib": { @@ -14931,6 +14797,11 @@ "mime-types": "~2.1.18" } }, + "typed-styles": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz", + "integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==" + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", diff --git a/ui/package.json b/ui/package.json index f18476937b8..839bd994aaa 100644 --- a/ui/package.json +++ b/ui/package.json @@ -161,6 +161,7 @@ "react": "^16.8.0", "react-codemirror2": "^4.2.1", "react-copy-to-clipboard": "^5.0.1", + "react-datepicker": "^2.1.0", "react-dimensions": "^1.2.0", "react-dnd": "^2.6.0", "react-dnd-html5-backend": "^2.6.0", diff --git a/ui/src/shared/components/ClickOutside.tsx b/ui/src/shared/components/ClickOutside.tsx index 85ef24e1b6e..7503aed5073 100644 --- a/ui/src/shared/components/ClickOutside.tsx +++ b/ui/src/shared/components/ClickOutside.tsx @@ -10,11 +10,11 @@ interface Props { @ErrorHandling export class ClickOutside extends PureComponent { public componentDidMount() { - document.addEventListener('click', this.handleClickOutside, true) + document.addEventListener('mousedown', this.handleClickOutside, true) } public componentWillUnmount() { - document.removeEventListener('click', this.handleClickOutside, true) + document.removeEventListener('mousedown', this.handleClickOutside, true) } public render() { diff --git a/ui/src/shared/components/TimeRangeDropdown.tsx b/ui/src/shared/components/TimeRangeDropdown.tsx index 39dc46a151d..f00f2dbfeab 100644 --- a/ui/src/shared/components/TimeRangeDropdown.tsx +++ b/ui/src/shared/components/TimeRangeDropdown.tsx @@ -1,11 +1,18 @@ // Libraries -import React, {PureComponent} from 'react' +import React, {PureComponent, createRef} from 'react' +import {get} from 'lodash' +import moment from 'moment' // Components import {Dropdown} from 'src/clockface' +import DateRangePicker from 'src/shared/components/dateRangePicker/DateRangePicker' // Constants -import {TIME_RANGES} from 'src/shared/constants/timeRanges' +import { + TIME_RANGES, + CUSTOM_TIME_RANGE, + TIME_RANGE_FORMAT, +} from 'src/shared/constants/timeRanges' // Types import {TimeRange} from 'src/types' @@ -15,34 +22,134 @@ interface Props { onSetTimeRange: (timeRange: TimeRange) => void } -class TimeRangeDropdown extends PureComponent { +interface State { + isDatePickerOpen: boolean + dropdownPosition: {top: number; right: number} +} + +class TimeRangeDropdown extends PureComponent { + private dropdownRef = createRef() + + constructor(props: Props) { + super(props) + + this.state = {isDatePickerOpen: false, dropdownPosition: undefined} + } + public render() { + const timeRange = this.timeRange + + return ( + <> + {this.isDatePickerVisible && ( + + )} +
+ + {TIME_RANGES.map(({label}) => ( + + {label} + + ))} + +
+ + ) + } + + private get dropdownWidth(): number { + if (this.isCustomTimeRange) { + return 250 + } + + return 100 + } + + private get isCustomTimeRange(): boolean { + const {timeRange} = this.props + return ( + get(timeRange, 'label', '') === CUSTOM_TIME_RANGE || !!timeRange.upper + ) + } + + private get formattedCustomTimeRange(): string { const {timeRange} = this.props + if (!this.isCustomTimeRange) { + return timeRange.label + } + + return `${moment(timeRange.lower).format(TIME_RANGE_FORMAT)} - ${moment( + timeRange.upper + ).format(TIME_RANGE_FORMAT)}` + } + + private get timeRange(): TimeRange { + const {timeRange} = this.props + const {isDatePickerOpen} = this.state + + if (isDatePickerOpen) { + const date = new Date().toISOString() + const upper = + timeRange.upper && this.isCustomTimeRange ? timeRange.upper : date + const lower = + timeRange.lower && this.isCustomTimeRange ? timeRange.lower : date + return { + label: CUSTOM_TIME_RANGE, + lower, + upper, + } + } + + if (this.isCustomTimeRange) { + return { + ...timeRange, + label: this.formattedCustomTimeRange, + } + } + const selectedTimeRange = TIME_RANGES.find(t => t.lower === timeRange.lower) if (!selectedTimeRange) { throw new Error('TimeRangeDropdown passed unknown TimeRange') } - return ( - - {TIME_RANGES.map(({label}) => ( - - {label} - - ))} - - ) + return selectedTimeRange + } + + private get isDatePickerVisible() { + return this.state.isDatePickerOpen + } + + private handleApplyTimeRange = (timeRange: TimeRange) => { + this.props.onSetTimeRange(timeRange) + this.handleHideDatePicker() + } + + private handleHideDatePicker = () => { + this.setState({isDatePickerOpen: false, dropdownPosition: null}) } private handleChange = (label: string): void => { const {onSetTimeRange} = this.props const timeRange = TIME_RANGES.find(t => t.label === label) + if (label === CUSTOM_TIME_RANGE) { + const {top, left} = this.dropdownRef.current.getBoundingClientRect() + const right = window.innerWidth - left + this.setState({isDatePickerOpen: true, dropdownPosition: {top, right}}) + return + } + onSetTimeRange(timeRange) } } diff --git a/ui/src/shared/components/dateRangePicker/DatePicker.tsx b/ui/src/shared/components/dateRangePicker/DatePicker.tsx new file mode 100644 index 00000000000..06335ef7016 --- /dev/null +++ b/ui/src/shared/components/dateRangePicker/DatePicker.tsx @@ -0,0 +1,85 @@ +// Libraries +import React, {PureComponent} from 'react' +import ReactDatePicker from 'react-datepicker' + +// Styles +import 'react-datepicker/dist/react-datepicker.css' +import {Input} from 'src/clockface' +import {ComponentSize} from '@influxdata/clockface' +import FormLabel from 'src/clockface/components/form_layout/FormLabel' + +interface Props { + label: string + dateTime: string + onSelectDate: (date: string) => void +} + +class DatePicker extends PureComponent { + private inCurrentMonth: boolean = false + + public render() { + const {dateTime, label} = this.props + const date = new Date(dateTime) + + return ( + +
+ +
+
+ ) + } + + private get customInput() { + return ( + + ) + } + + private dayClassName = (date: Date) => { + const day = date.getDate() + + if (day === 1) { + this.inCurrentMonth = !this.inCurrentMonth + } + + if (this.inCurrentMonth) { + return 'range-picker--day-in-month' + } + + return 'range-picker--day' + } + + private popperContainer({children}): JSX.Element { + return
{children}
+ } + + private handleSelectDate = (date: Date): void => { + const {onSelectDate} = this.props + + onSelectDate(date.toISOString()) + } +} + +export default DatePicker diff --git a/ui/src/shared/components/dateRangePicker/DateRangePicker.scss b/ui/src/shared/components/dateRangePicker/DateRangePicker.scss new file mode 100644 index 00000000000..f36d22586f1 --- /dev/null +++ b/ui/src/shared/components/dateRangePicker/DateRangePicker.scss @@ -0,0 +1,204 @@ +/* + Date Range Picker Styles + ------------------------------------------------------------------------------ +*/ + +@import 'src/style/modules'; + +.range-picker { + position: fixed; + text-align: center; + background-color: $g1-raven; + border: $ix-border solid $c-pool; + padding: $ix-marg-b; + border-radius: $ix-radius; + z-index: 9999; + height: 410px; + + .react-datepicker { + font-family: $ix-text-font; + font-size: $ix-text-base-1; + } + + .range-picker--date-pickers { + flex-wrap: nowrap; + display: flex; + flex-direction: row; + align-items: center; + margin: $ix-marg-b 0; + + .range-picker--date-picker { + .range-picker--popper-container { + position: relative; + } + + .range-picker--popper { + position: relative !important; + transform: none !important; + @include no-user-select(); + + .range-picker--calendar { + background-color: transparent; + border: none; + color: $c-pool; + display: inline-flex; + flex-direction: row; + + .react-datepicker__navigation { + outline: none; + cursor: pointer; + } + + .react-datepicker__navigation--next { + border-left-color: $g18-cloud; + } + + .react-datepicker__navigation--previous { + border-right-color: $g18-cloud; + } + + .range-picker--day { + color: $c-void; + font-weight: 400; + + &:hover { + background-color: $c-laser; + color: $g20-white; + } + } + + .range-picker--day-in-month { + color: $c-star; + + &:hover { + background-color: $c-laser; + color: $g20-white; + } + } + + .react-datepicker__day--selected { + background-color: $c-pool; + color: $g18-cloud; + } + + .react-datepicker__triangle { + display: none; + } + + .react-datepicker__header { + border-radius: 0; + padding: 0; + border: none; + background: transparent; + + .react-datepicker__day-name { + color: $c-rainforest; + } + + .react-datepicker__current-month { + width: 100%; + border-radius: $ix-radius $ix-radius 0 0; + background-color: $g4-onyx; + color: $g18-cloud; + font-weight: 700; + height: $ix-marg-d; + display: inline-flex; + flex-direction: row; + justify-content: center; + align-items: center; + } + } + + .react-datepicker__time-container { + width: 70px; + border: none; + margin-left: $ix-marg-a; + border-radius: $ix-radius; + background-color: transparent; + overflow: hidden; + + .react-datepicker__header--time { + width: 100%; + border-radius: $ix-radius $ix-radius 0 0; + background-color: $g4-onyx; + font-weight: 700; + height: $ix-marg-d; + display: inline-flex; + flex-direction: row; + justify-content: center; + align-items: center; + } + + .react-datepicker-time__header { + color: $g18-cloud; + } + + .react-datepicker__time { + background-color: transparent; + + .react-datepicker__time-box { + width: 100%; + background-color: $g2-kevlar; + color: $g18-cloud; + + .react-datepicker__time-list { + font-size: $ix-text-base; + + .react-datepicker__time-list-item:hover { + background-color: $c-laser; + color: $g20-white; + } + + .react-datepicker__time-list-item--selected { + background-color: $c-pool; + } + } + } + } + } + + } + } + } + } +} + +.range-picker--dismiss { + position: absolute; + z-index: 5000; + top: 0; + right: 0; + transform: translate(50%,-50%); + width: 24px; + height: 24px; + outline: none; + border-radius: 50%; + background-color: $c-pool; + transition: background-color 0.25s ease; + border: 0; + + &:before, + &:after { + content: ''; + position: absolute; + width: 13px; + height: 3px; + top: 50%; + left: 50%; + border-radius: 1px; + background-color: $g20-white; + } + + &:before { + transform: translate(-50%, -50%) rotate(45deg); + } + + &:after { + transform: translate(-50%, -50%) rotate(-45deg); + } + + &:hover { + background-color: $c-laser; + cursor: pointer; + } +} \ No newline at end of file diff --git a/ui/src/shared/components/dateRangePicker/DateRangePicker.tsx b/ui/src/shared/components/dateRangePicker/DateRangePicker.tsx new file mode 100644 index 00000000000..c8215845f50 --- /dev/null +++ b/ui/src/shared/components/dateRangePicker/DateRangePicker.tsx @@ -0,0 +1,136 @@ +// Libraries +import React, {PureComponent, createRef, CSSProperties} from 'react' + +// Components +import DatePicker from 'src/shared/components/dateRangePicker/DatePicker' +import {ClickOutside} from 'src/shared/components/ClickOutside' + +// Styles +import 'src/shared/components/dateRangePicker/DateRangePicker.scss' + +// Types +import {TimeRange} from 'src/types' +import {Button, ComponentColor, ComponentSize} from '@influxdata/clockface' + +interface Props { + timeRange: TimeRange + onSetTimeRange: (timeRange: TimeRange) => void + position?: {top: number; right: number} + onClose: () => void +} + +interface State { + lower: string + upper: string + bottomPosition?: number + topPosition?: number +} + +const PICKER_HEIGHT = 410 +const HORIZONTAL_PADDING = 2 +const VERTICAL_PADDING = 15 + +class DateRangePicker extends PureComponent { + private rangePickerRef = createRef() + + constructor(props: Props) { + super(props) + const { + timeRange: {lower, upper}, + } = props + + this.state = {lower, upper, bottomPosition: null} + } + + public componentDidMount() { + const { + bottom, + top, + height, + } = this.rangePickerRef.current.getBoundingClientRect() + + if (bottom > window.innerHeight) { + this.setState({bottomPosition: height / 2}) + } else if (top < 0) { + this.setState({topPosition: height / 2}) + } + } + + public render() { + const {onClose} = this.props + const {upper, lower} = this.state + + return ( + +
+
+
+ ) + } + + private get stylePosition(): CSSProperties { + const {position} = this.props + const {bottomPosition, topPosition} = this.state + + if (!position) { + return + } + + const {top, right} = position + + if (topPosition) { + return { + top: '14px', + right: `${right + HORIZONTAL_PADDING}px`, + } + } + + const bottomPx = + (bottomPosition || window.innerHeight - top - VERTICAL_PADDING) - + PICKER_HEIGHT / 2 + return { + bottom: `${bottomPx}px`, + right: `${right + HORIZONTAL_PADDING}px`, + } + } + + private handleSetTimeRange = (): void => { + const {onSetTimeRange, timeRange} = this.props + const {upper, lower} = this.state + + onSetTimeRange({...timeRange, lower, upper}) + } + + private handleSelectLower = (lower: string): void => { + this.setState({lower}) + } + + private handleSelectUpper = (upper: string): void => { + this.setState({upper}) + } +} + +export default DateRangePicker diff --git a/ui/src/shared/constants/index.ts b/ui/src/shared/constants/index.ts index 44a64065cd6..a4aac409d16 100644 --- a/ui/src/shared/constants/index.ts +++ b/ui/src/shared/constants/index.ts @@ -410,6 +410,7 @@ export const LAYOUT_MARGIN = 4 export const DASHBOARD_LAYOUT_ROW_HEIGHT = 83.5 export const TIME_RANGE_START = 'timeRangeStart' +export const TIME_RANGE_STOP = 'timeRangeStop' export const WINDOW_PERIOD = 'windowPeriod' export const DYGRAPH_CONTAINER_H_MARGIN = 16 diff --git a/ui/src/shared/constants/timeRanges.ts b/ui/src/shared/constants/timeRanges.ts index 74046b64692..31c02b18311 100644 --- a/ui/src/shared/constants/timeRanges.ts +++ b/ui/src/shared/constants/timeRanges.ts @@ -1,6 +1,13 @@ import {TimeRange} from 'src/types' +export const CUSTOM_TIME_RANGE = 'Custom Time Range' +export const TIME_RANGE_FORMAT = 'YYYY-MM-DD HH:mm' + export const TIME_RANGES: TimeRange[] = [ + { + lower: '', + label: CUSTOM_TIME_RANGE, + }, { seconds: 300, lower: 'now() - 5m', diff --git a/ui/src/shared/utils/timeRangeVariables.ts b/ui/src/shared/utils/timeRangeVariables.ts index cb1c1b7f7d0..7559ccedd4b 100644 --- a/ui/src/shared/utils/timeRangeVariables.ts +++ b/ui/src/shared/utils/timeRangeVariables.ts @@ -1,5 +1,5 @@ import {TimeRange} from 'src/types/v2' -import {TIME_RANGE_START} from 'src/shared/constants' +import {TIME_RANGE_START, TIME_RANGE_STOP} from 'src/shared/constants' export const timeRangeVariables = ( timeRange: TimeRange @@ -10,5 +10,11 @@ export const timeRangeVariables = ( .replace('now()', '') .replace(/\s/g, '') + if (timeRange.upper) { + result[TIME_RANGE_STOP] = timeRange.upper + } else { + result[TIME_RANGE_STOP] = 'now()' + } + return result } diff --git a/ui/src/timeMachine/components/flux_functions_toolbar/FluxFunctionsToolbar.scss b/ui/src/timeMachine/components/flux_functions_toolbar/FluxFunctionsToolbar.scss index 816890e8737..4a2f2c1772e 100644 --- a/ui/src/timeMachine/components/flux_functions_toolbar/FluxFunctionsToolbar.scss +++ b/ui/src/timeMachine/components/flux_functions_toolbar/FluxFunctionsToolbar.scss @@ -121,45 +121,6 @@ } } -.flux-functions-toolbar--tooltip-dismiss { - position: absolute; - z-index: 5000; - top: 0; - right: 0; - transform: translate(0,-50%); - width: 24px; - height: 24px; - outline: none; - border-radius: 50%; - background-color: $c-pool; - transition: background-color 0.25s ease; - border: 0; - - &:before, - &:after { - content: ''; - position: absolute; - width: 13px; - height: 3px; - top: 50%; - left: 50%; - border-radius: 1px; - background-color: $g20-white; - } - - &:before { - transform: translate(-50%, -50%) rotate(45deg); - } - - &:after { - transform: translate(-50%, -50%) rotate(-45deg); - } - - &:hover { - background-color: $c-laser; - cursor: pointer; - } -} .flux-functions-toolbar--tooltip-dismiss { position: absolute; diff --git a/ui/src/timeMachine/utils/queryBuilder.test.ts b/ui/src/timeMachine/utils/queryBuilder.test.ts index a0e911b8ecf..d5f0bd6793a 100644 --- a/ui/src/timeMachine/utils/queryBuilder.test.ts +++ b/ui/src/timeMachine/utils/queryBuilder.test.ts @@ -11,7 +11,7 @@ describe('buildQuery', () => { } const expected = `from(bucket: "b0") - |> range(start: timeRangeStart) + |> range(start: timeRangeStart, stop: timeRangeStop) |> filter(fn: (r) => r._measurement == "m0")` const actual = buildQuery(config) @@ -30,7 +30,7 @@ describe('buildQuery', () => { } const expected = `from(bucket: "b0") - |> range(start: timeRangeStart) + |> range(start: timeRangeStart, stop: timeRangeStop) |> filter(fn: (r) => r._measurement == "m0" or r._measurement == "m1") |> filter(fn: (r) => r._field == "f0" or r._field == "f1")` @@ -47,7 +47,7 @@ describe('buildQuery', () => { } const expected = `from(bucket: "b0") - |> range(start: timeRangeStart) + |> range(start: timeRangeStart, stop: timeRangeStop) |> filter(fn: (r) => r._measurement == "m0") |> window(period: windowPeriod) |> mean() @@ -55,7 +55,7 @@ describe('buildQuery', () => { |> yield(name: "mean") from(bucket: "b0") - |> range(start: timeRangeStart) + |> range(start: timeRangeStart, stop: timeRangeStop) |> filter(fn: (r) => r._measurement == "m0") |> window(period: windowPeriod) |> toFloat() diff --git a/ui/src/timeMachine/utils/queryBuilder.ts b/ui/src/timeMachine/utils/queryBuilder.ts index e68a397be39..112b8ef9978 100644 --- a/ui/src/timeMachine/utils/queryBuilder.ts +++ b/ui/src/timeMachine/utils/queryBuilder.ts @@ -1,6 +1,10 @@ import {BuilderConfig} from 'src/types/v2' import {FUNCTIONS} from 'src/timeMachine/constants/queryBuilder' -import {TIME_RANGE_START, WINDOW_PERIOD} from 'src/shared/constants' +import { + TIME_RANGE_START, + WINDOW_PERIOD, + TIME_RANGE_STOP, +} from 'src/shared/constants' export function isConfigValid(builderConfig: BuilderConfig): boolean { const {buckets, tags} = builderConfig @@ -35,7 +39,7 @@ function buildQueryHelper( const fnCall = fn ? formatFunctionCall(fn) : '' const query = `from(bucket: "${bucket}") - |> range(start: ${TIME_RANGE_START})${tagFilterCall}${fnCall}` + |> range(start: ${TIME_RANGE_START}, stop: ${TIME_RANGE_STOP})${tagFilterCall}${fnCall}` return query }