Merge pull request #2083 from influxdata/feature/time-after-time
FEATURE: Persist dashboard time ranges per dashboardpull/2095/head
commit
fa6e837ef0
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -1,3 +1,11 @@
|
||||||
|
## v1.3.10.0 [2017-10-06]
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
### Features
|
||||||
|
1.[#2083](https://github.com/influxdata/chronograf/pull/2083): Every dashboard can now have its own time range
|
||||||
|
|
||||||
|
### UI Improvements
|
||||||
|
|
||||||
## v1.3.9.0 [2017-10-06]
|
## v1.3.9.0 [2017-10-06]
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
1. [#2004](https://github.com/influxdata/chronograf/pull/2004): Fix Data Explorer disappearing query templates in dropdown
|
1. [#2004](https://github.com/influxdata/chronograf/pull/2004): Fix Data Explorer disappearing query templates in dropdown
|
||||||
|
@ -5,6 +13,12 @@
|
||||||
1. [#2015](https://github.com/influxdata/chronograf/pull/2015): Chronograf shows real status for windows hosts when metrics are saved in non-default db - thank you, @ar7z1!
|
1. [#2015](https://github.com/influxdata/chronograf/pull/2015): Chronograf shows real status for windows hosts when metrics are saved in non-default db - thank you, @ar7z1!
|
||||||
1. [#2019](https://github.com/influxdata/chronograf/pull/2006): Fix false error warning for duplicate kapacitor name
|
1. [#2019](https://github.com/influxdata/chronograf/pull/2006): Fix false error warning for duplicate kapacitor name
|
||||||
1. [#2018](https://github.com/influxdata/chronograf/pull/2018): Fix unresponsive display options and query builder in dashboards
|
1. [#2018](https://github.com/influxdata/chronograf/pull/2018): Fix unresponsive display options and query builder in dashboards
|
||||||
|
1.[#2004](https://github.com/influxdata/chronograf/pull/2004): Fix DE query templates dropdown disappearance
|
||||||
|
1.[#2006](https://github.com/influxdata/chronograf/pull/2006): Fix no alert for duplicate db name
|
||||||
|
1.[#2015](https://github.com/influxdata/chronograf/pull/2015): Chronograf shows real status for windows hosts when metrics are saved in non-default db - thank you, @ar7z1!
|
||||||
|
1.[#2019](https://github.com/influxdata/chronograf/pull/2006): Fix false error warning for duplicate kapacitor name
|
||||||
|
1.[#2018](https://github.com/influxdata/chronograf/pull/2018): Fix unresponsive display options and query builder in dashboards
|
||||||
|
1.[#1996](https://github.com/influxdata/chronograf/pull/1996): Able to switch InfluxDB sources on a per graph basis
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
1. [#1885](https://github.com/influxdata/chronograf/pull/1885): Add `fill` options to data explorer and dashboard queries
|
1. [#1885](https://github.com/influxdata/chronograf/pull/1885): Add `fill` options to data explorer and dashboard queries
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
import reducer from 'src/dashboards/reducers/dashTimeV1'
|
||||||
|
import {
|
||||||
|
addDashTimeV1,
|
||||||
|
setDashTimeV1,
|
||||||
|
deleteDashboard,
|
||||||
|
} from 'src/dashboards/actions/index'
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
ranges: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyState = undefined
|
||||||
|
const dashboardID = 1
|
||||||
|
const timeRange = {upper: null, lower: 'now() - 15m'}
|
||||||
|
|
||||||
|
describe('Dashboards.Reducers.DashTimeV1', () => {
|
||||||
|
it('can load initial state', () => {
|
||||||
|
const noopAction = () => ({type: 'NOOP'})
|
||||||
|
const actual = reducer(emptyState, noopAction)
|
||||||
|
const expected = {ranges: []}
|
||||||
|
|
||||||
|
expect(actual).to.deep.equal(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can add a dashboard time', () => {
|
||||||
|
const actual = reducer(emptyState, addDashTimeV1(dashboardID, timeRange))
|
||||||
|
const expected = [{dashboardID, timeRange}]
|
||||||
|
|
||||||
|
expect(actual.ranges).to.deep.equal(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can delete a dashboard time range', () => {
|
||||||
|
const state = {
|
||||||
|
ranges: [{dashboardID, timeRange}],
|
||||||
|
}
|
||||||
|
const dashboard = {id: dashboardID}
|
||||||
|
|
||||||
|
const actual = reducer(state, deleteDashboard(dashboard))
|
||||||
|
const expected = []
|
||||||
|
|
||||||
|
expect(actual.ranges).to.deep.equal(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('setting a dashboard time range', () => {
|
||||||
|
it('can update an existing dashboard', () => {
|
||||||
|
const state = {
|
||||||
|
ranges: [{dashboardID, upper: timeRange.upper, lower: timeRange.lower}],
|
||||||
|
}
|
||||||
|
|
||||||
|
const {upper, lower} = {
|
||||||
|
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}]
|
||||||
|
|
||||||
|
expect(actual.ranges).to.deep.equal(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can set a new time range if none exists', () => {
|
||||||
|
const actual = reducer(emptyState, setDashTimeV1(dashboardID, timeRange))
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{dashboardID, upper: timeRange.upper, lower: timeRange.lower},
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(actual.ranges).to.deep.equal(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,7 +1,8 @@
|
||||||
import buildInfluxQLQuery from 'utils/influxql'
|
import buildInfluxQLQuery, {buildQuery} from 'utils/influxql'
|
||||||
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
|
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
|
||||||
|
|
||||||
import {NONE, NULL_STRING} from 'shared/constants/queryFillOptions'
|
import {NONE, NULL_STRING} from 'shared/constants/queryFillOptions'
|
||||||
|
import {TYPE_QUERY_CONFIG} from 'src/dashboards/constants'
|
||||||
|
|
||||||
function mergeConfig(options) {
|
function mergeConfig(options) {
|
||||||
return Object.assign({}, defaultQueryConfig(123), options)
|
return Object.assign({}, defaultQueryConfig(123), options)
|
||||||
|
@ -271,4 +272,25 @@ describe('buildInfluxQLQuery', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('build query', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
config = mergeConfig({
|
||||||
|
database: 'db1',
|
||||||
|
measurement: 'm1',
|
||||||
|
retentionPolicy: 'rp1',
|
||||||
|
fields: [{field: 'f1', func: null}],
|
||||||
|
groupBy: {time: '10m', tags: []},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('builds an influxql relative time bound query', () => {
|
||||||
|
const timeRange = {upper: null, lower: 'now() - 15m'}
|
||||||
|
const expected =
|
||||||
|
'SELECT "f1" FROM "db1"."rp1"."m1" WHERE time > now() - 15m GROUP BY time(10m) FILL(null)'
|
||||||
|
const actual = buildQuery(TYPE_QUERY_CONFIG, timeRange, config)
|
||||||
|
|
||||||
|
expect(actual).to.equal(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
import normalizer from 'src/normalizers/dashboardTime'
|
||||||
|
|
||||||
|
const dashboardID = 1
|
||||||
|
const upper = null
|
||||||
|
const lower = 'now() - 15m'
|
||||||
|
const timeRange = {dashboardID, upper, lower}
|
||||||
|
|
||||||
|
describe('Normalizers.DashboardTime', () => {
|
||||||
|
it('can filter out non-objects', () => {
|
||||||
|
const ranges = [1, null, undefined, 'string', timeRange]
|
||||||
|
|
||||||
|
const actual = normalizer(ranges)
|
||||||
|
const expected = [timeRange]
|
||||||
|
|
||||||
|
expect(actual).to.deep.equal(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can remove objects with missing keys', () => {
|
||||||
|
const ranges = [
|
||||||
|
{},
|
||||||
|
{dashboardID, upper},
|
||||||
|
{dashboardID, lower},
|
||||||
|
{upper, lower},
|
||||||
|
timeRange,
|
||||||
|
]
|
||||||
|
|
||||||
|
const actual = normalizer(ranges)
|
||||||
|
const expected = [timeRange]
|
||||||
|
expect(actual).to.deep.equal(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can remove timeRanges with incorrect dashboardID', () => {
|
||||||
|
const ranges = [{dashboardID: '1', upper, lower}, timeRange]
|
||||||
|
|
||||||
|
const actual = normalizer(ranges)
|
||||||
|
const expected = [timeRange]
|
||||||
|
expect(actual).to.deep.equal(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can remove timeRange when is neither an upper or lower bound', () => {
|
||||||
|
const noBounds = {dashboardID, upper: null, lower: null}
|
||||||
|
const ranges = [timeRange, noBounds]
|
||||||
|
|
||||||
|
const actual = normalizer(ranges)
|
||||||
|
const expected = [timeRange]
|
||||||
|
expect(actual).to.deep.equal(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can remove a timeRange when upper and lower bounds are of the wrong type', () => {
|
||||||
|
const badTime = {dashboardID, upper: [], lower}
|
||||||
|
const reallyBadTime = {dashboardID, upper, lower: {bad: 'time'}}
|
||||||
|
const ranges = [timeRange, badTime, reallyBadTime]
|
||||||
|
|
||||||
|
const actual = normalizer(ranges)
|
||||||
|
const expected = [timeRange]
|
||||||
|
expect(actual).to.deep.equal(expected)
|
||||||
|
})
|
||||||
|
})
|
|
@ -28,6 +28,29 @@ export const loadDashboards = (dashboards, dashboardID) => ({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const loadDeafaultDashTimeV1 = dashboardID => ({
|
||||||
|
type: 'ADD_DASHBOARD_TIME_V1',
|
||||||
|
payload: {
|
||||||
|
dashboardID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const addDashTimeV1 = (dashboardID, timeRange) => ({
|
||||||
|
type: 'ADD_DASHBOARD_TIME_V1',
|
||||||
|
payload: {
|
||||||
|
dashboardID,
|
||||||
|
timeRange,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const setDashTimeV1 = (dashboardID, timeRange) => ({
|
||||||
|
type: 'SET_DASHBOARD_TIME_V1',
|
||||||
|
payload: {
|
||||||
|
dashboardID,
|
||||||
|
timeRange,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export const setTimeRange = timeRange => ({
|
export const setTimeRange = timeRange => ({
|
||||||
type: 'SET_DASHBOARD_TIME_RANGE',
|
type: 'SET_DASHBOARD_TIME_RANGE',
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -46,6 +69,7 @@ export const deleteDashboard = dashboard => ({
|
||||||
type: 'DELETE_DASHBOARD',
|
type: 'DELETE_DASHBOARD',
|
||||||
payload: {
|
payload: {
|
||||||
dashboard,
|
dashboard,
|
||||||
|
dashboardID: dashboard.id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -12,10 +12,13 @@ import DisplayOptions from 'src/dashboards/components/DisplayOptions'
|
||||||
import * as queryModifiers from 'src/utils/queryTransitions'
|
import * as queryModifiers from 'src/utils/queryTransitions'
|
||||||
|
|
||||||
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
|
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
|
||||||
import buildInfluxQLQuery from 'utils/influxql'
|
import {buildQuery} from 'utils/influxql'
|
||||||
import {getQueryConfig} from 'shared/apis'
|
import {getQueryConfig} from 'shared/apis'
|
||||||
|
|
||||||
import {removeUnselectedTemplateValues} from 'src/dashboards/constants'
|
import {
|
||||||
|
removeUnselectedTemplateValues,
|
||||||
|
TYPE_QUERY_CONFIG,
|
||||||
|
} from 'src/dashboards/constants'
|
||||||
import {OVERLAY_TECHNOLOGY} from 'shared/constants/classNames'
|
import {OVERLAY_TECHNOLOGY} from 'shared/constants/classNames'
|
||||||
import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants'
|
import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants'
|
||||||
|
|
||||||
|
@ -147,7 +150,7 @@ class CellEditorOverlay extends Component {
|
||||||
|
|
||||||
const queries = queriesWorkingDraft.map(q => {
|
const queries = queriesWorkingDraft.map(q => {
|
||||||
const timeRange = q.range || {upper: null, lower: ':dashboardTime:'}
|
const timeRange = q.range || {upper: null, lower: ':dashboardTime:'}
|
||||||
const query = q.rawText || buildInfluxQLQuery(timeRange, q)
|
const query = q.rawText || buildQuery(TYPE_QUERY_CONFIG, timeRange, q)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
queryConfig: q,
|
queryConfig: q,
|
||||||
|
|
|
@ -4,13 +4,14 @@ import EmptyQuery from 'src/shared/components/EmptyQuery'
|
||||||
import QueryTabList from 'src/shared/components/QueryTabList'
|
import QueryTabList from 'src/shared/components/QueryTabList'
|
||||||
import QueryTextArea from 'src/dashboards/components/QueryTextArea'
|
import QueryTextArea from 'src/dashboards/components/QueryTextArea'
|
||||||
import SchemaExplorer from 'src/shared/components/SchemaExplorer'
|
import SchemaExplorer from 'src/shared/components/SchemaExplorer'
|
||||||
import buildInfluxQLQuery from 'utils/influxql'
|
import {buildQuery} from 'utils/influxql'
|
||||||
|
import {TYPE_QUERY_CONFIG} from 'src/dashboards/constants'
|
||||||
|
|
||||||
const TEMPLATE_RANGE = {upper: null, lower: ':dashboardTime:'}
|
const TEMPLATE_RANGE = {upper: null, lower: ':dashboardTime:'}
|
||||||
const rawTextBinder = (links, id, action) => text =>
|
const rawTextBinder = (links, id, action) => text =>
|
||||||
action(links.queries, id, text)
|
action(links.queries, id, text)
|
||||||
const buildText = q =>
|
const buildText = q =>
|
||||||
q.rawText || buildInfluxQLQuery(q.range || TEMPLATE_RANGE, q) || ''
|
q.rawText || buildQuery(TYPE_QUERY_CONFIG, q.range || TEMPLATE_RANGE, q) || ''
|
||||||
|
|
||||||
const QueryMaker = ({
|
const QueryMaker = ({
|
||||||
source,
|
source,
|
||||||
|
|
|
@ -106,3 +106,6 @@ export const TOOLTIP_CONTENT = {
|
||||||
FORMAT:
|
FORMAT:
|
||||||
'<p><strong>K/M/B</strong> = Thousand / Million / Billion<br/><strong>K/M/G</strong> = Kilo / Mega / Giga </p>',
|
'<p><strong>K/M/B</strong> = Thousand / Million / Billion<br/><strong>K/M/G</strong> = Kilo / Mega / Giga </p>',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const TYPE_QUERY_CONFIG = 'queryConfig'
|
||||||
|
export const TYPE_IFQL = 'ifql'
|
||||||
|
|
|
@ -13,6 +13,7 @@ import Dashboard from 'src/dashboards/components/Dashboard'
|
||||||
import TemplateVariableManager from 'src/dashboards/components/template_variables/Manager'
|
import TemplateVariableManager from 'src/dashboards/components/template_variables/Manager'
|
||||||
|
|
||||||
import {errorThrown as errorThrownAction} from 'shared/actions/errors'
|
import {errorThrown as errorThrownAction} from 'shared/actions/errors'
|
||||||
|
import idNormalizer, {TYPE_ID} from 'src/normalizers/id'
|
||||||
|
|
||||||
import * as dashboardActionCreators from 'src/dashboards/actions'
|
import * as dashboardActionCreators from 'src/dashboards/actions'
|
||||||
|
|
||||||
|
@ -22,6 +23,13 @@ import {
|
||||||
} from 'shared/actions/app'
|
} from 'shared/actions/app'
|
||||||
import {presentationButtonDispatcher} from 'shared/dispatchers'
|
import {presentationButtonDispatcher} from 'shared/dispatchers'
|
||||||
|
|
||||||
|
const FORMAT_INFLUXQL = 'influxql'
|
||||||
|
const defaultTimeRange = {
|
||||||
|
upper: null,
|
||||||
|
lower: 'now() - 15m',
|
||||||
|
format: FORMAT_INFLUXQL,
|
||||||
|
}
|
||||||
|
|
||||||
class DashboardPage extends Component {
|
class DashboardPage extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
@ -47,7 +55,9 @@ class DashboardPage extends Component {
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
const dashboards = await getDashboardsAsync()
|
const dashboards = await getDashboardsAsync()
|
||||||
const dashboard = dashboards.find(d => d.id === +dashboardID)
|
const dashboard = dashboards.find(
|
||||||
|
d => d.id === idNormalizer(TYPE_ID, dashboardID)
|
||||||
|
)
|
||||||
|
|
||||||
// Refresh and persists influxql generated template variable values
|
// Refresh and persists influxql generated template variable values
|
||||||
await updateTempVarValues(source, dashboard)
|
await updateTempVarValues(source, dashboard)
|
||||||
|
@ -72,8 +82,9 @@ class DashboardPage extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSaveEditedCell = newCell => {
|
handleSaveEditedCell = newCell => {
|
||||||
this.props.dashboardActions
|
const {dashboardActions, dashboard} = this.props
|
||||||
.updateDashboardCell(this.getActiveDashboard(), newCell)
|
dashboardActions
|
||||||
|
.updateDashboardCell(dashboard, newCell)
|
||||||
.then(this.handleDismissOverlay)
|
.then(this.handleDismissOverlay)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,18 +92,26 @@ class DashboardPage extends Component {
|
||||||
this.setState({selectedCell: cell})
|
this.setState({selectedCell: cell})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChooseTimeRange = timeRange => {
|
handleChooseTimeRange = ({upper, lower}) => {
|
||||||
this.props.dashboardActions.setTimeRange(timeRange)
|
const {dashboard, dashboardActions} = this.props
|
||||||
|
dashboardActions.setDashTimeV1(dashboard.id, {
|
||||||
|
upper,
|
||||||
|
lower,
|
||||||
|
format: FORMAT_INFLUXQL,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUpdatePosition = cells => {
|
handleUpdatePosition = cells => {
|
||||||
const newDashboard = {...this.getActiveDashboard(), cells}
|
const {dashboardActions, dashboard} = this.props
|
||||||
this.props.dashboardActions.updateDashboard(newDashboard)
|
const newDashboard = {...dashboard, cells}
|
||||||
this.props.dashboardActions.putDashboard(newDashboard)
|
|
||||||
|
dashboardActions.updateDashboard(newDashboard)
|
||||||
|
dashboardActions.putDashboard(newDashboard)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAddCell = () => {
|
handleAddCell = () => {
|
||||||
this.props.dashboardActions.addDashboardCellAsync(this.getActiveDashboard())
|
const {dashboardActions, dashboard} = this.props
|
||||||
|
dashboardActions.addDashboardCellAsync(dashboard)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEditDashboard = () => {
|
handleEditDashboard = () => {
|
||||||
|
@ -104,42 +123,40 @@ class DashboardPage extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRenameDashboard = name => {
|
handleRenameDashboard = name => {
|
||||||
|
const {dashboardActions, dashboard} = this.props
|
||||||
this.setState({isEditMode: false})
|
this.setState({isEditMode: false})
|
||||||
const newDashboard = {...this.getActiveDashboard(), name}
|
const newDashboard = {...dashboard, name}
|
||||||
this.props.dashboardActions.updateDashboard(newDashboard)
|
|
||||||
this.props.dashboardActions.putDashboard(newDashboard)
|
dashboardActions.updateDashboard(newDashboard)
|
||||||
|
dashboardActions.putDashboard(newDashboard)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUpdateDashboardCell = newCell => {
|
handleUpdateDashboardCell = newCell => () => {
|
||||||
return () => {
|
const {dashboardActions, dashboard} = this.props
|
||||||
this.props.dashboardActions.updateDashboardCell(
|
dashboardActions.updateDashboardCell(dashboard, newCell)
|
||||||
this.getActiveDashboard(),
|
|
||||||
newCell
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteDashboardCell = cell => {
|
handleDeleteDashboardCell = cell => {
|
||||||
const dashboard = this.getActiveDashboard()
|
const {dashboardActions, dashboard} = this.props
|
||||||
this.props.dashboardActions.deleteDashboardCellAsync(dashboard, cell)
|
dashboardActions.deleteDashboardCellAsync(dashboard, cell)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSelectTemplate = templateID => values => {
|
handleSelectTemplate = templateID => values => {
|
||||||
const {params: {dashboardID}} = this.props
|
const {dashboardActions, dashboard} = this.props
|
||||||
this.props.dashboardActions.templateVariableSelected(
|
dashboardActions.templateVariableSelected(dashboard.id, templateID, [
|
||||||
+dashboardID,
|
values,
|
||||||
templateID,
|
])
|
||||||
[values]
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEditTemplateVariables = (
|
handleEditTemplateVariables = (
|
||||||
templates,
|
templates,
|
||||||
onSaveTemplatesSuccess
|
onSaveTemplatesSuccess
|
||||||
) => async () => {
|
) => async () => {
|
||||||
|
const {dashboardActions, dashboard} = this.props
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.props.dashboardActions.putDashboard({
|
await dashboardActions.putDashboard({
|
||||||
...this.getActiveDashboard(),
|
dashboard,
|
||||||
templates,
|
templates,
|
||||||
})
|
})
|
||||||
onSaveTemplatesSuccess()
|
onSaveTemplatesSuccess()
|
||||||
|
@ -155,8 +172,12 @@ class DashboardPage extends Component {
|
||||||
|
|
||||||
synchronizer = dygraph => {
|
synchronizer = dygraph => {
|
||||||
const dygraphs = [...this.state.dygraphs, dygraph]
|
const dygraphs = [...this.state.dygraphs, dygraph]
|
||||||
const {dashboards, params} = this.props
|
const {dashboards, params: {dashboardID}} = this.props
|
||||||
const dashboard = dashboards.find(d => d.id === +params.dashboardID)
|
|
||||||
|
const dashboard = dashboards.find(
|
||||||
|
d => d.id === idNormalizer(TYPE_ID, dashboardID)
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
dashboard &&
|
dashboard &&
|
||||||
dygraphs.length === dashboard.cells.length &&
|
dygraphs.length === dashboard.cells.length &&
|
||||||
|
@ -179,11 +200,6 @@ class DashboardPage extends Component {
|
||||||
this.setState({zoomedTimeRange: {zoomedLower, zoomedUpper}})
|
this.setState({zoomedTimeRange: {zoomedLower, zoomedUpper}})
|
||||||
}
|
}
|
||||||
|
|
||||||
getActiveDashboard() {
|
|
||||||
const {params: {dashboardID}, dashboards} = this.props
|
|
||||||
return dashboards.find(d => d.id === +dashboardID)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {zoomedTimeRange} = this.state
|
const {zoomedTimeRange} = this.state
|
||||||
const {zoomedLower, zoomedUpper} = zoomedTimeRange
|
const {zoomedLower, zoomedUpper} = zoomedTimeRange
|
||||||
|
@ -194,6 +210,7 @@ class DashboardPage extends Component {
|
||||||
timeRange,
|
timeRange,
|
||||||
timeRange: {lower, upper},
|
timeRange: {lower, upper},
|
||||||
showTemplateControlBar,
|
showTemplateControlBar,
|
||||||
|
dashboard,
|
||||||
dashboards,
|
dashboards,
|
||||||
autoRefresh,
|
autoRefresh,
|
||||||
cellQueryStatus,
|
cellQueryStatus,
|
||||||
|
@ -246,8 +263,6 @@ class DashboardPage extends Component {
|
||||||
values: [],
|
values: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
const dashboard = this.getActiveDashboard()
|
|
||||||
|
|
||||||
let templatesIncludingDashTime
|
let templatesIncludingDashTime
|
||||||
if (dashboard) {
|
if (dashboard) {
|
||||||
templatesIncludingDashTime = [
|
templatesIncludingDashTime = [
|
||||||
|
@ -366,6 +381,7 @@ DashboardPage.propTypes = {
|
||||||
pathname: string.isRequired,
|
pathname: string.isRequired,
|
||||||
query: shape({}),
|
query: shape({}),
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
|
dashboard: shape({}),
|
||||||
dashboardActions: shape({
|
dashboardActions: shape({
|
||||||
putDashboard: func.isRequired,
|
putDashboard: func.isRequired,
|
||||||
getDashboardsAsync: func.isRequired,
|
getDashboardsAsync: func.isRequired,
|
||||||
|
@ -401,7 +417,10 @@ DashboardPage.propTypes = {
|
||||||
handleChooseAutoRefresh: func.isRequired,
|
handleChooseAutoRefresh: func.isRequired,
|
||||||
autoRefresh: number.isRequired,
|
autoRefresh: number.isRequired,
|
||||||
templateControlBarVisibilityToggled: func.isRequired,
|
templateControlBarVisibilityToggled: func.isRequired,
|
||||||
timeRange: shape({}).isRequired,
|
timeRange: shape({
|
||||||
|
upper: string,
|
||||||
|
lower: string,
|
||||||
|
}),
|
||||||
showTemplateControlBar: bool.isRequired,
|
showTemplateControlBar: bool.isRequired,
|
||||||
inPresentationMode: bool.isRequired,
|
inPresentationMode: bool.isRequired,
|
||||||
handleClickPresentationButton: func,
|
handleClickPresentationButton: func,
|
||||||
|
@ -412,19 +431,30 @@ DashboardPage.propTypes = {
|
||||||
errorThrown: func,
|
errorThrown: func,
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = (state, {params: {dashboardID}}) => {
|
||||||
const {
|
const {
|
||||||
app: {
|
app: {
|
||||||
ephemeral: {inPresentationMode},
|
ephemeral: {inPresentationMode},
|
||||||
persisted: {autoRefresh, showTemplateControlBar},
|
persisted: {autoRefresh, showTemplateControlBar},
|
||||||
},
|
},
|
||||||
dashboardUI: {dashboards, timeRange, cellQueryStatus},
|
dashboardUI: {dashboards, cellQueryStatus},
|
||||||
sources,
|
sources,
|
||||||
|
dashTimeV1,
|
||||||
} = state
|
} = state
|
||||||
|
|
||||||
|
const timeRange =
|
||||||
|
dashTimeV1.ranges.find(
|
||||||
|
r => r.dashboardID === idNormalizer(TYPE_ID, dashboardID)
|
||||||
|
) || defaultTimeRange
|
||||||
|
|
||||||
|
const dashboard = dashboards.find(
|
||||||
|
d => d.id === idNormalizer(TYPE_ID, dashboardID)
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dashboards,
|
dashboards,
|
||||||
autoRefresh,
|
autoRefresh,
|
||||||
|
dashboard,
|
||||||
timeRange,
|
timeRange,
|
||||||
showTemplateControlBar,
|
showTemplateControlBar,
|
||||||
inPresentationMode,
|
inPresentationMode,
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import _ from 'lodash'
|
||||||
|
const initialState = {
|
||||||
|
ranges: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
const dashTimeV1 = (state = initialState, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'ADD_DASHBOARD_TIME_V1': {
|
||||||
|
const {dashboardID, timeRange} = action.payload
|
||||||
|
const ranges = [...state.ranges, {dashboardID, timeRange}]
|
||||||
|
|
||||||
|
return {...state, ranges}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'DELETE_DASHBOARD': {
|
||||||
|
const {dashboardID} = action.payload
|
||||||
|
const ranges = state.ranges.filter(r => r.dashboardID !== dashboardID)
|
||||||
|
|
||||||
|
return {...state, ranges}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_DASHBOARD_TIME_V1': {
|
||||||
|
const {dashboardID, timeRange} = action.payload
|
||||||
|
const newTimeRange = [{dashboardID, ...timeRange}]
|
||||||
|
const ranges = _.unionBy(newTimeRange, state.ranges, 'dashboardID')
|
||||||
|
|
||||||
|
return {...state, ranges}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
export default dashTimeV1
|
|
@ -35,6 +35,7 @@ class DataExplorer extends Component {
|
||||||
if (queryConfigs.length === 0) {
|
if (queryConfigs.length === 0) {
|
||||||
this.props.queryConfigActions.addQuery()
|
this.props.queryConfigActions.addQuery()
|
||||||
}
|
}
|
||||||
|
|
||||||
return queryConfigs[0]
|
return queryConfigs[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import _ from 'lodash'
|
||||||
|
import normalizer from 'src/normalizers/dashboardTime'
|
||||||
|
|
||||||
export const loadLocalStorage = errorsQueue => {
|
export const loadLocalStorage = errorsQueue => {
|
||||||
try {
|
try {
|
||||||
const serializedState = localStorage.getItem('state')
|
const serializedState = localStorage.getItem('state')
|
||||||
|
@ -12,8 +15,22 @@ export const loadLocalStorage = errorsQueue => {
|
||||||
console.log(errorText) // eslint-disable-line no-console
|
console.log(errorText) // eslint-disable-line no-console
|
||||||
errorsQueue.push(errorText)
|
errorsQueue.push(errorText)
|
||||||
|
|
||||||
window.localStorage.removeItem('state')
|
if (!state.dashTimeV1) {
|
||||||
return {}
|
window.localStorage.removeItem('state')
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ranges = normalizer(_.get(state, ['dashTimeV1', 'ranges'], []))
|
||||||
|
const dashTimeV1 = {ranges}
|
||||||
|
|
||||||
|
window.localStorage.setItem(
|
||||||
|
'state',
|
||||||
|
JSON.stringify({
|
||||||
|
dashTimeV1,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return {dashTimeV1}
|
||||||
}
|
}
|
||||||
|
|
||||||
delete state.VERSION
|
delete state.VERSION
|
||||||
|
@ -34,9 +51,11 @@ export const saveToLocalStorage = ({
|
||||||
dataExplorerQueryConfigs,
|
dataExplorerQueryConfigs,
|
||||||
timeRange,
|
timeRange,
|
||||||
dataExplorer,
|
dataExplorer,
|
||||||
|
dashTimeV1: {ranges},
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const appPersisted = Object.assign({}, {app: {persisted}})
|
const appPersisted = Object.assign({}, {app: {persisted}})
|
||||||
|
const dashTimeV1 = {ranges: normalizer(ranges)}
|
||||||
|
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
'state',
|
'state',
|
||||||
|
@ -46,6 +65,7 @@ export const saveToLocalStorage = ({
|
||||||
timeRange,
|
timeRange,
|
||||||
dataExplorer,
|
dataExplorer,
|
||||||
VERSION, // eslint-disable-line no-undef
|
VERSION, // eslint-disable-line no-undef
|
||||||
|
dashTimeV1,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
const dashtime = ranges => {
|
||||||
|
if (!Array.isArray(ranges)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = ranges.filter(r => {
|
||||||
|
if (!_.isObject(r)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for presence of keys
|
||||||
|
if (
|
||||||
|
!r.hasOwnProperty('dashboardID') ||
|
||||||
|
!r.hasOwnProperty('lower') ||
|
||||||
|
!r.hasOwnProperty('upper')
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const {dashboardID, lower, upper} = r
|
||||||
|
|
||||||
|
if (!dashboardID || typeof dashboardID !== 'number') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lower && !upper) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCorrectType = bound =>
|
||||||
|
_.isString(bound) || _.isNull(bound) || _.isInteger(bound)
|
||||||
|
|
||||||
|
if (!isCorrectType(lower) || !isCorrectType(upper)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
export default dashtime
|
|
@ -0,0 +1,18 @@
|
||||||
|
export const TYPE_ID = 'ID'
|
||||||
|
export const TYPE_URI = 'ID'
|
||||||
|
|
||||||
|
const idNormalizer = (type, id) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'ID': {
|
||||||
|
return +id
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'URI': {
|
||||||
|
// handle decode of URI here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
export default idNormalizer
|
|
@ -12,6 +12,7 @@ import dataExplorerReducers from 'src/data_explorer/reducers'
|
||||||
import adminReducer from 'src/admin/reducers/admin'
|
import adminReducer from 'src/admin/reducers/admin'
|
||||||
import kapacitorReducers from 'src/kapacitor/reducers'
|
import kapacitorReducers from 'src/kapacitor/reducers'
|
||||||
import dashboardUI from 'src/dashboards/reducers/ui'
|
import dashboardUI from 'src/dashboards/reducers/ui'
|
||||||
|
import dashTimeV1 from 'src/dashboards/reducers/dashTimeV1'
|
||||||
import persistStateEnhancer from './persistStateEnhancer'
|
import persistStateEnhancer from './persistStateEnhancer'
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
|
@ -21,6 +22,7 @@ const rootReducer = combineReducers({
|
||||||
...kapacitorReducers,
|
...kapacitorReducers,
|
||||||
admin: adminReducer,
|
admin: adminReducer,
|
||||||
dashboardUI,
|
dashboardUI,
|
||||||
|
dashTimeV1,
|
||||||
routing: routerReducer,
|
routing: routerReducer,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import buildInfluxQLQuery from 'utils/influxql'
|
import {buildQuery} from 'utils/influxql'
|
||||||
|
import {TYPE_QUERY_CONFIG} from 'src/dashboards/constants'
|
||||||
|
|
||||||
const buildQueries = (proxy, queryConfigs, timeRange) => {
|
const buildQueries = (proxy, queryConfigs, timeRange) => {
|
||||||
const statements = queryConfigs.map(query => {
|
const statements = queryConfigs.map(query => {
|
||||||
const text =
|
const text =
|
||||||
query.rawText || buildInfluxQLQuery(query.range || timeRange, query)
|
query.rawText ||
|
||||||
|
buildQuery(TYPE_QUERY_CONFIG, query.range || timeRange, query)
|
||||||
return {text, id: query.id, queryConfig: query}
|
return {text, id: query.id, queryConfig: query}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
DEFAULT_DASHBOARD_GROUP_BY_INTERVAL,
|
DEFAULT_DASHBOARD_GROUP_BY_INTERVAL,
|
||||||
} from 'shared/constants'
|
} from 'shared/constants'
|
||||||
import {NULL_STRING} from 'shared/constants/queryFillOptions'
|
import {NULL_STRING} from 'shared/constants/queryFillOptions'
|
||||||
|
import {TYPE_QUERY_CONFIG, TYPE_IFQL} from 'src/dashboards/constants'
|
||||||
import timeRanges from 'hson!shared/data/timeRanges.hson'
|
import timeRanges from 'hson!shared/data/timeRanges.hson'
|
||||||
|
|
||||||
/* eslint-disable quotes */
|
/* eslint-disable quotes */
|
||||||
|
@ -21,13 +22,9 @@ export const quoteIfTimestamp = ({lower, upper}) => {
|
||||||
}
|
}
|
||||||
/* eslint-enable quotes */
|
/* eslint-enable quotes */
|
||||||
|
|
||||||
export default function buildInfluxQLQuery(
|
export default function buildInfluxQLQuery(timeRange, config) {
|
||||||
timeBounds,
|
|
||||||
config,
|
|
||||||
isKapacitorRule
|
|
||||||
) {
|
|
||||||
const {groupBy, fill = NULL_STRING, tags, areTagsAccepted} = config
|
const {groupBy, fill = NULL_STRING, tags, areTagsAccepted} = config
|
||||||
const {upper, lower} = quoteIfTimestamp(timeBounds)
|
const {upper, lower} = quoteIfTimestamp(timeRange)
|
||||||
|
|
||||||
const select = _buildSelect(config)
|
const select = _buildSelect(config)
|
||||||
if (select === null) {
|
if (select === null) {
|
||||||
|
@ -36,7 +33,7 @@ export default function buildInfluxQLQuery(
|
||||||
|
|
||||||
const condition = _buildWhereClause({lower, upper, tags, areTagsAccepted})
|
const condition = _buildWhereClause({lower, upper, tags, areTagsAccepted})
|
||||||
const dimensions = _buildGroupBy(groupBy)
|
const dimensions = _buildGroupBy(groupBy)
|
||||||
const fillClause = isKapacitorRule || !groupBy.time ? '' : _buildFill(fill)
|
const fillClause = groupBy.time ? _buildFill(fill) : ''
|
||||||
|
|
||||||
return `${select}${condition}${dimensions}${fillClause}`
|
return `${select}${condition}${dimensions}${fillClause}`
|
||||||
}
|
}
|
||||||
|
@ -53,6 +50,21 @@ function _buildSelect({fields, database, retentionPolicy, measurement}) {
|
||||||
return statement
|
return statement
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// type arg will reason about new query types i.e. IFQL, GraphQL, or queryConfig
|
||||||
|
export const buildQuery = (type, timeRange, config) => {
|
||||||
|
switch (type) {
|
||||||
|
case `${TYPE_QUERY_CONFIG}`: {
|
||||||
|
return buildInfluxQLQuery(timeRange, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
case `${TYPE_IFQL}`: {
|
||||||
|
// build query usining IFQL here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildInfluxQLQuery(timeRange, config)
|
||||||
|
}
|
||||||
|
|
||||||
export function buildSelectStatement(config) {
|
export function buildSelectStatement(config) {
|
||||||
return _buildSelect(config)
|
return _buildSelect(config)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue