Refactor dashboard URL queries and temp var handling into thunks

Refactor zoomedTimeRange to be on DashboardPage props & use redux.

This cleans up DashboardPage considerably by adding a dedicated
loadDashboard action creator & reducer, moving zoomedTimeRange
out of component state and into redux state, and moving all of
the URL queries and template variable reconciliation into a single
series of async action creators (thunks) that hydrate a dashboard's
template variables and then reconcile them with URL queries.

This also moves URL query parsing for tempvars and time ranges
back out of the middleware and into this new dashboard thunk series.

This also renames a number of fns and vars for clarity.
pull/3556/head
Jared Scheib 2018-06-01 20:12:02 -07:00
parent 2e655f46fb
commit f7f8b2d93a
6 changed files with 236 additions and 131 deletions

View File

@ -1,9 +1,11 @@
import {bindActionCreators} from 'redux'
import {push} from 'react-router-redux'
import _ from 'lodash'
import queryString from 'query-string'
import {push} from 'react-router-redux'
import {
getDashboards as getDashboardsAJAX,
getDashboard as getDashboardAJAX,
updateDashboard as updateDashboardAJAX,
deleteDashboard as deleteDashboardAJAX,
updateDashboardCell as updateDashboardCellAJAX,
@ -15,7 +17,11 @@ import {
import {notify} from 'shared/actions/notifications'
import {errorThrown} from 'shared/actions/errors'
import {generateURLQueryFromTempVars} from 'src/dashboards/utils/tempVars'
import {
generateURLQueryFromTempVars,
findInvalidTempVarsInURLQuery,
} from 'src/dashboards/utils/tempVars'
import {validTimeRange, validAbsoluteTimeRange} from 'src/dashboards/utils/time'
import {
getNewDashboardCell,
getClonedDashboardCell,
@ -25,11 +31,19 @@ import {
notifyDashboardDeleteFailed,
notifyCellAdded,
notifyCellDeleted,
notifyDashboardNotFound,
notifyInvalidTempVarValueInURLQuery,
notifyInvalidZoomedTimeRangeValueInURLQuery,
notifyInvalidTimeRangeValueInURLQuery,
} from 'shared/copy/notifications'
import {makeQueryForTemplate} from 'src/dashboards/utils/tempVars'
import parsers from 'shared/parsing'
import idNormalizer, {TYPE_ID} from 'src/normalizers/id'
import {defaultTimeRange} from 'src/shared/data/timeRanges'
export const loadDashboards = (dashboards, dashboardID) => ({
type: 'LOAD_DASHBOARDS',
payload: {
@ -38,6 +52,13 @@ export const loadDashboards = (dashboards, dashboardID) => ({
},
})
export const loadDashboard = dashboard => ({
type: 'LOAD_DASHBOARD',
payload: {
dashboard,
},
})
export const setDashTimeV1 = (dashboardID, timeRange) => ({
type: 'SET_DASHBOARD_TIME_V1',
payload: {
@ -58,6 +79,13 @@ export const setTimeRange = timeRange => ({
},
})
export const setZoomedTimeRange = zoomedTimeRange => ({
type: 'SET_DASHBOARD_ZOOMED_TIME_RANGE',
payload: {
zoomedTimeRange,
},
})
export const updateDashboard = dashboard => ({
type: 'UPDATE_DASHBOARD',
payload: {
@ -161,11 +189,11 @@ export const templateVariableSelected = (dashboardID, templateID, values) => ({
},
})
export const templateVariablesSelectedByName = (dashboardID, query) => ({
export const templateVariablesSelectedByName = (dashboardID, queries) => ({
type: 'TEMPLATE_VARIABLES_SELECTED_BY_NAME',
payload: {
dashboardID,
query,
queries,
},
})
@ -211,6 +239,18 @@ export const getDashboardsAsync = () => async dispatch => {
}
}
export const getDashboardAsync = dashboardID => async dispatch => {
try {
const {data: dashboard} = await getDashboardAJAX(dashboardID)
dispatch(loadDashboard(dashboard))
return dashboard
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
return null
}
}
const removeUnselectedTemplateValues = dashboard => {
const templates = dashboard.templates.map(template => {
if (template.type === 'csv') {
@ -330,8 +370,15 @@ export const deleteDashboardCellAsync = (dashboard, cell) => async dispatch => {
}
}
export const hydrateTempVarValues = (source, dashboard) => async dispatch => {
export const hydrateTempVarValuesAsync = (dashboardID, source) => async (
dispatch,
getState
) => {
try {
const dashboard = getState().dashboardUI.dashboards.find(
d => d.id === dashboardID
)
const tempsWithQueries = dashboard.templates.filter(
({query}) => !!query.influxql
)
@ -402,3 +449,131 @@ export const syncURLQueryFromTempVars = (
)
)
}
const syncDashboardTempVarsFromURLQueries = (dashboardID, urlQueries) => (
dispatch,
getState
) => {
const dashboard = getState().dashboardUI.dashboards.find(
d => d.id === dashboardID
)
const urlQueryTempVarsWithInvalidValues = findInvalidTempVarsInURLQuery(
dashboard.templates,
urlQueries
)
urlQueryTempVarsWithInvalidValues.forEach(invalidURLQuery => {
dispatch(notify(notifyInvalidTempVarValueInURLQuery(invalidURLQuery)))
})
dispatch(templateVariablesSelectedByName(dashboardID, urlQueries))
}
const syncDashboardTimeRangeFromURLQueries = (
dashboardID,
urlQueries,
location
) => (dispatch, getState) => {
const dashboard = getState().dashboardUI.dashboards.find(
d => d.id === dashboardID
)
const {
upper = null,
lower = null,
zoomedUpper = null,
zoomedLower = null,
} = urlQueries
let timeRange
const {dashTimeV1} = getState()
const timeRangeFromQueries = {
upper: urlQueries.upper,
lower: urlQueries.lower,
}
const timeRangeOrNull = validTimeRange(timeRangeFromQueries)
if (timeRangeOrNull) {
timeRange = timeRangeOrNull
} else {
const dashboardTimeRange = dashTimeV1.ranges.find(
r => r.dashboardID === idNormalizer(TYPE_ID, dashboardID)
)
timeRange = dashboardTimeRange || defaultTimeRange
if (timeRangeFromQueries.lower || timeRangeFromQueries.upper) {
dispatch(
notify(notifyInvalidTimeRangeValueInURLQuery(timeRangeFromQueries))
)
}
}
dispatch(setDashTimeV1(dashboardID, timeRange))
if (!validAbsoluteTimeRange({lower: zoomedLower, upper: zoomedUpper})) {
if (zoomedLower || zoomedUpper) {
dispatch(notify(notifyInvalidZoomedTimeRangeValueInURLQuery()))
}
}
dispatch(setZoomedTimeRange({zoomedLower, zoomedUpper}))
const urlQueryTimeRanges = {
upper,
lower,
zoomedUpper,
zoomedLower,
}
dispatch(
syncURLQueryFromTempVars(
location,
dashboard.templates,
[],
urlQueryTimeRanges
)
)
}
const syncDashboardFromURLQueries = (dashboardID, location) => dispatch => {
const urlQueries = queryString.parse(window.location.search)
dispatch(syncDashboardTempVarsFromURLQueries(dashboardID, urlQueries))
dispatch(
syncDashboardTimeRangeFromURLQueries(dashboardID, urlQueries, location)
)
}
export const getDashboardWithHydratedAndSyncedTempVarsAsync = (
dashboardID,
source,
router,
location
) => async dispatch => {
const dashboard = await bindActionCreators(getDashboardAsync, dispatch)(
dashboardID,
source,
router
)
if (!dashboard) {
router.push(`/sources/${source.id}/dashboards`)
dispatch(notify(notifyDashboardNotFound(dashboardID)))
return
}
await bindActionCreators(hydrateTempVarValuesAsync, dispatch)(
+dashboardID,
source
)
dispatch(syncDashboardFromURLQueries(+dashboardID, location))
}
export const setZoomedTimeRangeAsync = (
zoomedTimeRange,
location
) => async dispatch => {
dispatch(setZoomedTimeRange(zoomedTimeRange))
dispatch(syncURLQueryFromQueriesObject(location, zoomedTimeRange))
}

View File

@ -8,6 +8,18 @@ export function getDashboards() {
})
}
export const getDashboard = async dashboardID => {
try {
return await AJAX({
method: 'GET',
url: `/chronograf/v1/dashboards/${dashboardID}`,
})
} catch (error) {
console.error(error)
throw error
}
}
export function updateDashboard(dashboard) {
return AJAX({
method: 'PUT',

View File

@ -5,7 +5,6 @@ import {withRouter} from 'react-router'
import {bindActionCreators} from 'redux'
import _ from 'lodash'
import queryString from 'query-string'
import {isUserAuthorized, EDITOR_ROLE} from 'src/auth/Authorized'
@ -30,11 +29,7 @@ import {
} from 'src/dashboards/actions/cellEditorOverlay'
import {showOverlay} from 'src/shared/actions/overlayTechnology'
import {
applyDashboardTempVarOverrides,
stripTempVar,
getInvalidTempVarsInURLQuery,
} from 'src/dashboards/utils/tempVars'
import {stripTempVar} from 'src/dashboards/utils/tempVars'
import {dismissEditingAnnotation} from 'src/shared/actions/annotations'
@ -50,12 +45,7 @@ import {
TEMP_VAR_UPPER_DASHBOARD_TIME,
} from 'shared/constants'
import {FORMAT_INFLUXQL, defaultTimeRange} from 'src/shared/data/timeRanges'
import {validAbsoluteTimeRange} from 'src/dashboards/utils/time'
import {
notifyDashboardNotFound,
notifyInvalidTempVarValueInURLQuery,
notifyInvalidZoomedTimeRangeValueInURLQuery,
} from 'shared/copy/notifications'
import {colorsStringSchema, colorsNumberSchema} from 'shared/schemas'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {OverlayContext} from 'src/shared/components/OverlayTechnology'
@ -65,22 +55,9 @@ class DashboardPage extends Component {
constructor(props) {
super(props)
const urlQueries = queryString.parse(window.location.search)
let {zoomedLower, zoomedUpper} = urlQueries
if (!validAbsoluteTimeRange({lower: zoomedLower, upper: zoomedUpper})) {
if (zoomedLower || zoomedUpper) {
props.notify(notifyInvalidZoomedTimeRangeValueInURLQuery())
}
zoomedLower = null
zoomedUpper = null
}
this.state = {
isEditMode: false,
selectedCell: null,
zoomedLower,
zoomedUpper,
scrollTop: 0,
windowHeight: window.innerHeight,
}
@ -90,23 +67,18 @@ class DashboardPage extends Component {
const {
params: {dashboardID},
dashboardActions: {
getDashboardsAsync,
hydrateTempVarValues,
getDashboardWithHydratedAndSyncedTempVarsAsync,
putDashboardByID,
syncURLQueryFromTempVars,
},
source,
meRole,
isUsingAuth,
router,
notify,
getAnnotationsAsync,
timeRange,
timeRange: {upper, lower},
autoRefresh,
location,
} = this.props
const {zoomedUpper, zoomedLower} = this.state
const annotationRange = millisecondTimeRange(timeRange)
getAnnotationsAsync(source.links.annotations, annotationRange)
@ -118,36 +90,19 @@ class DashboardPage extends Component {
}
window.addEventListener('resize', this.handleWindowResize, true)
const dashboards = await getDashboardsAsync()
const dashboard = dashboards.find(
d => d.id === idNormalizer(TYPE_ID, dashboardID)
await getDashboardWithHydratedAndSyncedTempVarsAsync(
dashboardID,
source,
router,
location
)
if (!dashboard) {
router.push(`/sources/${source.id}/dashboards`)
return notify(notifyDashboardNotFound(dashboardID))
}
const urlQueryTempVarsWithInvalidValues = getInvalidTempVarsInURLQuery(
dashboard.templates
)
urlQueryTempVarsWithInvalidValues.forEach(invalidURLQuery => {
notify(notifyInvalidTempVarValueInURLQuery(invalidURLQuery))
})
syncURLQueryFromTempVars(location, dashboard.templates, [], {
upper,
lower,
zoomedUpper,
zoomedLower,
})
// If using auth and role is Viewer, temp vars will be stale until dashboard
// is refactored so as not to require a write operation (a PUT in this case)
if (!isUsingAuth || isUserAuthorized(meRole, EDITOR_ROLE)) {
// putDashboardByID refreshes & persists influxql generated template variable values.
await putDashboardByID(dashboardID)
await hydrateTempVarValues(source, dashboard)
}
}
@ -297,6 +252,7 @@ class DashboardPage extends Component {
dashboardActions.deleteDashboardCellAsync(dashboard, cell)
}
// TODO: make this a thunk
handleSelectTemplate = templateID => value => {
const {
dashboardActions,
@ -360,12 +316,9 @@ class DashboardPage extends Component {
}
handleZoomedTimeRange = (zoomedLower, zoomedUpper) => {
this.setState({zoomedLower, zoomedUpper})
const {dashboardActions, location} = this.props
dashboardActions.syncURLQueryFromQueriesObject(location, {
zoomedLower,
zoomedUpper,
})
const zoomedTimeRange = {zoomedLower, zoomedUpper}
dashboardActions.setZoomedTimeRangeAsync(zoomedTimeRange, location)
}
setScrollTop = event => {
@ -380,6 +333,8 @@ class DashboardPage extends Component {
sources,
timeRange,
timeRange: {lower, upper},
zoomedTimeRange,
zoomedTimeRange: {lower: zoomedLower, upper: zoomedUpper},
showTemplateControlBar,
dashboard,
dashboards,
@ -400,7 +355,6 @@ class DashboardPage extends Component {
handleClickPresentationButton,
params: {sourceID, dashboardID},
} = this.props
const {zoomedLower, zoomedUpper} = this.state
const low = zoomedLower || lower
const up = zoomedUpper || upper
@ -483,7 +437,7 @@ class DashboardPage extends Component {
isHidden={inPresentationMode}
onAddCell={this.handleAddCell}
onManualRefresh={onManualRefresh}
zoomedTimeRange={{zoomedUpper, zoomedLower}}
zoomedTimeRange={zoomedTimeRange}
onSave={this.handleRenameDashboard}
onCancel={this.handleCancelEditDashboard}
onEditDashboard={this.handleEditDashboard}
@ -590,6 +544,10 @@ DashboardPage.propTypes = {
upper: string,
lower: string,
}),
zoomedTimeRange: shape({
upper: string,
lower: string,
}),
showTemplateControlBar: bool.isRequired,
inPresentationMode: bool.isRequired,
handleClickPresentationButton: func,
@ -622,7 +580,7 @@ const mapStateToProps = (state, {params: {dashboardID}}) => {
ephemeral: {inPresentationMode},
persisted: {autoRefresh, showTemplateControlBar},
},
dashboardUI: {dashboards, cellQueryStatus},
dashboardUI: {dashboards, cellQueryStatus, zoomedTimeRange},
sources,
dashTimeV1,
auth: {me, isUsingAuth},
@ -642,15 +600,10 @@ const mapStateToProps = (state, {params: {dashboardID}}) => {
r => r.dashboardID === idNormalizer(TYPE_ID, dashboardID)
) || defaultTimeRange
let dashboard = dashboards.find(
const dashboard = dashboards.find(
d => d.id === idNormalizer(TYPE_ID, dashboardID)
)
if (dashboard) {
const urlQueries = queryString.parse(window.location.search)
dashboard = applyDashboardTempVarOverrides(dashboard, urlQueries)
}
const selectedCell = cell
return {
@ -658,6 +611,7 @@ const mapStateToProps = (state, {params: {dashboardID}}) => {
meRole,
dashboard,
timeRange,
zoomedTimeRange,
dashboards,
autoRefresh,
isUsingAuth,

View File

@ -9,6 +9,7 @@ const {lower, upper} = timeRanges.find(tr => tr.lower === 'now() - 1h')
const initialState = {
dashboards: [],
timeRange: {lower, upper},
zoomedTimeRange: {lower: null, upper: null},
isEditMode: false,
cellQueryStatus: {queryID: null, status: null},
hoverTime: NULL_HOVER_TIME,
@ -29,12 +30,25 @@ export default function ui(state = initialState, action) {
return {...state, ...newState}
}
case 'LOAD_DASHBOARD': {
const {dashboard} = action.payload
const newDashboards = _.unionBy([dashboard], state.dashboards, 'id')
return {...state, dashboards: newDashboards}
}
case 'SET_DASHBOARD_TIME_RANGE': {
const {timeRange} = action.payload
return {...state, timeRange}
}
case 'SET_DASHBOARD_ZOOMED_TIME_RANGE': {
const {zoomedTimeRange} = action.payload
return {...state, zoomedTimeRange}
}
case 'UPDATE_DASHBOARD': {
const {dashboard} = action.payload
const newState = {
@ -235,12 +249,12 @@ export default function ui(state = initialState, action) {
}
case 'TEMPLATE_VARIABLES_SELECTED_BY_NAME': {
const {dashboardID, query} = action.payload
const {dashboardID, queries} = action.payload
const newDashboards = state.dashboards.map(
oldDashboard =>
oldDashboard.id === dashboardID
? applyDashboardTempVarOverrides(oldDashboard, query)
? applyDashboardTempVarOverrides(oldDashboard, queries)
: oldDashboard
)

View File

@ -1,5 +1,4 @@
import _ from 'lodash'
import queryString from 'query-string'
import {TEMPLATE_VARIABLE_QUERIES} from 'src/dashboards/constants'
@ -124,9 +123,7 @@ export const applyDashboardTempVarOverrides = (
),
})
export const getInvalidTempVarsInURLQuery = tempVars => {
const urlQueries = queryString.parse(window.location.search)
export const findInvalidTempVarsInURLQuery = (tempVars, urlQueries) => {
const urlQueryTempVarsWithInvalidValues = _.reduce(
urlQueries,
(acc, v, k) => {

View File

@ -2,59 +2,12 @@
import queryString from 'query-string'
import {enablePresentationMode} from 'src/shared/actions/app'
import {setDashTimeV1} from 'src/dashboards/actions'
import {notify as notifyAction} from 'shared/actions/notifications'
import {notifyInvalidTimeRangeValueInURLQuery} from 'shared/copy/notifications'
import {defaultTimeRange} from 'src/shared/data/timeRanges'
import idNormalizer, {TYPE_ID} from 'src/normalizers/id'
import {validTimeRange} from 'src/dashboards/utils/time'
export const queryStringConfig = store => {
let prevPath
return dispatch => action => {
dispatch(action)
const urlQueries = queryString.parse(window.location.search)
export const queryStringConfig = () => dispatch => action => {
dispatch(action)
const urlQueries = queryString.parse(window.location.search)
// Presentation Mode
if (urlQueries.present === 'true') {
dispatch(enablePresentationMode())
}
const dashboardRegex = /\/sources\/\d+\/dashboards\/(\d+)/
if (dashboardRegex.test(window.location.pathname)) {
const currentPath = window.location.pathname
const dashboardID = currentPath.match(dashboardRegex)[1]
if (currentPath !== prevPath) {
let timeRange
const {dashTimeV1} = store.getState()
const timeRangeFromQueries = {
upper: urlQueries.upper,
lower: urlQueries.lower,
}
const timeRangeOrNull = validTimeRange(timeRangeFromQueries)
if (timeRangeOrNull) {
timeRange = timeRangeOrNull
} else {
const dashboardTimeRange = dashTimeV1.ranges.find(
r => r.dashboardID === idNormalizer(TYPE_ID, dashboardID)
)
timeRange = dashboardTimeRange || defaultTimeRange
if (timeRangeFromQueries.lower || timeRangeFromQueries.upper) {
dispatch(
notifyAction(
notifyInvalidTimeRangeValueInURLQuery(timeRangeFromQueries)
)
)
}
}
dispatch(setDashTimeV1(+dashboardID, timeRange))
}
prevPath = currentPath
}
if (urlQueries.present === 'true') {
dispatch(enablePresentationMode())
}
}