Merge branch 'master' into bug/fix-rp-copy

pull/10616/head
Nathan Haugo 2017-10-10 11:07:06 -07:00 committed by GitHub
commit 33b165edce
34 changed files with 634 additions and 278 deletions

View File

@ -1,7 +1,10 @@
## v1.3.10.0 [unreleased] ## v1.3.10.0 [unreleased]
### Bug Fixes ### Bug Fixes
1. [#2095](https://github.com/influxdata/chronograf/pull/2095): Improve the copy in the retention policy edit page 1. [#2095](https://github.com/influxdata/chronograf/pull/2095): Improve the copy in the retention policy edit page
### Features ### Features
1.[#2083](https://github.com/influxdata/chronograf/pull/2083): Every dashboard can now have its own time range
### UI Improvements ### UI Improvements
## v1.3.9.0 [2017-10-06] ## v1.3.9.0 [2017-10-06]
@ -11,6 +14,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
@ -33,6 +42,8 @@
1. [#2057](https://github.com/influxdata/chronograf/pull/2057): Improve appearance of placeholder text in inputs 1. [#2057](https://github.com/influxdata/chronograf/pull/2057): Improve appearance of placeholder text in inputs
1. [#2057](https://github.com/influxdata/chronograf/pull/2057): Add ability to use "Default" values in Source Connection form 1. [#2057](https://github.com/influxdata/chronograf/pull/2057): Add ability to use "Default" values in Source Connection form
1. [#2069](https://github.com/influxdata/chronograf/pull/2069): Display name & port in SourceIndicator tooltip 1. [#2069](https://github.com/influxdata/chronograf/pull/2069): Display name & port in SourceIndicator tooltip
1. [#2078](https://github.com/influxdata/chronograf/pull/2078): Improve UX/UI of Kapacitor Rule Builder to be more intuitive
1. [#2078](https://github.com/influxdata/chronograf/pull/2078): Rename "Measurements" to "Measurements & Tags" in Query Builder
## v1.3.8.1 [unreleased] ## v1.3.8.1 [unreleased]
### Bug Fixes ### Bug Fixes

View File

@ -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)
})
})
})

View File

@ -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)
})
})
}) })

View File

@ -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)
})
})

View File

@ -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,
}, },
}) })

View File

@ -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,

View File

@ -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,

View File

@ -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'

View File

@ -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,

View File

@ -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

View File

