diff --git a/ui/src/chronograf/reducers/queryConfigs.js b/ui/src/chronograf/reducers/queryConfigs.js index 52bd40bb7c..82fe56c7c1 100644 --- a/ui/src/chronograf/reducers/queryConfigs.js +++ b/ui/src/chronograf/reducers/queryConfigs.js @@ -37,9 +37,9 @@ export default function queryConfigs(state = {}, action) { } case 'LOAD_KAPACITOR_QUERY': { - const {queryID, query} = action.payload; + const {query} = action.payload; const nextState = Object.assign({}, state, { - [queryID]: query, + [query.id]: query, }); return nextState; diff --git a/ui/src/index.js b/ui/src/index.js index fd1ba732fa..12dd3359ab 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -119,6 +119,7 @@ const Root = React.createClass({ <Route path="alerts" component={AlertsApp} /> <Route path="alert-rules" component={KapacitorRulesPage} /> <Route path="alert-rules/:ruleID" component={KapacitorRulePage} /> + <Route path="alert-rules/new" component={KapacitorRulePage} /> </Route> <Route path="tasks" component={TasksPage} /> <Route path="*" component={NotFound} /> diff --git a/ui/src/kapacitor/actions/view/index.js b/ui/src/kapacitor/actions/view/index.js index fc62d4652d..df3135d344 100644 --- a/ui/src/kapacitor/actions/view/index.js +++ b/ui/src/kapacitor/actions/view/index.js @@ -1,14 +1,38 @@ import uuid from 'node-uuid'; +import {getRules, getRule} from 'src/kapacitor/apis'; +import {getKapacitor} from 'src/shared/apis'; -export function fetchRule() { // ruleID +export function fetchRule(source, ruleID) { + return (dispatch) => { + getKapacitor(source).then((kapacitor) => { + getRule(kapacitor, ruleID).then(({data: rule}) => { + dispatch({ + type: 'LOAD_RULE', + payload: { + rule: Object.assign(rule, {queryID: rule.query.id}), + }, + }); + + dispatch({ + type: 'LOAD_KAPACITOR_QUERY', + payload: { + query: rule.query, + }, + }); + }); + }); + }; +} + +export function loadDefaultRule() { return (dispatch) => { - // do some ajax to get the rule. put it in the payload const queryID = uuid.v4(); dispatch({ - type: 'LOAD_RULE', - payload: {}, + type: 'LOAD_DEFAULT_RULE', + payload: { + queryID, + }, }); - dispatch({ type: 'ADD_KAPACITOR_QUERY', payload: { @@ -18,22 +42,17 @@ export function fetchRule() { // ruleID }; } -export function loadDefaultRule() { +export function fetchRules(source) { return (dispatch) => { - const queryID = uuid.v4(); - const ruleID = uuid.v4(); - dispatch({ - type: 'LOAD_DEFAULT_RULE', - payload: { - queryID, - ruleID, - }, - }); - dispatch({ - type: 'ADD_KAPACITOR_QUERY', - payload: { - queryId: queryID, - }, + getKapacitor(source).then((kapacitor) => { + getRules(kapacitor).then(({data: {rules}}) => { + dispatch({ + type: 'LOAD_RULES', + payload: { + rules, + }, + }); + }); }); }; } diff --git a/ui/src/kapacitor/apis/index.js b/ui/src/kapacitor/apis/index.js index 3b68e95c35..5a9c7ebd23 100644 --- a/ui/src/kapacitor/apis/index.js +++ b/ui/src/kapacitor/apis/index.js @@ -14,3 +14,18 @@ export function getRules(kapacitor) { url: kapacitor.links.rules, }); } + +export function getRule(kapacitor, ruleID) { + return AJAX({ + method: 'GET', + url: `${kapacitor.links.rules}/${ruleID}`, + }); +} + +export function editRule(rule) { + return AJAX({ + method: 'PUT', + url: rule.links.self, + data: rule, + }); +} diff --git a/ui/src/kapacitor/constants/index.js b/ui/src/kapacitor/constants/index.js index b32ec9b34f..f93d7483db 100644 --- a/ui/src/kapacitor/constants/index.js +++ b/ui/src/kapacitor/constants/index.js @@ -24,3 +24,5 @@ export const PERIODS = ['1m', '5m', '10m', '30m', '1h', '2h', '1d']; export const CHANGES = ['change', '% change']; export const SHIFTS = ['1m', '5m', '10m', '30m', '1h', '2h', '1d']; export const ALERTS = ['hipchat', 'opsgenie', 'pagerduty', 'sensu', 'slack', 'smtp', 'talk', 'telegram', 'victorops']; + +export const DEFAULT_RULE_ID = 'DEFAULT_RULE_ID'; diff --git a/ui/src/kapacitor/containers/KapacitorRulePage.js b/ui/src/kapacitor/containers/KapacitorRulePage.js index 07ca5d9495..d15fd480cd 100644 --- a/ui/src/kapacitor/containers/KapacitorRulePage.js +++ b/ui/src/kapacitor/containers/KapacitorRulePage.js @@ -1,4 +1,5 @@ import React, {PropTypes} from 'react'; +import {withRouter} from 'react-router'; import {connect} from 'react-redux'; import _ from 'lodash'; import DataSection from '../components/DataSection'; @@ -12,8 +13,8 @@ import LineGraph from 'shared/components/LineGraph'; const RefreshingLineGraph = AutoRefresh(LineGraph); import {getKapacitor, getKapacitorConfig} from 'shared/apis/index'; import Dropdown from 'shared/components/Dropdown'; -import {ALERTS} from 'src/kapacitor/constants'; -import {createRule} from 'src/kapacitor/apis'; +import {ALERTS, DEFAULT_RULE_ID} from 'src/kapacitor/constants'; +import {createRule, editRule} from 'src/kapacitor/apis'; export const KapacitorRulePage = React.createClass({ propTypes: { @@ -49,10 +50,15 @@ export const KapacitorRulePage = React.createClass({ }; }, + isEditing() { + const {params} = this.props; + return params.ruleID && params.ruleID !== 'new'; + }, + componentDidMount() { - const {ruleID} = false; // this.props.params; - if (ruleID) { - this.props.kapacitorActions.fetchRule(ruleID); + const {ruleID} = this.props.params; + if (this.isEditing()) { + this.props.kapacitorActions.fetchRule(this.props.source, ruleID); } else { this.props.kapacitorActions.loadDefaultRule(); } @@ -64,25 +70,34 @@ export const KapacitorRulePage = React.createClass({ }); this.setState({kapacitor, enabledAlerts}); }).catch(() => { - this.props.addFlashMessage({type: 'failure', message: `There was a problem communicating with Kapacitor`}); + this.props.addFlashMessage({type: 'failure', text: `There was a problem communicating with Kapacitor`}); }).catch(() => { - this.props.addFlashMessage({type: 'failure', message: `We couldn't find a configured Kapacitor for this source`}); + this.props.addFlashMessage({type: 'failure', text: `We couldn't find a configured Kapacitor for this source`}); }); }); }, handleSave() { - const {queryConfigs, rules} = this.props; - const rule = rules[Object.keys(rules)[0]]; // this.props.params.taskID - const newRule = Object.assign({}, rule, { - query: queryConfigs[rule.queryID], - }); - delete newRule.queryID; - createRule(this.state.kapacitor, newRule).then(() => { - // maybe update the default rule in redux state.. and update the URL - }).catch(() => { - this.props.addFlashMessage({type: 'failure', message: `There was a problem creating the rule`}); - }); + const {queryConfigs, rules, params, source} = this.props; + if (this.isEditing()) { // If we are editing updated rule if not, create a new one + editRule(rules[params.ruleID]).then(() => { + this.props.addFlashMessage({type: 'success', text: `Rule successfully updated!`}); + }).catch(() => { + this.props.addFlashMessage({type: 'failure', text: `There was a problem updating the rule`}); + }); + } else { + const rule = rules[DEFAULT_RULE_ID]; + const newRule = Object.assign({}, rule, { + query: queryConfigs[rule.queryID], + }); + delete newRule.queryID; + createRule(this.state.kapacitor, newRule).then(() => { + this.props.router.push(`sources/${source.id}/alert-rules`); + this.props.addFlashMessage({type: 'success', text: `Rule successfully created`}); + }).catch(() => { + this.props.addFlashMessage({type: 'failure', text: `There was a problem creating the rule`}); + }); + } }, handleChooseAlert(item) { @@ -132,13 +147,13 @@ export const KapacitorRulePage = React.createClass({ }, render() { - const {rules, queryConfigs, source} = this.props; - const rule = rules[Object.keys(rules)[0]]; // this.props.params.ruleID + const {rules, queryConfigs, source, params} = this.props; + const rule = this.isEditing() ? rules[params.ruleID] : rules[DEFAULT_RULE_ID]; const query = rule && queryConfigs[rule.queryID]; const autoRefreshMs = 30000; - if (!query) { // or somethin like that - return null; // or a spinner or somethin + if (!query) { + return <div className="page-spinner"></div>; } const queryText = selectStatement({lower: 'now() - 15m'}, query); @@ -218,7 +233,7 @@ export const KapacitorRulePage = React.createClass({ return ( <div className="kapacitor-rule-section"> <h3>Message</h3> - <textarea ref={(r) => this.message = r} onChange={() => this.handleMessageChange(rule)} /> + <textarea ref={(r) => this.message = r} value={rule.message} onChange={() => this.handleMessageChange(rule)} /> </div> ); }, @@ -256,4 +271,4 @@ function mapDispatchToProps(dispatch) { }; } -export default connect(mapStateToProps, mapDispatchToProps)(KapacitorRulePage); +export default connect(mapStateToProps, mapDispatchToProps)(withRouter(KapacitorRulePage)); diff --git a/ui/src/kapacitor/containers/KapacitorRulesPage.js b/ui/src/kapacitor/containers/KapacitorRulesPage.js index ebe6a11c2c..c42b69b203 100644 --- a/ui/src/kapacitor/containers/KapacitorRulesPage.js +++ b/ui/src/kapacitor/containers/KapacitorRulesPage.js @@ -1,39 +1,36 @@ import React, {PropTypes} from 'react'; +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; import {Link} from 'react-router'; -import {getRules} from 'src/kapacitor/apis'; -import {getKapacitor} from 'src/shared/apis'; +import * as kapacitorActionCreators from 'src/kapacitor/actions/view'; export const KapacitorRulesPage = React.createClass({ propTypes: { source: PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - type: PropTypes.string.isRequired, // 'influx-enterprise' - username: PropTypes.string.isRequired, links: PropTypes.shape({ + proxy: PropTypes.string.isRequired, + self: PropTypes.string.isRequired, kapacitors: PropTypes.string.isRequired, - }).isRequired, + }), + }), + rules: PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string.isRequired, + trigger: PropTypes.string.isRequired, + message: PropTypes.string.isRequired, + alerts: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, + })).isRequired, + actions: PropTypes.shape({ + fetchRules: PropTypes.func.isRequired, }).isRequired, addFlashMessage: PropTypes.func, }, - getInitialState() { - return { - rules: [], - }; - }, - componentDidMount() { - getKapacitor(this.props.source).then((kapacitor) => { - getRules(kapacitor).then(({data: {rules}}) => { - this.setState({rules}); - }); - }); + this.props.actions.fetchRules(this.props.source); }, render() { - const {rules} = this.state; - const {source} = this.props; + const {source, rules} = this.props; return ( <div className="kapacitor-rules-page"> @@ -48,6 +45,7 @@ export const KapacitorRulesPage = React.createClass({ <div className="panel panel-minimal"> <div className="panel-heading u-flex u-ai-center u-jc-space-between"> <h2 className="panel-title">Alert Rules</h2> + <Link to={`/sources/${source.id}/alert-rules/new`} className="btn btn-sm btn-primary">Add New Rule</Link> </div> <div className="panel-body"> <table className="table v-center"> @@ -82,4 +80,16 @@ export const KapacitorRulesPage = React.createClass({ }, }); -export default KapacitorRulesPage; +function mapStateToProps(state) { + return { + rules: Object.values(state.rules), + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators(kapacitorActionCreators, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(KapacitorRulesPage); diff --git a/ui/src/kapacitor/reducers/rules.js b/ui/src/kapacitor/reducers/rules.js index f153cc2a51..ff759192d7 100644 --- a/ui/src/kapacitor/reducers/rules.js +++ b/ui/src/kapacitor/reducers/rules.js @@ -1,12 +1,13 @@ -import {defaultRuleConfigs} from 'src/kapacitor/constants'; +import {defaultRuleConfigs, DEFAULT_RULE_ID} from 'src/kapacitor/constants'; +import _ from 'lodash'; export default function rules(state = {}, action) { switch (action.type) { case 'LOAD_DEFAULT_RULE': { - const {queryID, ruleID} = action.payload; + const {queryID} = action.payload; return Object.assign({}, state, { - [ruleID]: { - id: ruleID, + [DEFAULT_RULE_ID]: { + id: DEFAULT_RULE_ID, queryID, trigger: 'threshold', values: defaultRuleConfigs.threshold, @@ -18,10 +19,16 @@ export default function rules(state = {}, action) { }); } + case 'LOAD_RULES': { + const theRules = action.payload.rules; + const ruleIDs = theRules.map(r => r.id); + return _.zipObject(ruleIDs, theRules); + } + case 'LOAD_RULE': { - const {ruleID, rule} = action.payload; + const {rule} = action.payload; return Object.assign({}, state, { - [ruleID]: rule, + [rule.id]: rule, }); } diff --git a/ui/src/sources/containers/ManageSources.js b/ui/src/sources/containers/ManageSources.js index b4180db6df..9def5396a2 100644 --- a/ui/src/sources/containers/ManageSources.js +++ b/ui/src/sources/containers/ManageSources.js @@ -8,6 +8,13 @@ export const ManageSources = React.createClass({ location: PropTypes.shape({ pathname: PropTypes.string.isRequired, }).isRequired, + source: PropTypes.shape({ + id: PropTypes.string.isRequired, + links: PropTypes.shape({ + proxy: PropTypes.string.isRequired, + self: PropTypes.string.isRequired, + }), + }), }, getInitialState() { return { @@ -59,7 +66,7 @@ export const ManageSources = React.createClass({ <div className="panel panel-minimal"> <div className="panel-heading u-flex u-ai-center u-jc-space-between"> <h2 className="panel-title">{sourcesTitle}</h2> - <Link to={`/sources/1/manage-sources/new`} className="btn btn-sm btn-primary">Add New Source</Link> + <Link to={`/sources/${this.props.source.id}/manage-sources/new`} className="btn btn-sm btn-primary">Add New Source</Link> </div> <div className="panel-body"> <div className="table-responsive margin-bottom-zero">