Merge pull request #868 from influxdata/disable-alert

Change rule status
pull/870/head
Chris Goller 2017-02-10 16:41:28 -06:00 committed by GitHub
commit f81cd7e7ad
11 changed files with 313 additions and 21 deletions

View File

@ -7,6 +7,7 @@
1. [#838](https://github.com/influxdata/chronograf/issues/838): Add detail node to kapacitor alerts
2. [#853](https://github.com/influxdata/chronograf/issues/853): Updated builds to use yarn over npm install
3. [#860](https://github.com/influxdata/chronograf/issues/860): Add gzip encoding and caching of static assets to server
4. [#847](https://github.com/influxdata/chronograf/issues/847): Enable and disable kapacitor alerts from alert manager
### Upcoming UI Improvements
1. [#822](https://github.com/influxdata/chronograf/issues/822): Simplify and improve layout of the Data Explorer

View File

@ -119,6 +119,44 @@ func (c *Client) Enable(ctx context.Context, href string) (*Task, error) {
return c.updateStatus(ctx, href, client.Enabled)
}
// AllStatus returns the status of all tasks in kapacitor
func (c *Client) AllStatus(ctx context.Context) (map[string]string, error) {
kapa, err := c.kapaClient(ctx)
if err != nil {
return nil, err
}
// Only get the status, id and link section back
opts := &client.ListTasksOptions{
Fields: []string{"status"},
}
tasks, err := kapa.ListTasks(opts)
if err != nil {
return nil, err
}
taskStatuses := map[string]string{}
for _, task := range tasks {
taskStatuses[task.ID] = task.Status.String()
}
return taskStatuses, nil
}
// Status returns the status of a task in kapacitor
func (c *Client) Status(ctx context.Context, href string) (string, error) {
kapa, err := c.kapaClient(ctx)
if err != nil {
return "", err
}
task, err := kapa.Task(client.Link{Href: href}, nil)
if err != nil {
return "", err
}
return task.Status.String(), nil
}
// Update changes the tickscript of a given id.
func (c *Client) Update(ctx context.Context, href string, rule chronograf.AlertRule) (*Task, error) {
kapa, err := c.kapaClient(ctx)

View File

@ -349,6 +349,7 @@ func (h *Service) KapacitorRulesPost(w http.ResponseWriter, r *http.Request) {
Output: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/proxy?path=%s", srv.SrcID, srv.ID, url.QueryEscape(task.HrefOutput)),
},
TICKScript: string(task.TICKScript),
Status: "enabled",
}
w.Header().Add("Location", res.Links.Self)
@ -364,6 +365,7 @@ type alertLinks struct {
type alertResponse struct {
chronograf.AlertRule
TICKScript string `json:"tickscript"`
Status string `json:"status"`
Links alertLinks `json:"links"`
}
@ -438,6 +440,92 @@ func (h *Service) KapacitorRulesPut(w http.ResponseWriter, r *http.Request) {
Output: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/proxy?path=%s", srv.SrcID, srv.ID, url.QueryEscape(task.HrefOutput)),
},
TICKScript: string(task.TICKScript),
Status: "enabled",
}
encodeJSON(w, http.StatusOK, res, h.Logger)
}
type KapacitorStatus struct {
Status string `json:"status"`
}
func (k *KapacitorStatus) Valid() error {
if k.Status == "enabled" || k.Status == "disabled" {
return nil
}
return fmt.Errorf("Invalid Kapacitor status: %s", k.Status)
}
// KapacitorRulesStatus proxies PATCH to kapacitor to enable/disable tasks
func (h *Service) KapacitorRulesStatus(w http.ResponseWriter, r *http.Request) {
id, err := paramID("kid", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
return
}
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
return
}
ctx := r.Context()
srv, err := h.ServersStore.Get(ctx, id)
if err != nil || srv.SrcID != srcID {
notFound(w, id, h.Logger)
return
}
tid := httprouter.GetParamFromContext(ctx, "tid")
c := kapa.Client{
URL: srv.URL,
Username: srv.Username,
Password: srv.Password,
Ticker: &kapa.Alert{},
}
var req KapacitorStatus
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, h.Logger)
return
}
if err := req.Valid(); err != nil {
invalidData(w, err, h.Logger)
return
}
// Check if the rule exists and is scoped correctly
alert, err := h.AlertRulesStore.Get(ctx, srcID, id, tid)
if err != nil {
if err == chronograf.ErrAlertNotFound {
notFound(w, id, h.Logger)
return
}
Error(w, http.StatusInternalServerError, err.Error(), h.Logger)
return
}
var task *kapa.Task
if req.Status == "enabled" {
task, err = c.Enable(ctx, c.Href(tid))
} else {
task, err = c.Disable(ctx, c.Href(tid))
}
if err != nil {
Error(w, http.StatusInternalServerError, err.Error(), h.Logger)
return
}
res := alertResponse{
AlertRule: alert,
Links: alertLinks{
Self: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/rules/%s", srv.SrcID, srv.ID, task.ID),
Kapacitor: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/proxy?path=%s", srv.SrcID, srv.ID, url.QueryEscape(task.Href)),
Output: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/proxy?path=%s", srv.SrcID, srv.ID, url.QueryEscape(task.HrefOutput)),
},
TICKScript: string(task.TICKScript),
Status: req.Status,
}
encodeJSON(w, http.StatusOK, res, h.Logger)
}
@ -470,7 +558,18 @@ func (h *Service) KapacitorRulesGet(w http.ResponseWriter, r *http.Request) {
}
ticker := &kapa.Alert{}
c := kapa.Client{}
c := kapa.Client{
URL: srv.URL,
Username: srv.Username,
Password: srv.Password,
Ticker: ticker,
}
statuses, err := c.AllStatus(ctx)
if err != nil {
Error(w, http.StatusInternalServerError, err.Error(), h.Logger)
return
}
res := allAlertsResponse{
Rules: []alertResponse{},
}
@ -481,6 +580,11 @@ func (h *Service) KapacitorRulesGet(w http.ResponseWriter, r *http.Request) {
return
}
status, ok := statuses[rule.ID]
// The defined rule is not actually in kapacitor
if !ok {
continue
}
ar := alertResponse{
AlertRule: rule,
Links: alertLinks{
@ -489,6 +593,7 @@ func (h *Service) KapacitorRulesGet(w http.ResponseWriter, r *http.Request) {
Output: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/proxy?path=%s", srv.SrcID, srv.ID, url.QueryEscape(c.HrefOutput(rule.ID))),
},
TICKScript: string(tickscript),
Status: status,
}
res.Rules = append(res.Rules, ar)
}
@ -532,13 +637,24 @@ func (h *Service) KapacitorRulesID(w http.ResponseWriter, r *http.Request) {
}
ticker := &kapa.Alert{}
c := kapa.Client{}
c := kapa.Client{
URL: srv.URL,
Username: srv.Username,
Password: srv.Password,
Ticker: ticker,
}
tickscript, err := ticker.Generate(rule)
if err != nil {
Error(w, http.StatusInternalServerError, err.Error(), h.Logger)
return
}
status, err := c.Status(ctx, c.Href(rule.ID))
if err != nil {
Error(w, http.StatusInternalServerError, err.Error(), h.Logger)
return
}
res := alertResponse{
AlertRule: rule,
Links: alertLinks{
@ -547,6 +663,7 @@ func (h *Service) KapacitorRulesID(w http.ResponseWriter, r *http.Request) {
Output: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/proxy?path=%s", srv.SrcID, srv.ID, url.QueryEscape(c.HrefOutput(rule.ID))),
},
TICKScript: string(tickscript),
Status: status,
}
encodeJSON(w, http.StatusOK, res, h.Logger)
}

View File

@ -42,10 +42,13 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
// Prefix any URLs found in the React assets with any configured basepath
prefixedAssets := NewDefaultURLPrefixer(basepath, assets, opts.Logger)
// Compress the assets with gzip if an accepted encoding
compressed := gziphandler.GzipHandler(prefixedAssets)
// The react application handles all the routing if the server does not
// know about the route. This means that we never have unknown
// routes on the server.
router.NotFound = prefixedAssets
router.NotFound = compressed
/* Documentation */
router.GET("/swagger.json", Spec())
@ -80,6 +83,7 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
router.GET("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", service.KapacitorRulesID)
router.PUT("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", service.KapacitorRulesPut)
router.PATCH("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", service.KapacitorRulesStatus)
router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", service.KapacitorRulesDelete)
// Kapacitor Proxy
@ -121,8 +125,7 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
return Logger(opts.Logger, auth)
}
compressed := gziphandler.GzipHandler(router)
logged := Logger(opts.Logger, compressed)
logged := Logger(opts.Logger, router)
return logged
}