@ -99,7 +99,7 @@ const TagListItem = React.createClass({
}, },
render() { render() {
const {tagKey, tagValues} = this.props const {tagKey, tagValues, isUsingGroupBy} = this.props
const {isOpen} = this.state const {isOpen} = this.state
const tagItemLabel = `${tagKey}${tagValues.length}` const tagItemLabel = `${tagKey}${tagValues.length}`
@ -115,8 +115,9 @@ const TagListItem = React.createClass({
{tagItemLabel} {tagItemLabel}
</span> </span>
<div <div
className={classnames('btn btn-default btn-xs group-by-tag', { className={classnames('btn btn-xs group-by-tag', {
active: this.props.isUsingGroupBy, 'btn-primary': isUsingGroupBy,
'btn-default': !isUsingGroupBy,
})} })}
onClick={this.handleGroupBy} onClick={this.handleGroupBy}
> >

View File

@ -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]
} }

View File

@ -67,7 +67,7 @@ const DataSection = ({
return ( return (
<div className="rule-section"> <div className="rule-section">
<div className="query-builder rule-section--border-bottom"> <div className="query-builder">
<DatabaseList query={query} onChooseNamespace={handleChooseNamespace} /> <DatabaseList query={query} onChooseNamespace={handleChooseNamespace} />
<MeasurementList <MeasurementList
query={query} query={query}

View File

@ -7,7 +7,7 @@ const periods = PERIODS.map(text => {
}) })
const Deadman = ({rule, onChange}) => const Deadman = ({rule, onChange}) =>
<div className="rule-section--row"> <div className="rule-section--row rule-section--row-first rule-section--row-last">
<p>Send Alert if Data is missing for</p> <p>Send Alert if Data is missing for</p>
<Dropdown <Dropdown
className="dropdown-80" className="dropdown-80"

View File

@ -1,5 +1,6 @@
import React, {PropTypes, Component} from 'react' import React, {PropTypes, Component} from 'react'
import NameSection from 'src/kapacitor/components/NameSection'
import ValuesSection from 'src/kapacitor/components/ValuesSection' import ValuesSection from 'src/kapacitor/components/ValuesSection'
import RuleHeader from 'src/kapacitor/components/RuleHeader' import RuleHeader from 'src/kapacitor/components/RuleHeader'
import RuleMessage from 'src/kapacitor/components/RuleMessage' import RuleMessage from 'src/kapacitor/components/RuleMessage'
@ -51,19 +52,23 @@ class KapacitorRule extends Component {
} }
handleEdit = () => { handleEdit = () => {
const {addFlashMessage, queryConfigs, rule} = this.props const {addFlashMessage, queryConfigs, rule, router, source} = this.props
const updatedRule = Object.assign({}, rule, { const updatedRule = Object.assign({}, rule, {
query: queryConfigs[rule.queryID], query: queryConfigs[rule.queryID],
}) })
editRule(updatedRule) editRule(updatedRule)
.then(() => { .then(() => {
addFlashMessage({type: 'success', text: 'Rule successfully updated!'}) router.push(`/sources/${source.id}/alert-rules`)
addFlashMessage({
type: 'success',
text: `${rule.name} successfully saved!`,
})
}) })
.catch(() => { .catch(() => {
addFlashMessage({ addFlashMessage({
type: 'error', type: 'error',
text: 'There was a problem updating the rule', text: `There was a problem saving ${rule.name}`,
}) })
}) })
} }
@ -85,11 +90,11 @@ class KapacitorRule extends Component {
} }
if (!buildInfluxQLQuery({}, query)) { if (!buildInfluxQLQuery({}, query)) {
return 'Please select a database, measurement, and field' return 'Please select a Database, Measurement, and Field'
} }
if (!rule.values.value) { if (!rule.values.value) {
return 'Please enter a value in the Rule Conditions section' return 'Please enter a value in the Conditions section'
} }
return '' return ''
@ -98,7 +103,7 @@ class KapacitorRule extends Component {
deadmanValidation = () => { deadmanValidation = () => {
const {query} = this.props const {query} = this.props
if (query && (!query.database || !query.measurement)) { if (query && (!query.database || !query.measurement)) {
return 'Deadman requires a database and measurement' return 'Deadman rules require a Database and Measurement'
} }
return '' return ''
@ -144,19 +149,21 @@ class KapacitorRule extends Component {
return ( return (
<div className="page"> <div className="page">
<RuleHeader <RuleHeader
rule={rule}
actions={ruleActions}
onSave={isEditing ? this.handleEdit : this.handleCreate}
onChooseTimeRange={this.handleChooseTimeRange}
validationError={this.validationError()}
timeRange={timeRange}
source={source} source={source}
onSave={isEditing ? this.handleEdit : this.handleCreate}
validationError={this.validationError()}
/> />
<FancyScrollbar className="page-contents fancy-scroll--kapacitor"> <FancyScrollbar className="page-contents fancy-scroll--kapacitor">
<div className="container-fluid"> <div className="container-fluid">
<div className="row"> <div className="row">
<div className="col-xs-12"> <div className="col-xs-12">
<div className="rule-builder"> <div className="rule-builder">
<NameSection
isEditing={isEditing}
defaultName={rule.name}
onRuleRename={ruleActions.updateRuleName}
ruleID={rule.id}
/>
<ValuesSection <ValuesSection
rule={rule} rule={rule}
source={source} source={source}
@ -170,6 +177,7 @@ class KapacitorRule extends Component {
onDeadmanChange={this.handleDeadmanChange} onDeadmanChange={this.handleDeadmanChange}
onRuleTypeInputChange={this.handleRuleTypeInputChange} onRuleTypeInputChange={this.handleRuleTypeInputChange}
onRuleTypeDropdownChange={this.handleRuleTypeDropdownChange} onRuleTypeDropdownChange={this.handleRuleTypeDropdownChange}
onChooseTimeRange={this.handleChooseTimeRange}
/> />
<RuleMessage <RuleMessage
rule={rule} rule={rule}

View File

@ -0,0 +1,65 @@
import React, {Component, PropTypes} from 'react'
class NameSection extends Component {
constructor(props) {
super(props)
this.state = {
reset: false,
}
}
handleInputBlur = reset => e => {
const {defaultName, onRuleRename, ruleID} = this.props
onRuleRename(ruleID, reset ? defaultName : e.target.value)
this.setState({reset: false})
}
handleKeyDown = e => {
if (e.key === 'Enter') {
this.inputRef.blur()
}
if (e.key === 'Escape') {
this.inputRef.value = this.props.defaultName
this.setState({reset: true}, () => this.inputRef.blur())
}
}
render() {
const {isEditing, defaultName} = this.props
const {reset} = this.state
return (
<div className="rule-section">
<h3 className="rule-section--heading">
{isEditing ? 'Name' : 'Name this Alert Rule'}
</h3>
<div className="rule-section--body">
<div className="rule-section--row rule-section--row-first rule-section--row-last">
<input
type="text"
className="form-control input-md form-malachite"
defaultValue={defaultName}
onBlur={this.handleInputBlur(reset)}
onKeyDown={this.handleKeyDown}
placeholder="ex: Ruley McRuleface"
ref={r => (this.inputRef = r)}
/>
</div>
</div>
</div>
)
}
}
const {bool, func, string} = PropTypes
NameSection.propTypes = {
isEditing: bool,
defaultName: string.isRequired,
onRuleRename: func.isRequired,
ruleID: string.isRequired,
}
export default NameSection

View File

@ -12,7 +12,7 @@ const Relative = ({
onDropdownChange, onDropdownChange,
rule: {values: {change, shift, operator, value}}, rule: {values: {change, shift, operator, value}},
}) => }) =>
<div className="rule-section--row rule-section--border-bottom"> <div className="rule-section--row rule-section--row-first rule-section--border-bottom">
<p>Send Alert when</p> <p>Send Alert when</p>
<Dropdown <Dropdown
className="dropdown-110" className="dropdown-110"

View File

@ -2,30 +2,34 @@ import React, {PropTypes} from 'react'
import buildInfluxQLQuery from 'utils/influxql' import buildInfluxQLQuery from 'utils/influxql'
import AutoRefresh from 'shared/components/AutoRefresh' import AutoRefresh from 'shared/components/AutoRefresh'
import LineGraph from 'shared/components/LineGraph' import LineGraph from 'shared/components/LineGraph'
import TimeRangeDropdown from 'shared/components/TimeRangeDropdown'
const RefreshingLineGraph = AutoRefresh(LineGraph) const RefreshingLineGraph = AutoRefresh(LineGraph)
const {shape, string, func} = PropTypes
export const RuleGraph = React.createClass({ export const RuleGraph = React.createClass({
propTypes: { propTypes: {
source: PropTypes.shape({ source: shape({
links: PropTypes.shape({ links: shape({
proxy: PropTypes.string.isRequired, proxy: string.isRequired,
}).isRequired, }).isRequired,
}).isRequired, }).isRequired,
query: PropTypes.shape({}).isRequired, query: shape({}).isRequired,
rule: PropTypes.shape({}).isRequired, rule: shape({}).isRequired,
timeRange: PropTypes.shape({}).isRequired, timeRange: shape({}).isRequired,
onChooseTimeRange: func.isRequired,
}, },
render() { render() {
return ( const {
<div className="rule-builder--graph"> query,
{this.renderGraph()} source,
</div> timeRange: {lower},
) timeRange,
}, rule,
onChooseTimeRange,
renderGraph() { } = this.props
const {query, source, timeRange: {lower}, rule} = this.props
const autoRefreshMs = 30000 const autoRefreshMs = 30000
const queryText = buildInfluxQLQuery({lower}, query) const queryText = buildInfluxQLQuery({lower}, query)
const queries = [{host: source.links.proxy, text: queryText}] const queries = [{host: source.links.proxy, text: queryText}]
@ -42,14 +46,24 @@ export const RuleGraph = React.createClass({
} }
return ( return (
<RefreshingLineGraph <div className="rule-builder--graph">
queries={queries} <div className="rule-builder--graph-options">
autoRefresh={autoRefreshMs} <p>Preview Data from</p>
underlayCallback={this.createUnderlayCallback()} <TimeRangeDropdown
isGraphFilled={false} onChooseTimeRange={onChooseTimeRange}
overrideLineColors={kapacitorLineColors} selected={timeRange}
ruleValues={rule.values} preventCustomTimeRange={true}
/> />
</div>
<RefreshingLineGraph
queries={queries}
autoRefresh={autoRefreshMs}
underlayCallback={this.createUnderlayCallback()}
isGraphFilled={false}
overrideLineColors={kapacitorLineColors}
ruleValues={rule.values}
/>
</div>
) )
}, },

View File

@ -1,66 +1,24 @@
import React, {PropTypes, Component} from 'react' import React, {PropTypes, Component} from 'react'
import RuleHeaderEdit from 'src/kapacitor/components/RuleHeaderEdit'
import RuleHeaderSave from 'src/kapacitor/components/RuleHeaderSave' import RuleHeaderSave from 'src/kapacitor/components/RuleHeaderSave'
class RuleHeader extends Component { class RuleHeader extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = {
isEditingName: false,
}
}
toggleEditName = () => {
this.setState({isEditingName: !this.state.isEditingName})
}
handleEditName = rule => e => {
if (e.key === 'Enter') {
const {updateRuleName} = this.props.actions
updateRuleName(rule.id, e.target.value)
this.toggleEditName()
}
if (e.key === 'Escape') {
this.toggleEditName()
}
}
handleEditNameBlur = rule => e => {
const {updateRuleName} = this.props.actions
updateRuleName(rule.id, e.target.value)
this.toggleEditName()
} }
render() { render() {
const { const {source, onSave, validationError} = this.props
rule,
source,
onSave,
timeRange,
validationError,
onChooseTimeRange,
} = this.props
const {isEditingName} = this.state
return ( return (
<div className="page-header"> <div className="page-header">
<div className="page-header__container"> <div className="page-header__container">
<RuleHeaderEdit <div className="page-header__left">
rule={rule} <h1 className="page-header__title">Alert Rule Builder</h1>
isEditing={isEditingName} </div>
onToggleEdit={this.toggleEditName}
onEditName={this.handleEditName}
onEditNameBlur={this.handleEditNameBlur}
/>
<RuleHeaderSave <RuleHeaderSave
source={source} source={source}
onSave={onSave} onSave={onSave}
timeRange={timeRange}
validationError={validationError} validationError={validationError}
onChooseTimeRange={onChooseTimeRange}
/> />
</div> </div>
</div> </div>
@ -73,13 +31,7 @@ const {func, shape, string} = PropTypes
RuleHeader.propTypes = { RuleHeader.propTypes = {
source: shape({}).isRequired, source: shape({}).isRequired,
onSave: func.isRequired, onSave: func.isRequired,
rule: shape({}).isRequired,
actions: shape({
updateRuleName: func.isRequired,
}).isRequired,
validationError: string.isRequired, validationError: string.isRequired,
onChooseTimeRange: func.isRequired,
timeRange: shape({}).isRequired,
} }
export default RuleHeader export default RuleHeader

View File

@ -1,51 +0,0 @@
import React, {PropTypes} from 'react'
import ReactTooltip from 'react-tooltip'
const RuleHeaderEdit = ({
rule,
isEditing,
onToggleEdit,
onEditName,
onEditNameBlur,
}) =>
isEditing
? <input
className="page-header--editing kapacitor-theme"
autoFocus={true}
defaultValue={rule.name}
onKeyDown={onEditName(rule)}
onBlur={onEditNameBlur(rule)}
placeholder="Name your rule"
spellCheck={false}
autoComplete={false}
/>
: <div className="page-header__left">
<h1
className="page-header__title page-header--editable kapacitor-theme"
onClick={onToggleEdit}
data-for="rename-kapacitor-tooltip"
data-tip="<p>Click to Rename</p>"
>
{rule.name}
<span className="icon pencil" />
<ReactTooltip
id="rename-kapacitor-tooltip"
effect="solid"
html={true}
place="bottom"
class="influx-tooltip kapacitor-tooltip"
/>
</h1>
</div>
const {bool, func, shape} = PropTypes
RuleHeaderEdit.propTypes = {
rule: shape(),
isEditing: bool.isRequired,
onToggleEdit: func.isRequired,
onEditName: func.isRequired,
onEditNameBlur: func.isRequired,
}
export default RuleHeaderEdit

View File

@ -1,21 +1,10 @@
import React, {PropTypes} from 'react' import React, {PropTypes} from 'react'
import ReactTooltip from 'react-tooltip' import ReactTooltip from 'react-tooltip'
import TimeRangeDropdown from 'shared/components/TimeRangeDropdown'
import SourceIndicator from 'shared/components/SourceIndicator' import SourceIndicator from 'shared/components/SourceIndicator'
const RuleHeaderSave = ({ const RuleHeaderSave = ({onSave, validationError}) =>
onSave,
timeRange,
validationError,
onChooseTimeRange,
}) =>
<div className="page-header__right"> <div className="page-header__right">
<SourceIndicator /> <SourceIndicator />
<TimeRangeDropdown
onChooseTimeRange={onChooseTimeRange}
selected={timeRange}
preventCustomTimeRange={true}
/>
{validationError {validationError
? <button ? <button
className="btn btn-success btn-sm disabled" className="btn btn-success btn-sm disabled"
@ -36,13 +25,11 @@ const RuleHeaderSave = ({
/> />
</div> </div>
const {func, shape, string} = PropTypes const {func, string} = PropTypes
RuleHeaderSave.propTypes = { RuleHeaderSave.propTypes = {
onSave: func.isRequired, onSave: func.isRequired,
validationError: string.isRequired, validationError: string.isRequired,
onChooseTimeRange: func.isRequired,
timeRange: shape({}).isRequired,
} }
export default RuleHeaderSave export default RuleHeaderSave

View File

@ -1,13 +1,15 @@
import React, {PropTypes} from 'react' import React, {PropTypes} from 'react'
const RuleMessageText = ({rule, updateMessage}) => const RuleMessageText = ({rule, updateMessage}) =>
<textarea <div className="rule-builder--message">
className="form-control form-malachite monotype rule-builder--message" <textarea
onChange={updateMessage} className="form-control form-malachite monotype"
placeholder="Example: {{ .ID }} is {{ .Level }} value: {{ index .Fields &quot;value&quot; }}" onChange={updateMessage}
value={rule.message} placeholder="Example: {{ .ID }} is {{ .Level }} value: {{ index .Fields &quot;value&quot; }}"
spellCheck={false} value={rule.message}
/> spellCheck={false}
/>
</div>
const {func, shape} = PropTypes const {func, shape} = PropTypes

View File

@ -11,7 +11,7 @@ const Threshold = ({
onDropdownChange, onDropdownChange,
onRuleTypeInputChange, onRuleTypeInputChange,
}) => }) =>
<div className="rule-section--row rule-section--border-bottom"> <div className="rule-section--row rule-section--row-first rule-section--border-bottom">
<p>Send Alert where</p> <p>Send Alert where</p>
<span className="rule-builder--metric"> <span className="rule-builder--metric">
{query.fields.length ? query.fields[0].field : 'Select a Time-Series'} {query.fields.length ? query.fields[0].field : 'Select a Time-Series'}

View File

@ -28,11 +28,12 @@ const ValuesSection = ({
timeRange, timeRange,
onAddEvery, onAddEvery,
onRemoveEvery, onRemoveEvery,
onChooseTrigger,
onDeadmanChange, onDeadmanChange,
onChooseTimeRange,
queryConfigActions, queryConfigActions,
onRuleTypeInputChange, onRuleTypeInputChange,
onRuleTypeDropdownChange, onRuleTypeDropdownChange,
onChooseTrigger,
}) => }) =>
<div className="rule-section"> <div className="rule-section">
<h3 className="rule-section--heading">Alert Type</h3> <h3 className="rule-section--heading">Alert Type</h3>
@ -49,7 +50,7 @@ const ValuesSection = ({
)} )}
</TabList> </TabList>
<div> <div>
<h3 className="rule-builder--sub-header">Time Series</h3> <h3 className="rule-section--sub-heading">Time Series</h3>
<DataSection <DataSection
query={query} query={query}
timeRange={timeRange} timeRange={timeRange}
@ -60,7 +61,7 @@ const ValuesSection = ({
isDeadman={isDeadman(rule)} isDeadman={isDeadman(rule)}
/> />
</div> </div>
<h3 className="rule-builder--sub-header">Rule Conditions</h3> <h3 className="rule-section--sub-heading">Conditions</h3>
<TabPanels> <TabPanels>
<TabPanel> <TabPanel>
<Threshold <Threshold
@ -88,6 +89,7 @@ const ValuesSection = ({
query={query} query={query}
source={source} source={source}
timeRange={timeRange} timeRange={timeRange}
onChooseTimeRange={onChooseTimeRange}
/>} />}
</Tabs> </Tabs>
</div> </div>
@ -110,6 +112,7 @@ ValuesSection.propTypes = {
timeRange: shape({}).isRequired, timeRange: shape({}).isRequired,
queryConfigActions: shape({}).isRequired, queryConfigActions: shape({}).isRequired,
source: shape({}).isRequired, source: shape({}).isRequired,
onChooseTimeRange: func.isRequired,
} }
export default ValuesSection export default ValuesSection

View File

@ -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) {

View File

@ -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

18
ui/src/normalizers/id.js Normal file
View File

@ -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

View File

@ -100,7 +100,7 @@ const MeasurementList = React.createClass({
return ( return (
<div className="query-builder--column"> <div className="query-builder--column">
<div className="query-builder--heading"> <div className="query-builder--heading">
<span>Measurements</span> <span>Measurements & Tags</span>
{this.props.query.database {this.props.query.database
? <div className="query-builder--filter"> ? <div className="query-builder--filter">
<input <input

View File

@ -58,7 +58,7 @@ export const TabList = React.createClass({
if (this.props.isKapacitorTabs === 'true') { if (this.props.isKapacitorTabs === 'true') {
return ( return (
<div className="rule-section--row rule-section--row-first rule-section--border-bottom"> <div className="rule-section--row rule-section--row-first rule-section--row-last">
<p>Choose One:</p> <p>Choose One:</p>
<div className="nav nav-tablist nav-tablist-sm nav-tablist-malachite"> <div className="nav nav-tablist nav-tablist-sm nav-tablist-malachite">
{children} {children}

View File

@ -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,
}) })

View File

@ -198,7 +198,8 @@
} }
} }
.query-builder--list-item:hover .group-by-tag, .query-builder--list-item:hover .group-by-tag,
.query-builder--list-item.active .group-by-tag { .query-builder--list-item.active .group-by-tag,
.query-builder--list-item .group-by-tag.btn-primary {
visibility: visible; visibility: visible;
} }
.query-builder--db-dropdown { .query-builder--db-dropdown {

View File

@ -82,6 +82,27 @@ $rule-builder--radius-lg: 5px;
left: ($rule-builder--dot / 2); left: ($rule-builder--dot / 2);
} }
} }
.rule-section--sub-heading {
margin: 0;
padding: $rule-builder--section-gap 0 $rule-builder--padding-md 0;
font-size: $page-header-size;
font-weight: $page-header-weight;
color: $g12-forge;
position: relative;
// Dot
&:after {
content: '';
position: absolute;
width: $rule-builder--dot;
height: $rule-builder--dot;
background-color: $rule-builder--accent-color;
border: 6px solid $rule-builder--accent-line-color;
border-radius: 50%;
top: ($rule-builder--section-gap + 3px);
left: -$rule-builder--left-gutter;
}
}
// Override appearance of lines and dots for first section // Override appearance of lines and dots for first section
.rule-section:first-of-type { .rule-section:first-of-type {
.rule-section--heading { .rule-section--heading {
@ -165,10 +186,19 @@ $rule-builder--radius-lg: 5px;
margin-right: 0; margin-right: 0;
} }
} }
.query-builder--column:nth-child(4) .query-builder--list, .query-builder--column:first-of-type .query-builder--list {
.query-builder--column:nth-child(4) .query-builder--list-empty { border-bottom-left-radius: $rule-builder--radius-lg;
}
.query-builder--column:first-of-type .query-builder--heading {
border-top-left-radius: $rule-builder--radius-lg;
}
.query-builder--column:last-of-type .query-builder--list,
.query-builder--column:last-of-type .query-builder--list-empty {
border-bottom-right-radius: $rule-builder--radius-lg; border-bottom-right-radius: $rule-builder--radius-lg;
} }
.query-builder--column:last-of-type .query-builder--heading {
border-top-right-radius: $rule-builder--radius-lg;
}
.query-builder--heading { .query-builder--heading {
background-color: $rule-builder--section-bg; background-color: $rule-builder--section-bg;
margin-bottom: 2px; margin-bottom: 2px;
@ -201,16 +231,9 @@ $rule-builder--radius-lg: 5px;
} }
/* /*
Sectiom 2 - Rule Conditions Section 2 - Rule Conditions
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
*/ */
.rule-builder--sub-header {
margin: 0;
padding: 30px 0 13px 0;
font-size: 19px;
font-weight: 400 !important;
color: #a4a8b6;
}
.rule-builder--metric { .rule-builder--metric {
height: 30px; height: 30px;
line-height: 30px; line-height: 30px;
@ -222,43 +245,31 @@ $rule-builder--radius-lg: 5px;
padding: 0 9px; padding: 0 9px;
@include no-user-select(); @include no-user-select();
} }
.rule-builder--graph { .rule-builder--graph,
.rule-builder--graph-empty {
background-color: $rule-builder--section-bg; background-color: $rule-builder--section-bg;
border-radius: 0 0 $rule-builder--radius-lg $rule-builder--radius-lg; border-radius: 0 0 $rule-builder--radius-lg $rule-builder--radius-lg;
padding: 0 $rule-builder--padding-sm;
height: (300px + ($rule-builder--padding-sm * 2)); height: (300px + ($rule-builder--padding-sm * 2));
position: relative; position: relative;
}
.rule-builder--graph {
padding: 0 $rule-builder--padding-sm;
& > div { > div.dygraph {
position: absolute; position: absolute;
top: 0; top: ($rule-builder--padding-lg * 2);
left: $rule-builder--padding-sm; left: $rule-builder--padding-sm;
width: calc(100% - #{($rule-builder--padding-sm * 2)}); width: calc(100% - #{$rule-builder--padding-sm * 2});
height: 100%; height: calc(100% - #{$rule-builder--padding-lg * 2}) !important;;
& > div {
position: absolute;
width: 100%;
height: 100%;
padding: 8px 16px;
}
} }
> .dygraph > .dygraph-child {
.container--dygraph-legend { position: absolute;
transform: translateX(-50%); width: 100%;
background-color: $g5-pepper; height: 100%;
padding: 8px 16px;
> span:first-child {
border-top-color: $g7-graphite;
}
} }
} }
.rule-builder--graph-empty { .rule-builder--graph-empty {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -277,28 +288,34 @@ $rule-builder--radius-lg: 5px;
} }
} }
} }
.rule-builder--graph-options {
width: 100%;
padding: $rule-builder--padding-sm ($rule-builder--padding-lg - $rule-builder--padding-sm);
display: flex;
align-items: center;
height: ($rule-builder--padding-lg * 2);
p {
font-weight: 600;
color: $g15-platinum;
margin: 0 6px 0 0;
@include no-user-select();
}
}
/* /*
Section 3 - Rule Message Section 3 - Rule Message
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
*/ */
.rule-builder--message {
textarea.rule-builder--message {
border-color: $rule-builder--section-bg;
background-color: $rule-builder--section-bg; background-color: $rule-builder--section-bg;
padding: $rule-builder--padding-sm ($rule-builder--padding-lg - 2px); padding: $rule-builder--padding-sm ($rule-builder--padding-lg - 2px);
}
.rule-builder--message textarea {
height: 100px; height: 100px;
min-width: 100%; min-width: 100%;
max-width: 100%; max-width: 100%;
border-radius: 0;
@include custom-scrollbar($rule-builder--section-bg,$rule-builder--accent-color); @include custom-scrollbar($rule-builder--section-bg,$rule-builder--accent-color);
&:hover {
border-color: $g4-onyx;
}
&:focus {
background-color: $rule-builder--section-bg;
}
} }
.rule-builder--message-template { .rule-builder--message-template {
height: 30px; height: 30px;

View File

@ -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}
}) })

View File

@ -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)
} }