Merge branch 'master' into bug/fix-rp-copy
commit
33b165edce
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -1,7 +1,10 @@
|
|||
## v1.3.10.0 [unreleased]
|
||||
### Bug Fixes
|
||||
1. [#2095](https://github.com/influxdata/chronograf/pull/2095): Improve the copy in the retention policy edit page
|
||||
|
||||
### Features
|
||||
1.[#2083](https://github.com/influxdata/chronograf/pull/2083): Every dashboard can now have its own time range
|
||||
|
||||
### UI Improvements
|
||||
|
||||
## v1.3.9.0 [2017-10-06]
|
||||
|
@ -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. [#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.[#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
|
||||
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): 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. [#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]
|
||||
### Bug Fixes
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
import reducer from 'src/dashboards/reducers/dashTimeV1'
|
||||
import {
|
||||
addDashTimeV1,
|
||||
setDashTimeV1,
|
||||
deleteDashboard,
|
||||
} from 'src/dashboards/actions/index'
|
||||
|
||||
const initialState = {
|
||||
ranges: [],
|
||||
}
|
||||
|
||||
const emptyState = undefined
|
||||
const dashboardID = 1
|
||||
const timeRange = {upper: null, lower: 'now() - 15m'}
|
||||
|
||||
describe('Dashboards.Reducers.DashTimeV1', () => {
|
||||
it('can load initial state', () => {
|
||||
const noopAction = () => ({type: 'NOOP'})
|
||||
const actual = reducer(emptyState, noopAction)
|
||||
const expected = {ranges: []}
|
||||
|
||||
expect(actual).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('can add a dashboard time', () => {
|
||||
const actual = reducer(emptyState, addDashTimeV1(dashboardID, timeRange))
|
||||
const expected = [{dashboardID, timeRange}]
|
||||
|
||||
expect(actual.ranges).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('can delete a dashboard time range', () => {
|
||||
const state = {
|
||||
ranges: [{dashboardID, timeRange}],
|
||||
}
|
||||
const dashboard = {id: dashboardID}
|
||||
|
||||
const actual = reducer(state, deleteDashboard(dashboard))
|
||||
const expected = []
|
||||
|
||||
expect(actual.ranges).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
describe('setting a dashboard time range', () => {
|
||||
it('can update an existing dashboard', () => {
|
||||
const state = {
|
||||
ranges: [{dashboardID, upper: timeRange.upper, lower: timeRange.lower}],
|
||||
}
|
||||
|
||||
const {upper, lower} = {
|
||||
upper: '2017-10-07 12:05',
|
||||
lower: '2017-10-05 12:04',
|
||||
}
|
||||
|
||||
const actual = reducer(state, setDashTimeV1(dashboardID, {upper, lower}))
|
||||
const expected = [{dashboardID, upper, lower}]
|
||||
|
||||
expect(actual.ranges).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('can set a new time range if none exists', () => {
|
||||
const actual = reducer(emptyState, setDashTimeV1(dashboardID, timeRange))
|
||||
|
||||
const expected = [
|
||||
{dashboardID, upper: timeRange.upper, lower: timeRange.lower},
|
||||
]
|
||||
|
||||
expect(actual.ranges).to.deep.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,7 +1,8 @@
|
|||
import buildInfluxQLQuery from 'utils/influxql'
|
||||
import buildInfluxQLQuery, {buildQuery} from 'utils/influxql'
|
||||
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
|
||||
|
||||
import {NONE, NULL_STRING} from 'shared/constants/queryFillOptions'
|
||||
import {TYPE_QUERY_CONFIG} from 'src/dashboards/constants'
|
||||
|
||||
function mergeConfig(options) {
|
||||
return Object.assign({}, defaultQueryConfig(123), options)
|
||||
|
@ -271,4 +272,25 @@ describe('buildInfluxQLQuery', () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('build query', () => {
|
||||
beforeEach(() => {
|
||||
config = mergeConfig({
|
||||
database: 'db1',
|
||||
measurement: 'm1',
|
||||
retentionPolicy: 'rp1',
|
||||
fields: [{field: 'f1', func: null}],
|
||||
groupBy: {time: '10m', tags: []},
|
||||
})
|
||||
})
|
||||
|
||||
it('builds an influxql relative time bound query', () => {
|
||||
const timeRange = {upper: null, lower: 'now() - 15m'}
|
||||
const expected =
|
||||
'SELECT "f1" FROM "db1"."rp1"."m1" WHERE time > now() - 15m GROUP BY time(10m) FILL(null)'
|
||||
const actual = buildQuery(TYPE_QUERY_CONFIG, timeRange, config)
|
||||
|
||||
expect(actual).to.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import normalizer from 'src/normalizers/dashboardTime'
|
||||
|
||||
const dashboardID = 1
|
||||
const upper = null
|
||||
const lower = 'now() - 15m'
|
||||
const timeRange = {dashboardID, upper, lower}
|
||||
|
||||
describe('Normalizers.DashboardTime', () => {
|
||||
it('can filter out non-objects', () => {
|
||||
const ranges = [1, null, undefined, 'string', timeRange]
|
||||
|
||||
const actual = normalizer(ranges)
|
||||
const expected = [timeRange]
|
||||
|
||||
expect(actual).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('can remove objects with missing keys', () => {
|
||||
const ranges = [
|
||||
{},
|
||||
{dashboardID, upper},
|
||||
{dashboardID, lower},
|
||||
{upper, lower},
|
||||
timeRange,
|
||||
]
|
||||
|
||||
const actual = normalizer(ranges)
|
||||
const expected = [timeRange]
|
||||
expect(actual).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('can remove timeRanges with incorrect dashboardID', () => {
|
||||
const ranges = [{dashboardID: '1', upper, lower}, timeRange]
|
||||
|
||||
const actual = normalizer(ranges)
|
||||
const expected = [timeRange]
|
||||
expect(actual).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('can remove timeRange when is neither an upper or lower bound', () => {
|
||||
const noBounds = {dashboardID, upper: null, lower: null}
|
||||
const ranges = [timeRange, noBounds]
|
||||
|
||||
const actual = normalizer(ranges)
|
||||
const expected = [timeRange]
|
||||
expect(actual).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('can remove a timeRange when upper and lower bounds are of the wrong type', () => {
|
||||
const badTime = {dashboardID, upper: [], lower}
|
||||
const reallyBadTime = {dashboardID, upper, lower: {bad: 'time'}}
|
||||
const ranges = [timeRange, badTime, reallyBadTime]
|
||||
|
||||
const actual = normalizer(ranges)
|
||||
const expected = [timeRange]
|
||||
expect(actual).to.deep.equal(expected)
|
||||
})
|
||||
})
|
|
@ -28,6 +28,29 @@ export const loadDashboards = (dashboards, dashboardID) => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const loadDeafaultDashTimeV1 = dashboardID => ({
|
||||
type: 'ADD_DASHBOARD_TIME_V1',
|
||||
payload: {
|
||||
dashboardID,
|
||||
},
|
||||
})
|
||||
|
||||
export const addDashTimeV1 = (dashboardID, timeRange) => ({
|
||||
type: 'ADD_DASHBOARD_TIME_V1',
|
||||
payload: {
|
||||
dashboardID,
|
||||
timeRange,
|
||||
},
|
||||
})
|
||||
|
||||
export const setDashTimeV1 = (dashboardID, timeRange) => ({
|
||||
type: 'SET_DASHBOARD_TIME_V1',
|
||||
payload: {
|
||||
dashboardID,
|
||||
timeRange,
|
||||
},
|
||||
})
|
||||
|
||||
export const setTimeRange = timeRange => ({
|
||||
type: 'SET_DASHBOARD_TIME_RANGE',
|
||||
payload: {
|
||||
|
@ -46,6 +69,7 @@ export const deleteDashboard = dashboard => ({
|
|||
type: 'DELETE_DASHBOARD',
|
||||
payload: {
|
||||
dashboard,
|
||||
dashboardID: dashboard.id,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
@ -12,10 +12,13 @@ import DisplayOptions from 'src/dashboards/components/DisplayOptions'
|
|||
import * as queryModifiers from 'src/utils/queryTransitions'
|
||||
|
||||
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
|
||||
import buildInfluxQLQuery from 'utils/influxql'
|
||||
import {buildQuery} from 'utils/influxql'
|
||||
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 {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants'
|
||||
|
||||
|
@ -147,7 +150,7 @@ class CellEditorOverlay extends Component {
|
|||
|
||||
const queries = queriesWorkingDraft.map(q => {
|
||||
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 {
|
||||
queryConfig: q,
|
||||
|
|
|
@ -4,13 +4,14 @@ import EmptyQuery from 'src/shared/components/EmptyQuery'
|
|||
import QueryTabList from 'src/shared/components/QueryTabList'
|
||||
import QueryTextArea from 'src/dashboards/components/QueryTextArea'
|
||||
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 rawTextBinder = (links, id, action) => text =>
|
||||
action(links.queries, id, text)
|
||||
const buildText = q =>
|
||||
q.rawText || buildInfluxQLQuery(q.range || TEMPLATE_RANGE, q) || ''
|
||||
q.rawText || buildQuery(TYPE_QUERY_CONFIG, q.range || TEMPLATE_RANGE, q) || ''
|
||||
|
||||
const QueryMaker = ({
|
||||
source,
|
||||
|
|
|
@ -106,3 +106,6 @@ export const TOOLTIP_CONTENT = {
|
|||
FORMAT:
|
||||
'<p><strong>K/M/B</strong> = Thousand / Million / Billion<br/><strong>K/M/G</strong> = Kilo / Mega / Giga </p>',
|
||||
}
|
||||
|
||||
export const TYPE_QUERY_CONFIG = 'queryConfig'
|
||||
export const TYPE_IFQL = 'ifql'
|
||||
|
|
|
@ -13,6 +13,7 @@ import Dashboard from 'src/dashboards/components/Dashboard'
|
|||
import TemplateVariableManager from 'src/dashboards/components/template_variables/Manager'
|
||||
|
||||
import {errorThrown as errorThrownAction} from 'shared/actions/errors'
|
||||
import idNormalizer, {TYPE_ID} from 'src/normalizers/id'
|
||||
|
||||
import * as dashboardActionCreators from 'src/dashboards/actions'
|
||||
|
||||
|
@ -22,6 +23,13 @@ import {
|
|||
} from 'shared/actions/app'
|
||||
import {presentationButtonDispatcher} from 'shared/dispatchers'
|
||||
|
||||
const FORMAT_INFLUXQL = 'influxql'
|
||||
const defaultTimeRange = {
|
||||
upper: null,
|
||||
lower: 'now() - 15m',
|
||||
format: FORMAT_INFLUXQL,
|
||||
}
|
||||
|
||||
class DashboardPage extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
@ -47,7 +55,9 @@ class DashboardPage extends Component {
|
|||
} = this.props
|
||||
|
||||
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
|
||||
await updateTempVarValues(source, dashboard)
|
||||
|
@ -72,8 +82,9 @@ class DashboardPage extends Component {
|
|||
}
|
||||
|
||||
handleSaveEditedCell = newCell => {
|
||||
this.props.dashboardActions
|
||||
.updateDashboardCell(this.getActiveDashboard(), newCell)
|
||||
const {dashboardActions, dashboard} = this.props
|
||||
dashboardActions
|
||||
.updateDashboardCell(dashboard, newCell)
|
||||
.then(this.handleDismissOverlay)
|
||||
}
|
||||
|
||||
|
@ -81,18 +92,26 @@ class DashboardPage extends Component {
|
|||
this.setState({selectedCell: cell})
|
||||
}
|
||||
|
||||
handleChooseTimeRange = timeRange => {
|
||||
this.props.dashboardActions.setTimeRange(timeRange)
|
||||
handleChooseTimeRange = ({upper, lower}) => {
|
||||
const {dashboard, dashboardActions} = this.props
|
||||
dashboardActions.setDashTimeV1(dashboard.id, {
|
||||
upper,
|
||||
lower,
|
||||
format: FORMAT_INFLUXQL,
|
||||
})
|
||||
}
|
||||
|
||||
handleUpdatePosition = cells => {
|
||||
const newDashboard = {...this.getActiveDashboard(), cells}
|
||||
this.props.dashboardActions.updateDashboard(newDashboard)
|
||||
this.props.dashboardActions.putDashboard(newDashboard)
|
||||
const {dashboardActions, dashboard} = this.props
|
||||
const newDashboard = {...dashboard, cells}
|
||||
|
||||
dashboardActions.updateDashboard(newDashboard)
|
||||
dashboardActions.putDashboard(newDashboard)
|
||||
}
|
||||
|
||||
handleAddCell = () => {
|
||||
this.props.dashboardActions.addDashboardCellAsync(this.getActiveDashboard())
|
||||
const {dashboardActions, dashboard} = this.props
|
||||
dashboardActions.addDashboardCellAsync(dashboard)
|
||||
}
|
||||
|
||||
handleEditDashboard = () => {
|
||||
|
@ -104,42 +123,40 @@ class DashboardPage extends Component {
|
|||
}
|
||||
|
||||
handleRenameDashboard = name => {
|
||||
const {dashboardActions, dashboard} = this.props
|
||||
this.setState({isEditMode: false})
|
||||
const newDashboard = {...this.getActiveDashboard(), name}
|
||||
this.props.dashboardActions.updateDashboard(newDashboard)
|
||||
this.props.dashboardActions.putDashboard(newDashboard)
|
||||
const newDashboard = {...dashboard, name}
|
||||
|
||||
dashboardActions.updateDashboard(newDashboard)
|
||||
dashboardActions.putDashboard(newDashboard)
|
||||
}
|
||||
|
||||
handleUpdateDashboardCell = newCell => {
|
||||
return () => {
|
||||
this.props.dashboardActions.updateDashboardCell(
|
||||
this.getActiveDashboard(),
|
||||
newCell
|
||||
)
|
||||
}
|
||||
handleUpdateDashboardCell = newCell => () => {
|
||||
const {dashboardActions, dashboard} = this.props
|
||||
dashboardActions.updateDashboardCell(dashboard, newCell)
|
||||
}
|
||||
|
||||
handleDeleteDashboardCell = cell => {
|
||||
const dashboard = this.getActiveDashboard()
|
||||
this.props.dashboardActions.deleteDashboardCellAsync(dashboard, cell)
|
||||
const {dashboardActions, dashboard} = this.props
|
||||
dashboardActions.deleteDashboardCellAsync(dashboard, cell)
|
||||
}
|
||||
|
||||
handleSelectTemplate = templateID => values => {
|
||||
const {params: {dashboardID}} = this.props
|
||||
this.props.dashboardActions.templateVariableSelected(
|
||||
+dashboardID,
|
||||
templateID,
|
||||
[values]
|
||||
)
|
||||
const {dashboardActions, dashboard} = this.props
|
||||
dashboardActions.templateVariableSelected(dashboard.id, templateID, [
|
||||
values,
|
||||
])
|
||||
}
|
||||
|
||||
handleEditTemplateVariables = (
|
||||
templates,
|
||||
onSaveTemplatesSuccess
|
||||
) => async () => {
|
||||
const {dashboardActions, dashboard} = this.props
|
||||
|
||||
try {
|
||||
await this.props.dashboardActions.putDashboard({
|
||||
...this.getActiveDashboard(),
|
||||
await dashboardActions.putDashboard({
|
||||
dashboard,
|
||||
templates,
|
||||
})
|
||||
onSaveTemplatesSuccess()
|
||||
|
@ -155,8 +172,12 @@ class DashboardPage extends Component {
|
|||
|
||||
synchronizer = dygraph => {
|
||||
const dygraphs = [...this.state.dygraphs, dygraph]
|
||||
const {dashboards, params} = this.props
|
||||
const dashboard = dashboards.find(d => d.id === +params.dashboardID)
|
||||
const {dashboards, params: {dashboardID}} = this.props
|
||||
|
||||
const dashboard = dashboards.find(
|
||||
d => d.id === idNormalizer(TYPE_ID, dashboardID)
|
||||
)
|
||||
|
||||
if (
|
||||
dashboard &&
|
||||
dygraphs.length === dashboard.cells.length &&
|
||||
|
@ -179,11 +200,6 @@ class DashboardPage extends Component {
|
|||
this.setState({zoomedTimeRange: {zoomedLower, zoomedUpper}})
|
||||
}
|
||||
|
||||
getActiveDashboard() {
|
||||
const {params: {dashboardID}, dashboards} = this.props
|
||||
return dashboards.find(d => d.id === +dashboardID)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {zoomedTimeRange} = this.state
|
||||
const {zoomedLower, zoomedUpper} = zoomedTimeRange
|
||||
|
@ -194,6 +210,7 @@ class DashboardPage extends Component {
|
|||
timeRange,
|
||||
timeRange: {lower, upper},
|
||||
showTemplateControlBar,
|
||||
dashboard,
|
||||
dashboards,
|
||||
autoRefresh,
|
||||
cellQueryStatus,
|
||||
|
@ -246,8 +263,6 @@ class DashboardPage extends Component {
|
|||
values: [],
|
||||
}
|
||||
|
||||
const dashboard = this.getActiveDashboard()
|
||||
|
||||
let templatesIncludingDashTime
|
||||
if (dashboard) {
|
||||
templatesIncludingDashTime = [
|
||||
|
@ -366,6 +381,7 @@ DashboardPage.propTypes = {
|
|||
pathname: string.isRequired,
|
||||
query: shape({}),
|
||||
}).isRequired,
|
||||
dashboard: shape({}),
|
||||
dashboardActions: shape({
|
||||
putDashboard: func.isRequired,
|
||||
getDashboardsAsync: func.isRequired,
|
||||
|
@ -401,7 +417,10 @@ DashboardPage.propTypes = {
|
|||
handleChooseAutoRefresh: func.isRequired,
|
||||
autoRefresh: number.isRequired,
|
||||
templateControlBarVisibilityToggled: func.isRequired,
|
||||
timeRange: shape({}).isRequired,
|
||||
timeRange: shape({
|
||||
upper: string,
|
||||
lower: string,
|
||||
}),
|
||||
showTemplateControlBar: bool.isRequired,
|
||||
inPresentationMode: bool.isRequired,
|
||||
handleClickPresentationButton: func,
|
||||
|
@ -412,19 +431,30 @@ DashboardPage.propTypes = {
|
|||
errorThrown: func,
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const mapStateToProps = (state, {params: {dashboardID}}) => {
|
||||
const {
|
||||
app: {
|
||||
ephemeral: {inPresentationMode},
|
||||
persisted: {autoRefresh, showTemplateControlBar},
|
||||
},
|
||||
dashboardUI: {dashboards, timeRange, cellQueryStatus},
|
||||
dashboardUI: {dashboards, cellQueryStatus},
|
||||
sources,
|
||||
dashTimeV1,
|
||||
} = 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 {
|
||||
dashboards,
|
||||
autoRefresh,
|
||||
dashboard,
|
||||
timeRange,
|
||||
showTemplateControlBar,
|
||||
inPresentationMode,
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import _ from 'lodash'
|
||||
const initialState = {
|
||||
ranges: [],
|
||||
}
|
||||
|
||||
const dashTimeV1 = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case 'ADD_DASHBOARD_TIME_V1': {
|
||||
const {dashboardID, timeRange} = action.payload
|
||||
const ranges = [...state.ranges, {dashboardID, timeRange}]
|
||||
|
||||
return {...state, ranges}
|
||||
}
|
||||
|
||||
case 'DELETE_DASHBOARD': {
|
||||
const {dashboardID} = action.payload
|
||||
const ranges = state.ranges.filter(r => r.dashboardID !== dashboardID)
|
||||
|
||||
return {...state, ranges}
|
||||
}
|
||||
|
||||
case 'SET_DASHBOARD_TIME_V1': {
|
||||
const {dashboardID, timeRange} = action.payload
|
||||
const newTimeRange = [{dashboardID, ...timeRange}]
|
||||
const ranges = _.unionBy(newTimeRange, state.ranges, 'dashboardID')
|
||||
|
||||
return {...state, ranges}
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
export default dashTimeV1
|
|
@ -99,7 +99,7 @@ const TagListItem = React.createClass({
|
|||
},
|
||||
|
||||
render() {
|
||||
const {tagKey, tagValues} = this.props
|
||||
const {tagKey, tagValues, isUsingGroupBy} = this.props
|
||||
const {isOpen} = this.state
|
||||
const tagItemLabel = `${tagKey} — ${tagValues.length}`
|
||||
|
||||
|
@ -115,8 +115,9 @@ const TagListItem = React.createClass({
|
|||
{tagItemLabel}
|
||||
</span>
|
||||
<div
|
||||
className={classnames('btn btn-default btn-xs group-by-tag', {
|
||||
active: this.props.isUsingGroupBy,
|
||||
className={classnames('btn btn-xs group-by-tag', {
|
||||
'btn-primary': isUsingGroupBy,
|
||||
'btn-default': !isUsingGroupBy,
|
||||
})}
|
||||
onClick={this.handleGroupBy}
|
||||
>
|
||||
|
|
|
@ -35,6 +35,7 @@ class DataExplorer extends Component {
|
|||
if (queryConfigs.length === 0) {
|
||||
this.props.queryConfigActions.addQuery()
|
||||
}
|
||||
|
||||
return queryConfigs[0]
|
||||
}
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ const DataSection = ({
|
|||
|
||||
return (
|
||||
<div className="rule-section">
|
||||
<div className="query-builder rule-section--border-bottom">
|
||||
<div className="query-builder">
|
||||
<DatabaseList query={query} onChooseNamespace={handleChooseNamespace} />
|
||||
<MeasurementList
|
||||
query={query}
|
||||
|
|
|
@ -7,7 +7,7 @@ const periods = PERIODS.map(text => {
|
|||
})
|
||||
|
||||
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>
|
||||
<Dropdown
|
||||
className="dropdown-80"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, {PropTypes, Component} from 'react'
|
||||
|
||||
import NameSection from 'src/kapacitor/components/NameSection'
|
||||
import ValuesSection from 'src/kapacitor/components/ValuesSection'
|
||||
import RuleHeader from 'src/kapacitor/components/RuleHeader'
|
||||
import RuleMessage from 'src/kapacitor/components/RuleMessage'
|
||||
|
@ -51,19 +52,23 @@ class KapacitorRule extends Component {
|
|||
}
|
||||
|
||||
handleEdit = () => {
|
||||
const {addFlashMessage, queryConfigs, rule} = this.props
|
||||
const {addFlashMessage, queryConfigs, rule, router, source} = this.props
|
||||
const updatedRule = Object.assign({}, rule, {
|
||||
query: queryConfigs[rule.queryID],
|
||||
})
|
||||
|
||||
editRule(updatedRule)
|
||||
.then(() => {
|
||||
addFlashMessage({type: 'success', text: 'Rule successfully updated!'})
|
||||
router.push(`/sources/${source.id}/alert-rules`)
|
||||
addFlashMessage({
|
||||
type: 'success',
|
||||
text: `${rule.name} successfully saved!`,
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
addFlashMessage({
|
||||
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)) {
|
||||
return 'Please select a database, measurement, and field'
|
||||
return 'Please select a Database, Measurement, and Field'
|
||||
}
|
||||
|
||||
if (!rule.values.value) {
|
||||
return 'Please enter a value in the Rule Conditions section'
|
||||
return 'Please enter a value in the Conditions section'
|
||||
}
|
||||
|
||||
return ''
|
||||
|
@ -98,7 +103,7 @@ class KapacitorRule extends Component {
|
|||
deadmanValidation = () => {
|
||||
const {query} = this.props
|
||||
if (query && (!query.database || !query.measurement)) {
|
||||
return 'Deadman requires a database and measurement'
|
||||
return 'Deadman rules require a Database and Measurement'
|
||||
}
|
||||
|
||||
return ''
|
||||
|
@ -144,19 +149,21 @@ class KapacitorRule extends Component {
|
|||
return (
|
||||
<div className="page">
|
||||
<RuleHeader
|
||||
rule={rule}
|
||||
actions={ruleActions}
|
||||
onSave={isEditing ? this.handleEdit : this.handleCreate}
|
||||
onChooseTimeRange={this.handleChooseTimeRange}
|
||||
validationError={this.validationError()}
|
||||
timeRange={timeRange}
|
||||
source={source}
|
||||
onSave={isEditing ? this.handleEdit : this.handleCreate}
|
||||
validationError={this.validationError()}
|
||||
/>
|
||||
<FancyScrollbar className="page-contents fancy-scroll--kapacitor">
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-xs-12">
|
||||
<div className="rule-builder">
|
||||
<NameSection
|
||||
isEditing={isEditing}
|
||||
defaultName={rule.name}
|
||||
onRuleRename={ruleActions.updateRuleName}
|
||||
ruleID={rule.id}
|
||||
/>
|
||||
<ValuesSection
|
||||
rule={rule}
|
||||
source={source}
|
||||
|
@ -170,6 +177,7 @@ class KapacitorRule extends Component {
|
|||
onDeadmanChange={this.handleDeadmanChange}
|
||||
onRuleTypeInputChange={this.handleRuleTypeInputChange}
|
||||
onRuleTypeDropdownChange={this.handleRuleTypeDropdownChange}
|
||||
onChooseTimeRange={this.handleChooseTimeRange}
|
||||
/>
|
||||
<RuleMessage
|
||||
rule={rule}
|
||||
|
|
|
@ -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
|
|
@ -12,7 +12,7 @@ const Relative = ({
|
|||
onDropdownChange,
|
||||
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>
|
||||
<Dropdown
|
||||
className="dropdown-110"
|
||||
|
|
|
@ -2,30 +2,34 @@ import React, {PropTypes} from 'react'
|
|||
import buildInfluxQLQuery from 'utils/influxql'
|
||||
import AutoRefresh from 'shared/components/AutoRefresh'
|
||||
import LineGraph from 'shared/components/LineGraph'
|
||||
import TimeRangeDropdown from 'shared/components/TimeRangeDropdown'
|
||||
|
||||
const RefreshingLineGraph = AutoRefresh(LineGraph)
|
||||
|
||||
const {shape, string, func} = PropTypes
|
||||
|
||||
export const RuleGraph = React.createClass({
|
||||
propTypes: {
|
||||
source: PropTypes.shape({
|
||||
links: PropTypes.shape({
|
||||
proxy: PropTypes.string.isRequired,
|
||||
source: shape({
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
query: PropTypes.shape({}).isRequired,
|
||||
rule: PropTypes.shape({}).isRequired,
|
||||
timeRange: PropTypes.shape({}).isRequired,
|
||||
query: shape({}).isRequired,
|
||||
rule: shape({}).isRequired,
|
||||
timeRange: shape({}).isRequired,
|
||||
onChooseTimeRange: func.isRequired,
|
||||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="rule-builder--graph">
|
||||
{this.renderGraph()}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
renderGraph() {
|
||||
const {query, source, timeRange: {lower}, rule} = this.props
|
||||
const {
|
||||
query,
|
||||
source,
|
||||
timeRange: {lower},
|
||||
timeRange,
|
||||
rule,
|
||||
onChooseTimeRange,
|
||||
} = this.props
|
||||
const autoRefreshMs = 30000
|
||||
const queryText = buildInfluxQLQuery({lower}, query)
|
||||
const queries = [{host: source.links.proxy, text: queryText}]
|
||||
|
@ -42,6 +46,15 @@ export const RuleGraph = React.createClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="rule-builder--graph">
|
||||
<div className="rule-builder--graph-options">
|
||||
<p>Preview Data from</p>
|
||||
<TimeRangeDropdown
|
||||
onChooseTimeRange={onChooseTimeRange}
|
||||
selected={timeRange}
|
||||
preventCustomTimeRange={true}
|
||||
/>
|
||||
</div>
|
||||
<RefreshingLineGraph
|
||||
queries={queries}
|
||||
autoRefresh={autoRefreshMs}
|
||||
|
@ -50,6 +63,7 @@ export const RuleGraph = React.createClass({
|
|||
overrideLineColors={kapacitorLineColors}
|
||||
ruleValues={rule.values}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
|
|
|
@ -1,66 +1,24 @@
|
|||
import React, {PropTypes, Component} from 'react'
|
||||
import RuleHeaderEdit from 'src/kapacitor/components/RuleHeaderEdit'
|
||||
import RuleHeaderSave from 'src/kapacitor/components/RuleHeaderSave'
|
||||
|
||||
class RuleHeader extends Component {
|
||||
constructor(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() {
|
||||
const {
|
||||
rule,
|
||||
source,
|
||||
onSave,
|
||||
timeRange,
|
||||
validationError,
|
||||
onChooseTimeRange,
|
||||
} = this.props
|
||||
|
||||
const {isEditingName} = this.state
|
||||
const {source, onSave, validationError} = this.props
|
||||
|
||||
return (
|
||||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<RuleHeaderEdit
|
||||
rule={rule}
|
||||
isEditing={isEditingName}
|
||||
onToggleEdit={this.toggleEditName}
|
||||
onEditName={this.handleEditName}
|
||||
onEditNameBlur={this.handleEditNameBlur}
|
||||
/>
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">Alert Rule Builder</h1>
|
||||
</div>
|
||||
<RuleHeaderSave
|
||||
source={source}
|
||||
onSave={onSave}
|
||||
timeRange={timeRange}
|
||||
validationError={validationError}
|
||||
onChooseTimeRange={onChooseTimeRange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -73,13 +31,7 @@ const {func, shape, string} = PropTypes
|
|||
RuleHeader.propTypes = {
|
||||
source: shape({}).isRequired,
|
||||
onSave: func.isRequired,
|
||||
rule: shape({}).isRequired,
|
||||
actions: shape({
|
||||
updateRuleName: func.isRequired,
|
||||
}).isRequired,
|
||||
validationError: string.isRequired,
|
||||
onChooseTimeRange: func.isRequired,
|
||||
timeRange: shape({}).isRequired,
|
||||
}
|
||||
|
||||
export default RuleHeader
|
||||
|
|
|
@ -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
|
|
@ -1,21 +1,10 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import ReactTooltip from 'react-tooltip'
|
||||
import TimeRangeDropdown from 'shared/components/TimeRangeDropdown'
|
||||
import SourceIndicator from 'shared/components/SourceIndicator'
|
||||
|
||||
const RuleHeaderSave = ({
|
||||
onSave,
|
||||
timeRange,
|
||||
validationError,
|
||||
onChooseTimeRange,
|
||||
}) =>
|
||||
const RuleHeaderSave = ({onSave, validationError}) =>
|
||||
<div className="page-header__right">
|
||||
<SourceIndicator />
|
||||
<TimeRangeDropdown
|
||||
onChooseTimeRange={onChooseTimeRange}
|
||||
selected={timeRange}
|
||||
preventCustomTimeRange={true}
|
||||
/>
|
||||
{validationError
|
||||
? <button
|
||||
className="btn btn-success btn-sm disabled"
|
||||
|
@ -36,13 +25,11 @@ const RuleHeaderSave = ({
|
|||
/>
|
||||
</div>
|
||||
|
||||
const {func, shape, string} = PropTypes
|
||||
const {func, string} = PropTypes
|
||||
|
||||
RuleHeaderSave.propTypes = {
|
||||
onSave: func.isRequired,
|
||||
validationError: string.isRequired,
|
||||
onChooseTimeRange: func.isRequired,
|
||||
timeRange: shape({}).isRequired,
|
||||
}
|
||||
|
||||
export default RuleHeaderSave
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const RuleMessageText = ({rule, updateMessage}) =>
|
||||
<div className="rule-builder--message">
|
||||
<textarea
|
||||
className="form-control form-malachite monotype rule-builder--message"
|
||||
className="form-control form-malachite monotype"
|
||||
onChange={updateMessage}
|
||||
placeholder="Example: {{ .ID }} is {{ .Level }} value: {{ index .Fields "value" }}"
|
||||
value={rule.message}
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
const {func, shape} = PropTypes
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ const Threshold = ({
|
|||
onDropdownChange,
|
||||
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>
|
||||
<span className="rule-builder--metric">
|
||||
{query.fields.length ? query.fields[0].field : 'Select a Time-Series'}
|
||||
|
|
|
@ -28,11 +28,12 @@ const ValuesSection = ({
|
|||
timeRange,
|
||||
onAddEvery,
|
||||
onRemoveEvery,
|
||||
onChooseTrigger,
|
||||
onDeadmanChange,
|
||||
onChooseTimeRange,
|
||||
queryConfigActions,
|
||||
onRuleTypeInputChange,
|
||||
onRuleTypeDropdownChange,
|
||||
onChooseTrigger,
|
||||
}) =>
|
||||
<div className="rule-section">
|
||||
<h3 className="rule-section--heading">Alert Type</h3>
|
||||
|
@ -49,7 +50,7 @@ const ValuesSection = ({
|
|||
)}
|
||||
</TabList>
|
||||
<div>
|
||||
<h3 className="rule-builder--sub-header">Time Series</h3>
|
||||
<h3 className="rule-section--sub-heading">Time Series</h3>
|
||||
<DataSection
|
||||
query={query}
|
||||
timeRange={timeRange}
|
||||
|
@ -60,7 +61,7 @@ const ValuesSection = ({
|
|||
isDeadman={isDeadman(rule)}
|
||||
/>
|
||||
</div>
|
||||
<h3 className="rule-builder--sub-header">Rule Conditions</h3>
|
||||
<h3 className="rule-section--sub-heading">Conditions</h3>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<Threshold
|
||||
|
@ -88,6 +89,7 @@ const ValuesSection = ({
|
|||
query={query}
|
||||
source={source}
|
||||
timeRange={timeRange}
|
||||
onChooseTimeRange={onChooseTimeRange}
|
||||
/>}
|
||||
</Tabs>
|
||||
</div>
|
||||
|
@ -110,6 +112,7 @@ ValuesSection.propTypes = {
|
|||
timeRange: shape({}).isRequired,
|
||||
queryConfigActions: shape({}).isRequired,
|
||||
source: shape({}).isRequired,
|
||||
onChooseTimeRange: func.isRequired,
|
||||
}
|
||||
|
||||
export default ValuesSection
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import _ from 'lodash'
|
||||
import normalizer from 'src/normalizers/dashboardTime'
|
||||
|
||||
export const loadLocalStorage = errorsQueue => {
|
||||
try {
|
||||
const serializedState = localStorage.getItem('state')
|
||||
|
@ -12,10 +15,24 @@ export const loadLocalStorage = errorsQueue => {
|
|||
console.log(errorText) // eslint-disable-line no-console
|
||||
errorsQueue.push(errorText)
|
||||
|
||||
if (!state.dashTimeV1) {
|
||||
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
|
||||
|
||||
return state
|
||||
|
@ -34,9 +51,11 @@ export const saveToLocalStorage = ({
|
|||
dataExplorerQueryConfigs,
|
||||
timeRange,
|
||||
dataExplorer,
|
||||
dashTimeV1: {ranges},
|
||||
}) => {
|
||||
try {
|
||||
const appPersisted = Object.assign({}, {app: {persisted}})
|
||||
const dashTimeV1 = {ranges: normalizer(ranges)}
|
||||
|
||||
window.localStorage.setItem(
|
||||
'state',
|
||||
|
@ -46,6 +65,7 @@ export const saveToLocalStorage = ({
|
|||
timeRange,
|
||||
dataExplorer,
|
||||
VERSION, // eslint-disable-line no-undef
|
||||
dashTimeV1,
|
||||
})
|
||||
)
|
||||
} catch (err) {
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
const dashtime = ranges => {
|
||||
if (!Array.isArray(ranges)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const normalized = ranges.filter(r => {
|
||||
if (!_.isObject(r)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// check for presence of keys
|
||||
if (
|
||||
!r.hasOwnProperty('dashboardID') ||
|
||||
!r.hasOwnProperty('lower') ||
|
||||
!r.hasOwnProperty('upper')
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
const {dashboardID, lower, upper} = r
|
||||
|
||||
if (!dashboardID || typeof dashboardID !== 'number') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!lower && !upper) {
|
||||
return false
|
||||
}
|
||||
|
||||
const isCorrectType = bound =>
|
||||
_.isString(bound) || _.isNull(bound) || _.isInteger(bound)
|
||||
|
||||
if (!isCorrectType(lower) || !isCorrectType(upper)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
export default dashtime
|
|
@ -0,0 +1,18 @@
|
|||
export const TYPE_ID = 'ID'
|
||||
export const TYPE_URI = 'ID'
|
||||
|
||||
const idNormalizer = (type, id) => {
|
||||
switch (type) {
|
||||
case 'ID': {
|
||||
return +id
|
||||
}
|
||||
|
||||
case 'URI': {
|
||||
// handle decode of URI here
|
||||
}
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
export default idNormalizer
|
|
@ -100,7 +100,7 @@ const MeasurementList = React.createClass({
|
|||
return (
|
||||
<div className="query-builder--column">
|
||||
<div className="query-builder--heading">
|
||||
<span>Measurements</span>
|
||||
<span>Measurements & Tags</span>
|
||||
{this.props.query.database
|
||||
? <div className="query-builder--filter">
|
||||
<input
|
||||
|
|
|
@ -58,7 +58,7 @@ export const TabList = React.createClass({
|
|||
|
||||
if (this.props.isKapacitorTabs === 'true') {
|
||||
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>
|
||||
<div className="nav nav-tablist nav-tablist-sm nav-tablist-malachite">
|
||||
{children}
|
||||
|
|
|
@ -12,6 +12,7 @@ import dataExplorerReducers from 'src/data_explorer/reducers'
|
|||
import adminReducer from 'src/admin/reducers/admin'
|
||||
import kapacitorReducers from 'src/kapacitor/reducers'
|
||||
import dashboardUI from 'src/dashboards/reducers/ui'
|
||||
import dashTimeV1 from 'src/dashboards/reducers/dashTimeV1'
|
||||
import persistStateEnhancer from './persistStateEnhancer'
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
|
@ -21,6 +22,7 @@ const rootReducer = combineReducers({
|
|||
...kapacitorReducers,
|
||||
admin: adminReducer,
|
||||
dashboardUI,
|
||||
dashTimeV1,
|
||||
routing: routerReducer,
|
||||
})
|
||||
|
||||
|
|
|
@ -198,7 +198,8 @@
|
|||
}
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.query-builder--db-dropdown {
|
||||
|
|
|
@ -82,6 +82,27 @@ $rule-builder--radius-lg: 5px;
|
|||
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
|
||||
.rule-section:first-of-type {
|
||||
.rule-section--heading {
|
||||
|
@ -165,10 +186,19 @@ $rule-builder--radius-lg: 5px;
|
|||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
.query-builder--column:nth-child(4) .query-builder--list,
|
||||
.query-builder--column:nth-child(4) .query-builder--list-empty {
|
||||
.query-builder--column:first-of-type .query-builder--list {
|
||||
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;
|
||||
}
|
||||
.query-builder--column:last-of-type .query-builder--heading {
|
||||
border-top-right-radius: $rule-builder--radius-lg;
|
||||
}
|
||||
.query-builder--heading {
|
||||
background-color: $rule-builder--section-bg;
|
||||
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 {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
|
@ -222,43 +245,31 @@ $rule-builder--radius-lg: 5px;
|
|||
padding: 0 9px;
|
||||
@include no-user-select();
|
||||
}
|
||||
.rule-builder--graph {
|
||||
.rule-builder--graph,
|
||||
.rule-builder--graph-empty {
|
||||
background-color: $rule-builder--section-bg;
|
||||
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));
|
||||
position: relative;
|
||||
}
|
||||
.rule-builder--graph {
|
||||
padding: 0 $rule-builder--padding-sm;
|
||||
|
||||
& > div {
|
||||
> div.dygraph {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
top: ($rule-builder--padding-lg * 2);
|
||||
left: $rule-builder--padding-sm;
|
||||
width: calc(100% - #{($rule-builder--padding-sm * 2)});
|
||||
height: 100%;
|
||||
|
||||
& > div {
|
||||
width: calc(100% - #{$rule-builder--padding-sm * 2});
|
||||
height: calc(100% - #{$rule-builder--padding-lg * 2}) !important;;
|
||||
}
|
||||
> .dygraph > .dygraph-child {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.container--dygraph-legend {
|
||||
transform: translateX(-50%);
|
||||
background-color: $g5-pepper;
|
||||
|
||||
> span:first-child {
|
||||
border-top-color: $g7-graphite;
|
||||
}
|
||||
}
|
||||
}
|
||||
.rule-builder--graph-empty {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: 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
|
||||
-----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
textarea.rule-builder--message {
|
||||
border-color: $rule-builder--section-bg;
|
||||
.rule-builder--message {
|
||||
background-color: $rule-builder--section-bg;
|
||||
padding: $rule-builder--padding-sm ($rule-builder--padding-lg - 2px);
|
||||
}
|
||||
.rule-builder--message textarea {
|
||||
height: 100px;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
border-radius: 0;
|
||||
@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 {
|
||||
height: 30px;
|
||||
|
|
|
@ -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 statements = queryConfigs.map(query => {
|
||||
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}
|
||||
})
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
DEFAULT_DASHBOARD_GROUP_BY_INTERVAL,
|
||||
} from 'shared/constants'
|
||||
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'
|
||||
|
||||
/* eslint-disable quotes */
|
||||
|
@ -21,13 +22,9 @@ export const quoteIfTimestamp = ({lower, upper}) => {
|
|||
}
|
||||
/* eslint-enable quotes */
|
||||
|
||||
export default function buildInfluxQLQuery(
|
||||
timeBounds,
|
||||
config,
|
||||
isKapacitorRule
|
||||
) {
|
||||
export default function buildInfluxQLQuery(timeRange, config) {
|
||||
const {groupBy, fill = NULL_STRING, tags, areTagsAccepted} = config
|
||||
const {upper, lower} = quoteIfTimestamp(timeBounds)
|
||||
const {upper, lower} = quoteIfTimestamp(timeRange)
|
||||
|
||||
const select = _buildSelect(config)
|
||||
if (select === null) {
|
||||
|
@ -36,7 +33,7 @@ export default function buildInfluxQLQuery(
|
|||
|
||||
const condition = _buildWhereClause({lower, upper, tags, areTagsAccepted})
|
||||
const dimensions = _buildGroupBy(groupBy)
|
||||
const fillClause = isKapacitorRule || !groupBy.time ? '' : _buildFill(fill)
|
||||
const fillClause = groupBy.time ? _buildFill(fill) : ''
|
||||
|
||||
return `${select}${condition}${dimensions}${fillClause}`
|
||||
}
|
||||
|
@ -53,6 +50,21 @@ function _buildSelect({fields, database, retentionPolicy, measurement}) {
|
|||
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) {
|
||||
return _buildSelect(config)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue