Merge pull request #1717 from influxdata/feature/read-only-tickscript

View server generated TICKscripts
pull/1725/head
Andrew Watkins 2017-07-13 13:15:36 -07:00 committed by GitHub
commit 7200f08fe0
12 changed files with 299 additions and 138 deletions

View File

@ -4,8 +4,10 @@
1. [#1708](https://github.com/influxdata/chronograf/pull/1708): Fix z-index issue in dashboard cell context menu 1. [#1708](https://github.com/influxdata/chronograf/pull/1708): Fix z-index issue in dashboard cell context menu
### Features ### Features
1. [#1717](https://github.com/influxdata/chronograf/pull/1717): View server generated TICKscripts
1. [#1681](https://github.com/influxdata/chronograf/pull/1681): Add the ability to select Custom Time Ranges in the Hostpages, Data Explorer, and Dashboards. 1. [#1681](https://github.com/influxdata/chronograf/pull/1681): Add the ability to select Custom Time Ranges in the Hostpages, Data Explorer, and Dashboards.
### UI Improvements ### UI Improvements
1. [#1707](https://github.com/influxdata/chronograf/pull/1707): Polish alerts table in status page to wrap text less 1. [#1707](https://github.com/influxdata/chronograf/pull/1707): Polish alerts table in status page to wrap text less

View File

@ -220,7 +220,7 @@
'react/jsx-uses-react': 2, 'react/jsx-uses-react': 2,
'react/jsx-uses-vars': 2, 'react/jsx-uses-vars': 2,
'react/no-danger': 2, 'react/no-danger': 2,
'react/no-did-mount-set-state': 'error', 'react/no-did-mount-set-state': 0,
'react/no-did-update-set-state': 2, 'react/no-did-update-set-state': 2,
'react/no-direct-mutation-state': 2, 'react/no-direct-mutation-state': 2,
'react/no-is-mounted': 2, 'react/no-is-mounted': 2,

View File

@ -7,6 +7,7 @@ import {
deleteRule as deleteRuleAPI, deleteRule as deleteRuleAPI,
updateRuleStatus as updateRuleStatusAPI, updateRuleStatus as updateRuleStatusAPI,
} from 'src/kapacitor/apis' } from 'src/kapacitor/apis'
import {errorThrown} from 'shared/actions/errors'
export function fetchRule(source, ruleID) { export function fetchRule(source, ruleID) {
return dispatch => { return dispatch => {
@ -48,16 +49,12 @@ export function loadDefaultRule() {
} }
} }
export function fetchRules(kapacitor) { export const fetchRules = kapacitor => async dispatch => {
return dispatch => { try {
getRules(kapacitor).then(({data: {rules}}) => { const {data: {rules}} = await getRules(kapacitor)
dispatch({ dispatch({type: 'LOAD_RULES', payload: {rules}})
type: 'LOAD_RULES', } catch (error) {
payload: { dispatch(errorThrown(error))
rules,
},
})
})
} }
} }

View File

@ -5,6 +5,7 @@ import NoKapacitorError from 'shared/components/NoKapacitorError'
import SourceIndicator from 'shared/components/SourceIndicator' import SourceIndicator from 'shared/components/SourceIndicator'
import KapacitorRulesTable from 'src/kapacitor/components/KapacitorRulesTable' import KapacitorRulesTable from 'src/kapacitor/components/KapacitorRulesTable'
import FancyScrollbar from 'shared/components/FancyScrollbar' import FancyScrollbar from 'shared/components/FancyScrollbar'
import TICKscriptOverlay from 'src/kapacitor/components/TICKscriptOverlay'
const KapacitorRules = ({ const KapacitorRules = ({
source, source,
@ -12,8 +13,29 @@ const KapacitorRules = ({
hasKapacitor, hasKapacitor,
loading, loading,
onDelete, onDelete,
tickscript,
onChangeRuleStatus, onChangeRuleStatus,
onReadTickscript,
onCloseTickscript,
}) => { }) => {
if (loading) {
return (
<PageContents>
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
<h2 className="panel-title">Alert Rules</h2>
<button className="btn btn-primary btn-sm disabled" disabled={true}>
Create Rule
</button>
</div>
<div className="panel-body">
<div className="generic-empty-state">
<p>Loading Rules...</p>
</div>
</div>
</PageContents>
)
}
if (!hasKapacitor) { if (!hasKapacitor) {
return ( return (
<PageContents> <PageContents>
@ -22,18 +44,16 @@ const KapacitorRules = ({
) )
} }
if (loading) {
return (
<PageContents>
<h2>Loading...</h2>
</PageContents>
)
}
const tableHeader = rules.length === 1 const tableHeader = rules.length === 1
? '1 Alert Rule' ? '1 Alert Rule'
: `${rules.length} Alert Rules` : `${rules.length} Alert Rules`
return ( return (
<PageContents source={source}> <PageContents
source={source}
tickscript={tickscript}
onReadTickscript={onReadTickscript}
onCloseTickscript={onCloseTickscript}
>
<div className="panel-heading u-flex u-ai-center u-jc-space-between"> <div className="panel-heading u-flex u-ai-center u-jc-space-between">
<h2 className="panel-title">{tableHeader}</h2> <h2 className="panel-title">{tableHeader}</h2>
<Link <Link
@ -47,13 +67,14 @@ const KapacitorRules = ({
source={source} source={source}
rules={rules} rules={rules}
onDelete={onDelete} onDelete={onDelete}
onReadTickscript={onReadTickscript}
onChangeRuleStatus={onChangeRuleStatus} onChangeRuleStatus={onChangeRuleStatus}
/> />
</PageContents> </PageContents>
) )
} }
const PageContents = ({children, source}) => const PageContents = ({children, source, tickscript, onCloseTickscript}) => (
<div className="page"> <div className="page">
<div className="page-header"> <div className="page-header">
<div className="page-header__container"> <div className="page-header__container">
@ -76,9 +97,16 @@ const PageContents = ({children, source}) =>
</div> </div>
</div> </div>
</FancyScrollbar> </FancyScrollbar>
{tickscript
? <TICKscriptOverlay
tickscript={tickscript}
onClose={onCloseTickscript}
/>
: null}
</div> </div>
)
const {arrayOf, bool, func, shape, node} = PropTypes const {arrayOf, bool, func, node, shape, string} = PropTypes
KapacitorRules.propTypes = { KapacitorRules.propTypes = {
source: shape(), source: shape(),
@ -87,11 +115,16 @@ KapacitorRules.propTypes = {
loading: bool, loading: bool,
onChangeRuleStatus: func, onChangeRuleStatus: func,
onDelete: func, onDelete: func,
tickscript: string,
onReadTickscript: func,
onCloseTickscript: func,
} }
PageContents.propTypes = { PageContents.propTypes = {
children: node, children: node,
source: shape(), source: shape(),
tickscript: string,
onCloseTickscript: func,
} }
export default KapacitorRules export default KapacitorRules

View File

@ -1,21 +1,38 @@
import React, {PropTypes} from 'react' import React, {PropTypes} from 'react'
import {Link} from 'react-router' import {Link} from 'react-router'
import _ from 'lodash'
const KapacitorRulesTable = ({rules, source, onDelete, onChangeRuleStatus}) => import {KAPACITOR_RULES_TABLE} from 'src/kapacitor/constants/tableSizing'
const {
colName,
colType,
colMessage,
colAlerts,
colEnabled,
colActions,
} = KAPACITOR_RULES_TABLE
const KapacitorRulesTable = ({
rules,
source,
onDelete,
onReadTickscript,
onChangeRuleStatus,
}) =>
<div className="panel-body"> <div className="panel-body">
<table className="table v-center"> <table className="table v-center">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th style={{width: colName}}>Name</th>
<th>Rule Type</th> <th style={{width: colType}}>Rule Type</th>
<th>Message</th> <th style={{width: colMessage}}>Message</th>
<th>Alerts</th> <th style={{width: colAlerts}}>Alerts</th>
<th className="text-center">Enabled</th> <th style={{width: colEnabled}} className="text-center">Enabled</th>
<th /> <th style={{width: colActions}} />
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{rules.map(rule => { {_.sortBy(rules, r => r.name.toLowerCase()).map(rule => {
return ( return (
<RuleRow <RuleRow
key={rule.id} key={rule.id}
@ -23,6 +40,7 @@ const KapacitorRulesTable = ({rules, source, onDelete, onChangeRuleStatus}) =>
source={source} source={source}
onDelete={onDelete} onDelete={onDelete}
onChangeRuleStatus={onChangeRuleStatus} onChangeRuleStatus={onChangeRuleStatus}
onRead={onReadTickscript}
/> />
) )
})} })}
@ -30,15 +48,24 @@ const KapacitorRulesTable = ({rules, source, onDelete, onChangeRuleStatus}) =>
</table> </table>
</div> </div>
const RuleRow = ({rule, source, onDelete, onChangeRuleStatus}) => const RuleRow = ({rule, source, onRead, onDelete, onChangeRuleStatus}) =>
<tr key={rule.id}> <tr key={rule.id}>
<td className="monotype"> <td style={{width: colName}} className="monotype">
<RuleTitle rule={rule} source={source} /> <RuleTitle rule={rule} source={source} />
</td> </td>
<td className="monotype">{rule.trigger}</td> <td style={{width: colType}} className="monotype">{rule.trigger}</td>
<td className="monotype">{rule.message}</td> <td className="monotype">
<td className="monotype">{rule.alerts.join(', ')}</td> <span
<td className="monotype text-center"> className="table-cell-nowrap"
style={{display: 'inline-block', maxWidth: colMessage}}
>
{rule.message}
</span>
</td>
<td style={{width: colAlerts}} className="monotype">
{rule.alerts.join(', ')}
</td>
<td style={{width: colEnabled}} className="monotype text-center">
<div className="dark-checkbox"> <div className="dark-checkbox">
<input <input
id={`kapacitor-enabled ${rule.id}`} id={`kapacitor-enabled ${rule.id}`}
@ -50,7 +77,10 @@ const RuleRow = ({rule, source, onDelete, onChangeRuleStatus}) =>
<label htmlFor={`kapacitor-enabled ${rule.id}`} /> <label htmlFor={`kapacitor-enabled ${rule.id}`} />
</div> </div>
</td> </td>
<td className="text-right"> <td style={{width: colActions}} className="text-right table-cell-nowrap">
<button className="btn btn-info btn-xs" onClick={() => onRead(rule)}>
View TICKscript
</button>
<button className="btn btn-danger btn-xs" onClick={() => onDelete(rule)}> <button className="btn btn-danger btn-xs" onClick={() => onDelete(rule)}>
Delete Delete
</button> </button>
@ -75,6 +105,7 @@ KapacitorRulesTable.propTypes = {
source: shape({ source: shape({
id: string.isRequired, id: string.isRequired,
}).isRequired, }).isRequired,
onReadTickscript: func,
} }
RuleRow.propTypes = { RuleRow.propTypes = {
@ -82,6 +113,7 @@ RuleRow.propTypes = {
source: shape(), source: shape(),
onChangeRuleStatus: func, onChangeRuleStatus: func,
onDelete: func, onDelete: func,
onRead: func,
} }
RuleTitle.propTypes = { RuleTitle.propTypes = {
@ -90,7 +122,7 @@ RuleTitle.propTypes = {
query: shape(), query: shape(),
links: shape({ links: shape({
self: string.isRequired, self: string.isRequired,
}).isRequired, }),
}), }),
source: shape({ source: shape({
id: string.isRequired, id: string.isRequired,

View File

@ -0,0 +1,30 @@
import React, {PropTypes} from 'react'
import OverlayTechnologies from 'shared/components/OverlayTechnologies'
const TICKscriptOverlay = ({tickscript, onClose}) =>
<OverlayTechnologies>
<div className="tick-script-overlay">
<div className="write-data-form--header">
<div className="page-header__left">
<h1 className="page-header__title">Generated TICKscript</h1>
</div>
<div className="page-header__right">
<span className="page-header__dismiss" onClick={onClose} />
</div>
</div>
<div className="write-data-form--body">
<pre className="tick-script-overlay--sample">
{tickscript}
</pre>
</div>
</div>
</OverlayTechnologies>
const {string, func} = PropTypes
TICKscriptOverlay.propTypes = {
tickscript: string,
onClose: func.isRequired,
}
export default TICKscriptOverlay

View File

@ -0,0 +1,8 @@
export const KAPACITOR_RULES_TABLE = {
colName: '200px',
colType: '90px',
colMessage: '460px',
colAlerts: '120px',
colEnabled: '64px',
colActions: '176px',
}

View File

@ -1,4 +1,4 @@
import React, {PropTypes} from 'react' import React, {PropTypes, Component} from 'react'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import _ from 'lodash' import _ from 'lodash'
@ -10,45 +10,19 @@ import {getActiveKapacitor, getKapacitorConfig} from 'shared/apis/index'
import {ALERTS, DEFAULT_RULE_ID} from 'src/kapacitor/constants' import {ALERTS, DEFAULT_RULE_ID} from 'src/kapacitor/constants'
import KapacitorRule from 'src/kapacitor/components/KapacitorRule' import KapacitorRule from 'src/kapacitor/components/KapacitorRule'
export const KapacitorRulePage = React.createClass({ class KapacitorRulePage extends Component {
propTypes: { constructor(props) {
source: PropTypes.shape({ super(props)
links: PropTypes.shape({
proxy: PropTypes.string.isRequired,
self: PropTypes.string.isRequired,
}),
}),
addFlashMessage: PropTypes.func,
rules: PropTypes.shape({}).isRequired,
queryConfigs: PropTypes.shape({}).isRequired,
kapacitorActions: PropTypes.shape({
loadDefaultRule: PropTypes.func.isRequired,
fetchRule: PropTypes.func.isRequired,
chooseTrigger: PropTypes.func.isRequired,
addEvery: PropTypes.func.isRequired,
removeEvery: PropTypes.func.isRequired,
updateRuleValues: PropTypes.func.isRequired,
updateMessage: PropTypes.func.isRequired,
updateAlerts: PropTypes.func.isRequired,
updateRuleName: PropTypes.func.isRequired,
}).isRequired,
queryActions: PropTypes.shape({}).isRequired,
params: PropTypes.shape({
ruleID: PropTypes.string,
}).isRequired,
router: PropTypes.shape({
push: PropTypes.func.isRequired,
}).isRequired,
},
getInitialState() { this.state = {
return {
enabledAlerts: [], enabledAlerts: [],
kapacitor: {}, kapacitor: {},
} }
},
componentDidMount() { this.isEditing = ::this.isEditing
}
async componentDidMount() {
const {params, source, kapacitorActions, addFlashMessage} = this.props const {params, source, kapacitorActions, addFlashMessage} = this.props
if (this.isEditing()) { if (this.isEditing()) {
kapacitorActions.fetchRule(source, params.ruleID) kapacitorActions.fetchRule(source, params.ruleID)
@ -56,35 +30,36 @@ export const KapacitorRulePage = React.createClass({
kapacitorActions.loadDefaultRule() kapacitorActions.loadDefaultRule()
} }
getActiveKapacitor(source).then(kapacitor => { const kapacitor = await getActiveKapacitor(this.props.source)
this.setState({kapacitor}) if (!kapacitor) {
getKapacitorConfig(kapacitor) return addFlashMessage({
.then(({data: {sections}}) => { type: 'error',
const enabledAlerts = Object.keys(sections).filter(section => { text: "We couldn't find a configured Kapacitor for this source", // eslint-disable-line quotes
return ( })
_.get( }
sections,
[section, 'elements', '0', 'options', 'enabled'], try {
false const {data: {sections}} = await getKapacitorConfig(kapacitor)
) && ALERTS.includes(section) const enabledAlerts = Object.keys(sections).filter(section => {
) return (
}) _.get(
this.setState({enabledAlerts}) sections,
}) [section, 'elements', '0', 'options', 'enabled'],
.catch(() => { false
addFlashMessage({ ) && ALERTS.includes(section)
type: 'error', )
text: 'There was a problem communicating with Kapacitor', })
})
}) this.setState({kapacitor, enabledAlerts})
.catch(() => { } catch (error) {
addFlashMessage({ addFlashMessage({
type: 'error', type: 'error',
text: "We couldn't find a configured Kapacitor for this source", // eslint-disable-line quotes text: 'There was a problem communicating with Kapacitor',
}) })
}) console.error(error)
}) throw error
}, }
}
render() { render() {
const { const {
@ -123,26 +98,54 @@ export const KapacitorRulePage = React.createClass({
kapacitor={kapacitor} kapacitor={kapacitor}
/> />
) )
}, }
isEditing() { isEditing() {
const {params} = this.props const {params} = this.props
return params.ruleID && params.ruleID !== 'new' return params.ruleID && params.ruleID !== 'new'
}, }
}
const {func, shape, string} = PropTypes
KapacitorRulePage.propTypes = {
source: shape({
links: shape({
proxy: string.isRequired,
self: string.isRequired,
}),
}),
addFlashMessage: func,
rules: shape({}).isRequired,
queryConfigs: shape({}).isRequired,
kapacitorActions: shape({
loadDefaultRule: func.isRequired,
fetchRule: func.isRequired,
chooseTrigger: func.isRequired,
addEvery: func.isRequired,
removeEvery: func.isRequired,
updateRuleValues: func.isRequired,
updateMessage: func.isRequired,
updateAlerts: func.isRequired,
updateRuleName: func.isRequired,
}).isRequired,
queryActions: shape({}).isRequired,
params: shape({
ruleID: string,
}).isRequired,
router: shape({
push: func.isRequired,
}).isRequired,
}
const mapStateToProps = ({rules, queryConfigs}) => ({
rules,
queryConfigs,
}) })
function mapStateToProps(state) { const mapDispatchToProps = dispatch => ({
return { kapacitorActions: bindActionCreators(kapacitorActionCreators, dispatch),
rules: state.rules, queryActions: bindActionCreators(queryActionCreators, dispatch),
queryConfigs: state.queryConfigs, })
}
}
function mapDispatchToProps(dispatch) {
return {
kapacitorActions: bindActionCreators(kapacitorActionCreators, dispatch),
queryActions: bindActionCreators(queryActionCreators, dispatch),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(KapacitorRulePage) export default connect(mapStateToProps, mapDispatchToProps)(KapacitorRulePage)

View File

@ -11,19 +11,23 @@ class KapacitorRulesPage extends Component {
this.state = { this.state = {
hasKapacitor: false, hasKapacitor: false,
loading: true, loading: true,
tickscript: null,
} }
this.handleDeleteRule = ::this.handleDeleteRule this.handleDeleteRule = ::this.handleDeleteRule
this.handleRuleStatus = ::this.handleRuleStatus this.handleRuleStatus = ::this.handleRuleStatus
this.handleReadTickscript = ::this.handleReadTickscript
this.handleCloseTickscript = ::this.handleCloseTickscript
} }
componentDidMount() { async componentDidMount() {
getActiveKapacitor(this.props.source).then(kapacitor => { const kapacitor = await getActiveKapacitor(this.props.source)
if (kapacitor) { if (!kapacitor) {
this.props.actions.fetchRules(kapacitor) return
} }
this.setState({loading: false, hasKapacitor: !!kapacitor})
}) await this.props.actions.fetchRules(kapacitor)
this.setState({loading: false, hasKapacitor: !!kapacitor})
} }
handleDeleteRule(rule) { handleDeleteRule(rule) {
@ -39,9 +43,17 @@ class KapacitorRulesPage extends Component {
actions.updateRuleStatusSuccess(rule.id, status) actions.updateRuleStatusSuccess(rule.id, status)
} }
handleReadTickscript({tickscript}) {
this.setState({tickscript})
}
handleCloseTickscript() {
this.setState({tickscript: null})
}
render() { render() {
const {source, rules} = this.props const {source, rules} = this.props
const {hasKapacitor, loading} = this.state const {hasKapacitor, loading, tickscript} = this.state
return ( return (
<KapacitorRules <KapacitorRules
@ -51,6 +63,9 @@ class KapacitorRulesPage extends Component {
loading={loading} loading={loading}
onDelete={this.handleDeleteRule} onDelete={this.handleDeleteRule}
onChangeRuleStatus={this.handleRuleStatus} onChangeRuleStatus={this.handleRuleStatus}
onReadTickscript={this.handleReadTickscript}
tickscript={tickscript}
onCloseTickscript={this.handleCloseTickscript}
/> />
) )
} }

View File

@ -67,14 +67,19 @@ export function getKapacitor(source, kapacitorID) {
}) })
} }
export function getActiveKapacitor(source) { export const getActiveKapacitor = async source => {
return AJAX({ try {
url: source.links.kapacitors, const {data} = await AJAX({
method: 'GET', url: source.links.kapacitors,
}).then(({data}) => { method: 'GET',
})
const activeKapacitor = data.kapacitors.find(k => k.active) const activeKapacitor = data.kapacitors.find(k => k.active)
return activeKapacitor || data.kapacitors[0] return activeKapacitor || data.kapacitors[0]
}) } catch (error) {
console.error(error)
throw error
}
} }
export const getKapacitors = async source => { export const getKapacitors = async source => {
@ -138,11 +143,11 @@ export function updateKapacitor({
}) })
} }
export function getKapacitorConfig(kapacitor) { export const getKapacitorConfig = async kapacitor => {
return kapacitorProxy(kapacitor, 'GET', '/kapacitor/v1/config', '') return await kapacitorProxy(kapacitor, 'GET', '/kapacitor/v1/config', '')
} }
export function getKapacitorConfigSection(kapacitor, section) { export const getKapacitorConfigSection = (kapacitor, section) => {
return kapacitorProxy(kapacitor, 'GET', `/kapacitor/v1/config/${section}`, '') return kapacitorProxy(kapacitor, 'GET', `/kapacitor/v1/config/${section}`, '')
} }
@ -163,11 +168,9 @@ export function updateKapacitorConfigSection(kapacitor, section, properties) {
} }
export function testAlertOutput(kapacitor, outputName, properties) { export function testAlertOutput(kapacitor, outputName, properties) {
return kapacitorProxy( return kapacitorProxy(kapacitor, 'GET', '/kapacitor/v1/service-tests').then(({
kapacitor, data: {services},
'GET', }) => {
'/kapacitor/v1/service-tests'
).then(({data: {services}}) => {
const service = services.find(s => s.name === outputName) const service = services.find(s => s.name === outputName)
return kapacitorProxy( return kapacitorProxy(
kapacitor, kapacitor,

View File

@ -261,3 +261,14 @@ $table-tab-scrollbar-height: 6px;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
} }
/*
No Wrap Cells
----------------------------------------------
*/
table .table-cell-nowrap {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@ -217,3 +217,30 @@ br {
color: $g11-sidewalk; color: $g11-sidewalk;
font-size: 13px; font-size: 13px;
} }
/*
View TICKscript Overlay
-----------------------------------------------------------------------------
*/
$tick-script-overlay-margin: 30px;
.tick-script-overlay {
max-width: 960px;
margin: 0 auto $tick-script-overlay-margin auto;
height: calc(100% - #{$tick-script-overlay-margin});
position: relative;
.write-data-form--body {
height: calc(100% - 60px);
display: flex;
flex-direction: column;
}
}
.tick-script-overlay--sample {
margin: 0;
white-space: pre-wrap;
font-size: 14px;
padding: 20px;
border: 2px solid $g4-onyx;
}