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.testpull/16195/head
parent
b2ea95f512
commit
f64c63120a
|
@ -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',
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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'}
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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'}}
|
||||
/>
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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)}`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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[]
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Reference in New Issue