Support nested template variables

pull/3831/head
Christopher Henn 2018-07-02 16:18:07 -07:00 committed by Chris Henn
parent f5ca544edf
commit 983550cb9e
25 changed files with 494 additions and 731 deletions

View File

@ -46,6 +46,7 @@
"@types/node": "^9.4.6",
"@types/papaparse": "^4.1.34",
"@types/prop-types": "^15.5.2",
"@types/qs": "^6.5.1",
"@types/react": "^16.0.38",
"@types/react-dnd": "^2.0.36",
"@types/react-dnd-html5-backend": "^2.1.9",
@ -143,7 +144,7 @@
"nano-date": "^2.0.1",
"papaparse": "^4.4.0",
"prop-types": "^15.6.1",
"query-string": "^5.0.0",
"qs": "^6.5.2",
"react": "^16.3.1",
"react-addons-shallow-compare": "^15.0.2",
"react-codemirror2": "^4.2.1",

View File

@ -1,10 +1,6 @@
import {bindActionCreators} from 'redux'
import {replace} from 'react-router-redux'
import {replace, RouterAction} from 'react-router-redux'
import _ from 'lodash'
import queryString from 'query-string'
import {proxy} from 'src/utils/queryUrlGenerator'
import {parseMetaQuery} from 'src/tempVars/utils/parsing'
import qs from 'qs'
import {
getDashboards as getDashboardsAJAX,
@ -17,13 +13,15 @@ import {
createDashboard as createDashboardAJAX,
} from 'src/dashboards/apis'
import {getMe} from 'src/shared/apis/auth'
import {hydrateTemplate, isTemplateNested} from 'src/tempVars/apis'
import {notify} from 'src/shared/actions/notifications'
import {errorThrown} from 'src/shared/actions/errors'
import {
generateURLQueryParamsFromTempVars,
findInvalidTempVarsInURLQuery,
applySelections,
templateSelectionsFromQueryParams,
queryParamsFromTemplates,
} from 'src/dashboards/utils/tempVars'
import {validTimeRange, validAbsoluteTimeRange} from 'src/dashboards/utils/time'
import {
@ -38,12 +36,10 @@ import {
notifyDashboardImportFailed,
notifyDashboardImported,
notifyDashboardNotFound,
notifyInvalidTempVarValueInURLQuery,
notifyInvalidZoomedTimeRangeValueInURLQuery,
notifyInvalidTimeRangeValueInURLQuery,
} from 'src/shared/copy/notifications'
import {makeQueryForTemplate} from 'src/dashboards/utils/tempVars'
import {getDeep} from 'src/utils/wrappers'
import idNormalizer, {TYPE_ID} from 'src/normalizers/id'
@ -52,11 +48,7 @@ import {defaultTimeRange} from 'src/shared/data/timeRanges'
// Types
import {Dispatch} from 'redux'
import {InjectedRouter} from 'react-router'
import {Location} from 'history'
import {AxiosResponse} from 'axios'
import {LocationAction} from 'react-router-redux'
import * as AuthReducers from 'src/types/reducers/auth'
import * as DashboardsActions from 'src/types/actions/dashboards'
import * as DashboardsApis from 'src/types/apis/dashboards'
import * as DashboardsModels from 'src/types/dashboards'
@ -216,28 +208,11 @@ export const templateVariableLocalSelected: DashboardsActions.TemplateVariableLo
},
})
export const templateVariablesLocalSelectedByName: DashboardsActions.TemplateVariablesLocalSelectedByNameActionCreator = (
dashboardID: number,
queryParams: TempVarsModels.URLQueryParams
): DashboardsActions.TemplateVariablesLocalSelectedByNameAction => ({
type: 'TEMPLATE_VARIABLES_SELECTED_BY_NAME',
payload: {
dashboardID,
queryParams,
},
})
export const editTemplateVariableValues: DashboardsActions.EditTemplateVariableValuesActionCreator = (
dashboardID: number,
templateID: string,
values
): DashboardsActions.EditTemplateVariableValuesAction => ({
type: 'EDIT_TEMPLATE_VARIABLE_VALUES',
payload: {
dashboardID,
templateID,
values,
},
export const updateTemplates = (
templates: TempVarsModels.Template[]
): DashboardsActions.UpdateTemplatesAction => ({
type: 'UPDATE_TEMPLATES',
payload: {templates},
})
export const setHoverTime: DashboardsActions.SetHoverTimeActionCreator = (
@ -258,6 +233,21 @@ export const setActiveCell: DashboardsActions.SetActiveCellActionCreator = (
},
})
const getDashboard = (
state,
dashboardId: number
): DashboardsModels.Dashboard => {
const dashboard = state.dashboardUI.dashboards.find(
d => d.id === +dashboardId
)
if (!dashboard) {
throw new Error(`Could not find dashboard with id '${dashboardId}'`)
}
return dashboard
}
// Async Action Creators
export const getDashboardsAsync: DashboardsActions.GetDashboardsDispatcher = (): DashboardsActions.GetDashboardsThunk => async (
@ -280,20 +270,6 @@ export const getDashboardsAsync: DashboardsActions.GetDashboardsDispatcher = ():
}
}
export const getDashboardAsync = (dashboardID: number) => async (
dispatch
): Promise<DashboardsModels.Dashboard | null> => {
try {
const {data: dashboard} = await getDashboardAJAX(dashboardID)
dispatch(loadDashboard(dashboard))
return dashboard
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
return null
}
}
export const getChronografVersion = () => async (): Promise<string | void> => {
try {
const results = await getMe()
@ -366,12 +342,7 @@ export const putDashboardByID: DashboardsActions.PutDashboardByIDDispatcher = (
getState: () => DashboardsReducers.Dashboards
): Promise<void> => {
try {
const {
dashboardUI: {dashboards},
} = getState()
const dashboard: DashboardsModels.Dashboard = dashboards.find(
d => d.id === +dashboardID
)
const dashboard = getDashboard(getState(), dashboardID)
const templates = removeUnselectedTemplateValues(dashboard)
await updateDashboardAJAX({...dashboard, templates})
} catch (error) {
@ -531,147 +502,27 @@ export const importDashboardAsync = (
}
}
export const hydrateTempVarValuesAsync = (
dashboardID: number,
source: SourcesModels.Source
) => async (dispatch, getState): Promise<void> => {
try {
const dashboard = getState().dashboardUI.dashboards.find(
d => d.id === dashboardID
)
const templates: TempVarsModels.Template[] = dashboard.templates
const queries = templates
.filter(
template => getDeep<string>(template, 'query.influxql', '') !== ''
)
.map(async template => {
const query = makeQueryForTemplate(template.query)
const response = await proxy({source: source.links.proxy, query})
const values = parseMetaQuery(query, response.data)
return {template, values}
})
const results = await Promise.all(queries)
for (const {template, values} of results) {
dispatch(editTemplateVariableValues(+dashboard.id, template.id, values))
}
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
const removeNullValues = obj => _.pickBy(obj, o => o)
export const syncURLQueryParamsFromQueryParamsObject = (
location: Location,
updatedURLQueryParams: TempVarsModels.URLQueryParams,
deletedURLQueryParams: TempVarsModels.URLQueryParams = {}
): DashboardsActions.SyncURLQueryFromQueryParamsObjectActionCreator => (
dispatch: Dispatch<LocationAction>
const updateTimeRangeFromQueryParams = (dashboardID: number) => (
dispatch,
getState
): void => {
const updatedLocationQuery = removeNullValues({
...location.query,
...updatedURLQueryParams,
const {dashTimeV1} = getState()
const queryParams = qs.parse(window.location.search, {
ignoreQueryPrefix: true,
})
_.each(deletedURLQueryParams, (__, k) => {
delete updatedLocationQuery[k]
})
const updatedSearchString = queryString.stringify(updatedLocationQuery)
const updatedSearch = {search: updatedSearchString}
const updatedLocation = {
...location,
query: updatedLocationQuery,
...updatedSearch,
}
dispatch(replace(updatedLocation))
}
export const syncURLQueryFromTempVars: DashboardsActions.SyncURLQueryFromTempVarsDispatcher = (
location: Location,
tempVars: TempVarsModels.Template[],
deletedTempVars: TempVarsModels.Template[] = [],
urlQueryParamsTimeRanges?: TempVarsModels.URLQueryParams
): DashboardsActions.SyncURLQueryFromQueryParamsObjectActionCreator => (
dispatch: Dispatch<
DashboardsActions.SyncURLQueryFromQueryParamsObjectDispatcher
>
): void => {
const updatedURLQueryParams = generateURLQueryParamsFromTempVars(tempVars)
const deletedURLQueryParams = generateURLQueryParamsFromTempVars(
deletedTempVars
)
let updatedURLQueryParamsWithTimeRange = {
...updatedURLQueryParams,
}
if (urlQueryParamsTimeRanges) {
updatedURLQueryParamsWithTimeRange = {
...updatedURLQueryParamsWithTimeRange,
...urlQueryParamsTimeRanges,
}
}
syncURLQueryParamsFromQueryParamsObject(
location,
updatedURLQueryParamsWithTimeRange,
deletedURLQueryParams
)(dispatch)
}
const syncDashboardTempVarsFromURLQueryParams = (
dashboardID: number,
urlQueryParams: TempVarsModels.URLQueryParams
): DashboardsActions.SyncDashboardTempVarsFromURLQueryParamsDispatcher => (
dispatch: Dispatch<
| NotificationsActions.PublishNotificationActionCreator
| DashboardsActions.TemplateVariableLocalSelectedAction
>,
getState: () => DashboardsReducers.Dashboards & AuthReducers.Auth
): void => {
const {dashboardUI} = getState()
const dashboard = dashboardUI.dashboards.find(d => d.id === dashboardID)
const urlQueryParamsTempVarsWithInvalidValues = findInvalidTempVarsInURLQuery(
dashboard.templates,
urlQueryParams
)
urlQueryParamsTempVarsWithInvalidValues.forEach(invalidURLQuery => {
dispatch(notify(notifyInvalidTempVarValueInURLQuery(invalidURLQuery)))
})
dispatch(templateVariablesLocalSelectedByName(dashboardID, urlQueryParams))
}
const syncDashboardTimeRangeFromURLQueryParams = (
dashboardID: number,
urlQueryParams: TempVarsModels.URLQueryParams,
location: Location
): DashboardsActions.SyncDashboardTimeRangeFromURLQueryParamsDispatcher => (
dispatch: Dispatch<NotificationsActions.PublishNotificationActionCreator>,
getState: () => DashboardsReducers.Dashboards & DashboardsReducers.DashTimeV1
): void => {
const {
dashboardUI: {dashboards},
dashTimeV1,
} = getState()
const dashboard = dashboards.find(d => d.id === dashboardID)
const timeRangeFromQueries = {
lower: urlQueryParams.lower,
upper: urlQueryParams.upper,
lower: queryParams.lower,
upper: queryParams.upper,
}
const zoomedTimeRangeFromQueries = {
lower: urlQueryParams.zoomedLower,
upper: urlQueryParams.zoomedUpper,
lower: queryParams.zoomedLower,
upper: queryParams.zoomedUpper,
}
let validatedTimeRange = validTimeRange(timeRangeFromQueries)
if (!validatedTimeRange.lower) {
const dashboardTimeRange = dashTimeV1.ranges.find(
r => r.dashboardID === idNormalizer(TYPE_ID, dashboardID)
@ -683,100 +534,118 @@ const syncDashboardTimeRangeFromURLQueryParams = (
dispatch(notify(notifyInvalidTimeRangeValueInURLQuery()))
}
}
dispatch(setDashTimeV1(dashboardID, validatedTimeRange))
const validatedZoomedTimeRange = validAbsoluteTimeRange(
zoomedTimeRangeFromQueries
)
if (
!validatedZoomedTimeRange.lower &&
(urlQueryParams.zoomedLower || urlQueryParams.zoomedUpper)
(queryParams.zoomedLower || queryParams.zoomedUpper)
) {
dispatch(notify(notifyInvalidZoomedTimeRangeValueInURLQuery()))
}
dispatch(setZoomedTimeRange(validatedZoomedTimeRange))
const urlQueryParamsTimeRanges = {
const updatedQueryParams = {
lower: validatedTimeRange.lower,
upper: validatedTimeRange.upper,
zoomedLower: validatedZoomedTimeRange.lower,
zoomedUpper: validatedZoomedTimeRange.upper,
}
syncURLQueryFromTempVars(
location,
dashboard.templates,
[],
urlQueryParamsTimeRanges
)(dispatch)
dispatch(updateQueryParams(updatedQueryParams))
}
const syncDashboardFromURLQueryParams = (
dashboardID: number,
location: Location
): DashboardsActions.SyncDashboardFromURLQueryParamsDispatcher => (
dispatch: Dispatch<
| DashboardsActions.SyncDashboardTempVarsFromURLQueryParamsDispatcher
| DashboardsActions.SyncDashboardTimeRangeFromURLQueryParamsDispatcher
>
): void => {
const urlQueryParams = queryString.parse(window.location.search)
bindActionCreators(syncDashboardTempVarsFromURLQueryParams, dispatch)(
dashboardID,
urlQueryParams
)
export const getDashboardWithTemplatesAsync = (
dashboardId: number,
source: SourcesModels.Source
) => async (dispatch: Dispatch<any>): Promise<void> => {
let dashboard: DashboardsModels.Dashboard
bindActionCreators(syncDashboardTimeRangeFromURLQueryParams, dispatch)(
dashboardID,
urlQueryParams,
location
)
}
try {
const resp = await getDashboardAJAX(dashboardId)
dashboard = resp.data
} catch {
dispatch(replace(`/sources/${source.id}/dashboards`))
dispatch(notify(notifyDashboardNotFound(dashboardId)))
export const getDashboardWithHydratedAndSyncedTempVarsAsync: DashboardsActions.GetDashboardWithHydratedAndSyncedTempVarsAsyncDispatcher = (
dashboardID: number,
source: SourcesModels.Source,
router: InjectedRouter,
location: Location
): DashboardsActions.GetDashboardWithHydratedAndSyncedTempVarsAsyncThunk => async (
dispatch: Dispatch<NotificationsActions.PublishNotificationActionCreator>
): Promise<void> => {
const dashboard = await bindActionCreators(getDashboardAsync, dispatch)(
dashboardID
)
if (!dashboard) {
router.push(`/sources/${source.id}/dashboards`)
dispatch(notify(notifyDashboardNotFound(dashboardID)))
return
}
await bindActionCreators(hydrateTempVarValuesAsync, dispatch)(
dashboardID,
source
const templateSelections = templateSelectionsFromQueryParams()
const proxyLink = source.links.proxy
const nonNestedTemplates = await Promise.all(
dashboard.templates
.filter(t => !isTemplateNested(t))
.map(t => hydrateTemplate(proxyLink, t, []))
)
bindActionCreators(syncDashboardFromURLQueryParams, dispatch)(
dashboardID,
location
applySelections(nonNestedTemplates, templateSelections)
const nestedTemplates = await Promise.all(
dashboard.templates
.filter(t => isTemplateNested(t))
.map(t => hydrateTemplate(proxyLink, t, nonNestedTemplates))
)
applySelections(nestedTemplates, templateSelections)
const templates = [...nonNestedTemplates, ...nestedTemplates]
// TODO: Notify if any of the supplied query params were invalid
dispatch(loadDashboard({...dashboard, templates}))
dispatch<any>(updateTemplateQueryParams(dashboardId))
dispatch<any>(updateTimeRangeFromQueryParams(dashboardId))
}
export const setZoomedTimeRangeAsync: DashboardsActions.SetZoomedTimeRangeDispatcher = (
zoomedTimeRange: QueriesModels.TimeRange,
location: Location
): DashboardsActions.SetZoomedTimeRangeThunk => async (
dispatch: Dispatch<
| DashboardsActions.SetZoomedTimeRangeActionCreator
| DashboardsActions.SyncURLQueryFromQueryParamsObjectDispatcher
>
): Promise<void> => {
dispatch(setZoomedTimeRange(zoomedTimeRange))
const urlQueryParamsZoomedTimeRange = {
zoomedLower: zoomedTimeRange.lower,
zoomedUpper: zoomedTimeRange.upper,
export const rehydrateNestedTemplatesAsync = (
dashboardId: number,
source: SourcesModels.Source
) => async (dispatch: Dispatch<any>, getState): Promise<void> => {
const dashboard = getDashboard(getState(), dashboardId)
const proxyLink = source.links.proxy
const templateSelections = templateSelectionsFromQueryParams()
const nestedTemplates = await Promise.all(
dashboard.templates
.filter(t => isTemplateNested(t))
.map(t => hydrateTemplate(proxyLink, t, dashboard.templates))
)
applySelections(nestedTemplates, templateSelections)
dispatch(updateTemplates(nestedTemplates))
dispatch<any>(updateTemplateQueryParams(dashboardId))
}
export const updateTemplateQueryParams = (dashboardId: number) => (
dispatch,
getState
): void => {
const templates = getDashboard(getState(), dashboardId).templates
const updatedQueryParams = {
tempVars: queryParamsFromTemplates(templates),
}
syncURLQueryParamsFromQueryParamsObject(
location,
urlQueryParamsZoomedTimeRange
)(dispatch)
dispatch(updateQueryParams(updatedQueryParams))
}
export const updateQueryParams = (updatedQueryParams: object): RouterAction => {
const {search, pathname} = window.location
const newQueryParams = _.pickBy(
{
...qs.parse(search, {ignoreQueryPrefix: true}),
...updatedQueryParams,
},
v => !!v
)
const newSearch = qs.stringify(newQueryParams)
const newLocation = {pathname, search: `?${newSearch}`}
return replace(newLocation)
}

View File

@ -23,8 +23,8 @@ import * as notifyActions from 'src/shared/actions/notifications'
// Utils
import idNormalizer, {TYPE_ID} from 'src/normalizers/id'
import {millisecondTimeRange} from 'src/dashboards/utils/time'
import {stripTempVar} from 'src/dashboards/utils/tempVars'
import {getDeep} from 'src/utils/wrappers'
import {getDashboards as getDashboardsAJAX} from 'src/dashboards/apis'
// Constants
import {
@ -40,6 +40,9 @@ import {WithRouterProps} from 'react-router'
import {ManualRefreshProps} from 'src/shared/components/ManualRefresh'
import {Location} from 'history'
import {InjectedRouter} from 'react-router'
import {AxiosResponse} from 'axios'
import {RouterAction} from 'react-router-redux'
import {DashboardsResponse} from 'src/types/apis/dashboards'
import * as AnnotationsActions from 'src/types/actions/annotations'
import * as AppActions from 'src/types/actions/app'
import * as CellEditorOverlayActions from 'src/types/actions/cellEditorOverlay'
@ -54,21 +57,17 @@ import * as NotificationsActions from 'src/types/actions/notifications'
interface DashboardActions {
setDashTimeV1: DashboardsActions.SetDashTimeV1ActionCreator
setZoomedTimeRange: DashboardsActions.SetZoomedTimeRangeActionCreator
updateDashboard: DashboardsActions.UpdateDashboardActionCreator
syncURLQueryParamsFromQueryParamsObject: DashboardsActions.SyncURLQueryFromQueryParamsObjectDispatcher
putDashboard: DashboardsActions.PutDashboardDispatcher
putDashboardByID: DashboardsActions.PutDashboardByIDDispatcher
getDashboardsAsync: DashboardsActions.GetDashboardsDispatcher
getDashboardWithHydratedAndSyncedTempVarsAsync: DashboardsActions.GetDashboardWithHydratedAndSyncedTempVarsAsyncDispatcher
setTimeRange: DashboardsActions.SetTimeRangeActionCreator
addDashboardCellAsync: DashboardsActions.AddDashboardCellDispatcher
editCellQueryStatus: DashboardsActions.EditCellQueryStatusActionCreator
updateDashboardCell: DashboardsActions.UpdateDashboardCellDispatcher
cloneDashboardCellAsync: DashboardsActions.CloneDashboardCellDispatcher
deleteDashboardCellAsync: DashboardsActions.DeleteDashboardCellDispatcher
templateVariableLocalSelected: DashboardsActions.TemplateVariableLocalSelectedActionCreator
syncURLQueryFromTempVars: DashboardsActions.SyncURLQueryFromTempVarsDispatcher
setZoomedTimeRangeAsync: DashboardsActions.SetZoomedTimeRangeDispatcher
}
interface Props extends DashboardActions, ManualRefreshProps, WithRouterProps {
@ -108,6 +107,16 @@ interface Props extends DashboardActions, ManualRefreshProps, WithRouterProps {
thresholdsListColors: ColorsModels.ColorNumber[]
gaugeColors: ColorsModels.ColorNumber[]
lineColors: ColorsModels.ColorString[]
getDashboardWithTemplatesAsync: (
dashboardId: number,
source: SourcesModels.Source
) => Promise<void>
rehydrateNestedTemplatesAsync: (
dashboardId: number,
source: SourcesModels.Source
) => Promise<void>
updateTemplateQueryParams: (dashboardId: number) => void
updateQueryParams: (updatedQueryParams: object) => RouterAction
}
interface State {
@ -115,6 +124,7 @@ interface State {
selectedCell: DashboardsModels.Cell | null
scrollTop: number
windowHeight: number
dashboardLinks: DashboardsModels.DashboardSwitcherLink[]
}
@ErrorHandling
@ -129,17 +139,12 @@ class DashboardPage extends Component<Props, State> {
selectedCell: null,
scrollTop: 0,
windowHeight: window.innerHeight,
dashboardLinks: [],
}
}
public async componentDidMount() {
const {
source,
getAnnotationsAsync,
timeRange,
autoRefresh,
getDashboardsAsync,
} = this.props
const {source, getAnnotationsAsync, timeRange, autoRefresh} = this.props
const annotationRange = millisecondTimeRange(timeRange)
getAnnotationsAsync(source.links.annotations, annotationRange)
@ -154,10 +159,7 @@ class DashboardPage extends Component<Props, State> {
await this.getDashboard()
// We populate all dashboards in the redux store so that we can consume
// them in `this.dashboardLinks`. See
// https://github.com/influxdata/chronograf/issues/3594
getDashboardsAsync()
this.getDashboardLinks()
}
public componentWillReceiveProps(nextProps: Props) {
@ -264,7 +266,7 @@ class DashboardPage extends Component<Props, State> {
templatesIncludingDashTime = []
}
const {isEditMode} = this.state
const {isEditMode, dashboardLinks} = this.state
return (
<div className="page dashboard-page">
@ -299,7 +301,7 @@ class DashboardPage extends Component<Props, State> {
onSave={this.handleRenameDashboard}
onCancel={this.handleCancelEditDashboard}
onEditDashboard={this.handleEditDashboard}
dashboardLinks={this.dashboardLinks}
dashboardLinks={dashboardLinks}
activeDashboardLink={this.activeDashboardLink}
activeDashboard={dashboard ? dashboard.name : ''}
showTemplateControlBar={showTemplateControlBar}
@ -346,17 +348,10 @@ class DashboardPage extends Component<Props, State> {
this.setState({windowHeight: window.innerHeight})
}
private getDashboard = async (): Promise<
DashboardsActions.GetDashboardWithHydratedAndSyncedTempVarsAsyncThunk
> => {
const {dashboardID, source, router, location} = this.props
private getDashboard = async () => {
const {dashboardID, source, getDashboardWithTemplatesAsync} = this.props
return await this.props.getDashboardWithHydratedAndSyncedTempVarsAsync(
dashboardID,
source,
router,
location
)
return getDashboardWithTemplatesAsync(dashboardID, source)
}
private inView = (cell: DashboardsModels.Cell): boolean => {
@ -385,18 +380,18 @@ class DashboardPage extends Component<Props, State> {
): void => {
const {
dashboard,
getAnnotationsAsync,
source,
location,
setDashTimeV1,
updateQueryParams,
} = this.props
this.props.setDashTimeV1(dashboard.id, {
setDashTimeV1(dashboard.id, {
...timeRange,
format: FORMAT_INFLUXQL,
})
this.props.syncURLQueryParamsFromQueryParamsObject(location, {
updateQueryParams({
lower: timeRange.lower,
upper: timeRange.upper,
})
@ -450,38 +445,31 @@ class DashboardPage extends Component<Props, State> {
): ((value: TempVarsModels.TemplateValue) => void) => (
value: TempVarsModels.TemplateValue
): void => {
const {dashboard, location} = this.props
const {
dashboard,
source,
templateVariableLocalSelected,
updateTemplateQueryParams,
rehydrateNestedTemplatesAsync,
} = this.props
const currentTempVar = dashboard.templates.find(
tempVar => tempVar.id === templateID
)
const strippedTempVar = stripTempVar(currentTempVar.tempVar)
const updatedQueryParam = {
[strippedTempVar]: value.value,
}
this.props.syncURLQueryParamsFromQueryParamsObject(
location,
updatedQueryParam
)
this.props.templateVariableLocalSelected(dashboard.id, templateID, [value])
templateVariableLocalSelected(dashboard.id, templateID, [value])
updateTemplateQueryParams(dashboard.id)
rehydrateNestedTemplatesAsync(dashboard.id, source)
}
private handleSaveTemplateVariables = async (
templates: TempVarsModels.Template[]
): Promise<void> => {
const {location, dashboard} = this.props
const {dashboard, updateTemplateQueryParams} = this.props
try {
await this.props.putDashboard({
...dashboard,
templates,
})
const deletedTempVars = dashboard.templates.filter(
({tempVar: oldTempVar}) =>
!templates.find(({tempVar: newTempVar}) => oldTempVar === newTempVar)
)
this.props.syncURLQueryFromTempVars(location, templates, deletedTempVars)
updateTemplateQueryParams(dashboard.id)
} catch (error) {
console.error(error)
}
@ -494,8 +482,14 @@ class DashboardPage extends Component<Props, State> {
private handleZoomedTimeRange = (
zoomedTimeRange: QueriesModels.TimeRange
): void => {
const {location} = this.props
this.props.setZoomedTimeRangeAsync(zoomedTimeRange, location)
const {setZoomedTimeRange, updateQueryParams} = this.props
setZoomedTimeRange(zoomedTimeRange)
updateQueryParams({
zoomedLower: zoomedTimeRange.lower,
zoomedUpper: zoomedTimeRange.upper,
})
}
private setScrollTop = (e: MouseEvent<JSX.Element>): void => {
@ -504,16 +498,26 @@ class DashboardPage extends Component<Props, State> {
this.setState({scrollTop: target.scrollTop})
}
private get dashboardLinks(): DashboardsModels.DashboardSwitcherLink[] {
const {dashboards, source} = this.props
private getDashboardLinks = async (): Promise<void> => {
const {source} = this.props
return dashboards.map(d => {
return {
key: String(d.id),
text: d.name,
to: `/sources/${source.id}/dashboards/${d.id}`,
}
})
try {
const resp = (await getDashboardsAJAX()) as AxiosResponse<
DashboardsResponse
>
const dashboards = resp.data.dashboards
const dashboardLinks = dashboards.map(d => {
return {
key: String(d.id),
text: d.name,
to: `/sources/${source.id}/dashboards/${d.id}`,
}
})
this.setState({dashboardLinks})
} catch (error) {
console.error(error)
}
}
private get activeDashboardLink(): DashboardsModels.DashboardSwitcherLink | null {
@ -523,7 +527,7 @@ class DashboardPage extends Component<Props, State> {
return null
}
const {dashboardLinks} = this
const {dashboardLinks} = this.state
return dashboardLinks.find(link => link.key === String(dashboard.id))
}
@ -584,6 +588,11 @@ const mstp = (state, {params: {dashboardID}}) => {
const mdtp = {
...dashboardActions,
getDashboardWithTemplatesAsync:
dashboardActions.getDashboardWithTemplatesAsync,
rehydrateNestedTemplatesAsync: dashboardActions.rehydrateNestedTemplatesAsync,
updateTemplateQueryParams: dashboardActions.updateTemplateQueryParams,
updateQueryParams: dashboardActions.updateQueryParams,
handleChooseAutoRefresh: appActions.setAutoRefresh,
templateControlBarVisibilityToggled:
appActions.templateControlBarVisibilityToggled,

View File

@ -2,8 +2,6 @@ import _ from 'lodash'
import {timeRanges} from 'src/shared/data/timeRanges'
import {NULL_HOVER_TIME} from 'src/shared/constants/tableGraph'
import {applyDashboardTempVarOverrides} from 'src/dashboards/utils/tempVars'
const {lower, upper} = timeRanges.find(tr => tr.lower === 'now() - 1h')
export const initialState = {
@ -16,8 +14,6 @@ export const initialState = {
activeCellID: '',
}
import {TEMPLATE_VARIABLE_TYPES} from 'src/tempVars/constants'
const ui = (state = initialState, action) => {
switch (action.type) {
case 'LOAD_DASHBOARDS': {
@ -175,61 +171,26 @@ const ui = (state = initialState, action) => {
return {...state, dashboards: newDashboards}
}
case 'TEMPLATE_VARIABLES_SELECTED_BY_NAME': {
const {dashboardID, queryParams} = action.payload
const newDashboards = state.dashboards.map(
oldDashboard =>
oldDashboard.id === dashboardID
? applyDashboardTempVarOverrides(oldDashboard, queryParams)
: oldDashboard
)
return {...state, dashboards: newDashboards}
}
case 'EDIT_TEMPLATE_VARIABLE_VALUES': {
const {dashboardID, templateID, values} = action.payload
case 'UPDATE_TEMPLATES': {
const {templates: updatedTemplates} = action.payload
const dashboards = state.dashboards.map(dashboard => {
if (dashboard.id !== dashboardID) {
return dashboard
}
const templates = dashboard.templates.map(template => {
if (template.id !== templateID) {
return template
}
const localSelectedValue = _.get(template, 'values', []).find(
v => v.localSelected
)
const selectedValue = _.get(template, 'values', []).find(
v => v.selected
)
const templates = dashboard.templates.reduce(
(acc, existingTemplate) => {
const updatedTemplate = updatedTemplates.find(
t => t.id === existingTemplate.id
)
const newValues = values.map(value => {
const isLocalSelected =
_.get(localSelectedValue, 'value', null) === value
const isSelected = _.get(selectedValue, 'value', null) === value
const newValue = {
localSelected: localSelectedValue ? isLocalSelected : isSelected,
selected: isSelected,
value,
type: TEMPLATE_VARIABLE_TYPES[template.type],
if (updatedTemplate) {
return [...acc, updatedTemplate]
}
return newValue
})
return [...acc, existingTemplate]
},
[]
)
return {
...template,
values: newValues,
}
})
return {
...dashboard,
templates,
}
return {...dashboard, templates}
})
return {...state, dashboards}

View File

@ -1,14 +1,10 @@
import _ from 'lodash'
import {getDeep} from 'src/utils/wrappers'
import qs from 'qs'
import {
Dashboard,
Template,
TemplateQuery,
TemplateValue,
URLQueryParams,
} from 'src/types'
import {TemplateUpdate} from 'src/types'
import {formatTempVar} from 'src/tempVars/utils'
import {Template, TemplateQuery} from 'src/types'
import {TemplateQPSelections} from 'src/types/dashboards'
export const makeQueryForTemplate = ({
influxql,
@ -21,124 +17,58 @@ export const makeQueryForTemplate = ({
.replace(':measurement:', `"${measurement}"`)
.replace(':tagKey:', `"${tagKey}"`)
export const templateSelectionsFromQueryParams = (): TemplateQPSelections => {
const queryParams = qs.parse(window.location.search, {
ignoreQueryPrefix: true,
})
const tempVars = queryParams.tempVars || {}
return Object.entries(tempVars).reduce(
(acc, [tempVar, v]) => ({...acc, [formatTempVar(tempVar)]: v}),
{}
)
}
export const queryParamsFromTemplates = (templates: Template[]) => {
return templates.reduce((acc, template) => {
const tempVar = stripTempVar(template.tempVar)
const selection = template.values.find(t => t.localSelected)
if (!selection) {
return acc
}
return {
...acc,
[tempVar]: selection.value,
}
}, {})
}
export const applySelections = (
templates: Template[],
selections: TemplateQPSelections
): void => {
for (const {tempVar, values} of templates) {
if (!values.length) {
continue
}
let selection = selections[tempVar]
if (!selection || !values.find(v => v.value === selection)) {
selection = values.find(v => v.selected).value
}
for (const value of values) {
value.localSelected = value.value === selection
}
}
}
export const stripTempVar = (tempVarName: string): string =>
tempVarName.substr(1, tempVarName.length - 2)
export const generateURLQueryParamsFromTempVars = (
tempVars: Template[]
): URLQueryParams => {
const urlQueryParams = {}
tempVars.forEach(({tempVar, values}) => {
const localSelected = values.find(value => value.localSelected === true)
const strippedTempVar = stripTempVar(tempVar)
urlQueryParams[strippedTempVar] = _.get(localSelected, 'value', '')
})
return urlQueryParams
}
const isValidTempVarOverride = (
values: TemplateValue[],
overrideValue: string
): boolean => !!values.find(({value}) => value === overrideValue)
const reconcileTempVarsWithOverrides = (
currentTempVars: Template[],
tempVarOverrides: URLQueryParams
): Template[] => {
if (!tempVarOverrides) {
return currentTempVars
}
const reconciledTempVars = currentTempVars.map(tempVar => {
const {tempVar: name, values} = tempVar
const strippedTempVar = stripTempVar(name)
const overrideValue = tempVarOverrides[strippedTempVar]
if (overrideValue && isValidTempVarOverride(values, overrideValue)) {
const overriddenValues = values.map(tempVarValue => {
const {value} = tempVarValue
if (value === overrideValue) {
return {...tempVarValue, localSelected: true}
}
return {...tempVarValue, localSelected: false}
})
return {...tempVar, values: overriddenValues}
} else {
const valuesWithLocalSelected = values.map(tempVarValue => {
const isSelected = tempVarValue.selected
return {...tempVarValue, localSelected: isSelected}
})
return {...tempVar, values: valuesWithLocalSelected}
}
})
return reconciledTempVars
}
export const applyDashboardTempVarOverrides = (
dashboard: Dashboard,
tempVarOverrides: URLQueryParams
): Dashboard => ({
...dashboard,
templates: reconcileTempVarsWithOverrides(
dashboard.templates,
tempVarOverrides
),
})
export const findUpdatedTempVarsInURLQueryParams = (
tempVars: Template[],
urlQueryParams: URLQueryParams
): TemplateUpdate[] => {
const urlQueryParamsTempVarsWithInvalidValues = _.reduce(
urlQueryParams,
(acc, v, k) => {
const matchedTempVar = tempVars.find(
({tempVar}) => stripTempVar(tempVar) === k
)
if (matchedTempVar) {
const isDifferentTempVarValue = !!matchedTempVar.values.find(
({value, selected}) => selected && value !== v
)
if (isDifferentTempVarValue) {
acc.push({key: k, value: v})
}
}
return acc
},
[]
)
return urlQueryParamsTempVarsWithInvalidValues
}
export const findInvalidTempVarsInURLQuery = (
tempVars: Template[],
urlQueryParams: URLQueryParams
): TemplateUpdate[] => {
const urlQueryParamsTempVarsWithInvalidValues = _.reduce(
urlQueryParams,
(acc, v, k) => {
const matchedTempVar = tempVars.find(
({tempVar}) => stripTempVar(tempVar) === k
)
if (matchedTempVar) {
const isValidTempVarValue = !!matchedTempVar.values.find(
({value}) => value === v
)
if (!isValidTempVarValue) {
acc.push({key: k, value: v})
}
}
return acc
},
[]
)
return urlQueryParamsTempVarsWithInvalidValues
}
const makeSelected = (template: Template, value: string): Template => {
const found = template.values.find(v => v.value === value)

View File

@ -3,7 +3,7 @@ import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import {withRouter, InjectedRouter} from 'react-router'
import {Location} from 'history'
import queryString from 'query-string'
import qs from 'qs'
import _ from 'lodash'
@ -64,7 +64,7 @@ export class DataExplorer extends PureComponent<Props, State> {
public componentDidMount() {
const {source} = this.props
const {query} = queryString.parse(location.search)
const {query} = qs.parse(location.search, {ignoreQueryPrefix: true})
if (query && query.length) {
const qc = this.props.queryConfigs[0]
this.props.queryConfigActions.editRawTextAsync(
@ -80,10 +80,10 @@ export class DataExplorer extends PureComponent<Props, State> {
const {queryConfigs, timeRange} = nextProps
const query = buildRawText(_.get(queryConfigs, ['0'], ''), timeRange)
const qsCurrent = queryString.parse(location.search)
const qsCurrent = qs.parse(location.search, {ignoreQueryPrefix: true})
if (query.length && qsCurrent.query !== query) {
const qsNew = queryString.stringify({query})
const qsNew = qs.stringify({query})
const pathname = stripPrefix(location.pathname)
router.push(`${pathname}?${qsNew}`)
}

View File

@ -1,11 +1,14 @@
// Middleware generally used for actions needing parsed queryStrings
import queryString from 'query-string'
import qs from 'qs'
import {enablePresentationMode} from 'src/shared/actions/app'
export const queryStringConfig = () => dispatch => action => {
dispatch(action)
const urlQueryParams = queryString.parse(window.location.search)
const urlQueryParams = qs.parse(window.location.search, {
ignoreQueryPrefix: true,
})
if (urlQueryParams.present === 'true') {
dispatch(enablePresentationMode())

View File

@ -0,0 +1,62 @@
import {proxy} from 'src/utils/queryUrlGenerator'
import {makeQueryForTemplate} from 'src/dashboards/utils/tempVars'
import {parseMetaQuery} from 'src/tempVars/parsing'
import templateReplace from 'src/tempVars/utils/replace'
import {TEMPLATE_VARIABLE_TYPES} from 'src/tempVars/constants'
import {Template} from 'src/types'
export const hydrateTemplate = async (
proxyLink: string,
template: Template,
templates: Template[]
): Promise<Template> => {
if (!template.query || !template.query.influxql) {
return template
}
const query = templateReplace(makeQueryForTemplate(template.query), templates)
const response = await proxy({source: proxyLink, query})
const values = parseMetaQuery(query, response.data)
const type = TEMPLATE_VARIABLE_TYPES[template.type]
const selectedValue = getSelectedValue(template)
const selectedLocalValue = getLocalSelectedValue(template)
const templateValues = values.map(value => {
return {
type,
value,
selected: value === selectedValue,
localSelected: value === selectedLocalValue,
}
})
if (templateValues.length && !templateValues.find(v => v.selected)) {
// Handle stale selected value
templateValues[0].selected = true
}
return {...template, values: templateValues}
}
export const isTemplateNested = (template: Template): boolean => {
// A _nested template_ is one whose query references other templates
return (
template.query &&
template.query.influxql &&
!!makeQueryForTemplate(template.query).match(/(.*:.+:.*)+/)
)
}
const getSelectedValue = (template: Template): string | false => {
const selected = template.values.find(v => v.selected)
return selected ? selected.value : false
}
const getLocalSelectedValue = (template: Template): string | false => {
const selected = template.values.find(v => v.localSelected)
return selected ? selected.value : false
}

View File

@ -8,9 +8,9 @@ import {ErrorHandling} from 'src/shared/decorators/errors'
import TemplatePreviewList from 'src/tempVars/components/TemplatePreviewList'
import DragAndDrop from 'src/shared/components/DragAndDrop'
import {notifyCSVUploadFailed} from 'src/shared/copy/notifications'
import {trimAndRemoveQuotes} from 'src/tempVars/utils'
import {TemplateBuilderProps, TemplateValueType, TemplateValue} from 'src/types'
import {trimAndRemoveQuotes} from 'src/tempVars/utils/parsing'
interface State {
templateValuesString: string

View File

@ -23,6 +23,7 @@ class FieldKeysTemplateBuilder extends PureComponent<TemplateBuilderProps> {
public render() {
const {
template,
templates,
source,
onUpdateTemplate,
onUpdateDefaultTemplateValue,
@ -34,6 +35,7 @@ class FieldKeysTemplateBuilder extends PureComponent<TemplateBuilderProps> {
templateValueType={TemplateValueType.FieldKey}
fetchKeys={fetchKeys}
template={template}
templates={templates}
source={source}
onUpdateTemplate={onUpdateTemplate}
onUpdateDefaultTemplateValue={onUpdateDefaultTemplateValue}

View File

@ -13,7 +13,7 @@ import {
} from 'src/shared/copy/notifications'
import {TemplateBuilderProps, TemplateValueType} from 'src/types'
import {trimAndRemoveQuotes} from 'src/tempVars/utils/parsing'
import {trimAndRemoveQuotes} from 'src/tempVars/utils'
interface State {
templateValuesString: string

View File

@ -2,16 +2,12 @@ import React, {PureComponent, ChangeEvent} from 'react'
import _ from 'lodash'
import {getDeep} from 'src/utils/wrappers'
import {proxy} from 'src/utils/queryUrlGenerator'
import {ErrorHandling} from 'src/shared/decorators/errors'
import TemplateMetaQueryPreview from 'src/tempVars/components/TemplateMetaQueryPreview'
import {parseMetaQuery, isInvalidMetaQuery} from 'src/tempVars/utils/parsing'
import {hydrateTemplate} from 'src/tempVars/apis'
import {isInvalidMetaQuery} from 'src/tempVars/parsing'
import {
TemplateBuilderProps,
RemoteDataState,
TemplateValueType,
} from 'src/types'
import {TemplateBuilderProps, RemoteDataState} from 'src/types'
const DEBOUNCE_DELAY = 750
@ -115,7 +111,7 @@ class CustomMetaQueryTemplateBuilder extends PureComponent<
}
private executeQuery = async (): Promise<void> => {
const {template, source, onUpdateTemplate} = this.props
const {template, templates, source, onUpdateTemplate} = this.props
const {metaQuery} = this.state
if (this.isInvalidMetaQuery) {
@ -125,34 +121,21 @@ class CustomMetaQueryTemplateBuilder extends PureComponent<
this.setState({metaQueryResultsStatus: RemoteDataState.Loading})
try {
const {data} = await proxy({
source: source.links.proxy,
query: metaQuery,
})
const templateWithQuery = {
...template,
query: {influxql: metaQuery},
}
const metaQueryResults = parseMetaQuery(metaQuery, data)
const nextTemplate = await hydrateTemplate(
source.links.proxy,
templateWithQuery,
templates
)
this.setState({metaQueryResultsStatus: RemoteDataState.Done})
const nextValues = metaQueryResults.map(result => {
return {
type: TemplateValueType.MetaQuery,
value: result,
selected: false,
localSelected: false,
}
})
if (nextValues[0]) {
nextValues[0].selected = true
}
const nextTemplate = {
...template,
values: nextValues,
query: {
influxql: metaQuery,
},
if (nextTemplate.values[0]) {
nextTemplate.values[0].selected = true
}
onUpdateTemplate(nextTemplate)

View File

@ -26,6 +26,7 @@ class TagKeysTemplateBuilder extends PureComponent<TemplateBuilderProps> {
public render() {
const {
template,
templates,
source,
onUpdateTemplate,
onUpdateDefaultTemplateValue,
@ -37,6 +38,7 @@ class TagKeysTemplateBuilder extends PureComponent<TemplateBuilderProps> {
templateValueType={TemplateValueType.TagKey}
fetchKeys={fetchTagKeys}
template={template}
templates={templates}
source={source}
onUpdateTemplate={onUpdateTemplate}
onUpdateDefaultTemplateValue={onUpdateDefaultTemplateValue}

View File

@ -51,6 +51,7 @@ class TemplateControlBar extends Component<Props, State> {
meRole={meRole}
isUsingAuth={isUsingAuth}
template={template}
templates={templates}
source={source}
onPickTemplate={onPickTemplate}
onCreateTemplate={this.handleCreateTemplate}
@ -66,6 +67,7 @@ class TemplateControlBar extends Component<Props, State> {
)}
<OverlayTechnology visible={isAdding}>
<TemplateVariableEditor
templates={templates}
source={source}
onCreate={this.handleCreateTemplate}
onCancel={this.handleCancelAddVariable}

View File

@ -10,6 +10,7 @@ import {Template, Source, TemplateValueType} from 'src/types'
interface Props {
template: Template
templates: Template[]
meRole: string
isUsingAuth: boolean
source: Source
@ -33,7 +34,13 @@ class TemplateControlDropdown extends PureComponent<Props, State> {
}
public render() {
const {template, source, onPickTemplate, onCreateTemplate} = this.props
const {
template,
templates,
source,
onPickTemplate,
onCreateTemplate,
} = this.props
const {isEditing} = this.state
const dropdownItems = template.values.map(value => {
@ -73,6 +80,7 @@ class TemplateControlDropdown extends PureComponent<Props, State> {
<OverlayTechnology visible={isEditing}>
<TemplateVariableEditor
template={template}
templates={templates}
source={source}
onCreate={onCreateTemplate}
onUpdate={this.handleUpdateTemplate}

View File

@ -16,6 +16,7 @@ import ConfirmButton from 'src/shared/components/ConfirmButton'
import {getDeep} from 'src/utils/wrappers'
import {notify as notifyActionCreator} from 'src/shared/actions/notifications'
import {formatTempVar} from 'src/tempVars/utils'
import {
reconcileSelectedAndLocalSelectedValues,
pickSelected,
@ -49,6 +50,7 @@ import {FIVE_SECONDS} from 'src/shared/constants/index'
interface Props {
// We will assume we are creating a new template if none is passed in
template?: Template
templates: Template[]
source: Source
onCancel: () => void
onCreate?: (template: Template) => Promise<any>
@ -75,8 +77,6 @@ const TEMPLATE_BUILDERS = {
[TemplateType.MetaQuery]: MetaQueryTemplateBuilder,
}
const formatName = name => `:${name.replace(/:/g, '').replace(/\s/g, '')}:`
const DEFAULT_TEMPLATE = DEFAULT_TEMPLATES[TemplateType.Databases]
@ErrorHandling
@ -107,7 +107,7 @@ class TemplateVariableEditor extends PureComponent<Props, State> {
}
public render() {
const {source, onCancel, notify} = this.props
const {source, onCancel, notify, templates} = this.props
const {nextTemplate, isNew} = this.state
const TemplateBuilder = this.templateBuilder
@ -158,6 +158,7 @@ class TemplateVariableEditor extends PureComponent<Props, State> {
</div>
<TemplateBuilder
template={nextTemplate}
templates={templates}
source={source}
onUpdateTemplate={this.handleUpdateTemplate}
notify={notify}
@ -274,7 +275,7 @@ class TemplateVariableEditor extends PureComponent<Props, State> {
private formatName = (): void => {
const {nextTemplate} = this.state
let tempVar = formatName(nextTemplate.tempVar)
let tempVar = formatTempVar(nextTemplate.tempVar)
if (tempVar === '::') {
tempVar = ''
@ -290,7 +291,7 @@ class TemplateVariableEditor extends PureComponent<Props, State> {
const {onUpdate, onCreate, notify} = this.props
const {nextTemplate, isNew} = this.state
nextTemplate.tempVar = formatName(nextTemplate.tempVar)
nextTemplate.tempVar = formatTempVar(nextTemplate.tempVar)
this.setState({savingStatus: RemoteDataState.Loading})
@ -335,7 +336,7 @@ class TemplateVariableEditor extends PureComponent<Props, State> {
return (
tempVar !== '' &&
canSaveValues &&
!RESERVED_TEMPLATE_NAMES.includes(formatName(tempVar)) &&
!RESERVED_TEMPLATE_NAMES.includes(formatTempVar(tempVar)) &&
!this.isSaving
)
}

View File

@ -82,9 +82,3 @@ const EXTRACTORS = {
},
'SHOW SERIES': parsed => parsed.series,
}
export const trimAndRemoveQuotes = elt => {
const trimmed = elt.trim()
const dequoted = trimmed.replace(/(^")|("$)/g, '')
return dequoted
}

View File

@ -0,0 +1,9 @@
export const trimAndRemoveQuotes = elt => {
const trimmed = elt.trim()
const dequoted = trimmed.replace(/(^")|("$)/g, '')
return dequoted
}
export const formatTempVar = name =>
`:${name.replace(/:/g, '').replace(/\s/g, '')}:`

View File

@ -1,8 +1,5 @@
import {Dispatch} from 'redux'
import {InjectedRouter} from 'react-router'
import {LocationAction} from 'react-router-redux'
import {Source} from 'src/types'
import {Location} from 'history'
import * as DashboardsModels from 'src/types/dashboards'
import * as DashboardsReducers from 'src/types/reducers/dashboards'
import * as ErrorsActions from 'src/types/actions/errors'
@ -69,18 +66,6 @@ export interface SetTimeRangeAction {
}
}
export type SetZoomedTimeRangeDispatcher = (
zoomedTimeRange: QueriesModels.TimeRange,
location: Location
) => SetZoomedTimeRangeThunk
export type SetZoomedTimeRangeThunk = (
dispatch: Dispatch<
| SetZoomedTimeRangeActionCreator
| SyncURLQueryFromQueryParamsObjectDispatcher
>
) => Promise<void>
export type SetZoomedTimeRangeActionCreator = (
zoomedTimeRange: QueriesModels.TimeRange
) => SetZoomedTimeRangeAction
@ -242,31 +227,10 @@ export interface TemplateVariableLocalSelectedAction {
}
}
export type TemplateVariablesLocalSelectedByNameActionCreator = (
dashboardID: number,
queryParams: TempVarsModels.URLQueryParams
) => TemplateVariablesLocalSelectedByNameAction
export interface TemplateVariablesLocalSelectedByNameAction {
type: 'TEMPLATE_VARIABLES_SELECTED_BY_NAME'
export interface UpdateTemplatesAction {
type: 'UPDATE_TEMPLATES'
payload: {
dashboardID: number
queryParams: TempVarsModels.URLQueryParams
}
}
export type EditTemplateVariableValuesActionCreator = (
dashboardID: number,
templateID: string,
values: any[]
) => EditTemplateVariableValuesAction
export interface EditTemplateVariableValuesAction {
type: 'EDIT_TEMPLATE_VARIABLE_VALUES'
payload: {
dashboardID: number
templateID: string
values: any[]
templates: TempVarsModels.Template[]
}
}
@ -341,50 +305,7 @@ export type UpdateDashboardCellThunk = (
>
) => Promise<void>
export type SyncURLQueryFromQueryParamsObjectDispatcher = (
location: Location,
updatedURLQueryParams: TempVarsModels.URLQueryParams,
deletedURLQueryParams?: TempVarsModels.URLQueryParams
) => SyncURLQueryFromQueryParamsObjectActionCreator
export type SyncURLQueryFromTempVarsDispatcher = (
location: Location,
tempVars: TempVarsModels.Template[],
deletedTempVars: TempVarsModels.Template[],
urlQueryParamsTimeRanges?: TempVarsModels.URLQueryParams
) => SyncURLQueryFromQueryParamsObjectActionCreator
export type SyncURLQueryFromQueryParamsObjectActionCreator = (
dispatch: Dispatch<LocationAction>
) => void
export type SyncDashboardTempVarsFromURLQueryParamsDispatcher = (
dispatch: Dispatch<
| NotificationsActions.PublishNotificationActionCreator
| TemplateVariableLocalSelectedAction
>,
getState: () => DashboardsReducers.Dashboards & DashboardsReducers.Auth
) => void
export type SyncDashboardTimeRangeFromURLQueryParamsDispatcher = (
dispatch: Dispatch<NotificationsActions.PublishNotificationActionCreator>,
getState: () => DashboardsReducers.Dashboards & DashboardsReducers.DashTimeV1
) => void
export type SyncDashboardFromURLQueryParamsDispatcher = (
dispatch: Dispatch<
| SyncDashboardTempVarsFromURLQueryParamsDispatcher
| SyncDashboardTimeRangeFromURLQueryParamsDispatcher
>
) => void
export type GetDashboardWithHydratedAndSyncedTempVarsAsyncDispatcher = (
dashboardID: number,
source: Source,
router: InjectedRouter,
location: Location
) => GetDashboardWithHydratedAndSyncedTempVarsAsyncThunk
export type GetDashboardWithHydratedAndSyncedTempVarsAsyncThunk = (
dispatch: Dispatch<NotificationsActions.PublishNotificationActionCreator>
) => Promise<void>
export type GetDashboardWithTemplates = (
dashboardId: number,
source: Source
) => ((dispatch: Dispatch<any>) => Promise<void>)

View File

@ -134,3 +134,8 @@ export interface DashboardSwitcherLink {
text: string
to: string
}
export interface TemplateQPSelections {
// e.g. {':my-db:': 'telegraf'}
[tempVar: string]: string
}

View File

@ -6,7 +6,6 @@ import {
Template,
TemplateQuery,
TemplateValue,
URLQueryParams,
TemplateType,
TemplateValueType,
TemplateUpdate,
@ -103,7 +102,6 @@ export {
ScriptStatus,
SchemaFilter,
RemoteDataState,
URLQueryParams,
JSONFeedData,
AnnotationInterface,
TemplateType,

View File

@ -59,12 +59,16 @@ export interface TemplateUpdate {
value: string
}
export interface URLQueryParams {
[key: string]: string
export interface TimeRangeQueryParams {
lower?: string
upper?: string
zoomedLower?: string
zoomedUpper?: string
}
export interface TemplateBuilderProps {
template: Template
templates: Template[]
source: Source
onUpdateTemplate: (nextTemplate: Template) => void
onUpdateDefaultTemplateValue: (item: TemplateValue) => void

View File

@ -12,9 +12,8 @@ import {
syncDashboardCell,
deleteDashboardFailed,
templateVariableLocalSelected,
editTemplateVariableValues,
templateVariablesLocalSelectedByName,
setActiveCell,
updateTemplates,
} from 'src/dashboards/actions'
let state
@ -129,34 +128,6 @@ describe('DataExplorer.Reducers.UI', () => {
expect(actual.dashboards[0].templates[0].values[2].localSelected).toBe(true)
})
it('can select template variable values by name', () => {
const dash = _.cloneDeep(d1)
state = {
dashboards: [dash],
}
const localSelected = {region: 'us-west', temperature: '99.1'}
const actual = reducer(
state,
templateVariablesLocalSelectedByName(dash.id, localSelected)
)
expect(actual.dashboards[0].templates[0].values[0].localSelected).toBe(true)
expect(actual.dashboards[0].templates[0].values[1].localSelected).toBe(
false
)
expect(actual.dashboards[0].templates[0].values[2].localSelected).toBe(
false
)
expect(actual.dashboards[0].templates[1].values[0].localSelected).toBe(
false
)
expect(actual.dashboards[0].templates[1].values[1].localSelected).toBe(true)
expect(actual.dashboards[0].templates[1].values[2].localSelected).toBe(
false
)
})
describe('SET_ACTIVE_CELL', () => {
it('can set the active cell', () => {
const activeCellID = '1'
@ -166,56 +137,81 @@ describe('DataExplorer.Reducers.UI', () => {
})
})
describe('EDIT_TEMPLATE_VARIABLE_VALUES', () => {
it('can edit the tempvar values', () => {
const actual = reducer(
{...initialState, dashboards},
editTemplateVariableValues(d1.id, template.id, ['v1', 'v2'])
)
describe('UPDATE_TEMPLATE_VARIABLES', () => {
it('can update template variables', () => {
const thisState = {
...initialState,
dashboards: [
{
...dashboard,
templates: [
{
id: '0',
tempVar: ':foo:',
label: '',
type: TemplateType.CSV,
values: [],
},
{
id: '1',
tempVar: ':bar:',
label: '',
type: TemplateType.CSV,
values: [],
},
{
id: '2',
tempVar: ':baz:',
label: '',
type: TemplateType.CSV,
values: [],
},
],
},
],
}
const expected = [
const newTemplates = [
{
localSelected: false,
selected: false,
value: 'v1',
type: 'tagKey',
id: '0',
tempVar: ':foo:',
label: '',
type: TemplateType.CSV,
values: [
{
type: TemplateValueType.CSV,
value: '',
selected: true,
localSelected: true,
},
],
},
{
localSelected: false,
selected: false,
value: 'v2',
type: 'tagKey',
id: '1',
tempVar: ':bar:',
label: '',
type: TemplateType.CSV,
values: [
{
type: TemplateValueType.CSV,
value: '',
selected: false,
localSelected: false,
},
],
},
]
expect(actual.dashboards[0].templates[0].values).toEqual(expected)
})
const result = reducer(thisState, updateTemplates(newTemplates))
it('can handle an empty template.values', () => {
const ts = [{...template, values: []}]
const ds = [{...d1, templates: ts}]
// Variables present in payload are updated
expect(result.dashboards[0].templates).toContainEqual(newTemplates[0])
expect(result.dashboards[0].templates).toContainEqual(newTemplates[1])
const actual = reducer(
{...initialState, dashboards: ds},
editTemplateVariableValues(d1.id, template.id, ['v1', 'v2'])
// Variables not present in action payload are left untouched
expect(result.dashboards[0].templates).toContainEqual(
thisState.dashboards[0].templates[2]
)
const expected = [
{
localSelected: false,
selected: false,
value: 'v1',
type: 'tagKey',
},
{
localSelected: false,
selected: false,
value: 'v2',
type: 'tagKey',
},
]
expect(actual.dashboards[0].templates[0].values).toEqual(expected)
})
})
})

View File

@ -8,21 +8,24 @@ import {source} from 'test/resources'
import {TemplateType, TemplateValueType} from 'src/types'
const template = {
id: '0',
tempVar: ':my-var:',
label: '',
type: TemplateType.Databases,
values: [
{
value: 'db0',
type: TemplateValueType.Database,
selected: true,
localSelected: true,
},
],
}
const defaultProps = {
template: {
id: '0',
tempVar: ':my-var:',
label: '',
type: TemplateType.Databases,
values: [
{
value: 'db0',
type: TemplateValueType.Database,
selected: true,
localSelected: true,
},
],
},
template,
templates: [template],
meRole: 'EDITOR',
isUsingAuth: true,
source,

View File

@ -87,6 +87,10 @@
version "15.5.2"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.2.tgz#3c6b8dceb2906cc87fe4358e809f9d20c8d59be1"
"@types/qs@^6.5.1":
version "6.5.1"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.5.1.tgz#a38f69c62528d56ba7bd1f91335a8004988d72f7"
"@types/react-dnd-html5-backend@^2.1.9":
version "2.1.9"
resolved "https://registry.yarnpkg.com/@types/react-dnd-html5-backend/-/react-dnd-html5-backend-2.1.9.tgz#dfc9efe2a68bd12407815a2d61ddfd18bb8686fb"
@ -7588,6 +7592,10 @@ qs@6.5.1, qs@~6.5.1:
version "6.5.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
qs@^6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
qs@~6.3.0:
version "6.3.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c"
@ -7599,14 +7607,6 @@ query-string@^4.1.0, query-string@^4.2.2:
object-assign "^4.1.0"
strict-uri-encode "^1.0.0"
query-string@^5.0.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb"
dependencies:
decode-uri-component "^0.2.0"
object-assign "^4.1.0"
strict-uri-encode "^1.0.0"
querystring-es3@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"