Merge branch 'master' into fix-gauge-resize

pull/10616/head
Alex P 2018-02-26 11:59:03 -08:00
commit e9bee9b30d
23 changed files with 306 additions and 182 deletions

View File

@ -1,10 +1,15 @@
## v1.4.2.0 [unreleased]
### Features
1. [#2837] (https://github.com/influxdata/chronograf/pull/2837): Prevent execution of queries in cells that are not in view on the dashboard page
### UI Improvements
1. [#2848](https://github.com/influxdata/chronograf/pull/2848): Add ability to set a prefix and suffix on Single Stat and Gauge cell types
1. [#2831](https://github.com/influxdata/chronograf/pull/2831): Rename 'Create Alerts' page to 'Manage Tasks'; Redesign page to improve clarity of purpose
### Bug Fixes
1. [#2821](https://github.com/influxdata/chronograf/pull/2821): Save only selected template variable values into dashboards for non csv template variables
1. [#2842](https://github.com/influxdata/chronograf/pull/2842): Use Generic APIKey for Oauth2 group lookup
1. [#2850](https://github.com/influxdata/chronograf/pull/2850): Fix bug in which resizing any cell in a dashboard causes a Gauge cell to resize
1. [#2851] (https://github.com/influxdata/chronograf/pull/2851): Maintain y axis labels in dashboard cells
## v1.4.1.3 [2018-02-14]
### Bug Fixes

View File

@ -45,7 +45,7 @@ func newCellResponse(dID chronograf.DashboardID, cell chronograf.DashboardCell)
for _, lbl := range []string{"x", "y", "y2"} {
if _, found := newAxes[lbl]; !found {
newAxes[lbl] = chronograf.Axis{
Bounds: []string{},
Bounds: []string{"", ""},
}
}
}
@ -354,6 +354,13 @@ func (s *Service) ReplaceDashboardCell(w http.ResponseWriter, r *http.Request) {
return
}
for i, a := range cell.Axes {
if len(a.Bounds) == 0 {
a.Bounds = []string{"", ""}
cell.Axes[i] = a
}
}
if err := ValidDashboardCellRequest(&cell); err != nil {
invalidData(w, err, s.Logger)
return

View File

@ -192,13 +192,13 @@ func Test_Service_DashboardCells(t *testing.T) {
CellColors: []chronograf.CellColor{},
Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{
Bounds: []string{},
Bounds: []string{"", ""},
},
"y": chronograf.Axis{
Bounds: []string{},
Bounds: []string{"", ""},
},
"y2": chronograf.Axis{
Bounds: []string{},
Bounds: []string{"", ""},
},
},
},
@ -420,13 +420,13 @@ func TestService_ReplaceDashboardCell(t *testing.T) {
},
Axes: map[string]chronograf.Axis{
"x": {
Bounds: []string{},
Bounds: []string{"", ""},
},
"y": {
Bounds: []string{},
Bounds: []string{"", ""},
},
"y2": {
Bounds: []string{},
Bounds: []string{"", ""},
},
},
Type: "line",
@ -491,7 +491,7 @@ func TestService_ReplaceDashboardCell(t *testing.T) {
],
"axes": {
"x": {
"bounds": [],
"bounds": ["",""],
"label": "",
"prefix": "",
"suffix": "",
@ -499,7 +499,7 @@ func TestService_ReplaceDashboardCell(t *testing.T) {
"scale": ""
},
"y": {
"bounds": [],
"bounds": ["",""],
"label": "",
"prefix": "",
"suffix": "",
@ -507,7 +507,7 @@ func TestService_ReplaceDashboardCell(t *testing.T) {
"scale": ""
},
"y2": {
"bounds": [],
"bounds": ["",""],
"label": "",
"prefix": "",
"suffix": "",
@ -532,7 +532,7 @@ func TestService_ReplaceDashboardCell(t *testing.T) {
}
}
`))),
want: `{"i":"3c5c4102-fa40-4585-a8f9-917c77e37192","x":0,"y":0,"w":4,"h":4,"name":"Untitled Cell","queries":[{"query":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","queryConfig":{"id":"3cd3eaa4-a4b8-44b3-b69e-0c7bf6b91d9e","database":"telegraf","measurement":"cpu","retentionPolicy":"autogen","fields":[{"value":"mean","type":"func","alias":"mean_usage_user","args":[{"value":"usage_user","type":"field","alias":""}]}],"tags":{"cpu":["ChristohersMBP2.lan"]},"groupBy":{"time":"2s","tags":[]},"areTagsAccepted":true,"fill":"null","rawText":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","range":{"upper":"","lower":"now() - 15m"},"shifts":[]},"source":""}],"axes":{"x":{"bounds":[],"label":"","prefix":"","suffix":"","base":"","scale":""},"y":{"bounds":[],"label":"","prefix":"","suffix":"","base":"","scale":""},"y2":{"bounds":[],"label":"","prefix":"","suffix":"","base":"","scale":""}},"type":"line","colors":[{"id":"0","type":"min","hex":"#00C9FF","name":"laser","value":"0"},{"id":"1","type":"max","hex":"#9394FF","name":"comet","value":"100"}],"legend":{},"links":{"self":"/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192"}}
want: `{"i":"3c5c4102-fa40-4585-a8f9-917c77e37192","x":0,"y":0,"w":4,"h":4,"name":"Untitled Cell","queries":[{"query":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","queryConfig":{"id":"3cd3eaa4-a4b8-44b3-b69e-0c7bf6b91d9e","database":"telegraf","measurement":"cpu","retentionPolicy":"autogen","fields":[{"value":"mean","type":"func","alias":"mean_usage_user","args":[{"value":"usage_user","type":"field","alias":""}]}],"tags":{"cpu":["ChristohersMBP2.lan"]},"groupBy":{"time":"2s","tags":[]},"areTagsAccepted":true,"fill":"null","rawText":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","range":{"upper":"","lower":"now() - 15m"},"shifts":[]},"source":""}],"axes":{"x":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"","scale":""},"y":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"","scale":""},"y2":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"","scale":""}},"type":"line","colors":[{"id":"0","type":"min","hex":"#00C9FF","name":"laser","value":"0"},{"id":"1","type":"max","hex":"#9394FF","name":"comet","value":"100"}],"legend":{},"links":{"self":"/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192"}}
`,
},
{
@ -854,13 +854,13 @@ func Test_newCellResponses(t *testing.T) {
Queries: []chronograf.DashboardQuery{},
Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{
Bounds: []string{},
Bounds: []string{"", ""},
},
"y": chronograf.Axis{
Bounds: []string{},
Bounds: []string{"", ""},
},
"y2": chronograf.Axis{
Bounds: []string{},
Bounds: []string{"", ""},
},
},
CellColors: []chronograf.CellColor{},

View File

@ -299,7 +299,7 @@ func Test_newDashboardResponse(t *testing.T) {
Label: "foo",
},
"y2": chronograf.Axis{
Bounds: []string{},
Bounds: []string{"", ""},
},
},
},
@ -314,13 +314,13 @@ func Test_newDashboardResponse(t *testing.T) {
H: 4,
Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{
Bounds: []string{},
Bounds: []string{"", ""},
},
"y": chronograf.Axis{
Bounds: []string{},
Bounds: []string{"", ""},
},
"y2": chronograf.Axis{
Bounds: []string{},
Bounds: []string{"", ""},
},
},
CellColors: []chronograf.CellColor{},

View File

@ -124,6 +124,7 @@ class AxesOptions extends Component {
value={prefix}
labelText="Y-Value's Prefix"
onChange={this.handleSetPrefixSuffix}
maxLength="5"
/>
<Input
name="suffix"
@ -131,6 +132,7 @@ class AxesOptions extends Component {
value={suffix}
labelText="Y-Value's Suffix"
onChange={this.handleSetPrefixSuffix}
maxLength="5"
/>
<Tabber
labelText="Y-Value's Format"

View File

@ -23,9 +23,14 @@ const Dashboard = ({
onSummonOverlayTechnologies,
onSelectTemplate,
showTemplateControlBar,
setScrollTop,
inView,
}) => {
const cells = dashboard.cells.map(cell => {
const dashboardCell = {...cell}
const dashboardCell = {
...cell,
inView: inView(cell),
}
dashboardCell.queries = dashboardCell.queries.map(q => ({
...q,
database: q.db,
@ -39,6 +44,7 @@ const Dashboard = ({
className={classnames('page-contents', {
'presentation-mode': inPresentationMode,
})}
setScrollTop={setScrollTop}
>
<div className="dashboard container-fluid full-width">
{inPresentationMode
@ -119,6 +125,8 @@ Dashboard.propTypes = {
onSelectTemplate: func.isRequired,
showTemplateControlBar: bool,
onZoom: func,
setScrollTop: func,
inView: func,
}
export default Dashboard

View File

@ -15,7 +15,10 @@ import {
MIN_THRESHOLDS,
} from 'src/dashboards/constants/gaugeColors'
import {updateGaugeColors} from 'src/dashboards/actions/cellEditorOverlay'
import {
updateGaugeColors,
updateAxes,
} from 'src/dashboards/actions/cellEditorOverlay'
class GaugeOptions extends Component {
handleAddThreshold = () => {
@ -118,8 +121,22 @@ class GaugeOptions extends Component {
return allowedToUpdate
}
handleUpdatePrefix = e => {
const {handleUpdateAxes, axes} = this.props
const newAxes = {...axes, y: {...axes.y, prefix: e.target.value}}
handleUpdateAxes(newAxes)
}
handleUpdateSuffix = e => {
const {handleUpdateAxes, axes} = this.props
const newAxes = {...axes, y: {...axes.y, suffix: e.target.value}}
handleUpdateAxes(newAxes)
}
render() {
const {gaugeColors} = this.props
const {gaugeColors, axes: {y: {prefix, suffix}}} = this.props
const disableMaxColor = gaugeColors.length > MIN_THRESHOLDS
const disableAddThreshold = gaugeColors.length > MAX_THRESHOLDS
@ -157,6 +174,28 @@ class GaugeOptions extends Component {
/>
)}
</div>
<div className="single-stat-controls">
<div className="form-group col-xs-6">
<label>Prefix</label>
<input
className="form-control input-sm"
placeholder="%, MPH, etc."
defaultValue={prefix}
onChange={this.handleUpdatePrefix}
maxLength="5"
/>
</div>
<div className="form-group col-xs-6">
<label>Suffix</label>
<input
className="form-control input-sm"
placeholder="%, MPH, etc."
defaultValue={suffix}
onChange={this.handleUpdateSuffix}
maxLength="5"
/>
</div>
</div>
</div>
</FancyScrollbar>
)
@ -176,13 +215,17 @@ GaugeOptions.propTypes = {
}).isRequired
),
handleUpdateGaugeColors: func.isRequired,
handleUpdateAxes: func.isRequired,
axes: shape({}).isRequired,
}
const mapStateToProps = ({cellEditorOverlay: {gaugeColors}}) => ({
const mapStateToProps = ({cellEditorOverlay: {gaugeColors, cell: {axes}}}) => ({
gaugeColors,
axes,
})
const mapDispatchToProps = dispatch => ({
handleUpdateGaugeColors: bindActionCreators(updateGaugeColors, dispatch),
handleUpdateAxes: bindActionCreators(updateAxes, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(GaugeOptions)

View File

@ -109,6 +109,13 @@ class SingleStatOptions extends Component {
return !sortedColors.some(color => color.value === targetValue)
}
handleUpdatePrefix = e => {
const {handleUpdateAxes, axes} = this.props
const newAxes = {...axes, y: {...axes.y, prefix: e.target.value}}
handleUpdateAxes(newAxes)
}
handleUpdateSuffix = e => {
const {handleUpdateAxes, axes} = this.props
const newAxes = {...axes, y: {...axes.y, suffix: e.target.value}}
@ -117,7 +124,11 @@ class SingleStatOptions extends Component {
}
render() {
const {singleStatColors, singleStatType, axes: {y: {suffix}}} = this.props
const {
singleStatColors,
singleStatType,
axes: {y: {prefix, suffix}},
} = this.props
const disableAddThreshold = singleStatColors.length > MAX_THRESHOLDS
@ -162,6 +173,26 @@ class SingleStatOptions extends Component {
)}
</div>
<div className="single-stat-controls">
<div className="form-group col-xs-6">
<label>Prefix</label>
<input
className="form-control input-sm"
placeholder="%, MPH, etc."
defaultValue={prefix}
onChange={this.handleUpdatePrefix}
maxLength="5"
/>
</div>
<div className="form-group col-xs-6">
<label>Suffix</label>
<input
className="form-control input-sm"
placeholder="%, MPH, etc."
defaultValue={suffix}
onChange={this.handleUpdateSuffix}
maxLength="5"
/>
</div>
<div className="form-group col-xs-6">
<label>Coloring</label>
<ul className="nav nav-tablist nav-tablist-sm">
@ -183,16 +214,6 @@ class SingleStatOptions extends Component {
</li>
</ul>
</div>
<div className="form-group col-xs-6">
<label>Suffix</label>
<input
className="form-control input-sm"
placeholder="%, MPH, etc."
defaultValue={suffix}
onChange={this.handleUpdateSuffix}
maxLength="5"
/>
</div>
</div>
</div>
</FancyScrollbar>

View File

@ -30,6 +30,7 @@ import {
templateControlBarVisibilityToggled as templateControlBarVisibilityToggledAction,
} from 'shared/actions/app'
import {presentationButtonDispatcher} from 'shared/dispatchers'
import {DASHBOARD_LAYOUT_ROW_HEIGHT} from 'shared/constants'
const FORMAT_INFLUXQL = 'influxql'
const defaultTimeRange = {
@ -47,6 +48,8 @@ class DashboardPage extends Component {
selectedCell: null,
isTemplating: false,
zoomedTimeRange: {zoomedLower: null, zoomedUpper: null},
scrollTop: 0,
windowHeight: window.innerHeight,
}
}
@ -67,6 +70,8 @@ class DashboardPage extends Component {
notify,
} = this.props
window.addEventListener('resize', this.handleWindowResize, true)
const dashboards = await getDashboardsAsync()
const dashboard = dashboards.find(
d => d.id === idNormalizer(TYPE_ID, dashboardID)
@ -86,6 +91,27 @@ class DashboardPage extends Component {
}
}
handleWindowResize = () => {
this.setState({windowHeight: window.innerHeight})
}
componentWillUnMount() {
window.removeEventListener('resize', this.handleWindowResize, true)
}
inView = cell => {
const {scrollTop, windowHeight} = this.state
const bufferValue = 600
const cellTop = cell.y * DASHBOARD_LAYOUT_ROW_HEIGHT
const cellBottom = (cell.y + cell.h) * DASHBOARD_LAYOUT_ROW_HEIGHT
const bufferedWindowBottom = windowHeight + scrollTop + bufferValue
const bufferedWindowTop = scrollTop - bufferValue
const topInView = cellTop < bufferedWindowBottom
const bottomInView = cellBottom > bufferedWindowTop
return topInView && bottomInView
}
handleOpenTemplateManager = () => {
this.setState({isTemplating: true})
}
@ -228,6 +254,10 @@ class DashboardPage extends Component {
this.setState({zoomedTimeRange: {zoomedLower, zoomedUpper}})
}
setScrollTop = event => {
this.setState({scrollTop: event.target.scrollTop})
}
render() {
const {zoomedTimeRange} = this.state
const {zoomedLower, zoomedUpper} = zoomedTimeRange
@ -321,6 +351,7 @@ class DashboardPage extends Component {
}
const {isEditMode, isTemplating} = this.state
const names = dashboards.map(d => ({
name: d.name,
link: `/sources/${sourceID}/dashboards/${d.id}`,
@ -383,6 +414,8 @@ class DashboardPage extends Component {
? <Dashboard
source={source}
sources={sources}
setScrollTop={this.setScrollTop}
inView={this.inView}
dashboard={dashboard}
timeRange={timeRange}
autoRefresh={autoRefresh}

View File

@ -6,6 +6,7 @@ import SourceIndicator from 'shared/components/SourceIndicator'
import KapacitorRulesTable from 'src/kapacitor/components/KapacitorRulesTable'
import TasksTable from 'src/kapacitor/components/TasksTable'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import QuestionMarkTooltip from 'shared/components/QuestionMarkTooltip'
const KapacitorRules = ({
source,
@ -41,17 +42,21 @@ const KapacitorRules = ({
)
}
const rulez = rules.filter(r => r.query)
const tasks = rules.filter(r => !r.query)
const builderRules = rules.filter(r => r.query)
const rHeader = `${rulez.length} Alert Rule${rulez.length === 1 ? '' : 's'}`
const tHeader = `${tasks.length} TICKscript${tasks.length === 1 ? '' : 's'}`
const builderHeader = `${builderRules.length} Alert Rule${builderRules.length ===
1
? ''
: 's'}`
const scriptsHeader = `${rules.length} TICKscript${rules.length === 1
? ''
: 's'}`
return (
<PageContents source={source}>
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
<h2 className="panel-title">
{rHeader}
{builderHeader}
</h2>
<div className="u-flex u-ai-center u-jc-space-between">
<Link
@ -65,7 +70,7 @@ const KapacitorRules = ({
</div>
<KapacitorRulesTable
source={source}
rules={rulez}
rules={builderRules}
onDelete={onDelete}
onChangeRuleStatus={onChangeRuleStatus}
/>
@ -75,12 +80,12 @@ const KapacitorRules = ({
<div className="panel panel-minimal">
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
<h2 className="panel-title">
{tHeader}
{scriptsHeader}
</h2>
<div className="u-flex u-ai-center u-jc-space-between">
<Link
to={`/sources/${source.id}/tickscript/new`}
className="btn btn-sm btn-primary"
className="btn btn-sm btn-success"
style={{marginRight: '4px'}}
>
<span className="icon plus" /> Write TICKscript
@ -89,7 +94,7 @@ const KapacitorRules = ({
</div>
<TasksTable
source={source}
tasks={tasks}
tasks={rules}
onDelete={onDelete}
onChangeRuleStatus={onChangeRuleStatus}
/>
@ -105,11 +110,13 @@ const PageContents = ({children}) =>
<div className="page-header">
<div className="page-header__container">
<div className="page-header__left">
<h1 className="page-header__title">
Build Alert Rules or Write TICKscripts
</h1>
<h1 className="page-header__title">Manage Tasks</h1>
</div>
<div className="page-header__right">
<QuestionMarkTooltip
tipID="manage-tasks--tooltip"
tipContent="<b>Alert Rules</b> generate a TICKscript for<br/>you using our Builder UI.<br/><br/>Not all TICKscripts can be edited<br/>using the Builder."
/>
<SourceIndicator />
</div>
</div>

View File

@ -2,8 +2,9 @@ import React, {PropTypes} from 'react'
import {Link} from 'react-router'
import _ from 'lodash'
import ConfirmButton from 'src/shared/components/ConfirmButton'
import {parseAlertNodeList} from 'src/shared/parsing/parseHandlersFromRule'
import {KAPACITOR_RULES_TABLE} from 'src/kapacitor/constants/tableSizing'
import {TASKS_TABLE} from 'src/kapacitor/constants/tableSizing'
const {
colName,
colTrigger,
@ -11,19 +12,19 @@ const {
colAlerts,
colEnabled,
colActions,
} = KAPACITOR_RULES_TABLE
} = TASKS_TABLE
const KapacitorRulesTable = ({rules, source, onDelete, onChangeRuleStatus}) =>
<div className="panel-body">
<table className="table v-center">
<table className="table v-center table-highlight">
<thead>
<tr>
<th style={{width: colName}}>Name</th>
<th style={{width: colTrigger}}>Rule Trigger</th>
<th style={{minWidth: colName}}>Name</th>
<th style={{width: colTrigger}}>Rule Type</th>
<th style={{width: colMessage}}>Message</th>
<th style={{width: colAlerts}}>Alerts</th>
<th style={{width: colAlerts}}>Alert Handlers</th>
<th style={{width: colEnabled}} className="text-center">
Enabled
Task Enabled
</th>
<th style={{width: colActions}} />
</tr>
@ -48,24 +49,21 @@ const handleDelete = (rule, onDelete) => onDelete(rule)
const RuleRow = ({rule, source, onDelete, onChangeRuleStatus}) =>
<tr key={rule.id}>
<td style={{width: colName}} className="monotype">
<RuleTitle rule={rule} source={source} />
<td style={{minWidth: colName}}>
<Link to={`/sources/${source.id}/alert-rules/${rule.id}`}>
{rule.name}
</Link>
</td>
<td style={{width: colTrigger}} className="monotype">
<td style={{width: colTrigger, textTransform: 'capitalize'}}>
{rule.trigger}
</td>
<td className="monotype">
<span
className="table-cell-nowrap"
style={{display: 'inline-block', maxWidth: colMessage}}
>
{rule.message}
</span>
<td style={{width: colMessage}}>
{rule.message}
</td>
<td style={{width: colAlerts}} className="monotype">
<td style={{width: colAlerts}}>
{parseAlertNodeList(rule)}
</td>
<td style={{width: colEnabled}} className="monotype text-center">
<td style={{width: colEnabled}} className="text-center">
<div className="dark-checkbox">
<input
id={`kapacitor-enabled ${rule.id}`}
@ -77,39 +75,17 @@ const RuleRow = ({rule, source, onDelete, onChangeRuleStatus}) =>
<label htmlFor={`kapacitor-enabled ${rule.id}`} />
</div>
</td>
<td style={{width: colActions}} className="text-right table-cell-nowrap">
<Link
className="btn btn-info btn-xs"
to={`/sources/${source.id}/tickscript/${rule.id}`}
>
Edit TICKscript
</Link>
<button
className="btn btn-danger btn-xs"
onClick={handleDelete(rule, onDelete)}
>
Delete
</button>
<td style={{width: colActions}} className="text-right">
<ConfirmButton
text="Delete"
type="btn-danger"
size="btn-xs"
customClass="table--show-on-row-hover"
confirmAction={handleDelete(rule, onDelete)}
/>
</td>
</tr>
const RuleTitle = ({rule: {id, name, query}, source}) => {
// no queryConfig means the rule was manually created outside of Chronograf
if (!query) {
return (
<i>
{name}
</i>
)
}
return (
<Link to={`/sources/${source.id}/alert-rules/${id}`}>
{name}
</Link>
)
}
const {arrayOf, func, shape, string} = PropTypes
KapacitorRulesTable.propTypes = {
@ -128,17 +104,4 @@ RuleRow.propTypes = {
onDelete: func,
}
RuleTitle.propTypes = {
rule: shape({
name: string.isRequired,
query: shape(),
links: shape({
self: string.isRequired,
}),
}),
source: shape({
id: string.isRequired,
}).isRequired,
}
export default KapacitorRulesTable

View File

@ -2,25 +2,26 @@ import React, {PropTypes} from 'react'
import {Link} from 'react-router'
import _ from 'lodash'
import ConfirmButton from 'src/shared/components/ConfirmButton'
import {TASKS_TABLE} from 'src/kapacitor/constants/tableSizing'
const {colID, colType, colEnabled, colActions} = TASKS_TABLE
const {colName, colType, colEnabled, colActions} = TASKS_TABLE
const TasksTable = ({tasks, source, onDelete, onChangeRuleStatus}) =>
<div className="panel-body">
<table className="table v-center">
<table className="table v-center table-highlight">
<thead>
<tr>
<th style={{width: colID}}>ID</th>
<th style={{minWidth: colName}}>Name</th>
<th style={{width: colType}}>Type</th>
<th style={{width: colEnabled}} className="text-center">
Enabled
Task Enabled
</th>
<th style={{width: colActions}} />
</tr>
</thead>
<tbody>
{_.sortBy(tasks, t => t.name.toLowerCase()).map(task => {
{_.sortBy(tasks, t => t.id.toLowerCase()).map(task => {
return (
<TaskRow
key={task.id}
@ -39,15 +40,18 @@ const handleDelete = (task, onDelete) => onDelete(task)
const TaskRow = ({task, source, onDelete, onChangeRuleStatus}) =>
<tr key={task.id}>
<td style={{width: colID}} className="monotype">
<i>
{task.id}
</i>
<td style={{minWidth: colName}}>
<Link
className="link-success"
to={`/sources/${source.id}/tickscript/${task.id}`}
>
{task.name}
</Link>
</td>
<td style={{width: colType}} className="monotype">
<td style={{width: colType, textTransform: 'capitalize'}}>
{task.type}
</td>
<td style={{width: colEnabled}} className="monotype text-center">
<td style={{width: colEnabled}} className="text-center">
<div className="dark-checkbox">
<input
id={`kapacitor-enabled ${task.id}`}
@ -59,19 +63,14 @@ const TaskRow = ({task, source, onDelete, onChangeRuleStatus}) =>
<label htmlFor={`kapacitor-enabled ${task.id}`} />
</div>
</td>
<td style={{width: colActions}} className="text-right table-cell-nowrap">
<Link
className="btn btn-info btn-xs"
to={`/sources/${source.id}/tickscript/${task.id}`}
>
Edit TICKscript
</Link>
<button
className="btn btn-danger btn-xs"
onClick={handleDelete(task, onDelete)}
>
Delete
</button>
<td style={{width: colActions}} className="text-right">
<ConfirmButton
text="Delete"
type="btn-danger"
size="btn-xs"
customClass="table--show-on-row-hover"
confirmAction={handleDelete(task, onDelete)}
/>
</td>
</tr>

View File

@ -1,15 +1,9 @@
export const KAPACITOR_RULES_TABLE = {
export const TASKS_TABLE = {
colName: '200px',
colTrigger: '90px',
colMessage: '460px',
colMessage: '200px',
colAlerts: '120px',
colEnabled: '64px',
colActions: '176px',
}
export const TASKS_TABLE = {
colID: '200px',
colEnabled: '95px',
colActions: '68px',
colType: '90px',
colEnabled: '64px',
colActions: '176px',
}

View File

@ -9,7 +9,7 @@ const AutoRefresh = ComposedComponent => {
constructor() {
super()
this.state = {
lastQuerySuccessful: false,
lastQuerySuccessful: true,
timeSeries: [],
resolution: null,
}
@ -27,6 +27,8 @@ const AutoRefresh = ComposedComponent => {
}
componentWillReceiveProps(nextProps) {
const inViewDidUpdate = this.props.inView !== nextProps.inView
const queriesDidUpdate = this.queryDifference(
this.props.queries,
nextProps.queries
@ -37,10 +39,15 @@ const AutoRefresh = ComposedComponent => {
nextProps.templates
)
const shouldRefetch = queriesDidUpdate || tempVarsDidUpdate
const shouldRefetch =
queriesDidUpdate || tempVarsDidUpdate || inViewDidUpdate
if (shouldRefetch) {
this.executeQueries(nextProps.queries, nextProps.templates)
this.executeQueries(
nextProps.queries,
nextProps.templates,
nextProps.inView
)
}
if (this.props.autoRefresh !== nextProps.autoRefresh || shouldRefetch) {
@ -48,7 +55,12 @@ const AutoRefresh = ComposedComponent => {
if (nextProps.autoRefresh) {
this.intervalID = setInterval(
() => this.executeQueries(nextProps.queries, nextProps.templates),
() =>
this.executeQueries(
nextProps.queries,
nextProps.templates,
nextProps.inView
),
nextProps.autoRefresh
)
}
@ -64,10 +76,16 @@ const AutoRefresh = ComposedComponent => {
)
}
executeQueries = async (queries, templates = []) => {
executeQueries = async (
queries,
templates = [],
inView = this.props.inView
) => {
const {editQueryStatus, grabDataForDownload} = this.props
const {resolution} = this.state
if (!inView) {
return
}
if (!queries.length) {
this.setState({timeSeries: []})
return
@ -148,7 +166,15 @@ const AutoRefresh = ComposedComponent => {
const {timeSeries} = this.state
if (this.state.isFetching && this.state.lastQuerySuccessful) {
return this.renderFetching(timeSeries)
return (
<ComposedComponent
{...this.props}
data={timeSeries}
setResolution={this.setResolution}
isFetchingInitially={false}
isRefreshing={true}
/>
)
}
return (
@ -160,23 +186,6 @@ const AutoRefresh = ComposedComponent => {
)
}
/**
* Graphs can potentially show mulitple kinds of spinners based on whether
* a graph is being fetched for the first time, or is being refreshed.
*/
renderFetching = data => {
const isFirstFetch = !Object.keys(this.state.timeSeries).length
return (
<ComposedComponent
{...this.props}
data={data}
setResolution={this.setResolution}
isFetchingInitially={isFirstFetch}
isRefreshing={!isFirstFetch}
/>
)
}
_resultsForQuery = data =>
data.length
? data.every(({response}) =>
@ -204,6 +213,7 @@ const AutoRefresh = ComposedComponent => {
wrapper.propTypes = {
children: element,
autoRefresh: number.isRequired,
inView: bool,
templates: arrayOf(
shape({
type: string.isRequired,

View File

@ -10,6 +10,7 @@ class FancyScrollbar extends Component {
static defaultProps = {
autoHide: true,
autoHeight: false,
setScrollTop: () => {},
}
handleMakeDiv = className => props => {
@ -17,13 +18,21 @@ class FancyScrollbar extends Component {
}
render() {
const {autoHide, autoHeight, children, className, maxHeight} = this.props
const {
autoHide,
autoHeight,
children,
className,
maxHeight,
setScrollTop,
} = this.props
return (
<Scrollbars
className={classnames('fancy-scroll--container', {
[className]: className,
})}
onScroll={setScrollTop}
autoHide={autoHide}
autoHideTimeout={1000}
autoHideDuration={250}
@ -41,7 +50,7 @@ class FancyScrollbar extends Component {
}
}
const {bool, node, number, string} = PropTypes
const {bool, func, node, number, string} = PropTypes
FancyScrollbar.propTypes = {
children: node.isRequired,
@ -49,6 +58,7 @@ FancyScrollbar.propTypes = {
autoHide: bool,
autoHeight: bool,
maxHeight: number,
setScrollTop: func,
}
export default FancyScrollbar

View File

@ -226,6 +226,7 @@ class Gauge extends Component {
minValue,
maxValue
) => {
const {prefix, suffix} = this.props
const {degree, lineCount, labelColor, labelFontSize} = GAUGE_SPECS
const incrementValue = (maxValue - minValue) / lineCount
@ -258,12 +259,14 @@ class Gauge extends Component {
if (i > 3) {
ctx.textAlign = 'left'
}
const labelText = `${prefix}${gaugeValues[i]}${suffix}`
ctx.rotate(startDegree)
ctx.rotate(i * arcIncrement)
ctx.translate(labelRadius, 0)
ctx.rotate(i * -arcIncrement)
ctx.rotate(-startDegree)
ctx.fillText(gaugeValues[i], 0, 0)
ctx.fillText(labelText, 0, 0)
ctx.rotate(startDegree)
ctx.rotate(i * arcIncrement)
ctx.translate(-labelRadius, 0)
@ -273,7 +276,7 @@ class Gauge extends Component {
}
drawGaugeValue = (ctx, radius, labelValueFontSize) => {
const {gaugePosition} = this.props
const {gaugePosition, prefix, suffix} = this.props
const {valueColor} = GAUGE_SPECS
ctx.font = `${labelValueFontSize}px Roboto`
@ -282,7 +285,8 @@ class Gauge extends Component {
ctx.textAlign = 'center'
const textY = radius
ctx.fillText(gaugePosition.toString(), 0, textY)
const textContent = `${prefix}${gaugePosition.toString()}${suffix}`
ctx.fillText(textContent, 0, textY)
}
drawNeedle = (ctx, radius, minValue, maxValue) => {
@ -335,6 +339,8 @@ Gauge.propTypes = {
value: string.isRequired,
}).isRequired
).isRequired,
prefix: string.isRequired,
suffix: string.isRequired,
}
export default Gauge

View File

@ -18,6 +18,8 @@ class GaugeChart extends PureComponent {
colors,
resizeCoords,
resizerTopHeight,
prefix,
suffix,
} = this.props
// If data for this graph is being fetched for the first time, show a graph-wide spinner.
@ -61,6 +63,8 @@ class GaugeChart extends PureComponent {
height={height}
colors={colors}
gaugePosition={roundedValue}
prefix={prefix}
suffix={suffix}
/>
</div>
)
@ -89,6 +93,8 @@ GaugeChart.propTypes = {
value: string.isRequired,
}).isRequired
),
prefix: string.isRequired,
suffix: string.isRequired,
}
export default GaugeChart

View File

@ -76,6 +76,7 @@ const Layout = (
? <WidgetCell cell={cell} timeRange={timeRange} source={source} />
: <RefreshingGraph
colors={colors}
inView={cell.inView}
axes={axes}
type={type}
cellHeight={h}

View File

@ -33,13 +33,11 @@ class LayoutRenderer extends Component {
if (!this.props.onPositionChange) {
return
}
const newCells = this.props.cells.map(cell => {
const l = layout.find(ly => ly.i === cell.i)
const newLayout = {x: l.x, y: l.y, h: l.h, w: l.w}
return {...cell, ...newLayout}
})
this.props.onPositionChange(newCells)
}

View File

@ -13,6 +13,7 @@ const RefreshingGaugeChart = AutoRefresh(GaugeChart)
const RefreshingGraph = ({
axes,
inView,
type,
colors,
onZoom,
@ -29,6 +30,9 @@ const RefreshingGraph = ({
editQueryStatus,
grabDataForDownload,
}) => {
const prefix = axes.y.prefix || ''
const suffix = axes.y.suffix || ''
if (!queries.length) {
return (
<div className="graph-empty">
@ -40,8 +44,6 @@ const RefreshingGraph = ({
}
if (type === 'single-stat') {
const suffix = axes.y.suffix || ''
return (
<RefreshingSingleStat
colors={colors}
@ -50,6 +52,7 @@ const RefreshingGraph = ({
templates={templates}
autoRefresh={autoRefresh}
cellHeight={cellHeight}
prefix={prefix}
suffix={suffix}
/>
)
@ -67,6 +70,8 @@ const RefreshingGraph = ({
resizerTopHeight={resizerTopHeight}
resizeCoords={resizeCoords}
cellID={cellID}
prefix={prefix}
suffix={suffix}
/>
)
}
@ -82,6 +87,7 @@ const RefreshingGraph = ({
colors={colors}
onZoom={onZoom}
queries={queries}
inView={inView}
key={manualRefresh}
templates={templates}
timeRange={timeRange}
@ -97,7 +103,7 @@ const RefreshingGraph = ({
)
}
const {arrayOf, func, number, shape, string} = PropTypes
const {arrayOf, bool, func, number, shape, string} = PropTypes
RefreshingGraph.propTypes = {
timeRange: shape({
@ -126,10 +132,12 @@ RefreshingGraph.propTypes = {
}).isRequired
),
cellID: string,
inView: bool,
}
RefreshingGraph.defaultProps = {
manualRefresh: 0,
inView: true,
}
export default RefreshingGraph

View File

@ -1,8 +1,8 @@
import _ from 'lodash'
const emptyFunny = [
'Looks like you dont have any queries. Be a lot cooler if you did.',
'Create a query. Go on, I dare ya!',
'Looks like you dont have any queries. Be a lot cooler if you did!',
'Create a query. Go on!',
'Create a query. Have fun!',
]

View File

@ -115,13 +115,15 @@ const SideNav = React.createClass({
<NavBlock
matcher="alerts"
icon="alert-triangle"
link={`${sourcePrefix}/alerts`}
link={`${sourcePrefix}/alert-rules`}
location={location}
>
<NavHeader link={`${sourcePrefix}/alerts`} title="Alerting" />
<NavListItem link={`${sourcePrefix}/alerts`}>History</NavListItem>
<NavHeader link={`${sourcePrefix}/alert-rules`} title="Alerting" />
<NavListItem link={`${sourcePrefix}/alert-rules`}>
Create
Manage Tasks
</NavListItem>
<NavListItem link={`${sourcePrefix}/alerts`}>
Alert History
</NavListItem>
</NavBlock>

View File

@ -4778,7 +4778,8 @@ p .label {
.table .table--show-on-row-hover {
visibility: hidden;
}
.table > tbody > tr:hover .table--show-on-row-hover {
.table > tbody > tr:hover .table--show-on-row-hover,
.table > tbody > tr .active.table--show-on-row-hover {
visibility: visible;
}
.table-responsive {