commit
f81cd7e7ad
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}`));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue