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