Merge branch 'master' into responsive-admin-tabs
commit
511777396c
|
@ -1,13 +1,19 @@
|
|||
## 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
|
||||
1. [#2122](https://github.com/influxdata/chronograf/pull/2122): Fix 'could not connect to source' bug on source creation with unsafe-ssl
|
||||
1. [#2093](https://github.com/influxdata/chronograf/pull/2093): Fix when exporting `SHOW DATABASES` CSV has bad data
|
||||
1. [#2098](https://github.com/influxdata/chronograf/pull/2098): Fix not-equal-to highlighting in Kapacitor Rule Builder
|
||||
|
||||
### Features
|
||||
1. [#2083](https://github.com/influxdata/chronograf/pull/2083): Every dashboard can now have its own time range
|
||||
1. [#2045](https://github.com/influxdata/chronograf/pull/2045): Add CSV download option in dashboard cells
|
||||
|
||||
### UI Improvements
|
||||
1. [#2111](https://github.com/influxdata/chronograf/pull/2111): Increase size of Cell Editor query tabs to reveal more of their query strings
|
||||
1. [#2120](https://github.com/influxdata/chronograf/pull/2120): Improve appearance of Admin Page tabs on smaller screens
|
||||
1. [#2119](https://github.com/influxdata/chronograf/pull/2119): Add support for graph zooming and point display on the millisecond-level
|
||||
|
||||
|
||||
## v1.3.9.0 [2017-10-06]
|
||||
### Bug Fixes
|
||||
|
@ -30,6 +36,7 @@
|
|||
1. [#1992](https://github.com/influxdata/chronograf/pull/1992): Add .csv download button to data explorer
|
||||
1. [#2082](https://github.com/influxdata/chronograf/pull/2082): Add Data Explorer InfluxQL query and location query synchronization, so queries can be shared via a a URL
|
||||
1. [#1996](https://github.com/influxdata/chronograf/pull/1996): Able to switch InfluxDB sources on a per graph basis
|
||||
1. [#2041](https://github.com/influxdata/chronograf/pull/2041): Add now() as an option in the Dashboard date picker
|
||||
|
||||
### UI Improvements
|
||||
1. [#2002](https://github.com/influxdata/chronograf/pull/2002): Require a second click when deleting a dashboard cell
|
||||
|
|
|
@ -205,14 +205,23 @@ func (g *GroupByVar) Exec(query string) {
|
|||
durStr := query[start+len(whereClause):]
|
||||
|
||||
// attempt to parse out a relative time range
|
||||
dur, err := g.parseRelative(durStr)
|
||||
// locate duration literal start
|
||||
prefix := "time > now() - "
|
||||
lowerDuration, err := g.parseRelative(durStr, prefix)
|
||||
if err == nil {
|
||||
// we parsed relative duration successfully
|
||||
g.Duration = dur
|
||||
return
|
||||
prefix := "time < now() - "
|
||||
upperDuration, err := g.parseRelative(durStr, prefix)
|
||||
if err != nil {
|
||||
g.Duration = lowerDuration
|
||||
return
|
||||
}
|
||||
g.Duration = lowerDuration - upperDuration
|
||||
if g.Duration < 0 {
|
||||
g.Duration = -g.Duration
|
||||
}
|
||||
}
|
||||
|
||||
dur, err = g.parseAbsolute(durStr)
|
||||
dur, err := g.parseAbsolute(durStr)
|
||||
if err == nil {
|
||||
// we found an absolute time range
|
||||
g.Duration = dur
|
||||
|
@ -223,9 +232,7 @@ func (g *GroupByVar) Exec(query string) {
|
|||
// InfluxQL query following the "where" keyword. For example, in the fragment
|
||||
// "time > now() - 180d GROUP BY :interval:", parseRelative would return a
|
||||
// duration equal to 180d
|
||||
func (g *GroupByVar) parseRelative(fragment string) (time.Duration, error) {
|
||||
// locate duration literal start
|
||||
prefix := "time > now() - "
|
||||
func (g *GroupByVar) parseRelative(fragment string, prefix string) (time.Duration, error) {
|
||||
start := strings.Index(fragment, prefix)
|
||||
if start == -1 {
|
||||
return time.Duration(0), errors.New("not a relative duration")
|
||||
|
@ -298,11 +305,19 @@ func (g *GroupByVar) parseAbsolute(fragment string) (time.Duration, error) {
|
|||
}
|
||||
|
||||
func (g *GroupByVar) String() string {
|
||||
duration := int64(g.Duration/time.Second) / int64(g.Resolution) * 3
|
||||
if duration == 0 {
|
||||
duration = 1
|
||||
// The function is: ((total_seconds * millisecond_converstion) / group_by) = pixels / 3
|
||||
// Number of points given the pixels
|
||||
pixels := float64(g.Resolution) / 3.0
|
||||
msPerPixel := float64(g.Duration/time.Millisecond) / pixels
|
||||
secPerPixel := float64(g.Duration/time.Second) / pixels
|
||||
if secPerPixel < 1.0 {
|
||||
if msPerPixel < 1.0 {
|
||||
msPerPixel = 1.0
|
||||
}
|
||||
return "time(" + strconv.FormatInt(int64(msPerPixel), 10) + "ms)"
|
||||
}
|
||||
return "time(" + strconv.Itoa(int(duration)) + "s)"
|
||||
// If groupby is more than 1 second round to the second
|
||||
return "time(" + strconv.FormatInt(int64(secPerPixel), 10) + "s)"
|
||||
}
|
||||
|
||||
func (g *GroupByVar) Name() string {
|
||||
|
|
|
@ -2,54 +2,61 @@ package chronograf_test
|
|||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
func Test_GroupByVar(t *testing.T) {
|
||||
gbvTests := []struct {
|
||||
name string
|
||||
query string
|
||||
expected time.Duration
|
||||
resolution uint // the screen resolution to render queries into
|
||||
reportingInterval time.Duration
|
||||
name string
|
||||
query string
|
||||
want string
|
||||
resolution uint // the screen resolution to render queries into
|
||||
}{
|
||||
{
|
||||
"relative time",
|
||||
"SELECT mean(usage_idle) FROM cpu WHERE time > now() - 180d GROUP BY :interval:",
|
||||
4320 * time.Hour,
|
||||
1000,
|
||||
10 * time.Second,
|
||||
name: "relative time only lower bound with one day of duration",
|
||||
query: "SELECT mean(usage_idle) FROM cpu WHERE time > now() - 1d GROUP BY :interval:",
|
||||
resolution: 1000,
|
||||
want: "time(259s)",
|
||||
},
|
||||
{
|
||||
"absolute time",
|
||||
"SELECT mean(usage_idle) FROM cpu WHERE time > '1985-10-25T00:01:00Z' and time < '1985-10-25T00:02:00Z' GROUP BY :interval:",
|
||||
1 * time.Minute,
|
||||
1000,
|
||||
10 * time.Second,
|
||||
name: "relative time with relative upper bound with one minute of duration",
|
||||
query: "SELECT mean(usage_idle) FROM cpu WHERE time > now() - 3m AND time < now() - 2m GROUP BY :interval:",
|
||||
resolution: 1000,
|
||||
want: "time(180ms)",
|
||||
},
|
||||
{
|
||||
"absolute time with nano",
|
||||
"SELECT mean(usage_idle) FROM cpu WHERE time > '2017-07-24T15:33:42.994Z' and time < '2017-08-24T15:33:42.994Z' GROUP BY :interval:",
|
||||
744 * time.Hour,
|
||||
1000,
|
||||
10 * time.Second,
|
||||
name: "relative time with relative lower bound and now upper with one day of duration",
|
||||
query: "SELECT mean(usage_idle) FROM cpu WHERE time > now() - 1d AND time < now() GROUP BY :interval:",
|
||||
resolution: 1000,
|
||||
want: "time(259s)",
|
||||
},
|
||||
{
|
||||
name: "absolute time with one minute of duration",
|
||||
query: "SELECT mean(usage_idle) FROM cpu WHERE time > '1985-10-25T00:01:00Z' and time < '1985-10-25T00:02:00Z' GROUP BY :interval:",
|
||||
resolution: 1000,
|
||||
want: "time(180ms)",
|
||||
},
|
||||
{
|
||||
name: "absolute time with nano seconds and zero duraiton",
|
||||
query: "SELECT mean(usage_idle) FROM cpu WHERE time > '2017-07-24T15:33:42.994Z' and time < '2017-07-24T15:33:42.994Z' GROUP BY :interval:",
|
||||
resolution: 1000,
|
||||
want: "time(1ms)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range gbvTests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
gbv := chronograf.GroupByVar{
|
||||
Var: ":interval:",
|
||||
Resolution: test.resolution,
|
||||
ReportingInterval: test.reportingInterval,
|
||||
Var: ":interval:",
|
||||
Resolution: test.resolution,
|
||||
}
|
||||
|
||||
gbv.Exec(test.query)
|
||||
got := gbv.String()
|
||||
|
||||
if gbv.Duration != test.expected {
|
||||
t.Fatalf("%q - durations not equal! Want: %s, Got: %s", test.name, test.expected, gbv.Duration)
|
||||
if got != test.want {
|
||||
t.Fatalf("%q - durations not equal! Want: %s, Got: %s", test.name, test.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -274,16 +274,25 @@ func TestGroupByVarString(t *testing.T) {
|
|||
ReportingInterval: 10 * time.Second,
|
||||
Duration: 24 * time.Hour,
|
||||
},
|
||||
want: "time(369s)",
|
||||
want: "time(370s)",
|
||||
},
|
||||
{
|
||||
name: "String() outputs a minimum of 1s intervals",
|
||||
name: "String() milliseconds if less than one second intervals",
|
||||
tvar: &chronograf.GroupByVar{
|
||||
Resolution: 100000,
|
||||
ReportingInterval: 10 * time.Second,
|
||||
Duration: time.Hour,
|
||||
},
|
||||
want: "time(1s)",
|
||||
want: "time(107ms)",
|
||||
},
|
||||
{
|
||||
name: "String() milliseconds if less than one millisecond",
|
||||
tvar: &chronograf.GroupByVar{
|
||||
Resolution: 100000,
|
||||
ReportingInterval: 10 * time.Second,
|
||||
Duration: time.Second,
|
||||
},
|
||||
want: "time(1ms)",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
|
|
@ -101,7 +101,7 @@
|
|||
"bootstrap": "^3.3.7",
|
||||
"calculate-size": "^1.1.1",
|
||||
"classnames": "^2.2.3",
|
||||
"dygraphs": "^2.0.0",
|
||||
"dygraphs": "influxdata/dygraphs",
|
||||
"eslint-plugin-babel": "^4.1.2",
|
||||
"fast.js": "^0.1.1",
|
||||
"fixed-data-table": "^0.6.1",
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import resultsToCSV, {formatDate} from 'shared/parsing/resultsToCSV'
|
||||
import {
|
||||
resultsToCSV,
|
||||
formatDate,
|
||||
dashboardtoCSV,
|
||||
} from 'shared/parsing/resultsToCSV'
|
||||
|
||||
describe('formatDate', () => {
|
||||
it('converts timestamp to an excel compatible date string', () => {
|
||||
|
@ -29,6 +33,7 @@ describe('resultsToCSV', () => {
|
|||
]
|
||||
const response = resultsToCSV(results)
|
||||
const expected = {
|
||||
flag: 'ok',
|
||||
name: 'procstat',
|
||||
CSVString: `date,mean_cpu_usage\n${formatDate(
|
||||
1505262600000
|
||||
|
@ -36,10 +41,65 @@ describe('resultsToCSV', () => {
|
|||
1505264400000
|
||||
)},2.616484718180463\n${formatDate(1505266200000)},1.6174323943535571`,
|
||||
}
|
||||
expect(response).to.have.all.keys('name', 'CSVString')
|
||||
expect(response).to.have.all.keys('flag', 'name', 'CSVString')
|
||||
expect(response.flag).to.be.a('string')
|
||||
expect(response.name).to.be.a('string')
|
||||
expect(response.CSVString).to.be.a('string')
|
||||
expect(response.flag).to.equal(expected.flag)
|
||||
expect(response.name).to.equal(expected.name)
|
||||
expect(response.CSVString).to.equal(expected.CSVString)
|
||||
})
|
||||
})
|
||||
|
||||
describe('dashboardtoCSV', () => {
|
||||
it('parses the array of timeseries data displayed by the dashboard cell to a CSVstring for download', () => {
|
||||
const data = [
|
||||
{
|
||||
results: [
|
||||
{
|
||||
statement_id: 0,
|
||||
series: [
|
||||
{
|
||||
name: 'procstat',
|
||||
columns: ['time', 'mean_cpu_usage'],
|
||||
values: [
|
||||
[1505262600000, 0.06163066773148772],
|
||||
[1505264400000, 2.616484718180463],
|
||||
[1505266200000, 1.6174323943535571],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
results: [
|
||||
{
|
||||
statement_id: 0,
|
||||
series: [
|
||||
{
|
||||
name: 'procstat',
|
||||
columns: ['not-time', 'mean_cpu_usage'],
|
||||
values: [
|
||||
[1505262600000, 0.06163066773148772],
|
||||
[1505264400000, 2.616484718180463],
|
||||
[1505266200000, 1.6174323943535571],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
const result = dashboardtoCSV(data)
|
||||
const expected = `time,mean_cpu_usage,not-time,mean_cpu_usage\n${formatDate(
|
||||
1505262600000
|
||||
)},0.06163066773148772,1505262600000,0.06163066773148772\n${formatDate(
|
||||
1505264400000
|
||||
)},2.616484718180463,1505264400000,2.616484718180463\n${formatDate(
|
||||
1505266200000
|
||||
)},1.6174323943535571,1505266200000,1.6174323943535571`
|
||||
expect(result).to.be.a('string')
|
||||
expect(result).to.equal(expected)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -3,13 +3,17 @@ import classnames from 'classnames'
|
|||
import _ from 'lodash'
|
||||
|
||||
import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
|
||||
import resultsToCSV from 'src/shared/parsing/resultsToCSV.js'
|
||||
import {resultsToCSV} from 'src/shared/parsing/resultsToCSV.js'
|
||||
import download from 'src/external/download.js'
|
||||
|
||||
const getCSV = (query, errorThrown) => async () => {
|
||||
try {
|
||||
const {results} = await fetchTimeSeriesAsync({source: query.host, query})
|
||||
const {name, CSVString} = resultsToCSV(results)
|
||||
const {flag, name, CSVString} = resultsToCSV(results)
|
||||
if (flag === 'no_data') {
|
||||
errorThrown('no data', 'There are no data to download.')
|
||||
return
|
||||
}
|
||||
download(CSVString, `${name}.csv`, 'text/plain')
|
||||
} catch (error) {
|
||||
errorThrown(error, 'Unable to download .csv file')
|
||||
|
|
|
@ -59,6 +59,7 @@ const Header = React.createClass({
|
|||
<TimeRangeDropdown
|
||||
onChooseTimeRange={this.handleChooseTimeRange}
|
||||
selected={timeRange}
|
||||
page="DataExplorer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
TickscriptPage,
|
||||
} from 'src/kapacitor'
|
||||
import {AdminPage} from 'src/admin'
|
||||
import {CreateSource, SourcePage, ManageSources} from 'src/sources'
|
||||
import {SourcePage, ManageSources} from 'src/sources'
|
||||
import NotFound from 'shared/components/NotFound'
|
||||
|
||||
import {getMe} from 'shared/apis'
|
||||
|
@ -127,7 +127,7 @@ const Root = React.createClass({
|
|||
<Route path="/login" component={UserIsNotAuthenticated(Login)} />
|
||||
<Route
|
||||
path="/sources/new"
|
||||
component={UserIsAuthenticated(CreateSource)}
|
||||
component={UserIsAuthenticated(SourcePage)}
|
||||
/>
|
||||
<Route path="/sources/:sourceID" component={UserIsAuthenticated(App)}>
|
||||
<Route component={CheckSources}>
|
||||
|
|
|
@ -3,132 +3,66 @@ import buildInfluxQLQuery from 'utils/influxql'
|
|||
import AutoRefresh from 'shared/components/AutoRefresh'
|
||||
import LineGraph from 'shared/components/LineGraph'
|
||||
import TimeRangeDropdown from 'shared/components/TimeRangeDropdown'
|
||||
import underlayCallback from 'src/kapacitor/helpers/ruleGraphUnderlay'
|
||||
|
||||
const RefreshingLineGraph = AutoRefresh(LineGraph)
|
||||
|
||||
const {shape, string, func} = PropTypes
|
||||
const RuleGraph = ({
|
||||
query,
|
||||
source,
|
||||
timeRange: {lower},
|
||||
timeRange,
|
||||
rule,
|
||||
onChooseTimeRange,
|
||||
}) => {
|
||||
const autoRefreshMs = 30000
|
||||
const queryText = buildInfluxQLQuery({lower}, query)
|
||||
const queries = [{host: source.links.proxy, text: queryText}]
|
||||
const kapacitorLineColors = ['#4ED8A0']
|
||||
|
||||
export const RuleGraph = React.createClass({
|
||||
propTypes: {
|
||||
source: shape({
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
query: shape({}).isRequired,
|
||||
rule: shape({}).isRequired,
|
||||
timeRange: shape({}).isRequired,
|
||||
onChooseTimeRange: func.isRequired,
|
||||
},
|
||||
|
||||
render() {
|
||||
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}]
|
||||
const kapacitorLineColors = ['#4ED8A0']
|
||||
|
||||
if (!queryText) {
|
||||
return (
|
||||
<div className="rule-builder--graph-empty">
|
||||
<p>
|
||||
Select a <strong>Time-Series</strong> to preview on a graph
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!queryText) {
|
||||
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}
|
||||
underlayCallback={this.createUnderlayCallback()}
|
||||
isGraphFilled={false}
|
||||
overrideLineColors={kapacitorLineColors}
|
||||
ruleValues={rule.values}
|
||||
/>
|
||||
<div className="rule-builder--graph-empty">
|
||||
<p>
|
||||
Select a <strong>Time-Series</strong> to preview on a graph
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
createUnderlayCallback() {
|
||||
const {rule} = this.props
|
||||
return (canvas, area, dygraph) => {
|
||||
if (rule.trigger !== 'threshold' || rule.values.value === '') {
|
||||
return
|
||||
}
|
||||
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}
|
||||
isGraphFilled={false}
|
||||
ruleValues={rule.values}
|
||||
autoRefresh={autoRefreshMs}
|
||||
overrideLineColors={kapacitorLineColors}
|
||||
underlayCallback={underlayCallback(rule)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const theOnePercent = 0.01
|
||||
let highlightStart = 0
|
||||
let highlightEnd = 0
|
||||
|
||||
switch (rule.values.operator) {
|
||||
case 'equal to or greater':
|
||||
case 'greater than': {
|
||||
highlightStart = rule.values.value
|
||||
highlightEnd = dygraph.yAxisRange()[1]
|
||||
break
|
||||
}
|
||||
|
||||
case 'equal to or less than':
|
||||
case 'less than': {
|
||||
highlightStart = dygraph.yAxisRange()[0]
|
||||
highlightEnd = rule.values.value
|
||||
break
|
||||
}
|
||||
|
||||
case 'not equal to':
|
||||
case 'equal to': {
|
||||
const width =
|
||||
theOnePercent * (dygraph.yAxisRange()[1] - dygraph.yAxisRange()[0])
|
||||
highlightStart = +rule.values.value - width
|
||||
highlightEnd = +rule.values.value + width
|
||||
break
|
||||
}
|
||||
|
||||
case 'outside range': {
|
||||
const {rangeValue, value} = rule.values
|
||||
highlightStart = Math.min(+value, +rangeValue)
|
||||
highlightEnd = Math.max(+value, +rangeValue)
|
||||
|
||||
canvas.fillStyle = 'rgba(78, 216, 160, 0.3)'
|
||||
canvas.fillRect(area.x, area.y, area.w, area.h)
|
||||
break
|
||||
}
|
||||
case 'inside range': {
|
||||
const {rangeValue, value} = rule.values
|
||||
highlightStart = Math.min(+value, +rangeValue)
|
||||
highlightEnd = Math.max(+value, +rangeValue)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const bottom = dygraph.toDomYCoord(highlightStart)
|
||||
const top = dygraph.toDomYCoord(highlightEnd)
|
||||
|
||||
canvas.fillStyle =
|
||||
rule.values.operator === 'outside range'
|
||||
? 'rgba(41, 41, 51, 1)'
|
||||
: 'rgba(78, 216, 160, 0.3)'
|
||||
canvas.fillRect(area.x, top, area.w, bottom - top)
|
||||
}
|
||||
},
|
||||
})
|
||||
RuleGraph.propTypes = {
|
||||
source: shape({
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
query: shape({}).isRequired,
|
||||
rule: shape({}).isRequired,
|
||||
timeRange: shape({}).isRequired,
|
||||
onChooseTimeRange: func.isRequired,
|
||||
}
|
||||
|
||||
export default RuleGraph
|
||||
|
|
|
@ -4,6 +4,7 @@ import Dropdown from 'shared/components/Dropdown'
|
|||
|
||||
const mapToItems = (arr, type) => arr.map(text => ({text, type}))
|
||||
const operators = mapToItems(OPERATORS, 'operator')
|
||||
const noopSubmit = e => e.preventDefault()
|
||||
|
||||
const Threshold = ({
|
||||
rule: {values: {operator, value, rangeValue}},
|
||||
|
@ -24,7 +25,7 @@ const Threshold = ({
|
|||
selected={operator}
|
||||
onChoose={onDropdownChange}
|
||||
/>
|
||||
<form style={{display: 'flex'}}>
|
||||
<form style={{display: 'flex'}} onSubmit={noopSubmit}>
|
||||
<input
|
||||
className="form-control input-sm form-malachite monotype"
|
||||
style={{width: '160px', marginLeft: '6px'}}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
import TickscriptHeader from 'src/kapacitor/components/TickscriptHeader'
|
||||
import TickscriptEditor from 'src/kapacitor/components/TickscriptEditor'
|
||||
|
||||
|
@ -48,7 +49,9 @@ const {arrayOf, bool, func, shape, string} = PropTypes
|
|||
|
||||
Tickscript.propTypes = {
|
||||
onSave: func.isRequired,
|
||||
source: shape(),
|
||||
source: shape({
|
||||
id: string,
|
||||
}),
|
||||
task: shape({
|
||||
id: string,
|
||||
script: string,
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import {Link} from 'react-router'
|
||||
|
||||
import SourceIndicator from 'shared/components/SourceIndicator'
|
||||
import TickscriptType from 'src/kapacitor/components/TickscriptType'
|
||||
import MultiSelectDBDropdown from 'shared/components/MultiSelectDBDropdown'
|
||||
|
@ -11,6 +13,7 @@ const addName = list => list.map(l => ({...l, name: `${l.db}.${l.rp}`}))
|
|||
const TickscriptHeader = ({
|
||||
task: {id, type, dbrps},
|
||||
task,
|
||||
source,
|
||||
onSave,
|
||||
onChangeType,
|
||||
onChangeID,
|
||||
|
@ -31,6 +34,12 @@ const TickscriptHeader = ({
|
|||
selectedItems={addName(dbrps)}
|
||||
onApply={onSelectDbrps}
|
||||
/>
|
||||
<Link
|
||||
className="btn btn-sm btn-default"
|
||||
to={`/sources/${source.id}/alert-rules`}
|
||||
>
|
||||
Cancel
|
||||
</Link>
|
||||
<button
|
||||
className="btn btn-success btn-sm"
|
||||
title={id ? '' : 'ID your TICKscript to save'}
|
||||
|
@ -47,6 +56,9 @@ const {arrayOf, bool, func, shape, string} = PropTypes
|
|||
|
||||
TickscriptHeader.propTypes = {
|
||||
onSave: func,
|
||||
source: shape({
|
||||
id: string,
|
||||
}),
|
||||
onSelectDbrps: func.isRequired,
|
||||
task: shape({
|
||||
dbrps: arrayOf(
|
||||
|
|
|
@ -21,16 +21,27 @@ export const defaultRuleConfigs = {
|
|||
|
||||
export const defaultEveryFrequency = '30s'
|
||||
|
||||
// constants taken from https://github.com/influxdata/chronograf/blob/870dbc72d1a8b784eaacad5eeea79fc54968b656/kapacitor/operators.go#L13
|
||||
export const EQUAL_TO = 'equal to'
|
||||
export const LESS_THAN = 'less than'
|
||||
export const GREATER_THAN = 'greater than'
|
||||
export const NOT_EQUAL_TO = 'not equal to'
|
||||
export const INSIDE_RANGE = 'inside range'
|
||||
export const OUTSIDE_RANGE = 'outside range'
|
||||
export const EQUAL_TO_OR_GREATER_THAN = 'equal to or greater'
|
||||
export const EQUAL_TO_OR_LESS_THAN = 'equal to or less than'
|
||||
|
||||
export const OPERATORS = [
|
||||
'greater than',
|
||||
'equal to or greater',
|
||||
'equal to or less than',
|
||||
'less than',
|
||||
'equal to',
|
||||
'not equal to',
|
||||
'inside range',
|
||||
'outside range',
|
||||
GREATER_THAN,
|
||||
EQUAL_TO_OR_GREATER_THAN,
|
||||
EQUAL_TO_OR_LESS_THAN,
|
||||
LESS_THAN,
|
||||
EQUAL_TO,
|
||||
NOT_EQUAL_TO,
|
||||
INSIDE_RANGE,
|
||||
OUTSIDE_RANGE,
|
||||
]
|
||||
|
||||
// export const RELATIONS = ['once', 'more than ', 'less than'];
|
||||
export const PERIODS = ['1m', '5m', '10m', '30m', '1h', '2h', '24h']
|
||||
export const CHANGES = ['change', '% change']
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
import {
|
||||
EQUAL_TO,
|
||||
LESS_THAN,
|
||||
NOT_EQUAL_TO,
|
||||
GREATER_THAN,
|
||||
INSIDE_RANGE,
|
||||
OUTSIDE_RANGE,
|
||||
EQUAL_TO_OR_LESS_THAN,
|
||||
EQUAL_TO_OR_GREATER_THAN,
|
||||
} from 'src/kapacitor/constants'
|
||||
|
||||
const HIGHLIGHT = 'rgba(78, 216, 160, 0.3)'
|
||||
const BACKGROUND = 'rgba(41, 41, 51, 1)'
|
||||
|
||||
const getFillColor = operator => {
|
||||
const backgroundColor = BACKGROUND
|
||||
const highlightColor = HIGHLIGHT
|
||||
|
||||
if (operator === OUTSIDE_RANGE) {
|
||||
return backgroundColor
|
||||
}
|
||||
|
||||
if (operator === NOT_EQUAL_TO) {
|
||||
return backgroundColor
|
||||
}
|
||||
|
||||
return highlightColor
|
||||
}
|
||||
|
||||
const underlayCallback = rule => (canvas, area, dygraph) => {
|
||||
const {values} = rule
|
||||
const {operator, value} = values
|
||||
|
||||
if (rule.trigger !== 'threshold' || value === '' || !isFinite(value)) {
|
||||
return
|
||||
}
|
||||
|
||||
const theOnePercent = 0.01
|
||||
let highlightStart = 0
|
||||
let highlightEnd = 0
|
||||
|
||||
switch (operator) {
|
||||
case `${EQUAL_TO_OR_GREATER_THAN}`:
|
||||
case `${GREATER_THAN}`: {
|
||||
highlightStart = value
|
||||
highlightEnd = dygraph.yAxisRange()[1]
|
||||
break
|
||||
}
|
||||
|
||||
case `${EQUAL_TO_OR_LESS_THAN}`:
|
||||
case `${LESS_THAN}`: {
|
||||
highlightStart = dygraph.yAxisRange()[0]
|
||||
highlightEnd = value
|
||||
break
|
||||
}
|
||||
|
||||
case `${EQUAL_TO}`: {
|
||||
const width =
|
||||
theOnePercent * (dygraph.yAxisRange()[1] - dygraph.yAxisRange()[0])
|
||||
highlightStart = +value - width
|
||||
highlightEnd = +value + width
|
||||
break
|
||||
}
|
||||
|
||||
case `${NOT_EQUAL_TO}`: {
|
||||
const width =
|
||||
theOnePercent * (dygraph.yAxisRange()[1] - dygraph.yAxisRange()[0])
|
||||
highlightStart = +value - width
|
||||
highlightEnd = +value + width
|
||||
|
||||
canvas.fillStyle = HIGHLIGHT
|
||||
canvas.fillRect(area.x, area.y, area.w, area.h)
|
||||
break
|
||||
}
|
||||
|
||||
case `${OUTSIDE_RANGE}`: {
|
||||
highlightStart = Math.min(+value, +values.rangeValue)
|
||||
highlightEnd = Math.max(+value, +values.rangeValue)
|
||||
|
||||
canvas.fillStyle = HIGHLIGHT
|
||||
canvas.fillRect(area.x, area.y, area.w, area.h)
|
||||
break
|
||||
}
|
||||
|
||||
case `${INSIDE_RANGE}`: {
|
||||
highlightStart = Math.min(+value, +values.rangeValue)
|
||||
highlightEnd = Math.max(+value, +values.rangeValue)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const bottom = dygraph.toDomYCoord(highlightStart)
|
||||
const top = dygraph.toDomYCoord(highlightEnd)
|
||||
|
||||
const fillColor = getFillColor(operator)
|
||||
canvas.fillStyle = fillColor
|
||||
canvas.fillRect(area.x, top, area.w, bottom - top)
|
||||
}
|
||||
|
||||
export default underlayCallback
|
|
@ -51,6 +51,7 @@ const AutoRefresh = ComposedComponent => {
|
|||
}),
|
||||
}),
|
||||
editQueryStatus: func,
|
||||
grabDataForDownload: func,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
|
@ -111,7 +112,7 @@ const AutoRefresh = ComposedComponent => {
|
|||
},
|
||||
|
||||
executeQueries(queries, templates = []) {
|
||||
const {editQueryStatus} = this.props
|
||||
const {editQueryStatus, grabDataForDownload} = this.props
|
||||
const {resolution} = this.state
|
||||
|
||||
if (!queries.length) {
|
||||
|
@ -150,12 +151,14 @@ const AutoRefresh = ComposedComponent => {
|
|||
Promise.all(timeSeriesPromises).then(timeSeries => {
|
||||
const newSeries = timeSeries.map(response => ({response}))
|
||||
const lastQuerySuccessful = !this._noResultsForQuery(newSeries)
|
||||
|
||||
this.setState({
|
||||
timeSeries: newSeries,
|
||||
lastQuerySuccessful,
|
||||
isFetching: false,
|
||||
})
|
||||
if (grabDataForDownload) {
|
||||
grabDataForDownload(timeSeries)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
|
|
|
@ -8,6 +8,9 @@ const dateFormat = 'YYYY-MM-DD HH:mm'
|
|||
class CustomTimeRange extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isNow: this.props.timeRange.upper === 'now()',
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -68,6 +71,14 @@ class CustomTimeRange extends Component {
|
|||
this.upperCal.refresh()
|
||||
}
|
||||
|
||||
handleToggleNow = () => {
|
||||
this.setState({isNow: !this.state.isNow})
|
||||
}
|
||||
|
||||
handleNowOff = () => {
|
||||
this.setState({isNow: false})
|
||||
}
|
||||
|
||||
/*
|
||||
* Upper and lower time ranges are passed in with single quotes as part of
|
||||
* the string literal, i.e. "'2015-09-23T18:00:00.000Z'". Remove them
|
||||
|
@ -78,6 +89,10 @@ class CustomTimeRange extends Component {
|
|||
return ''
|
||||
}
|
||||
|
||||
if (timeRange === 'now()') {
|
||||
return moment(new Date()).format(dateFormat)
|
||||
}
|
||||
|
||||
// If the given time range is relative, create a fixed timestamp based on its value
|
||||
if (timeRange.match(/^now/)) {
|
||||
const [, duration, unitOfTime] = timeRange.match(/(\d+)(\w+)/)
|
||||
|
@ -89,10 +104,16 @@ class CustomTimeRange extends Component {
|
|||
|
||||
handleClick = () => {
|
||||
const {onApplyTimeRange, onClose} = this.props
|
||||
const {isNow} = this.state
|
||||
|
||||
const lower = this.lowerCal.getDate().toISOString()
|
||||
const upper = this.upperCal.getDate().toISOString()
|
||||
|
||||
onApplyTimeRange({lower, upper})
|
||||
if (isNow) {
|
||||
onApplyTimeRange({lower, upper: 'now()'})
|
||||
} else {
|
||||
onApplyTimeRange({lower, upper})
|
||||
}
|
||||
|
||||
if (onClose) {
|
||||
onClose()
|
||||
|
@ -142,6 +163,10 @@ class CustomTimeRange extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {isNow} = this.state
|
||||
const {page} = this.props
|
||||
const isNowDisplayed = page !== 'DataExplorer'
|
||||
|
||||
return (
|
||||
<div className="custom-time--container">
|
||||
<div className="custom-time--shortcuts">
|
||||
|
@ -159,7 +184,7 @@ class CustomTimeRange extends Component {
|
|||
<div className="custom-time--wrap">
|
||||
<div className="custom-time--dates" onClick={this.handleRefreshCals}>
|
||||
<div
|
||||
className="lower-container"
|
||||
className="custom-time--lower-container"
|
||||
ref={r => (this.lowerContainer = r)}
|
||||
>
|
||||
<input
|
||||
|
@ -170,15 +195,33 @@ class CustomTimeRange extends Component {
|
|||
/>
|
||||
</div>
|
||||
<div
|
||||
className="upper-container"
|
||||
className="custom-time--upper-container"
|
||||
ref={r => (this.upperContainer = r)}
|
||||
disabled={isNow}
|
||||
>
|
||||
{isNowDisplayed
|
||||
? <div
|
||||
className={`btn btn-xs custom-time--now ${isNow
|
||||
? 'btn-primary'
|
||||
: 'btn-default'}`}
|
||||
onClick={this.handleToggleNow}
|
||||
>
|
||||
Now
|
||||
</div>
|
||||
: null}
|
||||
<input
|
||||
className="custom-time--upper form-control input-sm"
|
||||
ref={r => (this.upper = r)}
|
||||
placeholder="to"
|
||||
onKeyUp={this.handleRefreshCals}
|
||||
disabled={isNow}
|
||||
/>
|
||||
{isNow && page !== 'DataExplorer'
|
||||
? <div
|
||||
className="custom-time--mask"
|
||||
onClick={this.handleNowOff}
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
@ -202,6 +245,7 @@ CustomTimeRange.propTypes = {
|
|||
upper: string,
|
||||
}).isRequired,
|
||||
onClose: func,
|
||||
page: string,
|
||||
}
|
||||
|
||||
export default CustomTimeRange
|
||||
|
|
|
@ -13,7 +13,7 @@ class CustomTimeRangeOverlay extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {onClose, timeRange, onApplyTimeRange} = this.props
|
||||
const {onClose, timeRange, onApplyTimeRange, page} = this.props
|
||||
|
||||
return (
|
||||
<div className="custom-time--overlay">
|
||||
|
@ -21,6 +21,7 @@ class CustomTimeRangeOverlay extends Component {
|
|||
onApplyTimeRange={onApplyTimeRange}
|
||||
timeRange={timeRange}
|
||||
onClose={onClose}
|
||||
page={page}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -36,6 +37,7 @@ CustomTimeRangeOverlay.propTypes = {
|
|||
upper: string,
|
||||
}).isRequired,
|
||||
onClose: func,
|
||||
page: string,
|
||||
}
|
||||
|
||||
export default OnClickOutside(CustomTimeRangeOverlay)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import React, {Component, PropTypes} from 'react'
|
||||
import WidgetCell from 'shared/components/WidgetCell'
|
||||
import LayoutCell from 'shared/components/LayoutCell'
|
||||
import RefreshingGraph from 'shared/components/RefreshingGraph'
|
||||
|
@ -15,6 +15,30 @@ const getSource = (cell, source, sources, defaultSource) => {
|
|||
return sources.find(src => src.links.self === s) || defaultSource
|
||||
}
|
||||
|
||||
class LayoutState extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
celldata: [],
|
||||
}
|
||||
}
|
||||
|
||||
grabDataForDownload = celldata => {
|
||||
this.setState({celldata})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {celldata} = this.state
|
||||
return (
|
||||
<Layout
|
||||
{...this.props}
|
||||
celldata={celldata}
|
||||
grabDataForDownload={this.grabDataForDownload}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const Layout = (
|
||||
{
|
||||
host,
|
||||
|
@ -33,6 +57,8 @@ const Layout = (
|
|||
resizeCoords,
|
||||
onCancelEditCell,
|
||||
onSummonOverlayTechnologies,
|
||||
grabDataForDownload,
|
||||
celldata,
|
||||
},
|
||||
{source: defaultSource}
|
||||
) =>
|
||||
|
@ -40,6 +66,7 @@ const Layout = (
|
|||
cell={cell}
|
||||
isEditable={isEditable}
|
||||
onEditCell={onEditCell}
|
||||
celldata={celldata}
|
||||
onDeleteCell={onDeleteCell}
|
||||
onCancelEditCell={onCancelEditCell}
|
||||
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
|
||||
|
@ -56,6 +83,7 @@ const Layout = (
|
|||
templates={templates}
|
||||
autoRefresh={autoRefresh}
|
||||
synchronizer={synchronizer}
|
||||
grabDataForDownload={grabDataForDownload}
|
||||
resizeCoords={resizeCoords}
|
||||
queries={buildQueriesForLayouts(
|
||||
cell,
|
||||
|
@ -72,7 +100,7 @@ Layout.contextTypes = {
|
|||
source: shape(),
|
||||
}
|
||||
|
||||
Layout.propTypes = {
|
||||
const propTypes = {
|
||||
autoRefresh: number.isRequired,
|
||||
timeRange: shape({
|
||||
lower: string.isRequired,
|
||||
|
@ -115,4 +143,11 @@ Layout.propTypes = {
|
|||
sources: arrayOf(shape()),
|
||||
}
|
||||
|
||||
export default Layout
|
||||
LayoutState.propTypes = {...propTypes}
|
||||
Layout.propTypes = {
|
||||
...propTypes,
|
||||
grabDataForDownload: func,
|
||||
celldata: arrayOf(shape()),
|
||||
}
|
||||
|
||||
export default LayoutState
|
||||
|
|
|
@ -3,6 +3,9 @@ import _ from 'lodash'
|
|||
|
||||
import LayoutCellMenu from 'shared/components/LayoutCellMenu'
|
||||
import LayoutCellHeader from 'shared/components/LayoutCellHeader'
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
import {dashboardtoCSV} from 'shared/parsing/resultsToCSV'
|
||||
import download from 'src/external/download.js'
|
||||
|
||||
class LayoutCell extends Component {
|
||||
constructor(props) {
|
||||
|
@ -30,8 +33,19 @@ class LayoutCell extends Component {
|
|||
this.props.onSummonOverlayTechnologies(cell)
|
||||
}
|
||||
|
||||
handleCSVDownload = cell => () => {
|
||||
const joinedName = cell.name.split(' ').join('_')
|
||||
const {celldata} = this.props
|
||||
try {
|
||||
download(dashboardtoCSV(celldata), `${joinedName}.csv`, 'text/plain')
|
||||
} catch (error) {
|
||||
errorThrown(error, 'Unable to download .csv file')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {cell, children, isEditable} = this.props
|
||||
const {cell, children, isEditable, celldata} = this.props
|
||||
|
||||
const {isDeleting} = this.state
|
||||
const queries = _.get(cell, ['queries'], [])
|
||||
|
@ -40,12 +54,14 @@ class LayoutCell extends Component {
|
|||
<div className="dash-graph">
|
||||
<LayoutCellMenu
|
||||
cell={cell}
|
||||
dataExists={!!celldata.length}
|
||||
isDeleting={isDeleting}
|
||||
isEditable={isEditable}
|
||||
onDelete={this.handleDeleteCell}
|
||||
onEdit={this.handleSummonOverlay}
|
||||
handleClickOutside={this.closeMenu}
|
||||
onDeleteClick={this.handleDeleteClick}
|
||||
onCSVDownload={this.handleCSVDownload}
|
||||
/>
|
||||
<LayoutCellHeader
|
||||
queries={queries}
|
||||
|
@ -84,6 +100,7 @@ LayoutCell.propTypes = {
|
|||
onSummonOverlayTechnologies: func,
|
||||
isEditable: bool,
|
||||
onCancelEditCell: func,
|
||||
celldata: arrayOf(shape()),
|
||||
}
|
||||
|
||||
export default LayoutCell
|
||||
|
|
|
@ -2,7 +2,15 @@ import React, {PropTypes} from 'react'
|
|||
import OnClickOutside from 'react-onclickoutside'
|
||||
|
||||
const LayoutCellMenu = OnClickOutside(
|
||||
({isDeleting, onEdit, onDeleteClick, onDelete, cell}) =>
|
||||
({
|
||||
isDeleting,
|
||||
onEdit,
|
||||
onDeleteClick,
|
||||
onDelete,
|
||||
onCSVDownload,
|
||||
dataExists,
|
||||
cell,
|
||||
}) =>
|
||||
<div
|
||||
className={
|
||||
isDeleting
|
||||
|
@ -13,6 +21,14 @@ const LayoutCellMenu = OnClickOutside(
|
|||
<div className="dash-graph-context--button" onClick={onEdit(cell)}>
|
||||
<span className="icon pencil" />
|
||||
</div>
|
||||
{dataExists
|
||||
? <div
|
||||
className="dash-graph-context--button"
|
||||
onClick={onCSVDownload(cell)}
|
||||
>
|
||||
<span className="icon download" />
|
||||
</div>
|
||||
: null}
|
||||
{isDeleting
|
||||
? <div className="dash-graph-context--button active">
|
||||
<span className="icon trash" />
|
||||
|
@ -46,6 +62,7 @@ LayoutCellMenuContainer.propTypes = {
|
|||
onDeleteClick: func,
|
||||
cell: shape(),
|
||||
isEditable: bool,
|
||||
dataExists: bool,
|
||||
}
|
||||
|
||||
LayoutCellMenu.propTypes = LayoutCellMenuContainer.propTypes
|
||||
|
|
|
@ -21,6 +21,7 @@ const RefreshingGraph = ({
|
|||
synchronizer,
|
||||
resizeCoords,
|
||||
editQueryStatus,
|
||||
grabDataForDownload,
|
||||
}) => {
|
||||
if (!queries.length) {
|
||||
return (
|
||||
|
@ -53,6 +54,7 @@ const RefreshingGraph = ({
|
|||
axes={axes}
|
||||
onZoom={onZoom}
|
||||
queries={queries}
|
||||
grabDataForDownload={grabDataForDownload}
|
||||
templates={templates}
|
||||
timeRange={timeRange}
|
||||
autoRefresh={autoRefresh}
|
||||
|
@ -82,6 +84,7 @@ RefreshingGraph.propTypes = {
|
|||
editQueryStatus: func,
|
||||
onZoom: func,
|
||||
resizeCoords: shape(),
|
||||
grabDataForDownload: func,
|
||||
}
|
||||
|
||||
export default RefreshingGraph
|
||||
|
|
|
@ -9,7 +9,9 @@ import CustomTimeRangeOverlay from 'shared/components/CustomTimeRangeOverlay'
|
|||
import timeRanges from 'hson!shared/data/timeRanges.hson'
|
||||
import {DROPDOWN_MENU_MAX_HEIGHT} from 'shared/constants/index'
|
||||
|
||||
const dateFormat = 'YYYY-MM-DD HH:mm'
|
||||
const emptyTime = {lower: '', upper: ''}
|
||||
const format = t => moment(t.replace(/\'/g, '')).format(dateFormat)
|
||||
|
||||
class TimeRangeDropdown extends Component {
|
||||
constructor(props) {
|
||||
|
@ -29,8 +31,10 @@ class TimeRangeDropdown extends Component {
|
|||
|
||||
findTimeRangeInputValue = ({upper, lower}) => {
|
||||
if (upper && lower) {
|
||||
const format = t =>
|
||||
moment(t.replace(/\'/g, '')).format('YYYY-MM-DD HH:mm')
|
||||
if (upper === 'now()') {
|
||||
return `${format(lower)} - Now`
|
||||
}
|
||||
|
||||
return `${format(lower)} - ${format(upper)}`
|
||||
}
|
||||
|
||||
|
@ -44,7 +48,7 @@ class TimeRangeDropdown extends Component {
|
|||
|
||||
handleSelection = timeRange => () => {
|
||||
this.props.onChooseTimeRange(timeRange)
|
||||
this.setState({isOpen: false})
|
||||
this.setState({customTimeRange: emptyTime, isOpen: false})
|
||||
}
|
||||
|
||||
toggleMenu = () => {
|
||||
|
@ -69,16 +73,18 @@ class TimeRangeDropdown extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {selected, preventCustomTimeRange} = this.props
|
||||
const {selected, preventCustomTimeRange, page} = this.props
|
||||
const {isOpen, customTimeRange, isCustomTimeRangeOpen} = this.state
|
||||
const isRelativeTimeRange = selected.upper === null
|
||||
const isNow = selected.upper === 'now()'
|
||||
|
||||
return (
|
||||
<div className="time-range-dropdown">
|
||||
<div
|
||||
className={classnames('dropdown', {
|
||||
'dropdown-160': isRelativeTimeRange,
|
||||
'dropdown-290': !isRelativeTimeRange,
|
||||
'dropdown-210': isNow,
|
||||
'dropdown-290': !isRelativeTimeRange && !isNow,
|
||||
open: isOpen,
|
||||
})}
|
||||
>
|
||||
|
@ -136,6 +142,7 @@ class TimeRangeDropdown extends Component {
|
|||
isVisible={isCustomTimeRangeOpen}
|
||||
onToggle={this.handleToggleCustomTimeRange}
|
||||
onClose={this.handleCloseCustomTimeRange}
|
||||
page={page}
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
|
@ -145,6 +152,10 @@ class TimeRangeDropdown extends Component {
|
|||
|
||||
const {bool, func, shape, string} = PropTypes
|
||||
|
||||
TimeRangeDropdown.defaultProps = {
|
||||
page: 'default',
|
||||
}
|
||||
|
||||
TimeRangeDropdown.propTypes = {
|
||||
selected: shape({
|
||||
lower: string,
|
||||
|
@ -152,6 +163,7 @@ TimeRangeDropdown.propTypes = {
|
|||
}).isRequired,
|
||||
onChooseTimeRange: func.isRequired,
|
||||
preventCustomTimeRange: bool,
|
||||
page: string,
|
||||
}
|
||||
|
||||
export default OnClickOutside(TimeRangeDropdown)
|
||||
|
|
|
@ -411,3 +411,14 @@ export const PAGE_HEADER_HEIGHT = 60 // TODO: get this dynamically to ensure lon
|
|||
export const PAGE_CONTAINER_MARGIN = 30 // TODO: get this dynamically to ensure longevity
|
||||
export const LAYOUT_MARGIN = 4
|
||||
export const DASHBOARD_LAYOUT_ROW_HEIGHT = 83.5
|
||||
|
||||
export const DEFAULT_SOURCE = {
|
||||
url: 'http://localhost:8086',
|
||||
name: 'Influx 1',
|
||||
username: '',
|
||||
password: '',
|
||||
default: true,
|
||||
telegraf: 'telegraf',
|
||||
insecureSkipVerify: false,
|
||||
metaUrl: '',
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ import BigNumber from 'bignumber.js'
|
|||
const ADD_FACTOR = 1.1
|
||||
const SUB_FACTOR = 0.9
|
||||
|
||||
const checkNumeric = num => (isFinite(num) ? num : null)
|
||||
|
||||
const considerEmpty = (userNumber, number) => {
|
||||
if (userNumber) {
|
||||
return +userNumber
|
||||
|
@ -17,13 +19,15 @@ const getRange = (
|
|||
ruleValues = {value: null, rangeValue: null, operator: ''}
|
||||
) => {
|
||||
const {value, rangeValue, operator} = ruleValues
|
||||
const [userMin, userMax] = userSelectedRange
|
||||
const [uMin, uMax] = userSelectedRange
|
||||
const userMin = checkNumeric(uMin)
|
||||
const userMax = checkNumeric(uMax)
|
||||
|
||||
const addPad = bigNum => bigNum.times(ADD_FACTOR).toNumber()
|
||||
const subPad = bigNum => bigNum.times(SUB_FACTOR).toNumber()
|
||||
|
||||
const pad = v => {
|
||||
if (v === null || v === '' || v === undefined) {
|
||||
if (v === null || v === '' || !isFinite(v)) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
@ -4,20 +4,50 @@ import moment from 'moment'
|
|||
export const formatDate = timestamp =>
|
||||
moment(timestamp).format('M/D/YYYY h:mm:ss A')
|
||||
|
||||
const resultsToCSV = results => {
|
||||
const {name, columns, values} = _.get(results, ['0', 'series', '0'], {})
|
||||
const [, ...cols] = columns
|
||||
export const resultsToCSV = results => {
|
||||
if (!_.get(results, ['0', 'series', '0'])) {
|
||||
return {flag: 'no_data', name: '', CSVString: ''}
|
||||
}
|
||||
|
||||
const CSVString = [['date', ...cols].join(',')]
|
||||
.concat(
|
||||
values.map(([timestamp, ...measurements]) =>
|
||||
// MS Excel format
|
||||
[formatDate(timestamp), ...measurements].join(',')
|
||||
const {name, columns, values} = _.get(results, ['0', 'series', '0'])
|
||||
|
||||
if (columns[0] === 'time') {
|
||||
const [, ...cols] = columns
|
||||
const CSVString = [['date', ...cols].join(',')]
|
||||
.concat(
|
||||
values.map(([timestamp, ...measurements]) =>
|
||||
// MS Excel format
|
||||
[formatDate(timestamp), ...measurements].join(',')
|
||||
)
|
||||
)
|
||||
)
|
||||
.join('\n')
|
||||
.join('\n')
|
||||
return {flag: 'ok', name, CSVString}
|
||||
}
|
||||
|
||||
return {name, CSVString}
|
||||
const CSVString = [columns.join(',')]
|
||||
.concat(values.map(row => row.join(',')))
|
||||
.join('\n')
|
||||
return {flag: 'ok', name, CSVString}
|
||||
}
|
||||
|
||||
export default resultsToCSV
|
||||
export const dashboardtoCSV = data => {
|
||||
const columnNames = _.flatten(
|
||||
data.map(r => _.get(r, 'results[0].series[0].columns', []))
|
||||
)
|
||||
const timeIndices = columnNames
|
||||
.map((e, i) => (e === 'time' ? i : -1))
|
||||
.filter(e => e >= 0)
|
||||
|
||||
let values = data.map(r => _.get(r, 'results[0].series[0].values', []))
|
||||
values = _.unzip(values).map(v => _.flatten(v))
|
||||
if (timeIndices) {
|
||||
values.map(v => {
|
||||
timeIndices.forEach(i => (v[i] = formatDate(v[i])))
|
||||
return v
|
||||
})
|
||||
}
|
||||
const CSVString = [columnNames.join(',')]
|
||||
.concat(values.map(v => v.join(',')))
|
||||
.join('\n')
|
||||
return CSVString
|
||||
}
|
||||
|
|
|
@ -8,32 +8,8 @@ class SourceForm extends Component {
|
|||
super(props)
|
||||
}
|
||||
|
||||
handleSubmitForm = e => {
|
||||
e.preventDefault()
|
||||
const newSource = {
|
||||
...this.props.source,
|
||||
url: this.sourceURL.value.trim(),
|
||||
name: this.sourceName.value,
|
||||
username: this.sourceUsername.value,
|
||||
password: this.sourcePassword.value,
|
||||
default: this.sourceDefault.checked,
|
||||
telegraf: this.sourceTelegraf.value,
|
||||
insecureSkipVerify: this.sourceInsecureSkipVerify
|
||||
? this.sourceInsecureSkipVerify.checked
|
||||
: false,
|
||||
metaUrl: this.metaUrl && this.metaUrl.value.trim(),
|
||||
}
|
||||
|
||||
this.props.onSubmit(newSource)
|
||||
}
|
||||
|
||||
handleUseDefaultValues = () => {
|
||||
this.sourceURL.value = 'http://localhost:8086'
|
||||
this.sourceName.value = 'My InfluxDB'
|
||||
}
|
||||
|
||||
handleBlurSourceURL = () => {
|
||||
const url = this.sourceURL.value.trim()
|
||||
const url = this.props.source.url.trim()
|
||||
|
||||
if (!url) {
|
||||
return
|
||||
|
@ -41,32 +17,31 @@ class SourceForm extends Component {
|
|||
|
||||
const newSource = {
|
||||
...this.props.source,
|
||||
url: this.sourceURL.value.trim(),
|
||||
url,
|
||||
}
|
||||
|
||||
this.props.onBlurSourceURL(newSource)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {source, editMode, onInputChange} = this.props
|
||||
const {source, editMode, onSubmit, onInputChange} = this.props
|
||||
|
||||
return (
|
||||
<div className="panel-body">
|
||||
<h4 className="text-center">Connection Details</h4>
|
||||
<br />
|
||||
|
||||
<form onSubmit={this.handleSubmitForm}>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="form-group col-xs-12 col-sm-6">
|
||||
<label htmlFor="connect-string">Connection String</label>
|
||||
<input
|
||||
type="text"
|
||||
name="url"
|
||||
ref={r => (this.sourceURL = r)}
|
||||
className="form-control"
|
||||
id="connect-string"
|
||||
placeholder="Address of InfluxDB"
|
||||
onChange={onInputChange}
|
||||
value={source.url || ''}
|
||||
value={source.url}
|
||||
onBlur={this.handleBlurSourceURL}
|
||||
required={true}
|
||||
/>
|
||||
|
@ -76,12 +51,11 @@ class SourceForm extends Component {
|
|||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
ref={r => (this.sourceName = r)}
|
||||
className="form-control"
|
||||
id="name"
|
||||
placeholder="Name this source"
|
||||
onChange={onInputChange}
|
||||
value={source.name || ''}
|
||||
value={source.name}
|
||||
required={true}
|
||||
/>
|
||||
</div>
|
||||
|
@ -90,11 +64,10 @@ class SourceForm extends Component {
|
|||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
ref={r => (this.sourceUsername = r)}
|
||||
className="form-control"
|
||||
id="username"
|
||||
onChange={onInputChange}
|
||||
value={source.username || ''}
|
||||
value={source.username}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-xs-12 col-sm-6">
|
||||
|
@ -102,11 +75,10 @@ class SourceForm extends Component {
|
|||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
ref={r => (this.sourcePassword = r)}
|
||||
className="form-control"
|
||||
id="password"
|
||||
onChange={onInputChange}
|
||||
value={source.password || ''}
|
||||
value={source.password}
|
||||
/>
|
||||
</div>
|
||||
{_.get(source, 'type', '').includes('enterprise')
|
||||
|
@ -115,12 +87,11 @@ class SourceForm extends Component {
|
|||
<input
|
||||
type="text"
|
||||
name="metaUrl"
|
||||
ref={r => (this.metaUrl = r)}
|
||||
className="form-control"
|
||||
id="meta-url"
|
||||
placeholder="http://localhost:8091"
|
||||
onChange={onInputChange}
|
||||
value={source.metaUrl || ''}
|
||||
value={source.metaUrl}
|
||||
/>
|
||||
</div>
|
||||
: null}
|
||||
|
@ -129,11 +100,10 @@ class SourceForm extends Component {
|
|||
<input
|
||||
type="text"
|
||||
name="telegraf"
|
||||
ref={r => (this.sourceTelegraf = r)}
|
||||
className="form-control"
|
||||
id="telegraf"
|
||||
onChange={onInputChange}
|
||||
value={source.telegraf || 'telegraf'}
|
||||
value={source.telegraf}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-xs-12">
|
||||
|
@ -141,8 +111,9 @@ class SourceForm extends Component {
|
|||
<input
|
||||
type="checkbox"
|
||||
id="defaultSourceCheckbox"
|
||||
defaultChecked={source.default}
|
||||
ref={r => (this.sourceDefault = r)}
|
||||
name="default"
|
||||
checked={source.default}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
<label htmlFor="defaultSourceCheckbox">
|
||||
Make this the default source
|
||||
|
@ -155,8 +126,9 @@ class SourceForm extends Component {
|
|||
<input
|
||||
type="checkbox"
|
||||
id="insecureSkipVerifyCheckbox"
|
||||
defaultChecked={source.insecureSkipVerify}
|
||||
ref={r => (this.sourceInsecureSkipVerify = r)}
|
||||
name="insecureSkipVerify"
|
||||
checked={source.insecureSkipVerify}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
<label htmlFor="insecureSkipVerifyCheckbox">Unsafe SSL</label>
|
||||
</div>
|
||||
|
@ -176,13 +148,6 @@ class SourceForm extends Component {
|
|||
{editMode ? 'Save Changes' : 'Add Source'}
|
||||
</button>
|
||||
<br />
|
||||
<a
|
||||
href="#"
|
||||
className="btn btn-link btn-sm"
|
||||
onClick={this.handleUseDefaultValues}
|
||||
>
|
||||
Use Default Values
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -190,10 +155,19 @@ class SourceForm extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
const {bool, func, shape} = PropTypes
|
||||
const {bool, func, shape, string} = PropTypes
|
||||
|
||||
SourceForm.propTypes = {
|
||||
source: shape({}).isRequired,
|
||||
source: shape({
|
||||
url: string.isRequired,
|
||||
name: string.isRequired,
|
||||
username: string.isRequired,
|
||||
password: string.isRequired,
|
||||
telegraf: string.isRequired,
|
||||
insecureSkipVerify: bool.isRequired,
|
||||
default: bool.isRequired,
|
||||
metaUrl: string.isRequired,
|
||||
}).isRequired,
|
||||
editMode: bool.isRequired,
|
||||
onInputChange: func.isRequired,
|
||||
onSubmit: func.isRequired,
|
||||
|
|
|
@ -1,149 +0,0 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import {withRouter} from 'react-router'
|
||||
import {connect} from 'react-redux'
|
||||
import {bindActionCreators} from 'redux'
|
||||
|
||||
import {
|
||||
createSource as createSourceAJAX,
|
||||
updateSource as updateSourceAJAX,
|
||||
} from 'shared/apis'
|
||||
import SourceForm from 'src/sources/components/SourceForm'
|
||||
import Notifications from 'shared/components/Notifications'
|
||||
import {
|
||||
addSource as addSourceAction,
|
||||
updateSource as updateSourceAction,
|
||||
} from 'shared/actions/sources'
|
||||
import {publishNotification} from 'shared/actions/notifications'
|
||||
|
||||
const {func, shape, string} = PropTypes
|
||||
|
||||
export const CreateSource = React.createClass({
|
||||
propTypes: {
|
||||
router: shape({
|
||||
push: func.isRequired,
|
||||
}).isRequired,
|
||||
location: shape({
|
||||
query: shape({
|
||||
redirectPath: string,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
addSource: func,
|
||||
updateSource: func,
|
||||
notify: func,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
source: {},
|
||||
}
|
||||
},
|
||||
|
||||
redirectToApp(source) {
|
||||
const {redirectPath} = this.props.location.query
|
||||
if (!redirectPath) {
|
||||
return this.props.router.push(`/sources/${source.id}/hosts`)
|
||||
}
|
||||
|
||||
const fixedPath = redirectPath.replace(
|
||||
/\/sources\/[^/]*/,
|
||||
`/sources/${source.id}`
|
||||
)
|
||||
return this.props.router.push(fixedPath)
|
||||
},
|
||||
|
||||
handleInputChange(e) {
|
||||
const val = e.target.value
|
||||
const name = e.target.name
|
||||
this.setState(prevState => {
|
||||
const newSource = Object.assign({}, prevState.source, {
|
||||
[name]: val,
|
||||
})
|
||||
return Object.assign({}, prevState, {source: newSource})
|
||||
})
|
||||
},
|
||||
|
||||
handleBlurSourceURL(newSource) {
|
||||
if (this.state.editMode) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!newSource.url) {
|
||||
return
|
||||
}
|
||||
|
||||
// if there is a type on source it has already been created
|
||||
if (newSource.type) {
|
||||
return
|
||||
}
|
||||
|
||||
createSourceAJAX(newSource)
|
||||
.then(({data: sourceFromServer}) => {
|
||||
this.props.addSource(sourceFromServer)
|
||||
this.setState({source: sourceFromServer, error: null})
|
||||
})
|
||||
.catch(({data: error}) => {
|
||||
this.setState({error: error.message})
|
||||
})
|
||||
},
|
||||
|
||||
handleSubmit(newSource) {
|
||||
const {error} = this.state
|
||||
const {notify, updateSource} = this.props
|
||||
|
||||
if (error) {
|
||||
return notify('error', error)
|
||||
}
|
||||
|
||||
updateSourceAJAX(newSource)
|
||||
.then(({data: sourceFromServer}) => {
|
||||
updateSource(sourceFromServer)
|
||||
this.redirectToApp(sourceFromServer)
|
||||
})
|
||||
.catch(() => {
|
||||
notify(
|
||||
'error',
|
||||
'There was a problem updating the source. Check the settings'
|
||||
)
|
||||
})
|
||||
},
|
||||
|
||||
render() {
|
||||
const {source} = this.state
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Notifications />
|
||||
<div className="select-source-page">
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-md-8 col-md-offset-2">
|
||||
<div className="panel panel-minimal">
|
||||
<div className="panel-heading text-center">
|
||||
<h2 className="deluxe">Welcome to Chronograf</h2>
|
||||
</div>
|
||||
<SourceForm
|
||||
source={source}
|
||||
editMode={false}
|
||||
onInputChange={this.handleInputChange}
|
||||
onSubmit={this.handleSubmit}
|
||||
onBlurSourceURL={this.handleBlurSourceURL}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
addSource: bindActionCreators(addSourceAction, dispatch),
|
||||
updateSource: bindActionCreators(updateSourceAction, dispatch),
|
||||
notify: bindActionCreators(publishNotification, dispatch),
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(withRouter(CreateSource))
|
|
@ -1,67 +1,73 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import React, {PropTypes, Component} from 'react'
|
||||
import {withRouter} from 'react-router'
|
||||
import _ from 'lodash'
|
||||
import {getSource} from 'shared/apis'
|
||||
import {createSource, updateSource} from 'shared/apis'
|
||||
import {
|
||||
addSource as addSourceAction,
|
||||
updateSource as updateSourceAction,
|
||||
} from 'shared/actions/sources'
|
||||
import {publishNotification} from 'shared/actions/notifications'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
import SourceForm from 'src/sources/components/SourceForm'
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
import SourceIndicator from 'shared/components/SourceIndicator'
|
||||
import {DEFAULT_SOURCE} from 'shared/constants'
|
||||
const initialPath = '/sources/new'
|
||||
|
||||
const {func, shape, string} = PropTypes
|
||||
class SourcePage extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
export const SourcePage = React.createClass({
|
||||
propTypes: {
|
||||
params: shape({
|
||||
id: string,
|
||||
sourceID: string,
|
||||
}),
|
||||
router: shape({
|
||||
push: func.isRequired,
|
||||
}).isRequired,
|
||||
location: shape({
|
||||
query: shape({
|
||||
redirectPath: string,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
addFlashMessage: func.isRequired,
|
||||
addSourceAction: func,
|
||||
updateSourceAction: func,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
source: {},
|
||||
editMode: this.props.params.id !== undefined,
|
||||
error: '',
|
||||
this.state = {
|
||||
isLoading: true,
|
||||
source: DEFAULT_SOURCE,
|
||||
editMode: props.params.id !== undefined,
|
||||
isInitialSource: props.router.location.pathname === initialPath,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.state.editMode) {
|
||||
return
|
||||
const {editMode} = this.state
|
||||
const {params} = this.props
|
||||
|
||||
if (!editMode) {
|
||||
return this.setState({isLoading: false})
|
||||
}
|
||||
getSource(this.props.params.id).then(({data: source}) => {
|
||||
this.setState({source})
|
||||
})
|
||||
},
|
||||
|
||||
handleInputChange(e) {
|
||||
const val = e.target.value
|
||||
const name = e.target.name
|
||||
this.setState(prevState => {
|
||||
const newSource = Object.assign({}, prevState.source, {
|
||||
[name]: val,
|
||||
getSource(params.id)
|
||||
.then(({data: source}) => {
|
||||
this.setState({
|
||||
source: {...DEFAULT_SOURCE, ...source},
|
||||
isLoading: false,
|
||||
})
|
||||
})
|
||||
return Object.assign({}, prevState, {source: newSource})
|
||||
})
|
||||
},
|
||||
.catch(error => {
|
||||
this.handleError('Could not connect to source', error)
|
||||
this.setState({isLoading: false})
|
||||
})
|
||||
}
|
||||
|
||||
handleBlurSourceURL(newSource) {
|
||||
handleInputChange = e => {
|
||||
let val = e.target.value
|
||||
const name = e.target.name
|
||||
|
||||
if (e.target.type === 'checkbox') {
|
||||
val = e.target.checked
|
||||
}
|
||||
|
||||
this.setState(prevState => {
|
||||
const source = {
|
||||
...prevState.source,
|
||||
[name]: val,
|
||||
}
|
||||
|
||||
return {...prevState, source}
|
||||
})
|
||||
}
|
||||
|
||||
handleBlurSourceURL = newSource => {
|
||||
if (this.state.editMode) {
|
||||
return
|
||||
}
|
||||
|
@ -78,57 +84,108 @@ export const SourcePage = React.createClass({
|
|||
createSource(newSource)
|
||||
.then(({data: sourceFromServer}) => {
|
||||
this.props.addSourceAction(sourceFromServer)
|
||||
this.setState({source: sourceFromServer, error: null})
|
||||
})
|
||||
.catch(({data: error}) => {
|
||||
// dont want to flash this until they submit
|
||||
this.setState({error: error.message})
|
||||
})
|
||||
},
|
||||
|
||||
handleSubmit(newSource) {
|
||||
const {router, params, addFlashMessage} = this.props
|
||||
const {error} = this.state
|
||||
|
||||
if (error) {
|
||||
return addFlashMessage({type: 'error', text: error})
|
||||
}
|
||||
|
||||
updateSource(newSource)
|
||||
.then(({data: sourceFromServer}) => {
|
||||
this.props.updateSourceAction(sourceFromServer)
|
||||
router.push(`/sources/${params.sourceID}/manage-sources`)
|
||||
addFlashMessage({type: 'success', text: 'The source info saved'})
|
||||
})
|
||||
.catch(() => {
|
||||
addFlashMessage({
|
||||
type: 'error',
|
||||
text: 'There was a problem updating the source. Check the settings',
|
||||
this.setState({
|
||||
source: {...DEFAULT_SOURCE, ...sourceFromServer},
|
||||
isCreated: true,
|
||||
})
|
||||
})
|
||||
},
|
||||
.catch(err => {
|
||||
// dont want to flash this until they submit
|
||||
const error = this.parseError(err)
|
||||
console.error('Error on source creation: ', error)
|
||||
})
|
||||
}
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault()
|
||||
const {notify} = this.props
|
||||
const {isCreated, source, editMode} = this.state
|
||||
const isNewSource = !editMode
|
||||
|
||||
if (!isCreated && isNewSource) {
|
||||
return createSource(source)
|
||||
.then(({data: sourceFromServer}) => {
|
||||
this.props.addSourceAction(sourceFromServer)
|
||||
this._redirect(sourceFromServer)
|
||||
})
|
||||
.catch(error => {
|
||||
this.handleError('Unable to create source', error)
|
||||
})
|
||||
}
|
||||
|
||||
updateSource(source)
|
||||
.then(({data: sourceFromServer}) => {
|
||||
this.props.updateSourceAction(sourceFromServer)
|
||||
this._redirect(sourceFromServer)
|
||||
notify('success', `New source ${source.name} added`)
|
||||
})
|
||||
.catch(error => {
|
||||
this.handleError('Unable to update source', error)
|
||||
})
|
||||
}
|
||||
|
||||
handleError = (bannerText, err) => {
|
||||
const {notify} = this.props
|
||||
const error = this.parseError(err)
|
||||
console.error('Error: ', error)
|
||||
notify('error', `${bannerText}: ${error}`)
|
||||
}
|
||||
|
||||
_redirect = source => {
|
||||
const {isInitialSource} = this.state
|
||||
const {params, router} = this.props
|
||||
|
||||
if (isInitialSource) {
|
||||
return this._redirectToApp(source)
|
||||
}
|
||||
|
||||
router.push(`/sources/${params.sourceID}/manage-sources`)
|
||||
}
|
||||
|
||||
_redirectToApp = source => {
|
||||
const {location, router} = this.props
|
||||
const {redirectPath} = location.query
|
||||
|
||||
if (!redirectPath) {
|
||||
return router.push(`/sources/${source.id}/hosts`)
|
||||
}
|
||||
|
||||
const fixedPath = redirectPath.replace(
|
||||
/\/sources\/[^/]*/,
|
||||
`/sources/${source.id}`
|
||||
)
|
||||
return router.push(fixedPath)
|
||||
}
|
||||
|
||||
parseError = error => {
|
||||
return _.get(error, ['data', 'message'], error)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {source, editMode} = this.state
|
||||
const {isLoading, source, editMode, isInitialSource} = this.state
|
||||
|
||||
if (editMode && !source.id) {
|
||||
if (isLoading) {
|
||||
return <div className="page-spinner" />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page" id="source-form-page">
|
||||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">
|
||||
{editMode ? 'Edit Source' : 'Add a New Source'}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<SourceIndicator />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${isInitialSource ? '' : 'page'}`}>
|
||||
{isInitialSource
|
||||
? null
|
||||
: <div className="page-header">
|
||||
<div className="page-header__container page-header__source-page">
|
||||
<div className="page-header__col-md-8">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">
|
||||
{editMode ? 'Edit Source' : 'Add a New Source'}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<SourceIndicator />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
<FancyScrollbar className="page-contents">
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
|
@ -148,13 +205,33 @@ export const SourcePage = React.createClass({
|
|||
</FancyScrollbar>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
function mapStateToProps(_) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, {addSourceAction, updateSourceAction})(
|
||||
withRouter(SourcePage)
|
||||
)
|
||||
const {func, shape, string} = PropTypes
|
||||
|
||||
SourcePage.propTypes = {
|
||||
params: shape({
|
||||
id: string,
|
||||
sourceID: string,
|
||||
}),
|
||||
router: shape({
|
||||
push: func.isRequired,
|
||||
}).isRequired,
|
||||
location: shape({
|
||||
query: shape({
|
||||
redirectPath: string,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
notify: func,
|
||||
addSourceAction: func,
|
||||
updateSourceAction: func,
|
||||
}
|
||||
|
||||
const mapStateToProps = () => ({})
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
notify: publishNotification,
|
||||
addSourceAction,
|
||||
updateSourceAction,
|
||||
})(withRouter(SourcePage))
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import CreateSource from './containers/CreateSource'
|
||||
import SourcePage from './containers/SourcePage'
|
||||
import ManageSources from './containers/ManageSources'
|
||||
export {CreateSource, SourcePage, ManageSources}
|
||||
export {SourcePage, ManageSources}
|
||||
|
|
|
@ -33,6 +33,10 @@
|
|||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.custom-time--upper-container,
|
||||
.custom-time--lower-container {
|
||||
position: relative;
|
||||
}
|
||||
.custom-time--lower {
|
||||
margin-right: 4px;
|
||||
text-align: center;
|
||||
|
@ -41,6 +45,21 @@
|
|||
margin-left: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
.custom-time--mask {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: calc(100% - 30px);
|
||||
top: 30px;
|
||||
left: 0;
|
||||
opacity: 0.5;
|
||||
background-color: $g5-pepper;
|
||||
z-index: 2;
|
||||
}
|
||||
.custom-time--now {
|
||||
position: absolute !important;
|
||||
right: 0;
|
||||
top: 4px;
|
||||
}
|
||||
.custom-time--shortcuts {
|
||||
@include no-user-select();
|
||||
align-items: stretch;
|
||||
|
|
|
@ -23,6 +23,7 @@ $page-header-weight: 400 !important;
|
|||
max-width: 100%;
|
||||
}
|
||||
.page-header__container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -31,6 +32,26 @@ $page-header-weight: 400 !important;
|
|||
width: 100%;
|
||||
max-width: ($page-wrapper-max-width - $page-wrapper-padding - $page-wrapper-padding);
|
||||
}
|
||||
.page-header__container.page-header__source-page {
|
||||
justify-content: center;
|
||||
}
|
||||
.page-header__col-md-8 {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
@media screen and (min-width: 992px) {
|
||||
/*
|
||||
NOTE:
|
||||
Breakpoint and % width are based on the bootstrap grid
|
||||
If the source form column sizing is ever changed, this
|
||||
will have to be manually updated
|
||||
*/
|
||||
.page-header__col-md-8 {
|
||||
width: 66.66667%;
|
||||
}
|
||||
}
|
||||
.page-header__left,
|
||||
.page-header__right {
|
||||
display: flex;
|
||||
|
|
|
@ -22,6 +22,11 @@
|
|||
width: 100%;
|
||||
height: calc(100% - #{$chronograf-page-header-height}) !important;
|
||||
@include gradient-v($g2-kevlar,$g0-obsidian);
|
||||
|
||||
&:only-child {
|
||||
top: 0;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.container-fluid {
|
||||
padding: ($chronograf-page-header-height / 2) $page-wrapper-padding;
|
||||
|
@ -36,7 +41,7 @@
|
|||
.page-contents.presentation-mode {
|
||||
top: 0;
|
||||
height: 100% !important;
|
||||
|
||||
|
||||
.container-fluid {padding: 8px !important;}
|
||||
.template-control--manage {display: none;}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2516,9 +2516,9 @@ dot-prop@^3.0.0:
|
|||
dependencies:
|
||||
is-obj "^1.0.0"
|
||||
|
||||
dygraphs@^2.0.0:
|
||||
dygraphs@influxdata/dygraphs:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dygraphs/-/dygraphs-2.0.0.tgz#30be283c7e32aa7536d4ea9da61ebe9f363d7c98"
|
||||
resolved "https://codeload.github.com/influxdata/dygraphs/tar.gz/9cc90443f58c11b45473516a97d4bb3a68a620c2"
|
||||
|
||||
ecc-jsbn@~0.1.1:
|
||||
version "0.1.1"
|
||||
|
|
Loading…
Reference in New Issue