Merge pull request #11990 from influxdata/feat/time-range-calendar

feat(ui): Add custom time range option in time range dropdown
pull/12001/head
Iris Scholten 2019-02-19 17:20:54 -08:00 committed by GitHub
commit d895f5aeb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 649 additions and 265 deletions

View File

@ -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

273
ui/package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -10,11 +10,11 @@ interface Props {
@ErrorHandling
export class ClickOutside extends PureComponent<Props> {
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() {

View File

@ -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<Props> {
interface State {
isDatePickerOpen: boolean
dropdownPosition: {top: number; right: number}
}
class TimeRangeDropdown extends PureComponent<Props, State> {
private dropdownRef = createRef<HTMLDivElement>()
constructor(props: Props) {
super(props)
this.state = {isDatePickerOpen: false, dropdownPosition: undefined}
}
public render() {
const timeRange = this.timeRange
return (
<>
{this.isDatePickerVisible && (
<DateRangePicker
timeRange={timeRange}
onSetTimeRange={this.handleApplyTimeRange}
onClose={this.handleHideDatePicker}
position={this.state.dropdownPosition}
/>
)}
<div ref={this.dropdownRef}>
<Dropdown
selectedID={timeRange.label}
onChange={this.handleChange}
widthPixels={this.dropdownWidth}
titleText={this.formattedCustomTimeRange}
>
{TIME_RANGES.map(({label}) => (
<Dropdown.Item key={label} value={label} id={label}>
{label}
</Dropdown.Item>
))}
</Dropdown>
</div>
</>
)
}
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 (
<Dropdown
selectedID={selectedTimeRange.label}
onChange={this.handleChange}
widthPixels={100}
>
{TIME_RANGES.map(({label}) => (
<Dropdown.Item key={label} value={label} id={label}>
{label}
</Dropdown.Item>
))}
</Dropdown>
)
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)
}
}

View File

@ -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<Props> {
private inCurrentMonth: boolean = false
public render() {
const {dateTime, label} = this.props
const date = new Date(dateTime)
return (
<FormLabel label={label}>
<div className="range-picker--date-picker">
<ReactDatePicker
selected={date}
onChange={this.handleSelectDate}
startOpen={true}
dateFormat="yyyy-MM-dd HH:mm"
showTimeSelect={true}
timeFormat="HH:mm"
shouldCloseOnSelect={false}
disabledKeyboardNavigation={true}
customInput={this.customInput}
popperContainer={this.popperContainer}
popperClassName="range-picker--popper"
calendarClassName="range-picker--calendar"
dayClassName={this.dayClassName}
timeIntervals={60}
fixedHeight={true}
/>
</div>
</FormLabel>
)
}
private get customInput() {
return (
<Input
widthPixels={314}
size={ComponentSize.Medium}
customClass="range-picker--input react-datepicker-ignore-onclickoutside"
titleText="Start"
/>
)
}
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 <div className="range-picker--popper-container">{children}</div>
}
private handleSelectDate = (date: Date): void => {
const {onSelectDate} = this.props
onSelectDate(date.toISOString())
}
}
export default DatePicker

View File

@ -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;
}
}

View File

@ -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<Props, State> {
private rangePickerRef = createRef<HTMLDivElement>()
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 (
<ClickOutside onClickOutside={onClose}>
<div
className="range-picker react-datepicker-ignore-onclickoutside"
ref={this.rangePickerRef}
style={this.stylePosition}
>
<button className="range-picker--dismiss" onClick={onClose} />
<div className="range-picker--date-pickers">
<DatePicker
dateTime={lower}
onSelectDate={this.handleSelectLower}
label="Start"
/>
<DatePicker
dateTime={upper}
onSelectDate={this.handleSelectUpper}
label="Stop"
/>
</div>
<Button
color={ComponentColor.Primary}
size={ComponentSize.Small}
onClick={this.handleSetTimeRange}
text="Apply Time Range"
/>
</div>
</ClickOutside>
)
}
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

View File

@ -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

View File

@ -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',

View File

@ -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
}

View File

@ -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;

View File

@ -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()

View File

@ -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
}