Merge pull request #376 from influxdata/feature/rules-index-#307

Refactor KapacitorRulePage
pull/10616/head
Nathan Haugo 2016-11-04 17:30:48 -07:00 committed by GitHub
commit ccd98b2b21
9 changed files with 150 additions and 74 deletions

View File

@ -37,9 +37,9 @@ export default function queryConfigs(state = {}, action) {
} }
case 'LOAD_KAPACITOR_QUERY': { case 'LOAD_KAPACITOR_QUERY': {
const {queryID, query} = action.payload; const {query} = action.payload;
const nextState = Object.assign({}, state, { const nextState = Object.assign({}, state, {
[queryID]: query, [query.id]: query,
}); });
return nextState; return nextState;

View File

@ -119,6 +119,7 @@ const Root = React.createClass({
<Route path="alerts" component={AlertsApp} /> <Route path="alerts" component={AlertsApp} />
<Route path="alert-rules" component={KapacitorRulesPage} /> <Route path="alert-rules" component={KapacitorRulesPage} />
<Route path="alert-rules/:ruleID" component={KapacitorRulePage} /> <Route path="alert-rules/:ruleID" component={KapacitorRulePage} />
<Route path="alert-rules/new" component={KapacitorRulePage} />
</Route> </Route>
<Route path="tasks" component={TasksPage} /> <Route path="tasks" component={TasksPage} />
<Route path="*" component={NotFound} /> <Route path="*" component={NotFound} />

View File

@ -1,14 +1,38 @@
import uuid from 'node-uuid'; 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) => { return (dispatch) => {
// do some ajax to get the rule. put it in the payload getKapacitor(source).then((kapacitor) => {
const queryID = uuid.v4(); getRule(kapacitor, ruleID).then(({data: rule}) => {
dispatch({ dispatch({
type: 'LOAD_RULE', type: 'LOAD_RULE',
payload: {}, payload: {
rule: Object.assign(rule, {queryID: rule.query.id}),
},
}); });
dispatch({
type: 'LOAD_KAPACITOR_QUERY',
payload: {
query: rule.query,
},
});
});
});
};
}
export function loadDefaultRule() {
return (dispatch) => {
const queryID = uuid.v4();
dispatch({
type: 'LOAD_DEFAULT_RULE',
payload: {
queryID,
},
});
dispatch({ dispatch({
type: 'ADD_KAPACITOR_QUERY', type: 'ADD_KAPACITOR_QUERY',
payload: { payload: {
@ -18,22 +42,17 @@ export function fetchRule() { // ruleID
}; };
} }
export function loadDefaultRule() { export function fetchRules(source) {
return (dispatch) => { return (dispatch) => {
const queryID = uuid.v4(); getKapacitor(source).then((kapacitor) => {
const ruleID = uuid.v4(); getRules(kapacitor).then(({data: {rules}}) => {
dispatch({ dispatch({
type: 'LOAD_DEFAULT_RULE', type: 'LOAD_RULES',
payload: { payload: {
queryID, rules,
ruleID,
}, },
}); });
dispatch({ });
type: 'ADD_KAPACITOR_QUERY',
payload: {
queryId: queryID,
},
}); });
}; };
} }

View File

@ -14,3 +14,18 @@ export function getRules(kapacitor) {
url: kapacitor.links.rules, 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,
});
}

View File

@ -24,3 +24,5 @@ export const PERIODS = ['1m', '5m', '10m', '30m', '1h', '2h', '1d'];
export const CHANGES = ['change', '% change']; export const CHANGES = ['change', '% change'];
export const SHIFTS = ['1m', '5m', '10m', '30m', '1h', '2h', '1d']; export const SHIFTS = ['1m', '5m', '10m', '30m', '1h', '2h', '1d'];
export const ALERTS = ['hipchat', 'opsgenie', 'pagerduty', 'sensu', 'slack', 'smtp', 'talk', 'telegram', 'victorops']; export const ALERTS = ['hipchat', 'opsgenie', 'pagerduty', 'sensu', 'slack', 'smtp', 'talk', 'telegram', 'victorops'];
export const DEFAULT_RULE_ID = 'DEFAULT_RULE_ID';

View File

@ -1,4 +1,5 @@
import React, {PropTypes} from 'react'; import React, {PropTypes} from 'react';
import {withRouter} from 'react-router';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import _ from 'lodash'; import _ from 'lodash';
import DataSection from '../components/DataSection'; import DataSection from '../components/DataSection';
@ -12,8 +13,8 @@ import LineGraph from 'shared/components/LineGraph';
const RefreshingLineGraph = AutoRefresh(LineGraph); const RefreshingLineGraph = AutoRefresh(LineGraph);
import {getKapacitor, getKapacitorConfig} from 'shared/apis/index'; import {getKapacitor, getKapacitorConfig} from 'shared/apis/index';
import Dropdown from 'shared/components/Dropdown'; import Dropdown from 'shared/components/Dropdown';
import {ALERTS} from 'src/kapacitor/constants'; import {ALERTS, DEFAULT_RULE_ID} from 'src/kapacitor/constants';
import {createRule} from 'src/kapacitor/apis'; import {createRule, editRule} from 'src/kapacitor/apis';
export const KapacitorRulePage = React.createClass({ export const KapacitorRulePage = React.createClass({
propTypes: { propTypes: {
@ -49,10 +50,15 @@ export const KapacitorRulePage = React.createClass({
}; };
}, },
isEditing() {
const {params} = this.props;
return params.ruleID && params.ruleID !== 'new';
},
componentDidMount() { componentDidMount() {
const {ruleID} = false; // this.props.params; const {ruleID} = this.props.params;
if (ruleID) { if (this.isEditing()) {
this.props.kapacitorActions.fetchRule(ruleID); this.props.kapacitorActions.fetchRule(this.props.source, ruleID);
} else { } else {
this.props.kapacitorActions.loadDefaultRule(); this.props.kapacitorActions.loadDefaultRule();
} }
@ -64,25 +70,34 @@ export const KapacitorRulePage = React.createClass({
}); });
this.setState({kapacitor, enabledAlerts}); this.setState({kapacitor, enabledAlerts});
}).catch(() => { }).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(() => { }).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() { handleSave() {
const {queryConfigs, rules} = this.props; const {queryConfigs, rules, params, source} = this.props;
const rule = rules[Object.keys(rules)[0]]; // this.props.params.taskID 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, { const newRule = Object.assign({}, rule, {
query: queryConfigs[rule.queryID], query: queryConfigs[rule.queryID],
}); });
delete newRule.queryID; delete newRule.queryID;
createRule(this.state.kapacitor, newRule).then(() => { createRule(this.state.kapacitor, newRule).then(() => {
// maybe update the default rule in redux state.. and update the URL this.props.router.push(`sources/${source.id}/alert-rules`);
this.props.addFlashMessage({type: 'success', text: `Rule successfully created`});
}).catch(() => { }).catch(() => {
this.props.addFlashMessage({type: 'failure', message: `There was a problem creating the rule`}); this.props.addFlashMessage({type: 'failure', text: `There was a problem creating the rule`});
}); });
}
}, },
handleChooseAlert(item) { handleChooseAlert(item) {
@ -132,13 +147,13 @@ export const KapacitorRulePage = React.createClass({
}, },
render() { render() {
const {rules, queryConfigs, source} = this.props; const {rules, queryConfigs, source, params} = this.props;
const rule = rules[Object.keys(rules)[0]]; // this.props.params.ruleID const rule = this.isEditing() ? rules[params.ruleID] : rules[DEFAULT_RULE_ID];
const query = rule && queryConfigs[rule.queryID]; const query = rule && queryConfigs[rule.queryID];
const autoRefreshMs = 30000; const autoRefreshMs = 30000;
if (!query) { // or somethin like that if (!query) {
return null; // or a spinner or somethin return <div className="page-spinner"></div>;
} }
const queryText = selectStatement({lower: 'now() - 15m'}, query); const queryText = selectStatement({lower: 'now() - 15m'}, query);
@ -218,7 +233,7 @@ export const KapacitorRulePage = React.createClass({
return ( return (
<div className="kapacitor-rule-section"> <div className="kapacitor-rule-section">
<h3>Message</h3> <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> </div>
); );
}, },
@ -256,4 +271,4 @@ function mapDispatchToProps(dispatch) {
}; };
} }
export default connect(mapStateToProps, mapDispatchToProps)(KapacitorRulePage); export default connect(mapStateToProps, mapDispatchToProps)(withRouter(KapacitorRulePage));

View File

@ -1,39 +1,36 @@
import React, {PropTypes} from 'react'; import React, {PropTypes} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {Link} from 'react-router'; import {Link} from 'react-router';
import {getRules} from 'src/kapacitor/apis'; import * as kapacitorActionCreators from 'src/kapacitor/actions/view';
import {getKapacitor} from 'src/shared/apis';
export const KapacitorRulesPage = React.createClass({ export const KapacitorRulesPage = React.createClass({
propTypes: { propTypes: {
source: PropTypes.shape({ source: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
type: PropTypes.string.isRequired, // 'influx-enterprise'
username: PropTypes.string.isRequired,
links: PropTypes.shape({ links: PropTypes.shape({
proxy: PropTypes.string.isRequired,
self: PropTypes.string.isRequired,
kapacitors: 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, }).isRequired,
addFlashMessage: PropTypes.func, addFlashMessage: PropTypes.func,
}, },
getInitialState() {
return {
rules: [],
};
},
componentDidMount() { componentDidMount() {
getKapacitor(this.props.source).then((kapacitor) => { this.props.actions.fetchRules(this.props.source);
getRules(kapacitor).then(({data: {rules}}) => {
this.setState({rules});
});
});
}, },
render() { render() {
const {rules} = this.state; const {source, rules} = this.props;
const {source} = this.props;
return ( return (
<div className="kapacitor-rules-page"> <div className="kapacitor-rules-page">
@ -48,6 +45,7 @@ export const KapacitorRulesPage = React.createClass({
<div className="panel panel-minimal"> <div className="panel panel-minimal">
<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">Alert Rules</h2> <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>
<div className="panel-body"> <div className="panel-body">
<table className="table v-center"> <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);

View File

@ -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) { export default function rules(state = {}, action) {
switch (action.type) { switch (action.type) {
case 'LOAD_DEFAULT_RULE': { case 'LOAD_DEFAULT_RULE': {
const {queryID, ruleID} = action.payload; const {queryID} = action.payload;
return Object.assign({}, state, { return Object.assign({}, state, {
[ruleID]: { [DEFAULT_RULE_ID]: {
id: ruleID, id: DEFAULT_RULE_ID,
queryID, queryID,
trigger: 'threshold', trigger: 'threshold',
values: defaultRuleConfigs.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': { case 'LOAD_RULE': {
const {ruleID, rule} = action.payload; const {rule} = action.payload;
return Object.assign({}, state, { return Object.assign({}, state, {
[ruleID]: rule, [rule.id]: rule,
}); });
} }

View File

@ -8,6 +8,13 @@ export const ManageSources = React.createClass({
location: PropTypes.shape({ location: PropTypes.shape({
pathname: PropTypes.string.isRequired, pathname: PropTypes.string.isRequired,
}).isRequired, }).isRequired,
source: PropTypes.shape({
id: PropTypes.string.isRequired,
links: PropTypes.shape({
proxy: PropTypes.string.isRequired,
self: PropTypes.string.isRequired,
}),
}),
}, },
getInitialState() { getInitialState() {
return { return {
@ -59,7 +66,7 @@ export const ManageSources = React.createClass({
<div className="panel panel-minimal"> <div className="panel panel-minimal">
<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">{sourcesTitle}</h2> <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>
<div className="panel-body"> <div className="panel-body">
<div className="table-responsive margin-bottom-zero"> <div className="table-responsive margin-bottom-zero">