Chore(ui): Use typed timeRanges throughout the app (#16130)

* chore(ui): Remove unused references to timeRange

* chore(ui): Add comment to timerange to duration function

* chore(ui): Fix getTimeRangeVars.test

* Refactor getTimeRangeVars.ts

* chore(ui): Remove references to zoomed time range

* chore(ui): Remove unused set dashboard time range v1 action

* chore(ui): Add preliminary timeRange types

* chore(ui): Add init pass at reasonable timeRange constants

* chore(ui): Finish Remove unused set dashboard time range v1 action

* fix(ui): Conver redux ranges state to object from array

* chore(ui): Read from and write to ranges local storage

* chore(ui): Reorder imported things

* chore(ui): Add typing to timeRanges from query params and localstorage

* chore(ui): Fix Dashboard and DE Time Range Dropdown

* chore(ui): Fix check timeRange

* chore(ui): Convert duration time Ranges to custom for time range dropdown

* chore(ui): Convert delete with predicate timeranges to TimeRange types

* chore(ui): Convert predicates timeRange to custom

* chore(ui): fix ValidateAndType bug

* fix(ui): Fix show bucketname in delete data form

* fix(ui): Allow timeRange to handle all duration time Ranges

* chore(ui): Separate DeleteDataOverlay update logic

* fix(ui): Fix deletedata overlay dismiss routing

* chore(ui) cleanup

* chore(ui): Allow timeRange dropdown to work with no prior timeRange selection

* chore(ui): Remove unused import

* fix(ui): Fix time Ranges tests

* fix(ui): Fix tests

* fix(ui): Fix tests

* fix(ui): Extract label creation function and use where needed

* fix(ui): Fix predicate testing

* fix(ui): Fix prettier issue

* chore(ui): Refactor duration.ts

* chore(ui): Remove labels from timeRange

* chore(ui): Return label for selectable-duration label

* chore(ui): Resolve merge error

* chore(ui): Make tests more robust

* feat(ui): protect query params access with get

* chore(ui): Add comment to warn against regex behavior

* Apply prettier to predicates.test
pull/16195/head
Deniz Kusefoglu 2019-12-10 12:52:03 -08:00 committed by GitHub
parent b2ea95f512
commit f64c63120a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 890 additions and 982 deletions

View File

@ -113,11 +113,6 @@ export const source: Source = {
telegraf: 'telegraf',
}
export const timeRange = {
lower: 'now() - 15m',
upper: null,
}
export const query = {
id: '0',
database: 'db1',

View File

@ -1,9 +1,17 @@
import {getCheckVisTimeRange} from 'src/alerting/utils/vis'
const duration = 'duration' as 'duration'
const TESTS = [
['5s', {lower: 'now() - 1500s'}],
['1m', {lower: 'now() - 300m'}],
['1m5s', {lower: 'now() - 300m1500s'}],
['5s', {type: duration, lower: 'now() - 1500s', upper: null}],
['1m', {type: duration, lower: 'now() - 300m', upper: null}],
[
'1m5s',
{
type: duration,
lower: 'now() - 300m1500s',
upper: null,
},
],
]
test.each(TESTS)('getCheckVisTimeRange(%s)', (input, expected) => {

View File

@ -9,7 +9,7 @@ import {extent} from 'src/shared/utils/vis'
import {flatMap} from 'lodash'
// Types
import {TimeRange, Threshold} from 'src/types'
import {Threshold, DurationTimeRange} from 'src/types'
const POINTS_PER_CHECK_PLOT = 300
@ -24,12 +24,16 @@ const POINTS_PER_CHECK_PLOT = 300
each minute. So to display a plot with say, 300 points, we need to query a
time range of the last 300 minutes.
*/
export const getCheckVisTimeRange = (durationStr: string): TimeRange => {
export const getCheckVisTimeRange = (
durationStr: string
): DurationTimeRange => {
const durationMultiple = parseDuration(durationStr)
.map(({magnitude, unit}) => `${magnitude * POINTS_PER_CHECK_PLOT}${unit}`)
.join('')
return {lower: `now() - ${durationMultiple}`}
const lower = `now() - ${durationMultiple}`
return {upper: null, lower, type: 'duration'}
}
/*

View File

@ -368,6 +368,7 @@ export const getDashboardAsync = (dashboardID: string) => async (
// Now that all the necessary state has been loaded, set the dashboard
dispatch(setDashboard(dashboard))
dispatch(updateTimeRangeFromQueryParams(dashboardID))
} catch (error) {
const {
orgs: {org},
@ -376,8 +377,6 @@ export const getDashboardAsync = (dashboardID: string) => async (
dispatch(notify(copy.dashboardGetFailed(dashboardID, error.message)))
return
}
dispatch(updateTimeRangeFromQueryParams(dashboardID))
}
export const updateDashboardAsync = (dashboard: Dashboard) => async (

View File

@ -2,14 +2,14 @@
import qs from 'qs'
import {replace, RouterAction} from 'react-router-redux'
import {Dispatch, Action} from 'redux'
import _ from 'lodash'
import {get, pickBy} from 'lodash'
// Actions
import {notify} from 'src/shared/actions/notifications'
// Utils
import {validTimeRange, validAbsoluteTimeRange} from 'src/dashboards/utils/time'
import {stripPrefix} from 'src/utils/basepath'
import {validateAndTypeRange} from 'src/dashboards/utils/time'
// Constants
import * as copy from 'src/shared/copy/notifications'
@ -19,17 +19,14 @@ import {DEFAULT_TIME_RANGE} from 'src/shared/constants/timeRanges'
import {TimeRange} from 'src/types'
export type Action =
| SetDashTimeV1Action
| SetZoomedTimeRangeAction
| SetDashboardTimeRangeAction
| DeleteTimeRangeAction
| RetainRangesDashTimeV1Action
export enum ActionTypes {
DeleteTimeRange = 'DELETE_TIME_RANGE',
SetTimeRange = 'SET_DASHBOARD_TIME_RANGE',
SetDashboardTimeV1 = 'SET_DASHBOARD_TIME_V1',
SetDashboardTimeRange = 'SET_DASHBOARD_TIME_RANGE',
RetainRangesDashboardTimeV1 = 'RETAIN_RANGES_DASHBOARD_TIME_V1',
SetZoomedTimeRange = 'SET_DASHBOARD_ZOOMED_TIME_RANGE',
}
export interface DeleteTimeRangeAction {
@ -39,21 +36,14 @@ export interface DeleteTimeRangeAction {
}
}
interface SetDashTimeV1Action {
type: ActionTypes.SetDashboardTimeV1
interface SetDashboardTimeRangeAction {
type: ActionTypes.SetDashboardTimeRange
payload: {
dashboardID: string
timeRange: TimeRange
}
}
interface SetZoomedTimeRangeAction {
type: ActionTypes.SetZoomedTimeRange
payload: {
zoomedTimeRange: TimeRange
}
}
interface RetainRangesDashTimeV1Action {
type: ActionTypes.RetainRangesDashboardTimeV1
payload: {
@ -68,18 +58,11 @@ export const deleteTimeRange = (
payload: {dashboardID},
})
export const setZoomedTimeRange = (
zoomedTimeRange: TimeRange
): SetZoomedTimeRangeAction => ({
type: ActionTypes.SetZoomedTimeRange,
payload: {zoomedTimeRange},
})
export const setDashTimeV1 = (
export const setDashboardTimeRange = (
dashboardID: string,
timeRange: TimeRange
): SetDashTimeV1Action => ({
type: ActionTypes.SetDashboardTimeV1,
): SetDashboardTimeRangeAction => ({
type: ActionTypes.SetDashboardTimeRange,
payload: {dashboardID, timeRange},
})
@ -94,7 +77,7 @@ export const updateQueryParams = (updatedQueryParams: object): RouterAction => {
const {search, pathname} = window.location
const strippedPathname = stripPrefix(pathname)
const newQueryParams = _.pickBy(
const newQueryParams = pickBy(
{
...qs.parse(search, {ignoreQueryPrefix: true}),
...updatedQueryParams,
@ -117,49 +100,27 @@ export const updateTimeRangeFromQueryParams = (dashboardID: string) => (
ignoreQueryPrefix: true,
})
const timeRangeFromQueries = {
lower: queryParams.lower,
upper: queryParams.upper,
}
const validatedTimeRangeFromQuery = validateAndTypeRange({
lower: get(queryParams, 'lower', null),
upper: get(queryParams, 'upper', null),
})
const zoomedTimeRangeFromQueries = {
lower: queryParams.zoomedLower,
upper: queryParams.zoomedUpper,
}
let validatedTimeRange = validTimeRange(timeRangeFromQueries)
if (!validatedTimeRange.lower) {
const dashboardTimeRange = ranges.find(r => r.dashboardID === dashboardID)
validatedTimeRange = dashboardTimeRange || DEFAULT_TIME_RANGE
if (timeRangeFromQueries.lower || timeRangeFromQueries.upper) {
dispatch(notify(copy.invalidTimeRangeValueInURLQuery()))
}
}
dispatch(setDashTimeV1(dashboardID, validatedTimeRange))
const validatedZoomedTimeRange = validAbsoluteTimeRange(
zoomedTimeRangeFromQueries
)
const validatedTimeRange =
validatedTimeRangeFromQuery || ranges[dashboardID] || DEFAULT_TIME_RANGE
if (
!validatedZoomedTimeRange.lower &&
(queryParams.zoomedLower || queryParams.zoomedUpper)
(queryParams.lower || queryParams.upper) &&
!validatedTimeRangeFromQuery
) {
dispatch(notify(copy.invalidZoomedTimeRangeValueInURLQuery()))
dispatch(notify(copy.invalidTimeRangeValueInURLQuery()))
}
dispatch(setZoomedTimeRange(validatedZoomedTimeRange))
dispatch(setDashboardTimeRange(dashboardID, validatedTimeRange))
const updatedQueryParams = {
lower: validatedTimeRange.lower,
upper: validatedTimeRange.upper,
zoomedLower: validatedZoomedTimeRange.lower,
zoomedUpper: validatedZoomedTimeRange.upper,
}
dispatch(updateQueryParams(updatedQueryParams))
dispatch(
updateQueryParams({
lower: validatedTimeRange.lower,
upper: validatedTimeRange.upper,
})
)
}

View File

@ -108,7 +108,7 @@ export const getViewForTimeMachine = (
const state = getState()
let view = getViewFromState(state, cellID) as QueryView
const timeRange = getTimeRangeByDashboardID(state.ranges, dashboardID)
const timeRange = getTimeRangeByDashboardID(state, dashboardID)
if (!view) {
dispatch(setView(cellID, null, RemoteDataState.Loading))

View File

@ -4,9 +4,7 @@ import _ from 'lodash'
// Components
import AutoRefreshDropdown from 'src/shared/components/dropdown_auto_refresh/AutoRefreshDropdown'
import TimeRangeDropdown, {
RangeType,
} from 'src/shared/components/TimeRangeDropdown'
import TimeRangeDropdown from 'src/shared/components/TimeRangeDropdown'
import GraphTips from 'src/shared/components/graph_tips/GraphTips'
import RenamablePageTitle from 'src/pageLayout/components/RenamablePageTitle'
import TimeZoneDropdown from 'src/shared/components/TimeZoneDropdown'
@ -26,17 +24,21 @@ import {
// Types
import * as AppActions from 'src/types/actions/app'
import * as QueriesModels from 'src/types/queries'
import {Dashboard} from '@influxdata/influx'
import {AutoRefresh, AutoRefreshStatus, Organization} from 'src/types'
import {
Dashboard,
AutoRefresh,
AutoRefreshStatus,
Organization,
TimeRange,
} from 'src/types'
interface Props {
org: Organization
activeDashboard: string
dashboard: Dashboard
timeRange: QueriesModels.TimeRange
timeRange: TimeRange
autoRefresh: AutoRefresh
handleChooseTimeRange: (timeRange: QueriesModels.TimeRange) => void
handleChooseTimeRange: (timeRange: TimeRange) => void
handleChooseAutoRefresh: (autoRefreshInterval: number) => void
onSetAutoRefreshStatus: (status: AutoRefreshStatus) => void
onManualRefresh: () => void
@ -46,31 +48,21 @@ interface Props {
toggleVariablesControlBar: () => void
isShowingVariablesControlBar: boolean
onAddNote: () => void
zoomedTimeRange: QueriesModels.TimeRange
}
export default class DashboardHeader extends Component<Props> {
public static defaultProps = {
zoomedTimeRange: {
upper: null,
lower: null,
},
}
public render() {
const {
org,
handleChooseAutoRefresh,
onManualRefresh,
timeRange,
timeRange: {upper, lower},
zoomedTimeRange: {upper: zoomedUpper, lower: zoomedLower},
toggleVariablesControlBar,
isShowingVariablesControlBar,
onRenameDashboard,
onAddCell,
activeDashboard,
autoRefresh,
timeRange,
} = this.props
return (
@ -106,11 +98,7 @@ export default class DashboardHeader extends Component<Props> {
/>
<TimeRangeDropdown
onSetTimeRange={this.handleChooseTimeRange}
timeRange={{
...timeRange,
upper: zoomedUpper || upper,
lower: zoomedLower || lower,
}}
timeRange={timeRange}
/>
<Button
icon={IconFont.Cube}
@ -140,10 +128,7 @@ export default class DashboardHeader extends Component<Props> {
this.props.handleClickPresentationButton()
}
private handleChooseTimeRange = (
timeRange: QueriesModels.TimeRange,
rangeType: RangeType = RangeType.Relative
) => {
private handleChooseTimeRange = (timeRange: TimeRange) => {
const {
autoRefresh,
onSetAutoRefreshStatus,
@ -152,7 +137,7 @@ export default class DashboardHeader extends Component<Props> {
handleChooseTimeRange(timeRange)
if (rangeType === RangeType.Absolute) {
if (timeRange.type === 'custom') {
onSetAutoRefreshStatus(AutoRefreshStatus.Disabled)
return
}

View File

@ -23,6 +23,7 @@ import {
setAutoRefreshInterval,
setAutoRefreshStatus,
} from 'src/shared/actions/autoRefresh'
import {toggleShowVariablesControls} from 'src/userSettings/actions'
// Utils
import {GlobalAutoRefresher} from 'src/utils/AutoRefresher'
@ -35,6 +36,9 @@ import {pageTitleSuffixer} from 'src/shared/utils/pageTitles'
// Constants
import {AUTOREFRESH_DEFAULT} from 'src/shared/constants'
// Selectors
import {getTimeRangeByDashboardID} from 'src/dashboards/selectors'
// Types
import {
Links,
@ -46,25 +50,20 @@ import {
AutoRefresh,
AutoRefreshStatus,
Organization,
RemoteDataState,
} from 'src/types'
import {RemoteDataState} from 'src/types'
import {WithRouterProps} from 'react-router'
import {ManualRefreshProps} from 'src/shared/components/ManualRefresh'
import {Location} from 'history'
import * as AppActions from 'src/types/actions/app'
import * as ColorsModels from 'src/types/colors'
import {toggleShowVariablesControls} from 'src/userSettings/actions'
import {LimitStatus} from 'src/cloud/actions/limits'
// Selector
import {getTimeRangeByDashboardID} from 'src/dashboards/selectors/index'
interface StateProps {
limitedResources: string[]
limitStatus: LimitStatus
org: Organization
links: Links
zoomedTimeRange: TimeRange
timeRange: TimeRange
dashboard: Dashboard
autoRefresh: AutoRefresh
@ -79,8 +78,7 @@ interface DispatchProps {
updateDashboard: typeof dashboardActions.updateDashboardAsync
updateCells: typeof dashboardActions.updateCellsAsync
updateQueryParams: typeof rangesActions.updateQueryParams
setDashTimeV1: typeof rangesActions.setDashTimeV1
setZoomedTimeRange: typeof rangesActions.setZoomedTimeRange
setDashboardTimeRange: typeof rangesActions.setDashboardTimeRange
handleChooseAutoRefresh: typeof setAutoRefreshInterval
onSetAutoRefreshStatus: typeof setAutoRefreshStatus
handleClickPresentationButton: AppActions.DelayEnablePresentationModeDispatcher
@ -143,7 +141,6 @@ class DashboardPage extends Component<Props> {
const {
org,
timeRange,
zoomedTimeRange,
dashboard,
autoRefresh,
limitStatus,
@ -168,7 +165,6 @@ class DashboardPage extends Component<Props> {
onAddCell={this.handleAddCell}
onAddNote={this.showNoteOverlay}
onManualRefresh={onManualRefresh}
zoomedTimeRange={zoomedTimeRange}
onRenameDashboard={this.handleRenameDashboard}
activeDashboard={dashboard ? dashboard.name : ''}
handleChooseAutoRefresh={this.handleChooseAutoRefresh}
@ -213,8 +209,8 @@ class DashboardPage extends Component<Props> {
}
private handleChooseTimeRange = (timeRange: TimeRange): void => {
const {dashboard, setDashTimeV1, updateQueryParams} = this.props
setDashTimeV1(dashboard.id, {...timeRange})
const {dashboard, setDashboardTimeRange, updateQueryParams} = this.props
setDashboardTimeRange(dashboard.id, timeRange)
updateQueryParams({
lower: timeRange.lower,
upper: timeRange.upper,
@ -302,7 +298,6 @@ class DashboardPage extends Component<Props> {
const mstp = (state: AppState, {params: {dashboardID}}): StateProps => {
const {
links,
ranges,
dashboards,
views: {views},
userSettings: {showVariablesControls},
@ -310,7 +305,7 @@ const mstp = (state: AppState, {params: {dashboardID}}): StateProps => {
cloud: {limits},
} = state
const timeRange = getTimeRangeByDashboardID(ranges, dashboardID)
const timeRange = getTimeRangeByDashboardID(state, dashboardID)
const autoRefresh = state.autoRefresh[dashboardID] || AUTOREFRESH_DEFAULT
@ -323,7 +318,6 @@ const mstp = (state: AppState, {params: {dashboardID}}): StateProps => {
org,
links,
views,
zoomedTimeRange: {lower: null, upper: null},
timeRange,
dashboard,
autoRefresh,
@ -342,9 +336,8 @@ const mdtp: DispatchProps = {
handleChooseAutoRefresh: setAutoRefreshInterval,
onSetAutoRefreshStatus: setAutoRefreshStatus,
handleClickPresentationButton: appActions.delayEnablePresentationMode,
setDashTimeV1: rangesActions.setDashTimeV1,
setDashboardTimeRange: rangesActions.setDashboardTimeRange,
updateQueryParams: rangesActions.updateQueryParams,
setZoomedTimeRange: rangesActions.setZoomedTimeRange,
onCreateCellWithView: dashboardActions.createCellWithView,
onUpdateView: dashboardActions.updateView,
onToggleShowVariablesControls: toggleShowVariablesControls,

View File

@ -1,44 +1,51 @@
import reducer from 'src/dashboards/reducers/ranges'
import {setDashTimeV1, deleteTimeRange} from 'src/dashboards/actions/ranges'
import {
setDashboardTimeRange,
deleteTimeRange,
} from 'src/dashboards/actions/ranges'
import {RangeState} from 'src/dashboards/reducers/ranges'
import {pastHourTimeRange} from 'src/shared/constants/timeRanges'
const emptyState = undefined
const emptyState = {}
const dashboardID = '1'
const timeRange = {upper: null, lower: 'now() - 15m'}
describe('Dashboards.Reducers.Ranges', () => {
it('can delete a dashboard time range', () => {
const state = [{dashboardID, ...timeRange}]
const state: RangeState = {[dashboardID]: pastHourTimeRange}
const actual = reducer(state, deleteTimeRange(dashboardID))
const expected = []
const expected = emptyState
expect(actual).toEqual(expected)
})
describe('setting a dashboard time range', () => {
it('can update an existing dashboard', () => {
const state = [
{dashboardID, upper: timeRange.upper, lower: timeRange.lower},
]
const state: RangeState = {[dashboardID]: pastHourTimeRange}
const {upper, lower} = {
const timeRange = {
type: 'custom' as 'custom',
upper: '2017-10-07 12:05',
lower: '2017-10-05 12:04',
}
const actual = reducer(state, setDashTimeV1(dashboardID, {upper, lower}))
const expected = [{dashboardID, upper, lower}]
const actual = reducer(
state,
setDashboardTimeRange(dashboardID, timeRange)
)
const expected = {[dashboardID]: timeRange}
expect(actual).toEqual(expected)
})
it('can set a new time range if none exists', () => {
const actual = reducer(emptyState, setDashTimeV1(dashboardID, timeRange))
const actual = reducer(
emptyState,
setDashboardTimeRange(dashboardID, pastHourTimeRange)
)
const expected = [
{dashboardID, upper: timeRange.upper, lower: timeRange.lower},
]
const expected = {[dashboardID]: pastHourTimeRange}
expect(actual).toEqual(expected)
})

View File

@ -1,37 +1,38 @@
import _ from 'lodash'
import {TimeRange} from 'src/types'
import {Action, ActionTypes} from 'src/dashboards/actions/ranges'
export interface Range extends TimeRange {
dashboardID: string
export type RangeState = {
[contextID: string]: TimeRange
}
export type RangeState = Range[]
const initialState: RangeState = {}
const initialState: RangeState = []
export default (state: RangeState = initialState, action: Action) => {
export default (
state: RangeState = initialState,
action: Action
): RangeState => {
switch (action.type) {
case ActionTypes.DeleteTimeRange: {
const {dashboardID} = action.payload
const ranges = state.filter(r => r.dashboardID !== dashboardID)
const {[dashboardID]: _, ...filteredRanges} = state
return ranges
return filteredRanges
}
case ActionTypes.RetainRangesDashboardTimeV1: {
const {dashboardIDs} = action.payload
const ranges = state.filter(r => dashboardIDs.includes(r.dashboardID))
const ranges = {}
for (const key in state) {
if (dashboardIDs.includes(key)) {
ranges[key] = state[key]
}
}
return ranges
}
case ActionTypes.SetDashboardTimeV1: {
case ActionTypes.SetDashboardTimeRange: {
const {dashboardID, timeRange} = action.payload
const newTimeRange = [{dashboardID, ...timeRange}]
const ranges = _.unionBy(newTimeRange, state, 'dashboardID')
return ranges
return {...state, [dashboardID]: timeRange}
}
}

View File

@ -5,7 +5,6 @@ import {
Dashboard,
FieldOption,
DecimalPlaces,
TimeRange,
TableOptions,
} from 'src/types'
@ -124,11 +123,6 @@ export const fullTimeRange = {
format: 'influxql',
}
export const timeRange: TimeRange = {
lower: 'now() - 5m',
upper: null,
}
export const thresholdsListColors: Color[] = [
{
type: 'text',

View File

@ -2,39 +2,43 @@
import {getTimeRangeByDashboardID} from 'src/dashboards/selectors/index'
// Types
import {Range} from 'src/dashboards/reducers/ranges'
import {RangeState} from 'src/dashboards/reducers/ranges'
import {TimeRange} from 'src/types'
// Constants
import {DEFAULT_TIME_RANGE} from 'src/shared/constants/timeRanges'
import {
DEFAULT_TIME_RANGE,
pastFifteenMinTimeRange,
pastHourTimeRange,
} from 'src/shared/constants/timeRanges'
const untypedGetTimeRangeByDashboardID = getTimeRangeByDashboardID as (
a: {ranges: RangeState},
dashID: string
) => TimeRange
describe('Dashboards.Selector', () => {
const ranges: Range[] = [
{
dashboardID: '04c6f3976f4b8001',
lower: 'now() - 5m',
upper: null,
},
{
dashboardID: '04c6f3976f4b8000',
lower: '2019-11-07T10:46:51.000Z',
upper: '2019-11-28T22:46:51.000Z',
},
]
const dashboardID: string = '04c6f3976f4b8000'
it('should return the default timerange when a no data is passed', () => {
expect(getTimeRangeByDashboardID()).toEqual(DEFAULT_TIME_RANGE)
})
const dashboardIDs = ['04c6f3976f4b8001', '04c6f3976f4b8000']
const ranges: RangeState = {
[dashboardIDs[0]]: pastFifteenMinTimeRange,
[dashboardIDs[1]]: pastHourTimeRange,
}
it('should return the the correct range when a matching dashboard ID is found', () => {
expect(getTimeRangeByDashboardID(ranges, dashboardID)).toEqual(ranges[1])
expect(untypedGetTimeRangeByDashboardID({ranges}, dashboardIDs[0])).toEqual(
pastFifteenMinTimeRange
)
})
it('should return the the default range when no matching dashboard ID is found', () => {
expect(getTimeRangeByDashboardID(ranges, 'Oogum Boogum')).toEqual(
expect(untypedGetTimeRangeByDashboardID({ranges}, 'Oogum Boogum')).toEqual(
DEFAULT_TIME_RANGE
)
})
it('should return the the default range when no ranges are passed in', () => {
expect(getTimeRangeByDashboardID([], dashboardID)).toEqual(
DEFAULT_TIME_RANGE
)
expect(
untypedGetTimeRangeByDashboardID({ranges: {}}, dashboardIDs[0])
).toEqual(DEFAULT_TIME_RANGE)
})
})

View File

@ -4,19 +4,18 @@ import {
AppState,
View,
Check,
TimeRange,
ViewType,
RemoteDataState,
TimeRange,
} from 'src/types'
import {Range} from 'src/dashboards/reducers/ranges'
import {
getValuesForVariable,
getTypeForVariable,
getArgumentValuesForVariable,
} from 'src/variables/selectors'
// Constants
import {DEFAULT_TIME_RANGE} from 'src/shared/constants/timeRanges'
export const getView = (state: AppState, id: string): View => {
@ -28,10 +27,9 @@ export const getViewStatus = (state: AppState, id: string): RemoteDataState => {
}
export const getTimeRangeByDashboardID = (
ranges: Range[] = [],
dashboardID: string = ''
): TimeRange =>
ranges.find(r => r.dashboardID === dashboardID) || DEFAULT_TIME_RANGE
state: AppState,
dashboardID: string
): TimeRange => state.ranges[dashboardID] || DEFAULT_TIME_RANGE
export const getCheckForView = (
state: AppState,

View File

@ -1,7 +1,8 @@
import {TimeRange} from 'src/types/queries'
import moment from 'moment'
import {CustomTimeRange, TimeRange, DurationTimeRange} from 'src/types/queries'
import {TIME_RANGES} from 'src/shared/constants/timeRanges'
import {SELECTABLE_TIME_RANGES} from 'src/shared/constants/timeRanges'
import {isDateParseable} from 'src/variables/utils/getTimeRangeVars'
import {isDurationParseable} from 'src/shared/utils/duration'
interface InputTimeRange {
seconds?: number
@ -34,37 +35,32 @@ export const millisecondTimeRange = ({
return {since, until}
}
const nullTimeRange: TimeRange = {lower: null, upper: null}
const validRelativeTimeRange = (timeRange: TimeRange): TimeRange => {
const validatedTimeRange = TIME_RANGES.find(t => t.lower === timeRange.lower)
if (validatedTimeRange) {
return validatedTimeRange
export const validateAndTypeRange = (timeRange: {
lower: string
upper: string
}): TimeRange => {
const {lower, upper} = timeRange
if (isDateParseable(lower) && isDateParseable(upper)) {
return {
...timeRange,
type: 'custom',
} as CustomTimeRange
}
return nullTimeRange
}
if (isDurationParseable(lower)) {
const selectableTimeRange = SELECTABLE_TIME_RANGES.find(
r => r.lower === lower
)
export const validAbsoluteTimeRange = (timeRange: TimeRange): TimeRange => {
if (timeRange.lower && timeRange.upper) {
if (moment(timeRange.lower).isValid()) {
if (
timeRange.upper === 'now()' ||
(moment(timeRange.upper).isValid() &&
moment(timeRange.lower).isBefore(moment(timeRange.upper)))
) {
return timeRange
}
if (selectableTimeRange) {
return selectableTimeRange
}
}
return nullTimeRange
}
export const validTimeRange = (timeRange: TimeRange): TimeRange => {
const validatedTimeRange = validRelativeTimeRange(timeRange)
if (validatedTimeRange.lower) {
return validatedTimeRange
return {
lower,
upper: null,
type: 'duration',
} as DurationTimeRange
}
return validAbsoluteTimeRange(timeRange)
return null
}

View File

@ -1,43 +1,37 @@
// Libraries
import React, {FunctionComponent} from 'react'
import React, {FunctionComponent, useEffect} from 'react'
import {connect} from 'react-redux'
import {withRouter, WithRouterProps} from 'react-router'
import {Overlay} from '@influxdata/clockface'
import {get} from 'lodash'
// Components
import {Overlay} from '@influxdata/clockface'
import DeleteDataForm from 'src/shared/components/DeleteDataForm/DeleteDataForm'
import GetResources, {ResourceType} from 'src/shared/components/GetResources'
// Utils
import {getActiveQuery, getActiveTimeMachine} from 'src/timeMachine/selectors'
import {convertTimeRangeToCustom} from 'src/shared/utils/duration'
// Types
import {AppState, TimeRange} from 'src/types'
// Actions
import {resetPredicateState} from 'src/shared/actions/predicates'
const resolveTimeRange = (timeRange: TimeRange): [number, number] | null => {
const [lower, upper] = [
Date.parse(timeRange.lower),
Date.parse(timeRange.upper),
]
if (!isNaN(lower) && !isNaN(upper)) {
return [lower, upper]
}
return null
}
import {
resetPredicateState,
setTimeRange,
setBucketAndKeys,
} from 'src/shared/actions/predicates'
interface StateProps {
selectedBucketName?: string
selectedTimeRange?: [number, number]
bucketNameFromDE: string
timeRangeFromDE: TimeRange
}
interface DispatchProps {
resetPredicateState: () => void
resetPredicateState: typeof resetPredicateState
setTimeRange: typeof setTimeRange
setBucketAndKeys: typeof setBucketAndKeys
}
type Props = StateProps & WithRouterProps & DispatchProps
@ -45,10 +39,24 @@ type Props = StateProps & WithRouterProps & DispatchProps
const DeleteDataOverlay: FunctionComponent<Props> = ({
router,
params: {orgID},
selectedBucketName,
selectedTimeRange,
bucketNameFromDE,
timeRangeFromDE,
resetPredicateState,
setTimeRange,
setBucketAndKeys,
}) => {
useEffect(() => {
if (bucketNameFromDE) {
setBucketAndKeys(bucketNameFromDE)
}
}, [bucketNameFromDE])
useEffect(() => {
if (timeRangeFromDE) {
setTimeRange(convertTimeRangeToCustom(timeRangeFromDE))
}
}, [timeRangeFromDE])
const handleDismiss = () => {
resetPredicateState()
router.push(`/orgs/${orgID}/data-explorer`)
@ -60,12 +68,7 @@ const DeleteDataOverlay: FunctionComponent<Props> = ({
<Overlay.Header title="Delete Data" onDismiss={handleDismiss} />
<Overlay.Body>
<GetResources resources={[ResourceType.Buckets]}>
<DeleteDataForm
handleDismiss={handleDismiss}
initialBucketName={selectedBucketName}
initialTimeRange={selectedTimeRange}
orgID={orgID}
/>
<DeleteDataForm handleDismiss={handleDismiss} />
</GetResources>
</Overlay.Body>
</Overlay.Container>
@ -75,19 +78,20 @@ const DeleteDataOverlay: FunctionComponent<Props> = ({
const mstp = (state: AppState): StateProps => {
const activeQuery = getActiveQuery(state)
const selectedBucketName = get(activeQuery, 'builderConfig.buckets.0')
const bucketNameFromDE = get(activeQuery, 'builderConfig.buckets.0')
const {timeRange} = getActiveTimeMachine(state)
const selectedTimeRange = resolveTimeRange(timeRange)
return {
selectedBucketName,
selectedTimeRange,
bucketNameFromDE,
timeRangeFromDE: timeRange,
}
}
const mdtp: DispatchProps = {
resetPredicateState,
setTimeRange,
setBucketAndKeys,
}
export default connect<StateProps, DispatchProps>(

View File

@ -6,9 +6,10 @@ import {render} from 'react-testing-library'
import {initialState as initialVariablesState} from 'src/variables/reducers'
import {initialState as initialUserSettingsState} from 'src/userSettings/reducers'
import configureStore from 'src/store/configureStore'
import {RemoteDataState, TimeZone} from 'src/types'
import {RemoteDataState, TimeZone, LocalStorage} from 'src/types'
import {pastFifteenMinTimeRange} from './shared/constants/timeRanges'
const localState = {
const localState: LocalStorage = {
app: {
ephemeral: {
inPresentationMode: false,
@ -20,21 +21,14 @@ const localState = {
},
},
orgs: {
items: [{name: 'org', orgID: 'orgid'}],
items: [{name: 'org', id: 'orgid'}],
org: {name: 'org', id: 'orgid'},
status: RemoteDataState.Done,
},
VERSION: '2.0.0',
ranges: [
{
dashboardID: '0349ecda531ea000',
seconds: 900,
lower: 'now() - 15m',
upper: null,
label: 'Past 15m',
duration: '15m',
},
],
ranges: {
'0349ecda531ea000': pastFifteenMinTimeRange,
},
autoRefresh: {},
variables: initialVariablesState(),
userSettings: initialUserSettingsState(),

View File

@ -0,0 +1,87 @@
import {
getLocalStateRanges,
setLocalStateRanges,
} from 'src/normalizers/localStorage/dashboardTime'
const getFunction: (a: any) => {} = getLocalStateRanges // This is to be able to test that these functions correctly filter badly formed inputs
const setFunction: (a: any) => {} = setLocalStateRanges
const dashboardID = '1'
const lowerDuration = 'now()-15m'
const arrayFormatRange = [{dashboardID, lower: lowerDuration, upper: null}]
const objFormatRange = {
[dashboardID]: {
lower: lowerDuration,
upper: null,
type: 'duration' as 'duration',
},
}
const badArrayFormats = [
{lower: lowerDuration, upper: null}, // no dashID
{dashboardID: '2', upper: null}, // no lower
{dashboardID: '3', lower: lowerDuration}, // no upper
{dashboardID: '4', lower: 3}, // lower is not string or null
{dashboardID: '5', lower: null, upper: null}, // lower is not string or null
]
const badObjFormats = {
'2': {
// no lower
upper: null,
type: 'custom' as 'custom',
},
['3']: {
// no upper
lower: lowerDuration,
type: 'custom' as 'custom',
},
['5']: {
// upper is not string or null
upper: 5,
lower: lowerDuration,
type: 'custom' as 'custom',
},
}
describe('Time Range interactions with LocalStorage', () => {
describe('can read timeRanges from localState', () => {
describe('can read timeRanges from localstate if they are in an array format', () => {
it('can read a timeRange and assign it to dashboard ID', () => {
expect(getLocalStateRanges(arrayFormatRange)).toEqual(objFormatRange)
})
it('rejects timeRanges that are not well formed', () => {
expect(getFunction([...arrayFormatRange, ...badArrayFormats])).toEqual(
objFormatRange
)
})
})
describe('can read timeRanges from localState if they are in object format', () => {
it('returns the object if timeRange well formed', () => {
expect(getFunction(objFormatRange)).toEqual(
getLocalStateRanges(objFormatRange)
)
})
it('rejects timeRanges that are not well formed', () => {
expect(getFunction({...objFormatRange, ...badObjFormats})).toEqual(
getLocalStateRanges(objFormatRange)
)
})
})
})
describe('can write timeRanges to localState', () => {
it('returns the object if timeRange well formed', () => {
expect(setFunction(objFormatRange)).toEqual(
getLocalStateRanges(objFormatRange)
)
})
it('rejects timeRanges that are not well formed', () => {
expect(setFunction({...objFormatRange, ...badObjFormats})).toEqual(
getLocalStateRanges(objFormatRange)
)
})
})
})

View File

@ -1,16 +1,17 @@
// Libraries
import _ from 'lodash'
import {isString, isNull, isObject} from 'lodash'
// Utils
import {validateAndTypeRange} from 'src/dashboards/utils/time'
// Types
import {Range} from 'src/dashboards/reducers/ranges'
import {RangeState} from 'src/dashboards/reducers/ranges'
export const normalizeRanges = (ranges: Range[]): Range[] => {
if (!Array.isArray(ranges)) {
return []
}
const isCorrectType = (bound: any) => isString(bound) || isNull(bound)
const normalized = ranges.filter(r => {
if (!_.isObject(r)) {
export const getLocalStateRangesAsArray = (ranges: any[]): RangeState => {
const normalizedRanges = ranges.filter(r => {
if (!isObject(r)) {
return false
}
@ -33,9 +34,6 @@ export const normalizeRanges = (ranges: Range[]): Range[] => {
return false
}
const isCorrectType = bound =>
_.isString(bound) || _.isNull(bound) || _.isInteger(bound)
if (!isCorrectType(lower) || !isCorrectType(upper)) {
return false
}
@ -43,5 +41,52 @@ export const normalizeRanges = (ranges: Range[]): Range[] => {
return true
})
const rangesObject: RangeState = {}
normalizedRanges.forEach(
(range: {dashboardID: string; lower: string; upper: string}) => {
const {dashboardID, lower, upper} = range
const timeRange = validateAndTypeRange({lower, upper})
if (timeRange) {
rangesObject[dashboardID] = timeRange
}
}
)
return rangesObject
}
const normalizeRangesState = (ranges: RangeState): RangeState => {
const normalized = {}
for (const key in ranges) {
if (
isObject(ranges[key]) &&
ranges[key].hasOwnProperty('upper') &&
ranges[key].hasOwnProperty('lower') &&
isCorrectType(ranges[key].lower) &&
isCorrectType(ranges[key].upper)
) {
const typedRange = validateAndTypeRange(ranges[key])
if (typedRange) {
normalized[key] = typedRange
}
}
}
return normalized
}
export const getLocalStateRanges = (ranges: RangeState | any[]) => {
if (Array.isArray(ranges)) {
return getLocalStateRangesAsArray(ranges)
} else if (isObject(ranges)) {
return normalizeRangesState(ranges)
} else {
return {}
}
}
export const setLocalStateRanges = (ranges: RangeState) => {
return normalizeRangesState(ranges)
}

View File

@ -9,7 +9,8 @@ import {VERSION} from 'src/shared/constants'
// Utils
import {
normalizeRanges,
getLocalStateRanges,
setLocalStateRanges,
normalizeApp,
normalizeOrgs,
} from 'src/normalizers/localStorage'
@ -18,7 +19,7 @@ export const normalizeGetLocalStorage = (state: LocalStorage): LocalStorage => {
let newState = state
if (state.ranges) {
newState = {...newState, ranges: normalizeRanges(state.ranges)}
newState = {...newState, ranges: getLocalStateRanges(state.ranges)}
}
const appPersisted = get(newState, 'app.persisted', false)
@ -41,6 +42,6 @@ export const normalizeSetLocalStorage = (state: LocalStorage): LocalStorage => {
userSettings,
app: normalizeApp(app),
orgs: normalizeOrgs(orgs),
ranges: normalizeRanges(ranges),
ranges: setLocalStateRanges(ranges),
}
}

View File

@ -7,13 +7,25 @@ jest.mock('src/timeMachine/apis/queryBuilder')
jest.mock('src/shared/apis/query')
// Types
import {predicateDeleteSucceeded} from 'src/shared/copy/notifications'
import {pastHourTimeRange} from 'src/shared/constants/timeRanges'
// Actions
import {
deleteWithPredicate,
setBucketAndKeys,
setValuesByKey,
} from 'src/shared/actions/predicates'
import {deleteWithPredicate} from 'src/shared/actions/predicates'
import {convertTimeRangeToCustom} from '../utils/duration'
const mockGetState = jest.fn(_ => {
return {
orgs: {
org: {id: '1'},
},
predicates: {
timeRange: convertTimeRangeToCustom(pastHourTimeRange),
bucketName: 'bucketName',
filters: [{key: 'k', value: 'v', equality: '='}],
},
}
})
describe('Shared.Actions.Predicates', () => {
beforeEach(() => {
@ -22,19 +34,26 @@ describe('Shared.Actions.Predicates', () => {
it('deletes then dispatches success messages', async () => {
const mockDispatch = jest.fn()
const params = {}
mocked(postDelete).mockImplementation(() => ({status: 204}))
await deleteWithPredicate(params)(mockDispatch)
await deleteWithPredicate()(mockDispatch, mockGetState)
expect(postDelete).toHaveBeenCalledTimes(1)
const [
setDeletionStatusDispatch,
firstSetDeletionStatusDispatch,
secondSetDeletionStatusDispatch,
notifySuccessCall,
resetPredicateStateCall,
] = mockDispatch.mock.calls
expect(setDeletionStatusDispatch).toEqual([
expect(firstSetDeletionStatusDispatch).toEqual([
{
type: 'SET_DELETION_STATUS',
payload: {deletionStatus: 'Loading'},
},
])
expect(secondSetDeletionStatusDispatch).toEqual([
{
type: 'SET_DELETION_STATUS',
payload: {deletionStatus: 'Done'},
@ -45,59 +64,11 @@ describe('Shared.Actions.Predicates', () => {
{
type: 'PUBLISH_NOTIFICATION',
payload: {
notification: {
duration: 5000,
icon: 'checkmark',
message: 'Successfully deleted data with predicate!',
style: 'success',
},
notification: predicateDeleteSucceeded(),
},
},
])
expect(resetPredicateStateCall).toEqual([{type: 'SET_PREDICATE_DEFAULT'}])
})
it('sets the keys based on the bucket name', async () => {
const mockDispatch = jest.fn()
const orgID = '1'
const bucketName = 'Foxygen'
await setBucketAndKeys(orgID, bucketName)(mockDispatch)
const [setBucketNameDispatch, setKeysDispatch] = mockDispatch.mock.calls
expect(setBucketNameDispatch).toEqual([
{type: 'SET_BUCKET_NAME', payload: {bucketName: 'Foxygen'}},
])
expect(setKeysDispatch).toEqual([
{
type: 'SET_KEYS_BY_BUCKET',
payload: {
keys: ['Talking Heads', 'This must be the place'],
},
},
])
})
it('sets the values based on the bucket and key name', async () => {
const mockDispatch = jest.fn()
const orgID = '1'
const bucketName = 'Simon & Garfunkel'
const keyName = 'America'
await setValuesByKey(orgID, bucketName, keyName)(mockDispatch)
const [setValuesDispatch] = mockDispatch.mock.calls
expect(setValuesDispatch).toEqual([
{
type: 'SET_VALUES_BY_KEY',
payload: {
values: ['Talking Heads', 'This must be the place'],
},
},
])
})
})

View File

@ -1,6 +1,7 @@
// Libraries
import {Dispatch} from 'redux-thunk'
import {extractBoxedCol} from 'src/timeMachine/apis/queryBuilder'
import moment from 'moment'
// Utils
import {postDelete} from 'src/client'
@ -23,7 +24,7 @@ import {
import {rateLimitReached, resultTooLarge} from 'src/shared/copy/notifications'
// Types
import {GetState, Filter, RemoteDataState} from 'src/types'
import {RemoteDataState, Filter, CustomTimeRange, GetState} from 'src/types'
export type Action =
| DeleteFilter
@ -144,10 +145,10 @@ export const setPreviewStatus = (
interface SetTimeRange {
type: 'SET_DELETE_TIME_RANGE'
payload: {timeRange: [number, number]}
payload: {timeRange: CustomTimeRange}
}
export const setTimeRange = (timeRange: [number, number]): SetTimeRange => ({
export const setTimeRange = (timeRange: CustomTimeRange): SetTimeRange => ({
type: 'SET_DELETE_TIME_RANGE',
payload: {timeRange},
})
@ -162,11 +163,39 @@ const setValues = (values: string[]): SetValuesByKey => ({
payload: {values},
})
export const deleteWithPredicate = params => async (
dispatch: Dispatch<Action>
const formatFilters = (filters: Filter[]) =>
filters.map(f => `${f.key} ${f.equality} ${f.value}`).join(' AND ')
export const deleteWithPredicate = () => async (
dispatch: Dispatch<Action>,
getState: GetState
) => {
dispatch(setDeletionStatus(RemoteDataState.Loading))
const {
orgs: {
org: {id: orgID},
},
predicates: {timeRange, bucketName, filters},
} = getState()
const data = {
start: moment(timeRange.lower).toISOString(),
stop: moment(timeRange.upper).toISOString(),
}
if (filters.length > 0) {
data['predicate'] = formatFilters(filters)
}
try {
const resp = await postDelete(params)
const resp = await postDelete({
data,
query: {
orgID,
bucket: bucketName,
},
})
if (resp.status !== 204) {
throw new Error(resp.data.message)
@ -223,9 +252,16 @@ export const executePreviewQuery = (query: string) => async (
}
}
export const setBucketAndKeys = (orgID: string, bucketName: string) => async (
dispatch: Dispatch<Action>
export const setBucketAndKeys = (bucketName: string) => async (
dispatch: Dispatch<Action>,
getState: GetState
) => {
const {
orgs: {
org: {id: orgID},
},
} = getState()
try {
const query = `import "influxdata/influxdb/v1"
v1.tagKeys(bucket: "${bucketName}")
@ -240,11 +276,16 @@ export const setBucketAndKeys = (orgID: string, bucketName: string) => async (
}
}
export const setValuesByKey = (
orgID: string,
bucketName: string,
keyName: string
) => async (dispatch: Dispatch<Action>) => {
export const setValuesByKey = (bucketName: string, keyName: string) => async (
dispatch: Dispatch<Action>,
getState: GetState
) => {
const {
orgs: {
org: {id: orgID},
},
} = getState()
try {
const query = `import "influxdata/influxdb/v1" v1.tagValues(bucket: "${bucketName}", tag: "${keyName}")`
const values = await extractBoxedCol(runQuery(orgID, query), '_value')

View File

@ -25,7 +25,7 @@ import PreviewDataTable from 'src/shared/components/DeleteDataForm/PreviewDataTa
import TimeRangeDropdown from 'src/shared/components/DeleteDataForm/TimeRangeDropdown'
// Types
import {AppState, Filter, RemoteDataState} from 'src/types'
import {Filter, RemoteDataState, CustomTimeRange, AppState} from 'src/types'
// Selectors
import {setCanDelete} from 'src/shared/selectors/canDelete'
@ -36,7 +36,6 @@ import {
deleteWithPredicate,
executePreviewQuery,
resetFilters,
setDeletionStatus,
setFilter,
setIsSerious,
setBucketAndKeys,
@ -44,42 +43,36 @@ import {
} from 'src/shared/actions/predicates'
interface OwnProps {
orgID: string
handleDismiss: () => void
initialBucketName?: string
initialTimeRange?: [number, number]
keys: string[]
values: (string | number)[]
}
interface StateProps {
bucketName: string
canDelete: boolean
deletionStatus: RemoteDataState
files: string[]
filters: Filter[]
isSerious: boolean
keys: string[]
timeRange: [number, number]
timeRange: CustomTimeRange
values: (string | number)[]
bucketName: string
orgID: string
}
interface DispatchProps {
deleteFilter: (index: number) => void
deleteFilter: typeof deleteFilter
deleteWithPredicate: typeof deleteWithPredicate
resetFilters: () => void
executePreviewQuery: typeof executePreviewQuery
setDeletionStatus: typeof setDeletionStatus
resetFilters: typeof resetFilters
setFilter: typeof setFilter
setIsSerious: (isSerious: boolean) => void
setBucketAndKeys: (orgID: string, bucketName: string) => void
setTimeRange: (timeRange: [number, number]) => void
setIsSerious: typeof setIsSerious
setBucketAndKeys: typeof setBucketAndKeys
setTimeRange: typeof setTimeRange
}
export type Props = StateProps & DispatchProps & OwnProps
const DeleteDataForm: FC<Props> = ({
bucketName,
canDelete,
deleteFilter,
deletionStatus,
@ -88,26 +81,23 @@ const DeleteDataForm: FC<Props> = ({
files,
filters,
handleDismiss,
initialBucketName,
initialTimeRange,
isSerious,
keys,
orgID,
resetFilters,
setDeletionStatus,
setFilter,
setIsSerious,
setBucketAndKeys,
setTimeRange,
timeRange,
values,
bucketName,
orgID,
}) => {
const name = bucketName || initialBucketName
const [count, setCount] = useState('0')
useEffect(() => {
// trigger the setBucketAndKeys if the bucketName hasn't been set
if (bucketName === '' && name !== undefined) {
setBucketAndKeys(orgID, name)
if (bucketName) {
setBucketAndKeys(bucketName)
}
})
@ -117,18 +107,7 @@ const DeleteDataForm: FC<Props> = ({
}
}, [filters])
const realTimeRange = initialTimeRange || timeRange
const formatPredicatesForDeletion = predicates => {
const result = []
predicates.forEach(predicate => {
const {key, equality, value} = predicate
result.push(`${key} ${equality} ${value}`)
})
return result.join(' AND ')
}
const formatPredicatesForPreview = predicates => {
const formatPredicatesForPreview = (predicates: Filter[]) => {
let result = ''
predicates.forEach(predicate => {
const {key, equality, value} = predicate
@ -140,11 +119,11 @@ const DeleteDataForm: FC<Props> = ({
}
const handleDeleteDataPreview = async () => {
const [start, stop] = realTimeRange
const {lower, upper} = timeRange
let query = `from(bucket: "${name}")
|> range(start: ${moment(start).toISOString()}, stop: ${moment(
stop
|> range(start: ${moment(lower).toISOString()}, stop: ${moment(
upper
).toISOString()})`
if (filters.length > 0) {
@ -168,38 +147,17 @@ const DeleteDataForm: FC<Props> = ({
}
const handleDelete = () => {
setDeletionStatus(RemoteDataState.Loading)
const [start, stop] = realTimeRange
const data = {
start: moment(start).toISOString(),
stop: moment(stop).toISOString(),
}
if (filters.length > 0) {
data['predicate'] = formatPredicatesForDeletion(filters)
}
const params = {
data,
query: {
orgID,
bucket: name,
},
}
deleteWithPredicate(params)
deleteWithPredicate()
handleDismiss()
}
const handleBucketClick = selectedBucket => {
setBucketAndKeys(orgID, selectedBucket)
const handleBucketClick = (selectedBucketName: string) => {
setBucketAndKeys(selectedBucketName)
resetFilters()
}
const formatNumber = num => {
if (num !== undefined) {
const formatNumber = (num: string) => {
if (num) {
return num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,')
}
return 0
@ -212,16 +170,16 @@ const DeleteDataForm: FC<Props> = ({
<Grid.Column widthXS={Columns.Four}>
<Form.Element label="Target Bucket">
<BucketsDropdown
bucketName={name}
onSetBucketName={bucketName => handleBucketClick(bucketName)}
bucketName={bucketName}
onSetBucketName={handleBucketClick}
/>
</Form.Element>
</Grid.Column>
<Grid.Column widthXS={Columns.Eight}>
<Form.Element label="Time Range">
<TimeRangeDropdown
timeRange={realTimeRange}
onSetTimeRange={timeRange => setTimeRange(timeRange)}
timeRange={timeRange}
onSetTimeRange={setTimeRange}
/>
</Form.Element>
</Grid.Column>
@ -232,9 +190,8 @@ const DeleteDataForm: FC<Props> = ({
bucket={name}
filters={filters}
keys={keys}
onDeleteFilter={index => deleteFilter(index)}
onSetFilter={(filter, index) => setFilter(filter, index)}
orgID={orgID}
onDeleteFilter={deleteFilter}
onSetFilter={setFilter}
shouldValidate={isSerious}
values={values}
/>
@ -303,7 +260,7 @@ const DeleteDataForm: FC<Props> = ({
)
}
const mstp = ({predicates}: AppState) => {
const mstp = ({predicates, orgs}: AppState): StateProps => {
const {
bucketName,
deletionStatus,
@ -315,6 +272,8 @@ const mstp = ({predicates}: AppState) => {
values,
} = predicates
const orgID = orgs.org.id
return {
bucketName,
canDelete: setCanDelete(predicates),
@ -325,15 +284,15 @@ const mstp = ({predicates}: AppState) => {
keys,
timeRange,
values,
orgID,
}
}
const mdtp = {
const mdtp: DispatchProps = {
deleteFilter,
deleteWithPredicate,
resetFilters,
executePreviewQuery,
setDeletionStatus,
setFilter,
setIsSerious,
setBucketAndKeys,

View File

@ -14,7 +14,6 @@ interface Props {
keys: string[]
onDeleteFilter: (index: number) => any
onSetFilter: (filter: Filter, index: number) => any
orgID: string
shouldValidate: boolean
values: (string | number)[]
}
@ -25,7 +24,6 @@ const FilterEditor: FunctionComponent<Props> = ({
keys,
onDeleteFilter,
onSetFilter,
orgID,
shouldValidate,
values,
}) => {
@ -51,7 +49,6 @@ const FilterEditor: FunctionComponent<Props> = ({
filter={filter}
onChange={filter => onSetFilter(filter, i)}
onDelete={() => onDeleteFilter(i)}
orgID={orgID}
shouldValidate={shouldValidate}
values={values}
/>

View File

@ -24,13 +24,12 @@ interface Props {
keys: string[]
onChange: (filter: Filter) => any
onDelete: () => any
orgID: string
shouldValidate: boolean
values: (string | number)[]
}
interface DispatchProps {
setValuesByKey: (orgID: string, bucketName: string, keyName: string) => void
setValuesByKey: typeof setValuesByKey
}
const FilterRow: FC<Props & DispatchProps> = ({
@ -39,7 +38,6 @@ const FilterRow: FC<Props & DispatchProps> = ({
keys,
onChange,
onDelete,
orgID,
setValuesByKey,
shouldValidate,
values,
@ -51,13 +49,15 @@ const FilterRow: FC<Props & DispatchProps> = ({
const valueErrorMessage =
shouldValidate && value.trim() === '' ? 'Value cannot be empty' : null
const onChangeKey = input => onChange({key: input, equality, value})
const onKeySelect = input => {
setValuesByKey(orgID, bucket, input)
const onChangeKey = (input: string) => onChange({key: input, equality, value})
const onKeySelect = (input: string) => {
setValuesByKey(bucket, input)
onChange({key: input, equality, value})
}
const onChangeValue = input => onChange({key, equality, value: input})
const onChangeEquality = e => onChange({key, equality: e, value})
const onChangeValue = (input: string) =>
onChange({key, equality, value: input})
const onChangeEquality = (e: string) => onChange({key, equality: e, value})
return (
<div className="delete-data-filter">

View File

@ -1,6 +1,7 @@
// Libraries
import React, {useRef, useState, FC} from 'react'
import moment from 'moment'
// Components
import {
Dropdown,
Popover,
@ -8,30 +9,40 @@ import {
PopoverInteraction,
Appearance,
} from '@influxdata/clockface'
// Components
import DateRangePicker from 'src/shared/components/dateRangePicker/DateRangePicker'
// Types
import {CustomTimeRange} from 'src/types'
import {pastHourTimeRange} from 'src/shared/constants/timeRanges'
import {
convertTimeRangeToCustom,
getTimeRangeLabel,
} from 'src/shared/utils/duration'
interface Props {
timeRange: [number, number]
onSetTimeRange: (timeRange: [number, number]) => any
timeRange: CustomTimeRange
onSetTimeRange: (timeRange: CustomTimeRange) => void
}
const TimeRangeDropdown: FC<Props> = ({timeRange, onSetTimeRange}) => {
const [pickerActive, setPickerActive] = useState(false)
const buttonRef = useRef<HTMLDivElement>(null)
const lower = moment(timeRange[0]).format('YYYY-MM-DD HH:mm:ss')
const upper = moment(timeRange[1]).format('YYYY-MM-DD HH:mm:ss')
let dropdownLabel = 'Select a Time Range'
const handleApplyTimeRange = (lower, upper) => {
onSetTimeRange([Date.parse(lower), Date.parse(upper)])
if (timeRange) {
dropdownLabel = getTimeRangeLabel(timeRange)
}
const handleApplyTimeRange = (timeRange: CustomTimeRange) => {
onSetTimeRange(timeRange)
setPickerActive(false)
}
return (
<div ref={buttonRef}>
<Dropdown.Button onClick={() => setPickerActive(!pickerActive)}>
{lower} - {upper}
{dropdownLabel}
</Dropdown.Button>
<Popover
appearance={Appearance.Outline}
@ -45,10 +56,8 @@ const TimeRangeDropdown: FC<Props> = ({timeRange, onSetTimeRange}) => {
enableDefaultStyles={false}
contents={() => (
<DateRangePicker
timeRange={{lower, upper}}
onSetTimeRange={({lower, upper}) =>
handleApplyTimeRange(lower, upper)
}
timeRange={timeRange || convertTimeRangeToCustom(pastHourTimeRange)}
onSetTimeRange={handleApplyTimeRange}
onClose={() => setPickerActive(false)}
position={{position: 'relative'}}
/>

View File

@ -1,5 +1,5 @@
// Libraries
import React, {FunctionComponent} from 'react'
import React, {FunctionComponent, useEffect} from 'react'
import {connect} from 'react-redux'
import {withRouter, WithRouterProps} from 'react-router'
import {Overlay, SpinnerContainer, TechnoSpinner} from '@influxdata/clockface'
@ -7,18 +7,22 @@ import {Overlay, SpinnerContainer, TechnoSpinner} from '@influxdata/clockface'
// Components
import DeleteDataForm from 'src/shared/components/DeleteDataForm/DeleteDataForm'
// Actions
import {
resetPredicateState,
setBucketAndKeys,
} from 'src/shared/actions/predicates'
// Types
import {Bucket, AppState, RemoteDataState} from 'src/types'
// Actions
import {resetPredicateState} from 'src/shared/actions/predicates'
interface StateProps {
buckets: Bucket[]
}
interface DispatchProps {
resetPredicateStateAction: () => void
resetPredicateState: typeof resetPredicateState
setBucketAndKeys: typeof setBucketAndKeys
}
type Props = WithRouterProps & DispatchProps & StateProps
@ -27,13 +31,22 @@ const DeleteDataOverlay: FunctionComponent<Props> = ({
buckets,
router,
params: {orgID, bucketID},
resetPredicateStateAction,
resetPredicateState,
setBucketAndKeys,
}) => {
const handleDismiss = () => {
resetPredicateStateAction()
router.push(`/orgs/${orgID}/load-data/buckets/${bucketID}`)
}
const bucket = buckets.find(bucket => bucket.id === bucketID)
useEffect(() => {
if (bucket) {
setBucketAndKeys(bucket.name)
}
}, [])
const handleDismiss = () => {
resetPredicateState()
router.push(`/orgs/${orgID}/load-data/buckets/`)
}
return (
<Overlay visible={true}>
<Overlay.Container maxWidth={600}>
@ -43,11 +56,7 @@ const DeleteDataOverlay: FunctionComponent<Props> = ({
spinnerComponent={<TechnoSpinner />}
loading={bucket ? RemoteDataState.Done : RemoteDataState.Loading}
>
<DeleteDataForm
handleDismiss={handleDismiss}
initialBucketName={bucket && bucket.name}
orgID={orgID}
/>
<DeleteDataForm handleDismiss={handleDismiss} />
</SpinnerContainer>
</Overlay.Body>
</Overlay.Container>
@ -62,7 +71,8 @@ const mstp = (state: AppState): StateProps => {
}
const mdtp: DispatchProps = {
resetPredicateStateAction: resetPredicateState,
resetPredicateState,
setBucketAndKeys,
}
export default connect<StateProps, DispatchProps>(

View File

@ -14,6 +14,10 @@ import {getVariableAssignments} from 'src/variables/selectors'
import {getDashboardValuesStatus} from 'src/variables/selectors'
import {checkResultsLength} from 'src/shared/utils/vis'
// Selectors
import {getEndTime, getStartTime} from 'src/timeMachine/selectors/index'
import {getTimeRangeByDashboardID} from 'src/dashboards/selectors/index'
// Types
import {
TimeRange,
@ -26,10 +30,6 @@ import {
Check,
} from 'src/types'
// Selectors
import {getEndTime, getStartTime} from 'src/timeMachine/selectors/index'
import {getTimeRangeByDashboardID} from 'src/dashboards/selectors/index'
interface OwnProps {
timeRange: TimeRange
manualRefresh: number
@ -164,12 +164,11 @@ class RefreshingView extends PureComponent<Props, State> {
}
const mstp = (state: AppState, ownProps: OwnProps): StateProps => {
const {ranges} = state
const variableAssignments = getVariableAssignments(
state,
ownProps.dashboardID
)
const timeRange = getTimeRangeByDashboardID(ranges, ownProps.dashboardID)
const timeRange = getTimeRangeByDashboardID(state, ownProps.dashboardID)
const valuesStatus = getDashboardValuesStatus(state, ownProps.dashboardID)

View File

@ -1,7 +1,5 @@
// Libraries
import React, {PureComponent, createRef} from 'react'
import {get} from 'lodash'
import moment from 'moment'
// Components
import {
@ -13,30 +11,32 @@ import {
} from '@influxdata/clockface'
import DateRangePicker from 'src/shared/components/dateRangePicker/DateRangePicker'
// Utils
import {
convertTimeRangeToCustom,
getTimeRangeLabel,
} from 'src/shared/utils/duration'
// Constants
import {
TIME_RANGES,
TIME_RANGE_LABEL,
SELECTABLE_TIME_RANGES,
CUSTOM_TIME_RANGE_LABEL,
TIME_RANGE_FORMAT,
} from 'src/shared/constants/timeRanges'
// Types
import {TimeRange} from 'src/types'
export enum RangeType {
Absolute = 'absolute',
Relative = 'relative',
}
import {
TimeRange,
CustomTimeRange,
SelectableDurationTimeRange,
} from 'src/types'
interface Props {
timeRange: TimeRange
onSetTimeRange: (timeRange: TimeRange, rangeType?: RangeType) => void
onSetTimeRange: (timeRange: TimeRange) => void
}
interface State {
isDatePickerOpen: boolean
dropdownPosition: {position: string}
}
class TimeRangeDropdown extends PureComponent<Props, State> {
@ -45,18 +45,19 @@ class TimeRangeDropdown extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = {isDatePickerOpen: false, dropdownPosition: undefined}
this.state = {isDatePickerOpen: false}
}
public render() {
const timeRange = this.timeRange
const timeRangeLabel = getTimeRangeLabel(timeRange)
return (
<>
<Popover
appearance={Appearance.Outline}
position={PopoverPosition.ToTheLeft}
triggerRef={this.dropdownRef}
visible={this.isDatePickerVisible}
visible={this.state.isDatePickerOpen}
showEvent={PopoverInteraction.None}
hideEvent={PopoverInteraction.None}
distanceFromTrigger={8}
@ -67,7 +68,9 @@ class TimeRangeDropdown extends PureComponent<Props, State> {
timeRange={timeRange}
onSetTimeRange={this.handleApplyTimeRange}
onClose={this.handleHideDatePicker}
position={this.state.dropdownPosition}
position={
this.state.isDatePickerOpen ? {position: 'relative'} : undefined
}
/>
)}
/>
@ -77,7 +80,7 @@ class TimeRangeDropdown extends PureComponent<Props, State> {
testID="timerange-dropdown"
button={(active, onClick) => (
<Dropdown.Button active={active} onClick={onClick}>
{this.formattedCustomTimeRange}
{timeRangeLabel}
</Dropdown.Button>
)}
menu={onCollapse => (
@ -85,12 +88,22 @@ class TimeRangeDropdown extends PureComponent<Props, State> {
onCollapse={onCollapse}
style={{width: `${this.dropdownWidth + 50}px`}}
>
{TIME_RANGES.map(({label}) => {
if (label === TIME_RANGE_LABEL) {
return (
<Dropdown.Divider key={label} text={label} id={label} />
)
}
<Dropdown.Divider
key="Time Range"
text="Time Range"
id="Time Range"
/>
<Dropdown.Item
key={CUSTOM_TIME_RANGE_LABEL}
value={CUSTOM_TIME_RANGE_LABEL}
id={CUSTOM_TIME_RANGE_LABEL}
testID="dropdown-item-customtimerange"
selected={this.state.isDatePickerOpen}
onClick={this.handleClickCustomTimeRange}
>
{CUSTOM_TIME_RANGE_LABEL}
</Dropdown.Item>
{SELECTABLE_TIME_RANGES.map(({label}) => {
const testID = label.toLowerCase().replace(/\s/g, '')
return (
<Dropdown.Item
@ -98,8 +111,8 @@ class TimeRangeDropdown extends PureComponent<Props, State> {
value={label}
id={label}
testID={`dropdown-item-${testID}`}
selected={label === timeRange.label}
onClick={this.handleChange}
selected={label === timeRangeLabel}
onClick={this.handleClickDropdownItem}
>
{label}
</Dropdown.Item>
@ -114,106 +127,43 @@ class TimeRangeDropdown extends PureComponent<Props, State> {
}
private get dropdownWidth(): number {
if (this.isCustomTimeRange) {
if (this.props.timeRange.type === 'custom') {
return 250
}
return 100
}
private get isCustomTimeRange(): boolean {
const {timeRange} = this.props
return (
get(timeRange, 'label', '') === CUSTOM_TIME_RANGE_LABEL ||
!!timeRange.upper
)
}
private get formattedCustomTimeRange(): string {
const {timeRange} = this.props
if (!this.isCustomTimeRange) {
return TIME_RANGES.find(range => range.lower === timeRange.lower).label
}
return `${moment(timeRange.lower).format(TIME_RANGE_FORMAT)} - ${moment(
timeRange.upper
).format(TIME_RANGE_FORMAT)}`
}
private get timeRange(): TimeRange {
private get timeRange(): CustomTimeRange | SelectableDurationTimeRange {
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
: this.calculatedLower
return {
label: CUSTOM_TIME_RANGE_LABEL,
lower,
upper,
}
if (isDatePickerOpen && timeRange.type !== 'custom') {
return convertTimeRangeToCustom(timeRange)
}
if (this.isCustomTimeRange) {
return {
...timeRange,
label: this.formattedCustomTimeRange,
}
if (timeRange.type === 'duration') {
return convertTimeRangeToCustom(timeRange)
}
const selectedTimeRange = TIME_RANGES.find(t => t.lower === timeRange.lower)
if (!selectedTimeRange) {
throw new Error('TimeRangeDropdown passed unknown TimeRange')
}
return selectedTimeRange
}
private get isDatePickerVisible() {
return this.state.isDatePickerOpen
}
private get calculatedLower() {
const {
timeRange: {seconds},
} = this.props
if (seconds) {
return moment()
.subtract(seconds, 's')
.toISOString()
}
return new Date().toISOString()
return timeRange
}
private handleApplyTimeRange = (timeRange: TimeRange) => {
this.props.onSetTimeRange(timeRange, RangeType.Absolute)
this.props.onSetTimeRange(timeRange)
this.handleHideDatePicker()
}
private handleHideDatePicker = () => {
this.setState({isDatePickerOpen: false, dropdownPosition: undefined})
this.setState({isDatePickerOpen: false})
}
private handleChange = (label: string): void => {
const {onSetTimeRange} = this.props
const timeRange = TIME_RANGES.find(t => t.label === label)
private handleClickCustomTimeRange = (): void => {
this.setState({isDatePickerOpen: true})
}
if (label === CUSTOM_TIME_RANGE_LABEL) {
this.setState({
isDatePickerOpen: true,
dropdownPosition: {position: 'relative'},
})
return
}
private handleClickDropdownItem = (label: string): void => {
const {onSetTimeRange} = this.props
const timeRange = SELECTABLE_TIME_RANGES.find(t => t.label === label)
onSetTimeRange(timeRange)
}

View File

@ -98,10 +98,10 @@ class DateRangePicker extends PureComponent<Props, State> {
}
private handleSetTimeRange = (): void => {
const {onSetTimeRange, timeRange} = this.props
const {onSetTimeRange} = this.props
const {upper, lower} = this.state
onSetTimeRange({...timeRange, lower, upper})
onSetTimeRange({lower, upper, type: 'custom'})
}
private handleSelectLower = (lower: string): void => {

View File

@ -1,45 +1,59 @@
import {TimeRange} from 'src/types'
import {TimeRange, SelectableDurationTimeRange} from 'src/types'
export const TIME_RANGE_LABEL = 'Time Range'
export const CUSTOM_TIME_RANGE_LABEL = 'Custom Time Range'
export const TIME_RANGE_FORMAT = 'YYYY-MM-DD HH:mm'
export const TIME_RANGES: TimeRange[] = [
{
lower: '',
label: TIME_RANGE_LABEL,
},
{
lower: '',
label: CUSTOM_TIME_RANGE_LABEL,
},
export const CUSTOM_TIME_RANGE_LABEL = 'Custom Time Range' as 'Custom Time Range'
export const pastHourTimeRange: SelectableDurationTimeRange = {
seconds: 3600,
lower: 'now() - 1h',
upper: null,
label: 'Past 1h',
duration: '1h',
type: 'selectable-duration',
}
export const pastThirtyDaysTimeRange: SelectableDurationTimeRange = {
seconds: 2592000,
lower: 'now() - 30d',
upper: null,
label: 'Past 30d',
duration: '30d',
type: 'selectable-duration',
}
export const pastFifteenMinTimeRange: SelectableDurationTimeRange = {
seconds: 900,
lower: 'now() - 15m',
upper: null,
label: 'Past 15m',
duration: '15m',
type: 'selectable-duration',
}
export const CUSTOM_TIME_RANGE: {label: string; type: 'custom'} = {
label: 'Custom Time Range' as 'Custom Time Range',
type: 'custom',
}
export const SELECTABLE_TIME_RANGES: SelectableDurationTimeRange[] = [
{
seconds: 300,
lower: 'now() - 5m',
upper: null,
label: 'Past 5m',
duration: '5m',
type: 'selectable-duration',
},
{
seconds: 900,
lower: 'now() - 15m',
upper: null,
label: 'Past 15m',
duration: '15m',
},
{
seconds: 3600,
lower: 'now() - 1h',
upper: null,
label: 'Past 1h',
duration: '1h',
},
pastFifteenMinTimeRange,
pastHourTimeRange,
{
seconds: 21600,
lower: 'now() - 6h',
upper: null,
label: 'Past 6h',
duration: '6h',
type: 'selectable-duration',
},
{
seconds: 43200,
@ -47,6 +61,7 @@ export const TIME_RANGES: TimeRange[] = [
upper: null,
label: 'Past 12h',
duration: '12h',
type: 'selectable-duration',
},
{
seconds: 86400,
@ -54,6 +69,7 @@ export const TIME_RANGES: TimeRange[] = [
upper: null,
label: 'Past 24h',
duration: '24h',
type: 'selectable-duration',
},
{
seconds: 172800,
@ -61,6 +77,7 @@ export const TIME_RANGES: TimeRange[] = [
upper: null,
label: 'Past 2d',
duration: '2d',
type: 'selectable-duration',
},
{
seconds: 604800,
@ -68,20 +85,9 @@ export const TIME_RANGES: TimeRange[] = [
upper: null,
label: 'Past 7d',
duration: '7d',
type: 'selectable-duration',
},
{
seconds: 2592000,
lower: 'now() - 30d',
upper: null,
label: 'Past 30d',
duration: '30d',
},
pastThirtyDaysTimeRange,
]
export const DEFAULT_TIME_RANGE: TimeRange = TIME_RANGES[2]
export const ABSOLUTE = 'absolute'
export const INVALID = 'invalid'
export const RELATIVE_LOWER = 'relative lower'
export const RELATIVE_UPPER = 'relative upper'
export const INFLUXQL = 'influxql'
export const DEFAULT_TIME_RANGE: TimeRange = pastHourTimeRange

View File

@ -215,12 +215,6 @@ export const invalidTimeRangeValueInURLQuery = (): Notification => ({
message: `Invalid URL query value supplied for lower or upper time range.`,
})
export const invalidZoomedTimeRangeValueInURLQuery = (): Notification => ({
...defaultErrorNotification,
icon: 'cube',
message: `Invalid URL query value supplied for zoomed lower or zoomed upper time range.`,
})
export const getVariablesFailed = (): Notification => ({
...defaultErrorNotification,
message: 'Failed to fetch variables',

View File

@ -1,110 +0,0 @@
import moment from 'moment'
import {
INFLUXQL,
ABSOLUTE,
INVALID,
RELATIVE_LOWER,
RELATIVE_UPPER,
} from 'src/shared/constants/timeRanges'
const now = /^now/
export const timeRangeType = ({upper, lower, type}) => {
if (!upper && !lower) {
return INVALID
}
if (type && type !== INFLUXQL) {
return INVALID
}
const isUpperValid = moment(new Date(upper)).isValid()
const isLowerValid = moment(new Date(lower)).isValid()
// {lower: <Date>, upper: <Date>}
if (isLowerValid && isUpperValid) {
return ABSOLUTE
}
// {lower: now - <Duration>, upper: <empty>}
if (now.test(lower) && !upper) {
return RELATIVE_LOWER
}
// {lower: <Date>, upper: now() - <Duration>}
if (isLowerValid && now.test(upper)) {
return RELATIVE_UPPER
}
return INVALID
}
export const shiftTimeRange = (timeRange, shift) => {
const {upper, lower} = timeRange
const {quantity, unit} = shift
const trType = timeRangeType(timeRange)
const duration = `${quantity}${unit}`
const type = 'shifted'
switch (trType) {
case RELATIVE_UPPER:
case ABSOLUTE: {
return {
lower: `${lower} - ${duration}`,
upper: `${upper} - ${duration}`,
type,
}
}
case RELATIVE_LOWER: {
return {
lower: `${lower} - ${duration}`,
upper: `now() - ${duration}`,
type,
}
}
default: {
return {lower, upper, type: 'unshifted'}
}
}
}
const getMomentUnit = unit => {
switch (unit) {
case 'ms': {
return 'milliseconds' // (1 thousandth of a second)
}
case 's': {
return 'seconds'
}
case 'm': {
return 'minute'
}
case 'h': {
return 'hour'
}
case 'd': {
return 'day'
}
case 'w': {
return 'week'
}
default: {
return unit
}
}
}
export const shiftDate = (date, quantity, unit) => {
if (!date && !quantity && !unit) {
return moment(new Date(date))
}
return moment(new Date(date)).add(quantity, getMomentUnit(unit))
}

View File

@ -1,10 +1,5 @@
// Reducer
import {
HOUR_MS,
initialState,
predicatesReducer,
recently,
} from 'src/shared/reducers/predicates'
import {initialState, predicatesReducer} from 'src/shared/reducers/predicates'
// Types
import {Filter} from 'src/types'
@ -18,51 +13,55 @@ import {
setIsSerious,
setTimeRange,
} from 'src/shared/actions/predicates'
import {pastHourTimeRange} from 'src/shared/constants/timeRanges'
import {convertTimeRangeToCustom} from 'src/shared/utils/duration'
describe('Predicates reducer test', () => {
it('should set the isSerious property', () => {
const filter: Filter = {key: 'mean', equality: '=', value: '100'}
describe('Predicates reducer', () => {
it('Can set the isSerious property', () => {
expect(initialState.isSerious).toEqual(false)
let result = predicatesReducer(initialState, setIsSerious(true))
expect(result.isSerious).toEqual(true)
result = predicatesReducer(initialState, setIsSerious(false))
result = predicatesReducer(result, setIsSerious(false))
expect(result.isSerious).toEqual(false)
})
it('should set the bucketName property', () => {
it('Can set the bucketName property', () => {
const bucketName = 'bucket_list'
expect(initialState.bucketName).toEqual('')
const result = predicatesReducer(initialState, setBucketName(bucketName))
expect(result.bucketName).toEqual(bucketName)
})
it('should set the timeRange property', () => {
expect(initialState.timeRange).toEqual([recently - HOUR_MS, recently])
const result = predicatesReducer(initialState, setTimeRange([1000, 2000]))
expect(result.timeRange).toEqual([1000, 2000])
it('Can set the timeRange property', () => {
expect(initialState.timeRange).toBeNull()
const instantiatedPastHour = convertTimeRangeToCustom(pastHourTimeRange)
const result = predicatesReducer(
initialState,
setTimeRange(instantiatedPastHour)
)
expect(result.timeRange).toEqual(instantiatedPastHour)
})
it('should set the filter property', () => {
const filter: Filter = {key: 'mean', equality: '=', value: '100'}
it('Can set the filter property', () => {
expect(initialState.filters).toEqual([])
const result = predicatesReducer(initialState, setFilter(filter, 0))
expect(result.filters).toEqual([filter])
})
it('should delete a filter that has been set', () => {
const filter: Filter = {key: 'mean', equality: '=', value: '100'}
it('Can delete a filter that has been set', () => {
let result = predicatesReducer(initialState, setFilter(filter, 0))
expect(result.filters).toEqual([filter])
result = predicatesReducer(initialState, deleteFilter(0))
expect(initialState.filters).toEqual([])
expect(result.filters).toEqual([])
})
it('should reset the state after a filter DWP has been successfully submitted', () => {
const state = Object.assign({}, initialState)
const filter: Filter = {key: 'mean', equality: '=', value: '100'}
initialState.isSerious = predicatesReducer(
initialState,
setIsSerious(true)
).isSerious
initialState.filters = predicatesReducer(
initialState,
setFilter(filter, 0)
).filters
const result = predicatesReducer(initialState, resetPredicateState())
it('Can reset the state after a filter DWP has been successfully submitted', () => {
const state = initialState
const intermediateState = predicatesReducer(state, setFilter(filter, 0))
const result = predicatesReducer(intermediateState, resetPredicateState())
expect(result).toEqual(state)
})
})

View File

@ -1,15 +1,9 @@
// Libraries
import moment from 'moment'
// Actions
import {Action} from 'src/shared/actions/predicates'
// Types
import {PredicatesState, RemoteDataState} from 'src/types'
export const recently = Date.parse(moment().format('YYYY-MM-DD HH:00:00'))
export const HOUR_MS = 1000 * 60 * 60
export const initialState: PredicatesState = {
bucketName: '',
deletionStatus: RemoteDataState.NotStarted,
@ -18,7 +12,7 @@ export const initialState: PredicatesState = {
isSerious: false,
keys: [],
previewStatus: RemoteDataState.NotStarted,
timeRange: [recently - HOUR_MS, recently],
timeRange: null,
values: [],
}
@ -77,17 +71,7 @@ export const predicatesReducer = (
return {...state, values: action.payload.values}
case 'SET_PREDICATE_DEFAULT':
return {
bucketName: '',
deletionStatus: RemoteDataState.NotStarted,
files: [],
filters: [],
isSerious: false,
keys: [],
previewStatus: RemoteDataState.NotStarted,
timeRange: [recently - HOUR_MS, recently],
values: [],
}
return {...initialState}
default:
return state

View File

@ -3,7 +3,9 @@ import {
durationToMilliseconds,
areDurationsEqual,
millisecondsToDuration,
isDurationParseable,
} from 'src/shared/utils/duration'
import {SELECTABLE_TIME_RANGES} from 'src/shared/constants/timeRanges'
const TEST_CASES = [
['1d', [{magnitude: 1, unit: 'd'}]],
@ -66,3 +68,19 @@ describe('millisecondsToDuration', () => {
expect(millisecondsToDuration(2 / 1_000_000)).toEqual('2ns')
})
})
describe('isDurationParseable', () => {
test('returns false when passed invalid durations', () => {
expect(isDurationParseable('1h')).toBe(false)
expect(isDurationParseable('moo')).toBe(false)
expect(isDurationParseable('123')).toBe(false)
expect(isDurationParseable('now()')).toBe(false)
})
test.each(SELECTABLE_TIME_RANGES)(
'returns true when passed valid duration',
({lower}) => {
expect(isDurationParseable(lower)).toEqual(true)
}
)
})

View File

@ -1,11 +1,29 @@
import {TimeRange} from 'src/types'
import moment from 'moment'
import {TimeRange, CustomTimeRange} from 'src/types'
import {Duration, DurationUnit} from 'src/types/ast'
import {TIME_RANGE_FORMAT} from 'src/shared/constants/timeRanges'
export const removeSpacesAndNow = (input: string): string =>
input.replace(/\s/g, '').replace(/now\(\)-/, '')
export const isDurationParseable = (lower: string): boolean => {
const durationRegExp = /([0-9]+)(y|mo|w|d|h|ms|s|m|us|µs|ns)/g
if (!lower || !lower.includes('now()')) {
return false
}
// warning! Using string.match(regex) here instead of regex.test(string) because regex.test() modifies the regex object, and can lead to unexpected behavior
const removedLower = removeSpacesAndNow(lower)
return !!removedLower.match(durationRegExp)
}
export const parseDuration = (input: string): Duration[] => {
const r = /([0-9]+)(y|mo|w|d|h|ms|s|m|us|µs|ns)/g
const result = []
const durationRegExp = /([0-9]+)(y|mo|w|d|h|ms|s|m|us|µs|ns)/g
let match = r.exec(input)
// warning! regex.exec(string) modifies the regex it is operating on so that subsequent calls on the same string behave differently
let match = durationRegExp.exec(input)
if (!match) {
throw new Error(`could not parse "${input}" as duration`)
@ -17,7 +35,7 @@ export const parseDuration = (input: string): Duration[] => {
unit: match[2],
})
match = r.exec(input)
match = durationRegExp.exec(input)
}
return result
@ -92,5 +110,49 @@ export const timeRangeToDuration = (timeRange: TimeRange): string => {
throw new Error('cannot convert time range to duration')
}
return timeRange.lower.replace(/\s/g, '').replace(/now\(\)-/, '')
return removeSpacesAndNow(timeRange.lower)
}
export const convertTimeRangeToCustom = (
timeRange: TimeRange
): CustomTimeRange => {
if (timeRange.type === 'custom') {
return timeRange
}
const upper = new Date().toISOString()
let lower = ''
if (timeRange.type === 'selectable-duration') {
lower = moment()
.subtract(timeRange.seconds, 's')
.toISOString()
} else if (timeRange.type === 'duration') {
const millisecondDuration = durationToMilliseconds(
parseDuration(timeRangeToDuration(timeRange))
)
lower = moment()
.subtract(millisecondDuration, 'milliseconds')
.toISOString()
}
return {
lower,
upper,
type: 'custom',
}
}
export const getTimeRangeLabel = (timeRange: TimeRange): string => {
if (timeRange.type === 'selectable-duration') {
return timeRange.label
}
if (timeRange.type === 'duration') {
return timeRange.lower
}
if (timeRange.type === 'custom') {
return `${moment(timeRange.lower).format(TIME_RANGE_FORMAT)} - ${moment(
timeRange.upper
).format(TIME_RANGE_FORMAT)}`
}
}

View File

@ -1,17 +1,24 @@
// Libraries
import {get, isEmpty} from 'lodash'
import {Dispatch} from 'redux-thunk'
// Actions
import {loadBuckets} from 'src/timeMachine/actions/queryBuilder'
import {saveAndExecuteQueries} from 'src/timeMachine/actions/queries'
// Types
import {Dispatch} from 'redux-thunk'
import {TimeMachineState} from 'src/timeMachine/reducers'
import {
reloadTagSelectors,
Action as QueryBuilderAction,
} from 'src/timeMachine/actions/queryBuilder'
import {setValues} from 'src/variables/actions'
// Selectors
import {getTimeRangeByDashboardID} from 'src/dashboards/selectors'
// Utils
import {createView} from 'src/shared/utils/view'
// Types
import {TimeMachineState} from 'src/timeMachine/reducers'
import {Action as QueryResultsAction} from 'src/timeMachine/actions/queries'
import {
TimeRange,
@ -30,13 +37,10 @@ import {
CheckStatusLevel,
XYViewProperties,
GetState,
RemoteDataState,
} from 'src/types'
import {Color} from 'src/types/colors'
import {HistogramPosition, LinePosition} from '@influxdata/giraffe'
import {RemoteDataState} from '@influxdata/clockface'
import {createView} from 'src/shared/utils/view'
import {setValues} from 'src/variables/actions'
import {getTimeRangeByDashboardID} from 'src/dashboards/selectors/index'
export type Action =
| QueryBuilderAction
@ -677,8 +681,7 @@ export const loadNewVEO = (dashboardID: string) => (
getState: GetState
): void => {
const state = getState()
const timeRange = getTimeRangeByDashboardID(state.ranges, dashboardID)
const timeRange = getTimeRangeByDashboardID(state, dashboardID)
dispatch(
setActiveTimeMachine('veo', {

View File

@ -12,8 +12,9 @@ import {formatExpression} from 'src/variables/utils/formatExpression'
// Types
import {TimeRange, BuilderConfig} from 'src/types'
import {CancelBox} from 'src/types/promises'
import {pastThirtyDaysTimeRange} from 'src/shared/constants/timeRanges'
const DEFAULT_TIME_RANGE: TimeRange = {lower: 'now() - 30d'}
const DEFAULT_TIME_RANGE: TimeRange = pastThirtyDaysTimeRange
const DEFAULT_LIMIT = 200
export interface FindBucketsOptions {

View File

@ -7,9 +7,7 @@ import TimeMachineFluxEditor from 'src/timeMachine/components/TimeMachineFluxEdi
import CSVExportButton from 'src/shared/components/CSVExportButton'
import TimeMachineQueriesSwitcher from 'src/timeMachine/components/QueriesSwitcher'
import TimeMachineRefreshDropdown from 'src/timeMachine/components/RefreshDropdown'
import TimeRangeDropdown, {
RangeType,
} from 'src/shared/components/TimeRangeDropdown'
import TimeRangeDropdown from 'src/shared/components/TimeRangeDropdown'
import TimeMachineQueryBuilder from 'src/timeMachine/components/QueryBuilder'
import SubmitQueryButton from 'src/timeMachine/components/SubmitQueryButton'
import RawDataToggle from 'src/timeMachine/components/RawDataToggle'
@ -94,15 +92,12 @@ class TimeMachineQueries extends PureComponent<Props> {
)
}
private handleSetTimeRange = (
timeRange: TimeRange,
rangeType: RangeType = RangeType.Relative
) => {
private handleSetTimeRange = (timeRange: TimeRange) => {
const {autoRefresh, onSetAutoRefresh, onSetTimeRange} = this.props
onSetTimeRange(timeRange)
if (rangeType === RangeType.Absolute) {
if (timeRange.type === 'custom') {
onSetAutoRefresh({...autoRefresh, status: AutoRefreshStatus.Disabled})
return
}

View File

@ -12,6 +12,7 @@ import {
THRESHOLD_TYPE_TEXT,
THRESHOLD_TYPE_BG,
} from 'src/shared/constants/thresholds'
import {pastHourTimeRange} from 'src/shared/constants/timeRanges'
import {
DEFAULT_THRESHOLD_CHECK,
DEFAULT_DEADMAN_CHECK,
@ -97,7 +98,7 @@ export interface TimeMachinesState {
}
export const initialStateHelper = (): TimeMachineState => ({
timeRange: {lower: 'now() - 1h'},
timeRange: pastHourTimeRange,
autoRefresh: AUTOREFRESH_DEFAULT,
view: createView(),
alerting: {

View File

@ -2,119 +2,60 @@
import {getStartTime, getEndTime} from 'src/timeMachine/selectors/index'
import moment from 'moment'
import {
pastThirtyDaysTimeRange,
pastHourTimeRange,
pastFifteenMinTimeRange,
} from 'src/shared/constants/timeRanges'
const custom = 'custom' as 'custom'
describe('TimeMachine.Selectors.Index', () => {
const thirty = moment()
.subtract(30, 'days')
.valueOf()
it(`getStartTime should return ${thirty} when lower is now() - 30d`, () => {
const timeRange = {
lower: 'now() - 30d',
upper: null,
}
expect(getStartTime(timeRange)).toBeGreaterThanOrEqual(thirty)
})
const seven = moment()
.subtract(7, 'days')
.valueOf()
it(`getStartTime should return ${seven} when lower is now() - 7d`, () => {
const timeRange = {
lower: 'now() - 7d',
upper: null,
}
expect(getStartTime(timeRange)).toBeGreaterThanOrEqual(seven)
})
const two = moment()
.subtract(2, 'days')
.valueOf()
it(`getStartTime should return ${two} when lower is now() - 2d`, () => {
const timeRange = {
lower: 'now() - 2d',
upper: null,
}
expect(getStartTime(timeRange)).toBeGreaterThanOrEqual(two)
})
const twentyFour = moment()
.subtract(24, 'hours')
.valueOf()
it(`getStartTime should return ${twentyFour} when lower is now() - 24h`, () => {
const timeRange = {
lower: 'now() - 24h',
upper: null,
}
expect(getStartTime(timeRange)).toBeGreaterThanOrEqual(twentyFour)
})
const twelve = moment()
.subtract(12, 'hours')
.valueOf()
it(`getStartTime should return ${twelve} when lower is now() - 12h`, () => {
const timeRange = {
lower: 'now() - 12h',
upper: null,
}
expect(getStartTime(timeRange)).toBeGreaterThanOrEqual(twelve)
})
const six = moment()
.subtract(6, 'hours')
.valueOf()
it(`getStartTime should return ${six} when lower is now() - 6h`, () => {
const timeRange = {
lower: 'now() - 6h',
upper: null,
}
expect(getStartTime(timeRange)).toBeGreaterThanOrEqual(six)
expect(getStartTime(pastThirtyDaysTimeRange)).toBeGreaterThanOrEqual(thirty)
})
const hour = moment()
.subtract(1, 'hours')
.valueOf()
it(`getStartTime should return ${hour} when lower is now() - 1h`, () => {
const timeRange = {
lower: 'now() - 1h',
upper: null,
}
expect(getStartTime(timeRange)).toBeGreaterThanOrEqual(hour)
expect(getStartTime(pastHourTimeRange)).toBeGreaterThanOrEqual(hour)
})
const fifteen = moment()
.subtract(15, 'minutes')
.valueOf()
it(`getStartTime should return ${fifteen} when lower is now() - 15m`, () => {
const timeRange = {
lower: 'now() - 15m',
upper: null,
}
expect(getStartTime(timeRange)).toBeGreaterThanOrEqual(fifteen)
})
const five = moment()
.subtract(5, 'minutes')
.valueOf()
it(`getStartTime should return ${five} when lower is now() - 5m`, () => {
const timeRange = {
lower: 'now() - 5m',
upper: null,
}
expect(getStartTime(timeRange)).toBeGreaterThanOrEqual(five)
it(`getStartTime should return ${hour} when lower is now() - 1h`, () => {
expect(getStartTime(pastFifteenMinTimeRange)).toBeGreaterThanOrEqual(
fifteen
)
})
const date = 'January 1, 2019'
const newYears = moment(date).valueOf()
it(`getStartTime should return ${newYears} when lower is ${date}`, () => {
const timeRange = {
type: custom,
lower: date,
upper: null,
upper: date,
}
expect(getStartTime(timeRange)).toEqual(newYears)
})
it(`getEndTime should return ${newYears} when lower is ${date}`, () => {
const timeRange = {
type: custom,
lower: date,
upper: date,
}
expect(getEndTime(timeRange)).toEqual(newYears)
})
const now = moment().valueOf()
it(`getEndTime should return ${now} when upper is null and lower includes now()`, () => {
const timeRange = {
lower: 'now() - 30d',
upper: null,
}
expect(getEndTime(timeRange)).toBeGreaterThanOrEqual(now)
expect(getEndTime(pastThirtyDaysTimeRange)).toBeGreaterThanOrEqual(now)
})
})

View File

@ -16,6 +16,11 @@ import {
import {getVariableAssignments as getVariableAssignmentsForContext} from 'src/variables/selectors'
import {getTimeRangeVars} from 'src/variables/utils/getTimeRangeVars'
import {getWindowPeriod} from 'src/variables/utils/getWindowVars'
import {
timeRangeToDuration,
parseDuration,
durationToMilliseconds,
} from 'src/shared/utils/duration'
// Types
import {
@ -251,50 +256,28 @@ export const getSaveableView = (state: AppState): QueryView & {id?: string} => {
return saveableView
}
export const getStartTime = (timeRange: TimeRange): number => {
export const getStartTime = (timeRange: TimeRange) => {
if (!timeRange) {
return Infinity
}
const {lower} = timeRange
switch (lower) {
case 'now() - 30d':
switch (timeRange.type) {
case 'custom':
return moment(timeRange.lower).valueOf()
case 'selectable-duration':
return moment()
.subtract(30, 'days')
.subtract(timeRange.seconds, 'seconds')
.valueOf()
case 'now() - 7d':
case 'duration':
const millisecondDuration = durationToMilliseconds(
parseDuration(timeRangeToDuration(timeRange))
)
return moment()
.subtract(7, 'days')
.valueOf()
case 'now() - 2d':
return moment()
.subtract(2, 'days')
.valueOf()
case 'now() - 24h':
return moment()
.subtract(24, 'hours')
.valueOf()
case 'now() - 12h':
return moment()
.subtract(12, 'hours')
.valueOf()
case 'now() - 6h':
return moment()
.subtract(6, 'hours')
.valueOf()
case 'now() - 1h':
return moment()
.subtract(1, 'hours')
.valueOf()
case 'now() - 15m':
return moment()
.subtract(15, 'minutes')
.valueOf()
case 'now() - 5m':
return moment()
.subtract(5, 'minutes')
.subtract(millisecondDuration, 'milliseconds')
.valueOf()
default:
return moment(lower).valueOf()
throw new Error(
'unknown timeRange type ${timeRange.type} provided to getStartTime'
)
}
}
@ -302,12 +285,8 @@ export const getEndTime = (timeRange: TimeRange): number => {
if (!timeRange) {
return null
}
const {lower, upper} = timeRange
if (upper) {
return moment(upper).valueOf()
if (timeRange.type === 'custom') {
return moment(timeRange.upper).valueOf()
}
if (lower.includes('now()')) {
return moment().valueOf()
}
return null
return moment().valueOf()
}

View File

@ -3,11 +3,12 @@ import {VariablesState} from 'src/variables/reducers'
import {UserSettingsState} from 'src/userSettings/reducers'
import {OrgsState} from 'src/organizations/reducers/orgs'
import {AutoRefreshState} from 'src/shared/reducers/autoRefresh'
import {RangeState} from 'src/dashboards/reducers/ranges'
export interface LocalStorage {
VERSION: string
app: AppState
ranges: any[]
ranges: RangeState
autoRefresh: AutoRefreshState
variables: VariablesState
userSettings: UserSettingsState

View File

@ -1,4 +1,4 @@
import {Filter, RemoteDataState} from 'src/types'
import {Filter, RemoteDataState, CustomTimeRange} from 'src/types'
export interface PredicatesState {
bucketName: string
@ -8,6 +8,6 @@ export interface PredicatesState {
isSerious: boolean
keys: string[]
previewStatus: RemoteDataState
timeRange: [number, number]
timeRange: CustomTimeRange
values: string[]
}

View File

@ -1,10 +1,39 @@
export {Query, Dialect} from 'src/client'
export interface TimeRange {
lower: string
upper?: string | null
seconds?: number
export type SelectableTimeRangeLower =
| 'now() - 5m'
| 'now() - 15m'
| 'now() - 1h'
| 'now() - 6h'
| 'now() - 12h'
| 'now() - 24h'
| 'now() - 2d'
| 'now() - 7d'
| 'now() - 30d'
export type TimeRange =
| SelectableDurationTimeRange
| DurationTimeRange
| CustomTimeRange
export interface SelectableDurationTimeRange {
lower: SelectableTimeRangeLower
upper: null
seconds: number
format?: string
label?: string
duration?: string
label: string
duration: string
type: 'selectable-duration'
}
export interface DurationTimeRange {
lower: string
upper: null
type: 'duration'
}
export interface CustomTimeRange {
lower: string
upper: string
type: 'custom'
}

View File

@ -1,13 +1,11 @@
import {getTimeRangeVars} from 'src/variables/utils/getTimeRangeVars'
import {TimeRange} from 'src/types'
import {pastHourTimeRange} from 'src/shared/constants/timeRanges'
const custom = 'custom' as 'custom'
describe('getTimeRangeVars', () => {
test('should handle relative lower dates', () => {
const timeRange: TimeRange = {
lower: 'now() - 1h',
}
const actual = getTimeRangeVars(timeRange)
const actual = getTimeRangeVars(pastHourTimeRange)
const init = actual[0].init as any
@ -17,9 +15,11 @@ describe('getTimeRangeVars', () => {
expect(init.argument.values).toEqual([{magnitude: 1, unit: 'h'}])
})
test('should handle absolute lower dates', () => {
const timeRange: TimeRange = {
test('should handle custom lower dates', () => {
const timeRange = {
type: custom,
lower: '2019-02-28T15:00:00Z',
upper: '2019-03-28T15:00:00Z',
}
const actual = getTimeRangeVars(timeRange)
@ -29,7 +29,8 @@ describe('getTimeRangeVars', () => {
})
test('should handle absolute upper dates', () => {
const timeRange: TimeRange = {
const timeRange = {
type: custom,
lower: '2019-02-26T15:00:00Z',
upper: '2019-02-27T15:00:00Z',
}
@ -40,12 +41,8 @@ describe('getTimeRangeVars', () => {
expect((actual[1].init as any).value).toEqual('2019-02-27T15:00:00.000Z')
})
test('should handle non-existant upper dates', () => {
const timeRange: TimeRange = {
lower: 'now() - 1h',
}
const actual = getTimeRangeVars(timeRange)
test('should set non-existent upper dates to now', () => {
const actual = getTimeRangeVars(pastHourTimeRange)
expect(actual[1].init).toEqual({
type: 'CallExpression',

View File

@ -13,18 +13,8 @@ export const getTimeRangeVars = (
): VariableAssignment[] => {
let startValue: VariableAssignment
if (isDate(timeRange.lower)) {
startValue = {
type: 'VariableAssignment',
id: {
type: 'Identifier',
name: TIME_RANGE_START,
},
init: {
type: 'DateTimeLiteral',
value: new Date(timeRange.lower).toISOString(),
},
}
if (isDateParseable(timeRange.lower)) {
startValue = generateDateTimeLiteral(TIME_RANGE_START, timeRange.lower)
} else {
startValue = {
type: 'VariableAssignment',
@ -45,18 +35,8 @@ export const getTimeRangeVars = (
let stopValue: VariableAssignment
if (timeRange.upper && isDate(timeRange.upper)) {
stopValue = {
type: 'VariableAssignment',
id: {
type: 'Identifier',
name: TIME_RANGE_STOP,
},
init: {
type: 'DateTimeLiteral',
value: new Date(timeRange.upper).toISOString(),
},
}
if (timeRange.upper && isDateParseable(timeRange.upper)) {
stopValue = generateDateTimeLiteral(TIME_RANGE_STOP, timeRange.upper)
} else {
stopValue = {
type: 'VariableAssignment',
@ -73,9 +53,25 @@ export const getTimeRangeVars = (
},
}
}
return [startValue, stopValue]
}
const isDate = (ambiguousString: string): boolean =>
const generateDateTimeLiteral = (
name: string,
value: string
): VariableAssignment => {
return {
type: 'VariableAssignment',
id: {
type: 'Identifier',
name,
},
init: {
type: 'DateTimeLiteral',
value: new Date(value).toISOString(),
},
}
}
export const isDateParseable = (ambiguousString: string): boolean =>
!isNaN(Date.parse(ambiguousString))