diff --git a/main.go b/main.go index f215446e..47515fe5 100644 --- a/main.go +++ b/main.go @@ -34,12 +34,19 @@ func main() { defer aggregator.Close() webService := &web.WebService{ + // REST API Service. AlertManagerService: &api.AlertManagerService{ Aggregator: aggregator, + Suppressor: suppressor, }, + + // Template-based page handlers. AlertsHandler: &web.AlertsHandler{ Aggregator: aggregator, }, + SilencesHandler: &web.SilencesHandler{ + Suppressor: suppressor, + }, } go webService.ServeForever() diff --git a/manager/event.go b/manager/event.go index bc7cd546..8d309c46 100644 --- a/manager/event.go +++ b/manager/event.go @@ -19,15 +19,25 @@ import ( "sort" ) +const eventNameLabel = "name" + type EventFingerprint uint64 +type EventLabels map[string]string +type EventPayload map[string]string + // Event models an action triggered by Prometheus. type Event struct { // Label value pairs for purpose of aggregation, matching, and disposition // dispatching. This must minimally include a "name" label. - Labels map[string]string + Labels EventLabels // Extra key/value information which is not used for aggregation. - Payload map[string]string + Payload EventPayload +} + +func (e Event) Name() string { + // BUG: ensure in a proper place that all events have a name? + return e.Labels[eventNameLabel] } func (e Event) Fingerprint() EventFingerprint { diff --git a/manager/suppressor.go b/manager/suppressor.go index 56aca9bd..aba3bef3 100644 --- a/manager/suppressor.go +++ b/manager/suppressor.go @@ -14,6 +14,7 @@ package manager import ( + "encoding/json" "fmt" "log" "sync" @@ -21,6 +22,7 @@ import ( ) type SuppressionId uint +type Suppressions []*Suppression type Suppression struct { // The numeric ID of the suppression. @@ -40,7 +42,53 @@ type Suppression struct { expiryTimer *time.Timer } -type Suppressions []*Suppression +type ApiSilence struct { + CreatedBy string + CreatedAtSeconds int64 + EndsAtSeconds int64 + Comment string + Filters map[string]string +} + +func (s *Suppression) MarshalJSON() ([]byte, error) { + filters := map[string]string{} + for _, f := range s.Filters { + name := f.Name.String()[1 : len(f.Name.String())-1] + value := f.Value.String()[1 : len(f.Value.String())-1] + filters[name] = value + } + + return json.Marshal(&ApiSilence{ + CreatedBy: s.CreatedBy, + CreatedAtSeconds: s.CreatedAt.Unix(), + EndsAtSeconds: s.EndsAt.Unix(), + Comment: s.Comment, + Filters: filters, + }) +} + +func (s *Suppression) UnmarshalJSON(data []byte) error { + sc := &ApiSilence{} + json.Unmarshal(data, sc) + + filters := make(Filters, 0, len(sc.Filters)) + for label, value := range sc.Filters { + filters = append(filters, NewFilter(label, value)) + } + + if sc.EndsAtSeconds == 0 { + sc.EndsAtSeconds = time.Now().Add(time.Hour).Unix() + } + + *s = Suppression{ + CreatedBy: sc.CreatedBy, + CreatedAt: time.Now().UTC(), + EndsAt: time.Unix(sc.EndsAtSeconds, 0).UTC(), + Comment: sc.Comment, + Filters: filters, + } + return nil +} type Suppressor struct { // Suppressions managed by this Suppressor. diff --git a/web/alerts.go b/web/alerts.go index e9e46833..123e291c 100644 --- a/web/alerts.go +++ b/web/alerts.go @@ -14,8 +14,9 @@ package web import ( - "github.com/prometheus/alert_manager/manager" "net/http" + + "github.com/prometheus/alert_manager/manager" ) type AlertStatus struct { diff --git a/web/api/api.go b/web/api/api.go index 8ffabff3..9280fb3d 100644 --- a/web/api/api.go +++ b/web/api/api.go @@ -22,7 +22,13 @@ import ( type AlertManagerService struct { gorest.RestService `root:"/api/" consumes:"application/json" produces:"application/json"` - addEvents gorest.EndPoint `method:"POST" path:"/event" postdata:"Events"` + addEvents gorest.EndPoint `method:"POST" path:"/events" postdata:"Events"` + addSilence gorest.EndPoint `method:"POST" path:"/silences" postdata:"Suppression"` + getSilence gorest.EndPoint `method:"GET" path:"/silences/{id:int}" output:"string"` + updateSilence gorest.EndPoint `method:"POST" path:"/silences/{id:int}" postdata:"Suppression"` + delSilence gorest.EndPoint `method:"DELETE" path:"/silences/{id:int}"` + silenceSummary gorest.EndPoint `method:"GET" path:"/silences" output:"string"` Aggregator *manager.Aggregator + Suppressor *manager.Suppressor } diff --git a/web/api/silence.go b/web/api/silence.go new file mode 100644 index 00000000..654542d9 --- /dev/null +++ b/web/api/silence.go @@ -0,0 +1,93 @@ +// Copyright 2013 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + + "code.google.com/p/gorest" + + "github.com/prometheus/alert_manager/manager" +) + +type Silence struct { + CreatedBy string + CreatedAtSeconds int64 + EndsAtSeconds int64 + Comment string + Filters map[string]string +} + +func (s AlertManagerService) AddSilence(sc manager.Suppression) { + // BUG: add server-side form validation. + id := s.Suppressor.AddSuppression(&sc) + + rb := s.ResponseBuilder() + rb.SetResponseCode(http.StatusCreated) + rb.Location(fmt.Sprintf("/api/silences/%d", id)) +} + +func (s AlertManagerService) GetSilence(id int) string { + rb := s.ResponseBuilder() + rb.SetContentType(gorest.Application_Json) + silence, err := s.Suppressor.GetSuppression(manager.SuppressionId(id)) + if err != nil { + log.Printf("Error getting silence: %s", err) + rb.SetResponseCode(http.StatusNotFound) + return err.Error() + } + + resultBytes, err := json.Marshal(&silence) + if err != nil { + log.Printf("Error marshalling silence: %s", err) + rb.SetResponseCode(http.StatusInternalServerError) + return err.Error() + } + return string(resultBytes) +} + +func (s AlertManagerService) UpdateSilence(sc manager.Suppression, id int) { + // BUG: add server-side form validation. + sc.Id = manager.SuppressionId(id) + if err := s.Suppressor.UpdateSuppression(&sc); err != nil { + log.Printf("Error updating silence: %s", err) + rb := s.ResponseBuilder() + rb.SetResponseCode(http.StatusNotFound) + } +} + +func (s AlertManagerService) DelSilence(id int) { + if err := s.Suppressor.DelSuppression(manager.SuppressionId(id)); err != nil { + log.Printf("Error deleting silence: %s", err) + rb := s.ResponseBuilder() + rb.SetResponseCode(http.StatusNotFound) + } +} + +func (s AlertManagerService) SilenceSummary() string { + rb := s.ResponseBuilder() + rb.SetContentType(gorest.Application_Json) + silenceSummary := s.Suppressor.SuppressionSummary() + + resultBytes, err := json.Marshal(silenceSummary) + if err != nil { + log.Printf("Error marshalling silences: %s", err) + rb.SetResponseCode(http.StatusInternalServerError) + return err.Error() + } + return string(resultBytes) +} diff --git a/web/silences.go b/web/silences.go index 82bcd2e4..40a5dcd8 100644 --- a/web/silences.go +++ b/web/silences.go @@ -15,10 +15,21 @@ package web import ( "net/http" + + "github.com/prometheus/alert_manager/manager" ) -type SilencesHandler struct{} +type SilenceStatus struct { + Silences manager.Suppressions +} + +type SilencesHandler struct { + Suppressor *manager.Suppressor +} func (h *SilencesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - executeTemplate(w, "silences", nil) + silenceStatus := &SilenceStatus{ + Silences: h.Suppressor.SuppressionSummary(), + } + executeTemplate(w, "silences", silenceStatus) } diff --git a/web/static/css/default.css b/web/static/css/default.css index 2eb08a8a..351687ef 100644 --- a/web/static/css/default.css +++ b/web/static/css/default.css @@ -2,14 +2,10 @@ body { padding-top: 60px; } -#create_silence_modal th { +#edit_silence_modal th { text-align: left; } .add_silence_form { display: inline; } - -.del_label_button { - margin-bottom: 10px; -} diff --git a/web/static/js/alerts.js b/web/static/js/alerts.js index a821362d..925aa110 100644 --- a/web/static/js/alerts.js +++ b/web/static/js/alerts.js @@ -1,5 +1,8 @@ +var silenceRow = null; +var silenceId = null; + function clearSilenceLabels() { - $("#silence_label_table").empty(); + $("#silence_filters_table").empty(); } function addSilenceLabel(label, value) { @@ -9,11 +12,11 @@ function addSilenceLabel(label, value) { if (!value) { value = ""; } - $("#silence_label_table").append( + $("#silence_filters_table").append( '' + - ' ' + - ' ' + - ' ' + + ' ' + + ' ' + + ' ' + ''); bindDelLabel(); } @@ -25,25 +28,173 @@ function bindDelLabel() { }); } -function init() { - $("#new_silence_btn").click(function() { - clearSilenceLabels(); +function silenceJsonFromForm() { + var filters = {}; + var labels = $('input[name="silence_filter_label[]"]'); + var values = $('input[name="silence_filter_value[]"]'); + for (var i = 0; i < labels.length; i++) { + filters[labels.get(i).value] = values.get(i).value; + } + + var endsAt = 0; + if ($("#silence_ends_at").val() != "") { + var picker = $("#ends_at_datetimepicker").data("datetimepicker"); + endsAt = Math.round(picker.getLocalDate().getTime() / 1000); + } + + return JSON.stringify({ + CreatedBy: $("#silence_created_by").val(), + EndsAtSeconds: endsAt, + Comment: $("#silence_comment").val(), + Filters: filters }); +} - $(".add_silence_btn").click(function() { - clearSilenceLabels(); - - var form = $(this).parents("form"); - var labels = form.children('input[name="label[]"]'); - var values = form.children('input[name="value[]"]'); - for (var i = 0; i < labels.length; i++) { - addSilenceLabel(labels.get(i).value, values.get(i).value); +function createSilence() { + $.ajax({ + type: "POST", + url: "/api/silences", + data: silenceJsonFromForm(), + dataType: "text", + success: function(data, textStatus, jqXHR) { + location.reload(); + }, + error: function(data, textStatus, jqXHR) { + alert("Creating silence failed: " + textStatus); } }); +} - $("#add_label_button").click(function() { +function updateSilence() { + $.ajax({ + type: "POST", + url: "/api/silences/" + silenceId, + data: silenceJsonFromForm(), + dataType: "text", + success: function(data, textStatus, jqXHR) { + location.reload(); + }, + error: function(data, textStatus, jqXHR) { + alert("Updating silence failed: " + textStatus); + } + }); +} + +function getSilence(silenceId, successFn) { + $.ajax({ + type: "GET", + url: "/api/silences/" + silenceId, + async: false, + success: successFn, + error: function(data, textStatus, jqXHR) { + alert("Creating silence failed: " + textStatus); + } + }); +} + +function deleteSilence(silenceId, silenceRow) { + $.ajax({ + type: "DELETE", + url: "/api/silences/" + silenceId, + success: function(data, textStatus, jqXHR) { + silenceRow.remove(); + $("#del_silence_modal").modal("hide"); + }, + error: function(data, textStatus, jqXHR) { + alert("Removing silence failed: " + textStatus); + } + }); +} + +function initNewSilence() { + silenceId = null; + $("#edit_silence_header, #edit_silence_btn").html("Create Silence"); + $("#edit_silence_form")[0].reset(); +} + +function populateSilenceLabels(form) { + var labels = form.children('input[name="label[]"]'); + var values = form.children('input[name="value[]"]'); + for (var i = 0; i < labels.length; i++) { + addSilenceLabel(labels.get(i).value, values.get(i).value); + } +} + +function init() { + $.ajaxSetup({ + cache: false + }); + + $("#ends_at_datetimepicker").datetimepicker({ + language: "en", + pickSeconds: false + }); + + $("#edit_silence_modal").on("shown", function(e) { + $("#silence_created_by").focus(); + }); + + $("#edit_silence_modal").on("hidden", function(e) { + clearSilenceLabels(); + }); + + // This is the "Silences" page button to open the dialog for creating a new + // silence. + $("#new_silence_btn").click(function() { + initNewSilence(); + }); + + // These are the "Alerts" page buttons to open the dialog for creating a new + // silence for the alert / alert instance. + $(".add_silence_btn").click(function() { + initNewSilence(); + populateSilenceLabels($(this).parents("form")); + }); + + $("#add_filter_button").click(function() { addSilenceLabel("", ""); }); + + $("#edit_silence_form").submit(function() { + if (silenceId != null) { + updateSilence(); + } else { + createSilence(); + } + return false; + }); + + $(".edit_silence_btn").click(function() { + $("#edit_silence_header, #edit_silence_btn").html("Update Silence"); + + silenceRow = $(this).parents("tr"); + silenceId = silenceRow.find("input[name='silence_id']").val(); + $("#edit_silence_form input[name='silence_id']").val(silenceId); + getSilence(silenceId, function(silence) { + var picker = $("#ends_at_datetimepicker").data('datetimepicker'); + var endsAt = new Date(silence.EndsAtSeconds * 1000); + picker.setLocalDate(endsAt); + + $("#silence_created_by").val(silence.CreatedBy); + $("#silence_comment").val(silence.Comment); + for (var f in silence.Filters) { + addSilenceLabel(f, silence.Filters[f]); + } + }); + }); + + // When clicking the "Remove Silence" button in the Silences table, save the + // table row and silence ID to global variables (ugh) so they can be used + // from the modal dialog to remove that silence. + $(".del_silence_modal_btn").click(function() { + silenceRow = $(this).parents("tr"); + silenceId = silenceRow.find("input[name='silence_id']").val(); + }); + + // Deletion confirmation button action. + $(".del_silence_btn").click(function() { + deleteSilence(silenceId, silenceRow); + }); } $(init); diff --git a/web/static/vendor/tarruda_bootstrap_datetimepicker/css/bootstrap-datetimepicker.min.css b/web/static/vendor/tarruda_bootstrap_datetimepicker/css/bootstrap-datetimepicker.min.css new file mode 100644 index 00000000..36394e27 --- /dev/null +++ b/web/static/vendor/tarruda_bootstrap_datetimepicker/css/bootstrap-datetimepicker.min.css @@ -0,0 +1,8 @@ +/*! + * Datepicker for Bootstrap + * + * Copyright 2012 Stefan Petre + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-datetimepicker-widget{top:0;left:0;width:250px;padding:4px;margin-top:1px;z-index:3000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.bootstrap-datetimepicker-widget:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0,0,0,0.2);position:absolute;top:-7px;left:6px}.bootstrap-datetimepicker-widget:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;position:absolute;top:-6px;left:7px}.bootstrap-datetimepicker-widget.pull-right:before{left:auto;right:6px}.bootstrap-datetimepicker-widget.pull-right:after{left:auto;right:7px}.bootstrap-datetimepicker-widget>ul{list-style-type:none;margin:0}.bootstrap-datetimepicker-widget .timepicker-hour,.bootstrap-datetimepicker-widget .timepicker-minute,.bootstrap-datetimepicker-widget .timepicker-second{width:100%;font-weight:bold;font-size:1.2em}.bootstrap-datetimepicker-widget table[data-hour-format="12"] .separator{width:4px;padding:0;margin:0}.bootstrap-datetimepicker-widget .datepicker>div{display:none}.bootstrap-datetimepicker-widget .picker-switch{text-align:center}.bootstrap-datetimepicker-widget table{width:100%;margin:0}.bootstrap-datetimepicker-widget td,.bootstrap-datetimepicker-widget th{text-align:center;width:20px;height:20px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.bootstrap-datetimepicker-widget td.day:hover,.bootstrap-datetimepicker-widget td.hour:hover,.bootstrap-datetimepicker-widget td.minute:hover,.bootstrap-datetimepicker-widget td.second:hover{background:#eee;cursor:pointer}.bootstrap-datetimepicker-widget td.old,.bootstrap-datetimepicker-widget td.new{color:#999}.bootstrap-datetimepicker-widget td.active,.bootstrap-datetimepicker-widget td.active:hover{color:#fff;background-color:#006dcc;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#04c;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.bootstrap-datetimepicker-widget td.active:hover,.bootstrap-datetimepicker-widget td.active:hover:hover,.bootstrap-datetimepicker-widget td.active:active,.bootstrap-datetimepicker-widget td.active:hover:active,.bootstrap-datetimepicker-widget td.active.active,.bootstrap-datetimepicker-widget td.active:hover.active,.bootstrap-datetimepicker-widget td.active.disabled,.bootstrap-datetimepicker-widget td.active:hover.disabled,.bootstrap-datetimepicker-widget td.active[disabled],.bootstrap-datetimepicker-widget td.active:hover[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.bootstrap-datetimepicker-widget td.active:active,.bootstrap-datetimepicker-widget td.active:hover:active,.bootstrap-datetimepicker-widget td.active.active,.bootstrap-datetimepicker-widget td.active:hover.active{background-color:#039 \9}.bootstrap-datetimepicker-widget td.disabled,.bootstrap-datetimepicker-widget td.disabled:hover{background:0;color:#999;cursor:not-allowed}.bootstrap-datetimepicker-widget td span{display:block;width:47px;height:54px;line-height:54px;float:left;margin:2px;cursor:pointer;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.bootstrap-datetimepicker-widget td span:hover{background:#eee}.bootstrap-datetimepicker-widget td span.active{color:#fff;background-color:#006dcc;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#04c;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.bootstrap-datetimepicker-widget td span.active:hover,.bootstrap-datetimepicker-widget td span.active:active,.bootstrap-datetimepicker-widget td span.active.active,.bootstrap-datetimepicker-widget td span.active.disabled,.bootstrap-datetimepicker-widget td span.active[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.bootstrap-datetimepicker-widget td span.active:active,.bootstrap-datetimepicker-widget td span.active.active{background-color:#039 \9}.bootstrap-datetimepicker-widget td span.old{color:#999}.bootstrap-datetimepicker-widget td span.disabled,.bootstrap-datetimepicker-widget td span.disabled:hover{background:0;color:#999;cursor:not-allowed}.bootstrap-datetimepicker-widget th.switch{width:145px}.bootstrap-datetimepicker-widget th.next,.bootstrap-datetimepicker-widget th.prev{font-size:21px}.bootstrap-datetimepicker-widget th.disabled,.bootstrap-datetimepicker-widget th.disabled:hover{background:0;color:#999;cursor:not-allowed}.bootstrap-datetimepicker-widget thead tr:first-child th{cursor:pointer}.bootstrap-datetimepicker-widget thead tr:first-child th:hover{background:#eee}.input-append.date .add-on i,.input-prepend.date .add-on i{display:block;cursor:pointer;width:16px;height:16px}.bootstrap-datetimepicker-widget.left-oriented:before{left:auto;right:6px}.bootstrap-datetimepicker-widget.left-oriented:after{left:auto;right:7px} \ No newline at end of file diff --git a/web/static/vendor/tarruda_bootstrap_datetimepicker/js/bootstrap-datetimepicker.min.js b/web/static/vendor/tarruda_bootstrap_datetimepicker/js/bootstrap-datetimepicker.min.js new file mode 100644 index 00000000..a30f7764 --- /dev/null +++ b/web/static/vendor/tarruda_bootstrap_datetimepicker/js/bootstrap-datetimepicker.min.js @@ -0,0 +1,26 @@ +/** + * @license + * ========================================================= + * bootstrap-datetimepicker.js + * http://www.eyecon.ro/bootstrap-datepicker + * ========================================================= + * Copyright 2012 Stefan Petre + * + * Contributions: + * - Andrew Rowls + * - Thiago de Arruda + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================= + */ +(function($){var smartPhone=window.orientation!=undefined;var DateTimePicker=function(element,options){this.id=dpgId++;this.init(element,options)};var dateToDate=function(dt){if(typeof dt==="string"){return new Date(dt)}return dt};DateTimePicker.prototype={constructor:DateTimePicker,init:function(element,options){var icon;if(!(options.pickTime||options.pickDate))throw new Error("Must choose at least one picker");this.options=options;this.$element=$(element);this.language=options.language in dates?options.language:"en";this.pickDate=options.pickDate;this.pickTime=options.pickTime;this.isInput=this.$element.is("input");this.component=false;if(this.$element.find(".input-append")||this.$element.find(".input-prepend"))this.component=this.$element.find(".add-on");this.format=options.format;if(!this.format){if(this.isInput)this.format=this.$element.data("format");else this.format=this.$element.find("input").data("format");if(!this.format)this.format="MM/dd/yyyy"}this._compileFormat();if(this.component){icon=this.component.find("i")}if(this.pickTime){if(icon&&icon.length)this.timeIcon=icon.data("time-icon");if(!this.timeIcon)this.timeIcon="icon-time";icon.addClass(this.timeIcon)}if(this.pickDate){if(icon&&icon.length)this.dateIcon=icon.data("date-icon");if(!this.dateIcon)this.dateIcon="icon-calendar";icon.removeClass(this.timeIcon);icon.addClass(this.dateIcon)}this.widget=$(getTemplate(this.timeIcon,options.pickDate,options.pickTime,options.pick12HourFormat,options.pickSeconds,options.collapse)).appendTo("body");this.minViewMode=options.minViewMode||this.$element.data("date-minviewmode")||0;if(typeof this.minViewMode==="string"){switch(this.minViewMode){case"months":this.minViewMode=1;break;case"years":this.minViewMode=2;break;default:this.minViewMode=0;break}}this.viewMode=options.viewMode||this.$element.data("date-viewmode")||0;if(typeof this.viewMode==="string"){switch(this.viewMode){case"months":this.viewMode=1;break;case"years":this.viewMode=2;break;default:this.viewMode=0;break}}this.startViewMode=this.viewMode;this.weekStart=options.weekStart||this.$element.data("date-weekstart")||0;this.weekEnd=this.weekStart===0?6:this.weekStart-1;this.setStartDate(options.startDate||this.$element.data("date-startdate"));this.setEndDate(options.endDate||this.$element.data("date-enddate"));this.fillDow();this.fillMonths();this.fillHours();this.fillMinutes();this.fillSeconds();this.update();this.showMode();this._attachDatePickerEvents()},show:function(e){this.widget.show();this.height=this.component?this.component.outerHeight():this.$element.outerHeight();this.place();this.$element.trigger({type:"show",date:this._date});this._attachDatePickerGlobalEvents();if(e){e.stopPropagation();e.preventDefault()}},disable:function(){this.$element.find("input").prop("disabled",true);this._detachDatePickerEvents()},enable:function(){this.$element.find("input").prop("disabled",false);this._attachDatePickerEvents()},hide:function(){var collapse=this.widget.find(".collapse");for(var i=0;i");while(dowCnt'+dates[this.language].daysMin[dowCnt++%7]+"")}this.widget.find(".datepicker-days thead").append(html)},fillMonths:function(){var html="";var i=0;while(i<12){html+=''+dates[this.language].monthsShort[i++]+""}this.widget.find(".datepicker-months td").append(html)},fillDate:function(){var year=this.viewDate.getUTCFullYear();var month=this.viewDate.getUTCMonth();var currentDate=UTCDate(this._date.getUTCFullYear(),this._date.getUTCMonth(),this._date.getUTCDate(),0,0,0,0);var startYear=typeof this.startDate==="object"?this.startDate.getUTCFullYear():-Infinity;var startMonth=typeof this.startDate==="object"?this.startDate.getUTCMonth():-1;var endYear=typeof this.endDate==="object"?this.endDate.getUTCFullYear():Infinity;var endMonth=typeof this.endDate==="object"?this.endDate.getUTCMonth():12;this.widget.find(".datepicker-days").find(".disabled").removeClass("disabled");this.widget.find(".datepicker-months").find(".disabled").removeClass("disabled");this.widget.find(".datepicker-years").find(".disabled").removeClass("disabled");this.widget.find(".datepicker-days th:eq(1)").text(dates[this.language].months[month]+" "+year);var prevMonth=UTCDate(year,month-1,28,0,0,0,0);var day=DPGlobal.getDaysInMonth(prevMonth.getUTCFullYear(),prevMonth.getUTCMonth());prevMonth.setUTCDate(day);prevMonth.setUTCDate(day-(prevMonth.getUTCDay()-this.weekStart+7)%7);if(year==startYear&&month<=startMonth||year=endMonth||year>endYear){this.widget.find(".datepicker-days th:eq(2)").addClass("disabled")}var nextMonth=new Date(prevMonth.valueOf());nextMonth.setUTCDate(nextMonth.getUTCDate()+42);nextMonth=nextMonth.valueOf();var html=[];var row;var clsName;while(prevMonth.valueOf()");html.push(row)}clsName="";if(prevMonth.getUTCFullYear()year||prevMonth.getUTCFullYear()==year&&prevMonth.getUTCMonth()>month){clsName+=" new"}if(prevMonth.valueOf()===currentDate.valueOf()){clsName+=" active"}if(prevMonth.valueOf()+864e5<=this.startDate){clsName+=" disabled"}if(prevMonth.valueOf()>this.endDate){clsName+=" disabled"}row.append(''+prevMonth.getUTCDate()+"");prevMonth.setUTCDate(prevMonth.getUTCDate()+1)}this.widget.find(".datepicker-days tbody").empty().append(html);var currentYear=this._date.getUTCFullYear();var months=this.widget.find(".datepicker-months").find("th:eq(1)").text(year).end().find("span").removeClass("active");if(currentYear===year){months.eq(this._date.getUTCMonth()).addClass("active")}if(currentYear-1endYear){this.widget.find(".datepicker-months th:eq(2)").addClass("disabled")}for(var i=0;i<12;i++){if(year==startYear&&startMonth>i||yearendYear){$(months[i]).addClass("disabled")}}html="";year=parseInt(year/10,10)*10;var yearCont=this.widget.find(".datepicker-years").find("th:eq(1)").text(year+"-"+(year+9)).end().find("td");this.widget.find(".datepicker-years").find("th").removeClass("disabled");if(startYear>year){this.widget.find(".datepicker-years").find("th:eq(0)").addClass("disabled")}if(endYearendYear?" disabled":"")+'">'+year+"";year+=1}yearCont.html(html)},fillHours:function(){var table=this.widget.find(".timepicker .timepicker-hours table");table.parent().hide();var html="";if(this.options.pick12HourFormat){var current=1;for(var i=0;i<3;i+=1){html+="";for(var j=0;j<4;j+=1){var c=current.toString();html+=''+padLeft(c,2,"0")+"";current++}html+=""}}else{var current=0;for(var i=0;i<6;i+=1){html+="";for(var j=0;j<4;j+=1){var c=current.toString();html+=''+padLeft(c,2,"0")+"";current++}html+=""}}table.html(html)},fillMinutes:function(){var table=this.widget.find(".timepicker .timepicker-minutes table");table.parent().hide();var html="";var current=0;for(var i=0;i<5;i++){html+="";for(var j=0;j<4;j+=1){var c=current.toString();html+=''+padLeft(c,2,"0")+"";current+=3}html+=""}table.html(html)},fillSeconds:function(){var table=this.widget.find(".timepicker .timepicker-seconds table");table.parent().hide();var html="";var current=0;for(var i=0;i<5;i++){html+="";for(var j=0;j<4;j+=1){var c=current.toString();html+=''+padLeft(c,2,"0")+"";current+=3}html+=""}table.html(html)},fillTime:function(){if(!this._date)return;var timeComponents=this.widget.find(".timepicker span[data-time-component]");var table=timeComponents.closest("table");var is12HourFormat=this.options.pick12HourFormat;var hour=this._date.getUTCHours();var period="AM";if(is12HourFormat){if(hour>=12)period="PM";if(hour===0)hour=12;else if(hour!=12)hour=hour%12;this.widget.find(".timepicker [data-action=togglePeriod]").text(period)}hour=padLeft(hour.toString(),2,"0");var minute=padLeft(this._date.getUTCMinutes().toString(),2,"0");var second=padLeft(this._date.getUTCSeconds().toString(),2,"0");timeComponents.filter("[data-time-component=hours]").text(hour);timeComponents.filter("[data-time-component=minutes]").text(minute);timeComponents.filter("[data-time-component=seconds]").text(second)},click:function(e){e.stopPropagation();e.preventDefault();this._unset=false;var target=$(e.target).closest("span, td, th");if(target.length===1){if(!target.is(".disabled")){switch(target[0].nodeName.toLowerCase()){case"th":switch(target[0].className){case"switch":this.showMode(1);break;case"prev":case"next":var vd=this.viewDate;var navFnc=DPGlobal.modes[this.viewMode].navFnc;var step=DPGlobal.modes[this.viewMode].navStep;if(target[0].className==="prev")step=step*-1;vd["set"+navFnc](vd["get"+navFnc]()+step);this.fillDate();this.set();break}break;case"span":if(target.is(".month")){var month=target.parent().find("span").index(target);this.viewDate.setUTCMonth(month)}else{var year=parseInt(target.text(),10)||0;this.viewDate.setUTCFullYear(year)}if(this.viewMode!==0){this._date=UTCDate(this.viewDate.getUTCFullYear(),this.viewDate.getUTCMonth(),this.viewDate.getUTCDate(),this._date.getUTCHours(),this._date.getUTCMinutes(),this._date.getUTCSeconds(),this._date.getUTCMilliseconds());this.notifyChange()}this.showMode(-1);this.fillDate();this.set();break;case"td":if(target.is(".day")){var day=parseInt(target.text(),10)||1;var month=this.viewDate.getUTCMonth();var year=this.viewDate.getUTCFullYear();if(target.is(".old")){if(month===0){month=11;year-=1}else{month-=1}}else if(target.is(".new")){if(month==11){month=0;year+=1}else{month+=1}}this._date=UTCDate(year,month,day,this._date.getUTCHours(),this._date.getUTCMinutes(),this._date.getUTCSeconds(),this._date.getUTCMilliseconds());this.viewDate=UTCDate(year,month,Math.min(28,day),0,0,0,0);this.fillDate();this.set();this.notifyChange()}break}}}},actions:{incrementHours:function(e){this._date.setUTCHours(this._date.getUTCHours()+1)},incrementMinutes:function(e){this._date.setUTCMinutes(this._date.getUTCMinutes()+1)},incrementSeconds:function(e){this._date.setUTCSeconds(this._date.getUTCSeconds()+1)},decrementHours:function(e){this._date.setUTCHours(this._date.getUTCHours()-1)},decrementMinutes:function(e){this._date.setUTCMinutes(this._date.getUTCMinutes()-1)},decrementSeconds:function(e){this._date.setUTCSeconds(this._date.getUTCSeconds()-1)},togglePeriod:function(e){var hour=this._date.getUTCHours();if(hour>=12)hour-=12;else hour+=12;this._date.setUTCHours(hour)},showPicker:function(){this.widget.find(".timepicker > div:not(.timepicker-picker)").hide();this.widget.find(".timepicker .timepicker-picker").show()},showHours:function(){this.widget.find(".timepicker .timepicker-picker").hide();this.widget.find(".timepicker .timepicker-hours").show()},showMinutes:function(){this.widget.find(".timepicker .timepicker-picker").hide();this.widget.find(".timepicker .timepicker-minutes").show()},showSeconds:function(){this.widget.find(".timepicker .timepicker-picker").hide();this.widget.find(".timepicker .timepicker-seconds").show()},selectHour:function(e){var tgt=$(e.target);var value=parseInt(tgt.text(),10);if(this.options.pick12HourFormat){var current=this._date.getUTCHours();if(current>=12){if(value!=12)value=(value+12)%24}else{if(value===12)value=0;else value=value%12}}this._date.setUTCHours(value);this.actions.showPicker.call(this)},selectMinute:function(e){var tgt=$(e.target);var value=parseInt(tgt.text(),10);this._date.setUTCMinutes(value);this.actions.showPicker.call(this)},selectSecond:function(e){var tgt=$(e.target);var value=parseInt(tgt.text(),10);this._date.setUTCSeconds(value);this.actions.showPicker.call(this)}},doAction:function(e){e.stopPropagation();e.preventDefault();if(!this._date)this._date=UTCDate(1970,0,0,0,0,0,0);var action=$(e.currentTarget).data("action");var rv=this.actions[action].apply(this,arguments);this.set();this.fillTime();this.notifyChange();return rv},stopEvent:function(e){e.stopPropagation();e.preventDefault()},keydown:function(e){var self=this,k=e.which,input=$(e.target);if(k==8||k==46){setTimeout(function(){self._resetMaskPos(input)})}},keypress:function(e){var k=e.which;if(k==8||k==46){return}var input=$(e.target);var c=String.fromCharCode(k);var val=input.val()||"";val+=c;var mask=this._mask[this._maskPos];if(!mask){return false}if(mask.end!=val.length){return}if(!mask.pattern.test(val.slice(mask.start))){val=val.slice(0,val.length-1);while((mask=this._mask[this._maskPos])&&mask.character){val+=mask.character;this._maskPos++}val+=c;if(mask.end!=val.length){input.val(val);return false}else{if(!mask.pattern.test(val.slice(mask.start))){input.val(val.slice(0,mask.start));return false}else{input.val(val);this._maskPos++;return false}}}else{this._maskPos++}},change:function(e){var input=$(e.target);var val=input.val();if(this._formatPattern.test(val)){this.update();this.setValue(this._date.getTime());this.notifyChange();this.set()}else if(val&&val.trim()){this.setValue(this._date.getTime());if(this._date)this.set();else input.val("")}else{if(this._date){this.setValue(null);this.notifyChange();this._unset=true}}this._resetMaskPos(input)},showMode:function(dir){if(dir){this.viewMode=Math.max(this.minViewMode,Math.min(2,this.viewMode+dir))}this.widget.find(".datepicker > div").hide().filter(".datepicker-"+DPGlobal.modes[this.viewMode].clsName).show()},destroy:function(){this._detachDatePickerEvents();this._detachDatePickerGlobalEvents();this.widget.remove();this.$element.removeData("datetimepicker");this.component.removeData("datetimepicker")},formatDate:function(d){return this.format.replace(formatReplacer,function(match){var methodName,property,rv,len=match.length;if(match==="ms")len=1;property=dateFormatComponents[match].property;if(property==="Hours12"){rv=d.getUTCHours();if(rv===0)rv=12;else if(rv!==12)rv=rv%12}else if(property==="Period12"){if(d.getUTCHours()>=12)return"PM";else return"AM"}else{methodName="get"+property;rv=d[methodName]()}if(methodName==="getUTCMonth")rv=rv+1;if(methodName==="getUTCYear")rv=rv+1900-2e3;return padLeft(rv.toString(),len,"0")})},parseDate:function(str){var match,i,property,methodName,value,parsed={};if(!(match=this._formatPattern.exec(str)))return null;for(i=1;ival.length){this._maskPos=i;break}else if(this._mask[i].end===val.length){this._maskPos=i+1;break}}},_finishParsingDate:function(parsed){var year,month,date,hours,minutes,seconds,milliseconds;year=parsed.UTCFullYear;if(parsed.UTCYear)year=2e3+parsed.UTCYear;if(!year)year=1970;if(parsed.UTCMonth)month=parsed.UTCMonth-1;else month=0;date=parsed.UTCDate||1;hours=parsed.UTCHours||0;minutes=parsed.UTCMinutes||0;seconds=parsed.UTCSeconds||0;milliseconds=parsed.UTCMilliseconds||0;if(parsed.Hours12){hours=parsed.Hours12}if(parsed.Period12){if(/pm/i.test(parsed.Period12)){if(hours!=12)hours=(hours+12)%24}else{hours=hours%12}}return UTCDate(year,month,date,hours,minutes,seconds,milliseconds)},_compileFormat:function(){var match,component,components=[],mask=[],str=this.format,propertiesByIndex={},i=0,pos=0;while(match=formatComponent.exec(str)){component=match[0];if(component in dateFormatComponents){i++;propertiesByIndex[i]=dateFormatComponents[component].property;components.push("\\s*"+dateFormatComponents[component].getPattern(this)+"\\s*");mask.push({pattern:new RegExp(dateFormatComponents[component].getPattern(this)),property:dateFormatComponents[component].property,start:pos,end:pos+=component.length})}else{components.push(escapeRegExp(component));mask.push({pattern:new RegExp(escapeRegExp(component)),character:component,start:pos,end:++pos})}str=str.slice(component.length)}this._mask=mask;this._maskPos=0;this._formatPattern=new RegExp("^\\s*"+components.join("")+"\\s*$");this._propertiesByIndex=propertiesByIndex},_attachDatePickerEvents:function(){var self=this;this.widget.on("click",".datepicker *",$.proxy(this.click,this));this.widget.on("click","[data-action]",$.proxy(this.doAction,this));this.widget.on("mousedown",$.proxy(this.stopEvent,this));if(this.pickDate&&this.pickTime){this.widget.on("click.togglePicker",".accordion-toggle",function(e){e.stopPropagation();var $this=$(this);var $parent=$this.closest("ul");var expanded=$parent.find(".collapse.in");var closed=$parent.find(".collapse:not(.in)");if(expanded&&expanded.length){var collapseData=expanded.data("collapse");if(collapseData&&collapseData.transitioning)return;expanded.collapse("hide");closed.collapse("show");$this.find("i").toggleClass(self.timeIcon+" "+self.dateIcon);self.$element.find(".add-on i").toggleClass(self.timeIcon+" "+self.dateIcon)}})}if(this.isInput){this.$element.on({focus:$.proxy(this.show,this),change:$.proxy(this.change,this)});if(this.options.maskInput){this.$element.on({keydown:$.proxy(this.keydown,this),keypress:$.proxy(this.keypress,this)})}}else{this.$element.on({change:$.proxy(this.change,this)},"input");if(this.options.maskInput){this.$element.on({keydown:$.proxy(this.keydown,this),keypress:$.proxy(this.keypress,this)},"input")}if(this.component){this.component.on("click",$.proxy(this.show,this))}else{this.$element.on("click",$.proxy(this.show,this))}}},_attachDatePickerGlobalEvents:function(){$(window).on("resize.datetimepicker"+this.id,$.proxy(this.place,this));if(!this.isInput){$(document).on("mousedown.datetimepicker"+this.id,$.proxy(this.hide,this))}},_detachDatePickerEvents:function(){this.widget.off("click",".datepicker *",this.click);this.widget.off("click","[data-action]");this.widget.off("mousedown",this.stopEvent);if(this.pickDate&&this.pickTime){this.widget.off("click.togglePicker")}if(this.isInput){this.$element.off({focus:this.show,change:this.change});if(this.options.maskInput){this.$element.off({keydown:this.keydown,keypress:this.keypress})}}else{this.$element.off({change:this.change},"input");if(this.options.maskInput){this.$element.off({keydown:this.keydown,keypress:this.keypress},"input")}if(this.component){this.component.off("click",this.show)}else{this.$element.off("click",this.show)}}},_detachDatePickerGlobalEvents:function(){$(window).off("resize.datetimepicker"+this.id);if(!this.isInput){$(document).off("mousedown.datetimepicker"+this.id)}},_isInFixed:function(){if(this.$element){var parents=this.$element.parents();var inFixed=false;for(var i=0;i'+"
    "+""+'
    '+DPGlobal.template+"
    "+""+'
  • '+""+'
    '+TPGlobal.getTemplate(is12Hours,showSeconds)+"
    "+""+"
"+""}else if(pickTime){return'"}else{return'"}}function UTCDate(){return new Date(Date.UTC.apply(Date,arguments))}var DPGlobal={modes:[{clsName:"days",navFnc:"UTCMonth",navStep:1},{clsName:"months",navFnc:"UTCFullYear",navStep:1},{clsName:"years",navFnc:"UTCFullYear",navStep:10}],isLeapYear:function(year){return year%4===0&&year%100!==0||year%400===0},getDaysInMonth:function(year,month){return[31,DPGlobal.isLeapYear(year)?29:28,31,30,31,30,31,31,30,31,30,31][month]},headTemplate:""+""+'‹'+''+'›'+""+"",contTemplate:''};DPGlobal.template='
'+''+DPGlobal.headTemplate+""+"
"+"
"+'
'+''+DPGlobal.headTemplate+DPGlobal.contTemplate+"
"+"
"+'
'+''+DPGlobal.headTemplate+DPGlobal.contTemplate+"
"+"
";var TPGlobal={hourTemplate:'',minuteTemplate:'',secondTemplate:''};TPGlobal.getTemplate=function(is12Hours,showSeconds){return'
'+'"+""+''+''+''+(showSeconds?''+'':"")+(is12Hours?'':"")+""+""+" "+''+" "+(showSeconds?''+"":"")+(is12Hours?''+"":"")+""+""+''+''+''+(showSeconds?''+'':"")+(is12Hours?'':"")+""+"
"+TPGlobal.hourTemplate+":"+TPGlobal.minuteTemplate+":"+TPGlobal.secondTemplate+""+''+"
"+"
"+'
'+''+"
"+"
"+'
'+''+"
"+"
"+(showSeconds?'
'+''+"
"+"
":"")}})(window.jQuery); \ No newline at end of file diff --git a/web/templates/_base.html b/web/templates/_base.html index 79970b81..5e651584 100644 --- a/web/templates/_base.html +++ b/web/templates/_base.html @@ -10,6 +10,9 @@ + + + {{template "head" .}} @@ -45,43 +48,49 @@ -{{define "createSilenceModal"}} +{{define "editSilenceModal"}} - {{end}} diff --git a/web/templates/alerts.html b/web/templates/alerts.html index 6cf43a4f..315afcb3 100644 --- a/web/templates/alerts.html +++ b/web/templates/alerts.html @@ -24,23 +24,23 @@ {{range .AlertAggregates}} - {{index .Event.Name}} + {{index .Event.Name}}
- Silence Alert + Silence Alert
{{range $label, $value := .Event.Labels}} - {{$label}}="{{$value}}", + {{$label}}="{{$value}}" {{end}}
{{range $label, $value := .Event.Labels}} {{end}} - Silence Instance + Silence Instance
{{timeSince .Created}} ago @@ -50,5 +50,5 @@ {{end}} - {{template "createSilenceModal" .}} + {{template "editSilenceModal" .}} {{end}} diff --git a/web/templates/silences.html b/web/templates/silences.html index 5e3d2c98..5719535b 100644 --- a/web/templates/silences.html +++ b/web/templates/silences.html @@ -6,89 +6,54 @@ {{define "content"}}

Silences

-

New Silence

+

New Silence

+ - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {{range .Silences}} + + + + + + + + + + {{end}}
ID Labels CreatorExpires AtCreated AtEnds At Notes Actions
{service="Taxes",country="*"}your government2050-01-01 10:23:00Taxes are here to stay. Sorry. - - -
{service="Taxes",country="*"}your government2050-01-01 10:23:00Taxes are here to stay. Sorry. - - -
{service="Taxes",country="*"}your government2050-01-01 10:23:00Taxes are here to stay. Sorry. - - -
{service="Taxes",country="*"}your government2050-01-01 10:23:00Taxes are here to stay. Sorry. - - -
{service="Taxes",country="*"}your government2050-01-01 10:23:00Taxes are here to stay. Sorry. - - -
{service="Taxes",country="*"}your government2050-01-01 10:23:00Taxes are here to stay. Sorry. - - -
{service="Taxes",country="*"}your government2050-01-01 10:23:00Taxes are here to stay. Sorry. - - -
{{.Id}} + {{range .Filters}} + {{.Name}}="{{.Value}}", + {{end}} + {{.CreatedBy}}{{.CreatedAt}}{{.EndsAt}}{{.Comment}} + + + Edit Silence + Remove Silence +
- {{template "createSilenceModal" .}} + {{template "editSilenceModal" .}} + {{end}}