View File

@ -1851,6 +1851,14 @@
"type": "string",
"description": "TICKscript representing this rule"
},
"status": {
"type": "string",
"description": "Represents if this rule is enabled or disabled in kapacitor",
"enum": [
"enabled",
"disabled"
]
},
"links": {
"type": "object",
"required": [

View File

@ -9,6 +9,7 @@ import {
updateAlerts,
updateRuleName,
deleteRuleSuccess,
updateRuleStatusSuccess,
} from 'src/kapacitor/actions/view';
describe('Kapacitor.Reducers.rules', () => {
@ -134,4 +135,20 @@ describe('Kapacitor.Reducers.rules', () => {
const newState = reducer(initialState, updateDetails(ruleID, details));
expect(newState[ruleID].details).to.equal(details);
});
it('can update status', () => {
const ruleID = 1;
const status = 'enabled';
const initialState = {
[ruleID]: {
id: ruleID,
queryID: 988,
status: 'disabled',
}
};
const newState = reducer(initialState, updateRuleStatusSuccess(ruleID, status));
expect(newState[ruleID].status).to.equal(status);
});
});

View File

@ -1,7 +1,12 @@
import uuid from 'node-uuid';
import {getRules, getRule, deleteRule as deleteRuleAPI} from 'src/kapacitor/apis';
import {getKapacitor} from 'src/shared/apis';
import {publishNotification} from 'src/shared/actions/notifications';
import {
getRules,
getRule,
deleteRule as deleteRuleAPI,
updateRuleStatus as updateRuleStatusAPI,
} from 'src/kapacitor/apis';
export function fetchRule(source, ruleID) {
return (dispatch) => {
@ -126,6 +131,16 @@ export function deleteRuleSuccess(ruleID) {
};
}
export function updateRuleStatusSuccess(ruleID, status) {
return {
type: 'UPDATE_RULE_STATUS_SUCCESS',
payload: {
ruleID,
status,
},
};
}
export function deleteRule(rule) {
return (dispatch) => {
deleteRuleAPI(rule).then(() => {
@ -136,3 +151,14 @@ export function deleteRule(rule) {
});
};
}
export function updateRuleStatus(rule, {status}) {
return (dispatch) => {
updateRuleStatusAPI(rule, status).then(() => {
dispatch(publishNotification('success', `${rule.name} ${status} successfully`));
}).catch(() => {
dispatch(updateRuleStatusSuccess(rule.id, status));
dispatch(publishNotification('error', `${rule.name} could not be ${status}`));
});
};
}

View File

@ -47,3 +47,11 @@ export function deleteRule(rule) {
url: rule.links.self,
});
}
export function updateRuleStatus(rule, status) {
return AJAX({
method: 'PATCH',
url: rule.links.self,
data: {status},
});
}

View File

@ -6,27 +6,35 @@ import {getKapacitor} from 'src/shared/apis';
import * as kapacitorActionCreators from '../actions/view';
import NoKapacitorError from '../../shared/components/NoKapacitorError';
const {
arrayOf,
func,
shape,
string,
} = PropTypes;
export const KapacitorRulesPage = React.createClass({
propTypes: {
source: PropTypes.shape({
id: PropTypes.string.isRequired,
links: PropTypes.shape({
proxy: PropTypes.string.isRequired,
self: PropTypes.string.isRequired,
kapacitors: PropTypes.string.isRequired,
source: shape({
id: string.isRequired,
links: shape({
proxy: string.isRequired,
self: string.isRequired,
kapacitors: string.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,
rules: arrayOf(shape({
name: string.isRequired,
trigger: string.isRequired,
message: string.isRequired,
alerts: arrayOf(string.isRequired).isRequired,
})).isRequired,
actions: PropTypes.shape({
fetchRules: PropTypes.func.isRequired,
deleteRule: PropTypes.func.isRequired,
actions: shape({
fetchRules: func.isRequired,
deleteRule: func.isRequired,
updateRuleStatus: func.isRequired,
}).isRequired,
addFlashMessage: PropTypes.func,
addFlashMessage: func,
},
getInitialState() {
@ -50,6 +58,14 @@ export const KapacitorRulesPage = React.createClass({
actions.deleteRule(rule);
},
handleRuleStatus(e, rule) {
const {actions} = this.props;
const status = e.target.checked ? 'enabled' : 'disabled';
actions.updateRuleStatusSuccess(rule.id, status);
actions.updateRuleStatus(rule, {status});
},
renderSubComponent() {
const {source} = this.props;
const {hasKapacitor, loading} = this.state;
@ -72,6 +88,7 @@ export const KapacitorRulesPage = React.createClass({
<th>Trigger</th>
<th>Message</th>
<th>Alerts</th>
<th className="text-center">Enabled</th>
<th></th>
</tr>
</thead>
@ -129,6 +146,12 @@ export const KapacitorRulesPage = React.createClass({
<td className="monotype">{rule.trigger}</td>
<td className="monotype">{rule.message}</td>
<td className="monotype">{rule.alerts.join(', ')}</td>
<td className="monotype text-center">
<div className="dark-checkbox">
<input id="kapacitor-enabled" className="form-control-static" type="checkbox" ref={(r) => this.enabled = r} checked={rule.status === "enabled"} onClick={(e) => this.handleRuleStatus(e, rule)} />
<label htmlFor="kapacitor-enabled"></label>
</div>
</td>
<td className="text-right"><button className="btn btn-danger btn-xs" onClick={() => this.handleDeleteRule(rule)}>Delete</button></td>
</tr>
);

View File

@ -93,6 +93,14 @@ export default function rules(state = {}, action) {
[ruleID]: {...state[ruleID], details},
}};
}
case 'UPDATE_RULE_STATUS_SUCCESS': {
const {ruleID, status} = action.payload;
return {...state, ...{
[ruleID]: {...state[ruleID], status},
}};
}
}
return state;
}

View File

@ -611,4 +611,47 @@ $form-static-checkbox-size: 16px;
}
}
}
}
.dark-checkbox {
input {
position: absolute;
left: -9999px;
visibility: hidden;
}
label {
display: inline-block;
width: $form-static-checkbox-size;
height: $form-static-checkbox-size;
background-color: $g1-raven;
border-radius: $radius-small;
position: relative;
vertical-align: middle;
margin: 0;
transition: background-color 0.25s ease;
}
label:hover {
cursor: pointer;
background-color: $g2-kevlar;
}
label:after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 6px;
height: 6px;
background-color: $c-pool;
border-radius: 50%;
transform: translate(-50%,-50%) scale(2,2);
opacity: 0;
z-index: 3;
transition:
opacity 0.25s ease,
transform 0.25s ease;
}
input:checked + label:after {
opacity: 1;
transform: translate(-50%,-50%) scale(1,1);
}
}