From 4598d0688e1a20622e1e7446222f696d26936cda Mon Sep 17 00:00:00 2001 From: Hunter Long Date: Tue, 18 Sep 2018 15:02:27 -0700 Subject: [PATCH] service date range --- Makefile | 4 +- cmd/main_debug.go | 15 + core/failures.go | 2 +- core/hits.go | 2 +- core/services.go | 60 +- core/services_test.go | 6 +- handlers/api.go | 19 +- handlers/handlers.go | 6 + handlers/services.go | 61 +- handlers/settings.go | 5 + notifiers/discord_test.go | 15 + notifiers/email_test.go | 15 + notifiers/notifiers_test.go | 15 + notifiers/slack_test.go | 15 + notifiers/twilio_test.go | 15 + source/css/base.css | 614 +++++++++++++---- source/js/charts.js | 19 +- source/js/pikaday.js | 1257 +++++++++++++++++++++++++++++++++++ source/scss/base.scss | 38 +- source/scss/mobile.scss | 16 + source/scss/pikaday.scss | 255 +++++++ source/scss/variables.scss | 17 + source/source.go | 1 + source/tmpl/service.html | 145 ++-- types/checkin.go | 15 + types/core.go | 15 + types/failure.go | 15 + types/service.go | 1 + types/user.go | 15 + 29 files changed, 2491 insertions(+), 187 deletions(-) create mode 100644 source/js/pikaday.js create mode 100644 source/scss/pikaday.scss diff --git a/Makefile b/Makefile index a3ced665..878a7471 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -VERSION=0.63 +VERSION=0.64 BINARY_NAME=statup GOPATH:=$(GOPATH) GOCMD=go @@ -61,8 +61,8 @@ run: build # compile assets using SASS and Rice. compiles scss -> css, and run rice embed-go compile: - cd source && $(GOPATH)/bin/rice embed-go sass source/scss/base.scss source/css/base.css + cd source && $(GOPATH)/bin/rice embed-go rm -rf .sass-cache # benchmark testing diff --git a/cmd/main_debug.go b/cmd/main_debug.go index 73f3e59b..6c99caf5 100644 --- a/cmd/main_debug.go +++ b/cmd/main_debug.go @@ -1,3 +1,18 @@ +// Statup +// Copyright (C) 2018. Hunter Long and the project contributors +// Written by Hunter Long and the project contributors +// +// https://github.com/hunterlong/statup +// +// The licenses for most software and other practical works are designed +// to take away your freedom to share and change the works. By contrast, +// the GNU General Public License is intended to guarantee your freedom to +// share and change all versions of a program--to make sure it remains free +// software for all its users. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + // +build debug // Statup diff --git a/core/failures.go b/core/failures.go index 669c598e..d4b2e698 100644 --- a/core/failures.go +++ b/core/failures.go @@ -123,7 +123,7 @@ func (s *Service) TotalFailures() (uint64, error) { // TotalFailuresSince returns the total amount of failures for a service since a specific time/date func (s *Service) TotalFailuresSince(ago time.Time) (uint64, error) { var count uint64 - rows := failuresDB().Where("service = ? AND created_at > ?", s.Id, ago.Format("2006-01-02 15:04:05")) + rows := failuresDB().Where("service = ? AND created_at > ?", s.Id, ago.UTC().Format("2006-01-02 15:04:05")) err := rows.Count(&count) return count, err.Error } diff --git a/core/hits.go b/core/hits.go index 67d5d5f2..f730a3b2 100644 --- a/core/hits.go +++ b/core/hits.go @@ -78,7 +78,7 @@ func (s *Service) TotalHits() (uint64, error) { // TotalHitsSince returns the total amount of hits based on a specific time/date func (s *Service) TotalHitsSince(ago time.Time) (uint64, error) { var count uint64 - rows := hitsDB().Where("service = ? AND created_at > ?", s.Id, ago.Format("2006-01-02 15:04:05")) + rows := hitsDB().Where("service = ? AND created_at > ?", s.Id, ago.UTC().Format("2006-01-02 15:04:05")) err := rows.Count(&count) return count, err.Error } diff --git a/core/services.go b/core/services.go index babf4121..05480776 100644 --- a/core/services.go +++ b/core/services.go @@ -18,6 +18,7 @@ package core import ( "encoding/json" "fmt" + "github.com/ararog/timeago" "github.com/hunterlong/statup/core/notifier" "github.com/hunterlong/statup/types" "github.com/hunterlong/statup/utils" @@ -125,9 +126,17 @@ type DateScan struct { Value int64 `json:"y"` } +// DateScanObj struct is for creating the charts.js graph JSON array +type DateScanObj struct { + Array []DateScan +} + // lastFailure returns the last failure a service had func (s *Service) lastFailure() *Failure { limited := s.LimitedFailures() + if len(limited) == 0 { + return nil + } last := limited[len(limited)-1] return last } @@ -146,47 +155,61 @@ func (s *Service) SmallText() string { } if len(last) > 0 { lastFailure := s.lastFailure() - return fmt.Sprintf("%v on %v", lastFailure.ParseError(), utils.Timezoner(last[0].CreatedAt, zone).Format("Monday 3:04:05PM, Jan _2 2006")) + got, _ := timeago.TimeAgoWithTime(time.Now().Add(s.Downtime()), time.Now()) + return fmt.Sprintf("Reported offline %v, %v", got, lastFailure.ParseError()) } else { return fmt.Sprintf("%v is currently offline", s.Name) } } +func (s *Service) DowntimeText() string { + lastFailure := s.lastFailure() + if lastFailure == nil { + return "" + } + got, _ := timeago.TimeAgoWithTime(time.Now().UTC().Add(s.Downtime()), time.Now().UTC()) + return fmt.Sprintf("Reported offline %v, %v", got, lastFailure.ParseError()) +} + // GroupDataBy returns a SQL query as a string to group a column by a time -func GroupDataBy(column string, id int64, tm time.Time, increment string) string { +func GroupDataBy(column string, id int64, start, end time.Time, increment string) string { var sql string switch CoreApp.DbConnection { case "mysql": - sql = fmt.Sprintf("SELECT CONCAT(date_format(created_at, '%%Y-%%m-%%dT%%H:%%i:00Z')) AS created_at, AVG(latency)*1000 AS value FROM %v WHERE service=%v AND DATE_FORMAT(created_at, '%%Y-%%m-%%dT%%TZ') BETWEEN DATE_FORMAT('%v', '%%Y-%%m-%%dT%%TZ') AND DATE_FORMAT(NOW(), '%%Y-%%m-%%dT%%TZ') GROUP BY 1 ORDER BY created_at ASC;", column, id, tm.Format(time.RFC3339)) + sql = fmt.Sprintf("SELECT CONCAT(date_format(created_at, '%%Y-%%m-%%dT%%H:%%i:00Z')) AS created_at, AVG(latency)*1000 AS value FROM %v WHERE service=%v AND DATE_FORMAT(created_at, '%%Y-%%m-%%dT%%TZ') BETWEEN DATE_FORMAT('%v', '%%Y-%%m-%%dT%%TZ') AND DATE_FORMAT('%v', '%%Y-%%m-%%dT%%TZ') GROUP BY 1 ORDER BY created_at ASC;", column, id, start.UTC().Format(time.RFC3339), end.UTC().Format(time.RFC3339)) case "sqlite": - sql = fmt.Sprintf("SELECT strftime('%%Y-%%m-%%dT%%H:%%M:00Z', created_at), AVG(latency)*1000 as value FROM %v WHERE service=%v AND created_at >= '%v' GROUP BY strftime('%%M:00', created_at) ORDER BY created_at ASC;", column, id, tm.Format(time.RFC3339)) + sql = fmt.Sprintf("SELECT strftime('%%Y-%%m-%%dT%%H:%%M:00Z', created_at), AVG(latency)*1000 as value FROM %v WHERE service=%v AND created_at >= '%v' AND created_at <= '%v' GROUP BY strftime('%%M:00', created_at) ORDER BY created_at ASC;", column, id, start.UTC().Format(time.RFC3339), end.UTC().Format(time.RFC3339)) case "postgres": - sql = fmt.Sprintf("SELECT date_trunc('%v', created_at), AVG(latency)*1000 AS value FROM %v WHERE service=%v AND created_at >= '%v' GROUP BY 1 ORDER BY date_trunc ASC;", increment, column, id, tm.Format(time.RFC3339)) + sql = fmt.Sprintf("SELECT date_trunc('%v', created_at), AVG(latency)*1000 AS value FROM %v WHERE service=%v AND created_at >= '%v' AND created_at <= '%v' GROUP BY 1 ORDER BY date_trunc ASC;", increment, column, id, start.UTC().Format(time.RFC3339), end.UTC().Format(time.RFC3339)) } return sql } +// Downtime returns the amount of time of a offline service func (s *Service) Downtime() time.Duration { hits, _ := s.Hits() + if len(hits) == 0 { + return time.Duration(0) + } fails := s.LimitedFailures() if len(fails) == 0 { return time.Duration(0) } - since := fails[0].CreatedAt.Sub(hits[0].CreatedAt) + since := fails[0].CreatedAt.UTC().Sub(hits[0].CreatedAt.UTC()) return since } -func (s *Service) GraphDataRaw() []*DateScan { - var d []*DateScan - since := time.Now().Add(time.Hour*-24 + time.Minute*0 + time.Second*0) - sql := GroupDataBy("hits", s.Id, since, "minute") +func GraphDataRaw(service types.ServiceInterface, start, end time.Time) *DateScanObj { + var d []DateScan + s := service.Select() + sql := GroupDataBy("hits", s.Id, start, end, "minute") rows, err := DbSession.Raw(sql).Rows() if err != nil { utils.Log(2, err) return nil } for rows.Next() { - gd := new(DateScan) + var gd DateScan var tt string var ff float64 err := rows.Scan(&tt, &ff) @@ -201,12 +224,23 @@ func (s *Service) GraphDataRaw() []*DateScan { gd.Value = int64(ff) d = append(d, gd) } - return d + return &DateScanObj{d} +} + +func (d *DateScanObj) ToString() string { + data, err := json.Marshal(d.Array) + if err != nil { + utils.Log(2, err) + return "{}" + } + return string(data) } // GraphData returns the JSON object used by Charts.js to render the chart func (s *Service) GraphData() string { - obj := s.GraphDataRaw() + start := time.Now().Add(time.Hour*-24 + time.Minute*0 + time.Second*0) + end := time.Now() + obj := GraphDataRaw(s, start, end) data, err := json.Marshal(obj) if err != nil { utils.Log(2, err) diff --git a/core/services_test.go b/core/services_test.go index 7b1d2eaa..9afdfce9 100644 --- a/core/services_test.go +++ b/core/services_test.go @@ -345,19 +345,19 @@ func TestGroupGraphData(t *testing.T) { service := SelectService(1) CoreApp.DbConnection = "mysql" lastWeek := time.Now().Add(time.Hour*-(24*7) + time.Minute*0 + time.Second*0) - out := GroupDataBy("services", service.Id, lastWeek, "hour") + out := GroupDataBy("services", service.Id, lastWeek, time.Now(), "hour") t.Log(out) assert.Contains(t, out, "SELECT CONCAT(date_format(created_at, '%Y-%m-%dT%H:%i:00Z'))") CoreApp.DbConnection = "postgres" lastWeek = time.Now().Add(time.Hour*-(24*7) + time.Minute*0 + time.Second*0) - out = GroupDataBy("services", service.Id, lastWeek, "hour") + out = GroupDataBy("services", service.Id, lastWeek, time.Now(), "hour") t.Log(out) assert.Contains(t, out, "SELECT date_trunc('hour', created_at)") CoreApp.DbConnection = "sqlite" lastWeek = time.Now().Add(time.Hour*-(24*7) + time.Minute*0 + time.Second*0) - out = GroupDataBy("services", service.Id, lastWeek, "hour") + out = GroupDataBy("services", service.Id, lastWeek, time.Now(), "hour") t.Log(out) assert.Contains(t, out, "SELECT strftime('%Y-%m-%dT%H:%M:00Z'") diff --git a/handlers/api.go b/handlers/api.go index 12e8eddd..f01f1257 100644 --- a/handlers/api.go +++ b/handlers/api.go @@ -23,6 +23,7 @@ import ( "github.com/hunterlong/statup/utils" "net/http" "os" + "time" ) type ApiResponse struct { @@ -99,6 +100,22 @@ func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) { return } vars := mux.Vars(r) + fields := parseGet(r) + + startField := utils.StringInt(fields.Get("start")) + endField := utils.StringInt(fields.Get("end")) + var start time.Time + var end time.Time + if startField == 0 { + start = time.Now().Add(-24 * time.Hour).UTC() + } else { + start = time.Unix(startField, 0) + } + if endField == 0 { + end = time.Now().UTC() + } else { + end = time.Unix(endField, 0) + } service := core.SelectService(utils.StringInt(vars["id"])) if service == nil { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) @@ -106,7 +123,7 @@ func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(service.GraphDataRaw()) + json.NewEncoder(w).Encode(core.GraphDataRaw(service, start, end).Array) } func apiCreateServiceHandler(w http.ResponseWriter, r *http.Request) { diff --git a/handlers/handlers.go b/handlers/handlers.go index ccd16c9e..83ad95c7 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -147,6 +147,12 @@ func executeResponse(w http.ResponseWriter, r *http.Request, file string, data i "ToString": func(v interface{}) string { return utils.ToString(v) }, + "ToUnix": func(t time.Time) int64 { + return t.UTC().Unix() + }, + "FromUnix": func(t int64) string { + return utils.Timezoner(time.Unix(t, 0), core.CoreApp.Timezone).Format("Monday, January 02") + }, }) t, err = t.Parse(nav) if err != nil { diff --git a/handlers/services.go b/handlers/services.go index 165af12b..3e3e4579 100644 --- a/handlers/services.go +++ b/handlers/services.go @@ -24,6 +24,7 @@ import ( "github.com/hunterlong/statup/utils" "net/http" "strconv" + "time" ) type Service struct { @@ -36,17 +37,56 @@ func renderServiceChartHandler(w http.ResponseWriter, r *http.Request) { return } vars := mux.Vars(r) + fields := parseGet(r) + + startField := fields.Get("start") + endField := fields.Get("end") + var start time.Time + var end time.Time + if startField == "" { + start = time.Now().Add(-24 * time.Hour) + } else { + start = time.Unix(utils.StringInt(startField), 0) + } + if endField == "" { + end = time.Now() + } else { + end = time.Unix(utils.StringInt(endField), 0) + } + service := core.SelectService(utils.StringInt(vars["id"])) w.Header().Set("Content-Type", "text/javascript") w.Header().Set("Cache-Control", "max-age=60") - executeJSResponse(w, r, "charts.js", []*core.Service{service}) + + data := core.GraphDataRaw(service, start, end).ToString() + + out := struct { + Services []*core.Service + Data []string + }{[]*core.Service{service}, []string{data}} + + executeJSResponse(w, r, "charts.js", out) } func renderServiceChartsHandler(w http.ResponseWriter, r *http.Request) { services := core.CoreApp.Services w.Header().Set("Content-Type", "text/javascript") w.Header().Set("Cache-Control", "max-age=60") - executeJSResponse(w, r, "charts.js", services) + + var data []string + end := time.Now() + start := end.Add(-24 * time.Hour) + for _, s := range services { + d := core.GraphDataRaw(s, start, end).ToString() + data = append(data, d) + } + + out := struct { + Services []types.ServiceInterface + Data []string + }{services, data} + + executeJSResponse(w, r, "charts.js", out) } func servicesHandler(w http.ResponseWriter, r *http.Request) { @@ -139,12 +179,27 @@ func servicesDeleteHandler(w http.ResponseWriter, r *http.Request) { func servicesViewHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) + fields := parseGet(r) + startField := utils.StringInt(fields.Get("start")) + endField := utils.StringInt(fields.Get("end")) serv := core.SelectService(utils.StringInt(vars["id"])) if serv == nil { w.WriteHeader(http.StatusNotFound) return } - executeResponse(w, r, "service.html", serv, nil) + + if startField == 0 || endField == 0 { + startField = time.Now().Add(-24 * time.Hour).UTC().Unix() + endField = time.Now().UTC().Unix() + } + + out := struct { + Service *core.Service + Start int64 + End int64 + }{serv, startField, endField} + + executeResponse(w, r, "service.html", out, nil) } func servicesUpdateHandler(w http.ResponseWriter, r *http.Request) { diff --git a/handlers/settings.go b/handlers/settings.go index dce4c231..1ef9d34f 100644 --- a/handlers/settings.go +++ b/handlers/settings.go @@ -129,6 +129,11 @@ func parseForm(r *http.Request) url.Values { return r.PostForm } +func parseGet(r *http.Request) url.Values { + r.ParseForm() + return r.Form +} + func saveNotificationHandler(w http.ResponseWriter, r *http.Request) { var err error if !IsAuthenticated(r) { diff --git a/notifiers/discord_test.go b/notifiers/discord_test.go index 41c325fa..7de38123 100644 --- a/notifiers/discord_test.go +++ b/notifiers/discord_test.go @@ -1,3 +1,18 @@ +// Statup +// Copyright (C) 2018. Hunter Long and the project contributors +// Written by Hunter Long and the project contributors +// +// https://github.com/hunterlong/statup +// +// The licenses for most software and other practical works are designed +// to take away your freedom to share and change the works. By contrast, +// the GNU General Public License is intended to guarantee your freedom to +// share and change all versions of a program--to make sure it remains free +// software for all its users. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + package notifiers import ( diff --git a/notifiers/email_test.go b/notifiers/email_test.go index 5df58e73..85490c11 100644 --- a/notifiers/email_test.go +++ b/notifiers/email_test.go @@ -1,3 +1,18 @@ +// Statup +// Copyright (C) 2018. Hunter Long and the project contributors +// Written by Hunter Long and the project contributors +// +// https://github.com/hunterlong/statup +// +// The licenses for most software and other practical works are designed +// to take away your freedom to share and change the works. By contrast, +// the GNU General Public License is intended to guarantee your freedom to +// share and change all versions of a program--to make sure it remains free +// software for all its users. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + package notifiers import ( diff --git a/notifiers/notifiers_test.go b/notifiers/notifiers_test.go index 2e527b37..dce1a4ea 100644 --- a/notifiers/notifiers_test.go +++ b/notifiers/notifiers_test.go @@ -1,3 +1,18 @@ +// Statup +// Copyright (C) 2018. Hunter Long and the project contributors +// Written by Hunter Long and the project contributors +// +// https://github.com/hunterlong/statup +// +// The licenses for most software and other practical works are designed +// to take away your freedom to share and change the works. By contrast, +// the GNU General Public License is intended to guarantee your freedom to +// share and change all versions of a program--to make sure it remains free +// software for all its users. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + package notifiers import ( diff --git a/notifiers/slack_test.go b/notifiers/slack_test.go index f2315664..4e777294 100644 --- a/notifiers/slack_test.go +++ b/notifiers/slack_test.go @@ -1,3 +1,18 @@ +// Statup +// Copyright (C) 2018. Hunter Long and the project contributors +// Written by Hunter Long and the project contributors +// +// https://github.com/hunterlong/statup +// +// The licenses for most software and other practical works are designed +// to take away your freedom to share and change the works. By contrast, +// the GNU General Public License is intended to guarantee your freedom to +// share and change all versions of a program--to make sure it remains free +// software for all its users. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + package notifiers import ( diff --git a/notifiers/twilio_test.go b/notifiers/twilio_test.go index bd2b2cac..17210cc2 100644 --- a/notifiers/twilio_test.go +++ b/notifiers/twilio_test.go @@ -1,3 +1,18 @@ +// Statup +// Copyright (C) 2018. Hunter Long and the project contributors +// Written by Hunter Long and the project contributors +// +// https://github.com/hunterlong/statup +// +// The licenses for most software and other practical works are designed +// to take away your freedom to share and change the works. By contrast, +// the GNU General Public License is intended to guarantee your freedom to +// share and change all versions of a program--to make sure it remains free +// software for all its users. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + package notifiers import ( diff --git a/source/css/base.css b/source/css/base.css index 1b23bc02..a60583a6 100644 --- a/source/css/base.css +++ b/source/css/base.css @@ -1,3 +1,36 @@ +@charset "UTF-8"; +/*! + * Statup + * Copyright (C) 2018. Hunter Long and the project contributors + * Written by Hunter Long and the project contributors + * + * https://github.com/hunterlong/statup + * + * The licenses for most software and other practical works are designed + * to take away your freedom to share and change the works. By contrast, + * the GNU General Public License is intended to guarantee your freedom to + * share and change all versions of a program--to make sure it remains free + * software for all its users. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +/*! + * Statup + * Copyright (C) 2018. Hunter Long and the project contributors + * Written by Hunter Long and the project contributors + * + * https://github.com/hunterlong/statup + * + * The licenses for most software and other practical works are designed + * to take away your freedom to share and change the works. By contrast, + * the GNU General Public License is intended to guarantee your freedom to + * share and change all versions of a program--to make sure it remains free + * software for all its users. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ /* Index Page */ /* Status Container */ /* Button Colors */ @@ -6,53 +39,65 @@ /* Mobile Settings */ /* Mobile Service Container */ HTML, BODY { - background-color: #fcfcfc; } + background-color: #fcfcfc; +} .container { padding-top: 20px; padding-bottom: 20px; - max-width: 860px; } + max-width: 860px; +} .header-title { - color: #464646; } + color: #464646; +} .header-desc { - color: #939393; } + color: #939393; +} .btn { - border-radius: 0.2rem; } + border-radius: 0.2rem; +} .online_list .badge { - margin-top: 0.2rem; } + margin-top: 0.2rem; +} .navbar { - margin-bottom: 30px; } + margin-bottom: 30px; +} .btn-sm { line-height: 1.3; - font-size: 0.75rem; } + font-size: 0.75rem; +} .view_service_btn { position: absolute; bottom: -40px; - right: 40px; } + right: 40px; +} .service_lower_info { position: absolute; bottom: -40px; left: 40px; color: #d1ffca; - font-size: 0.85rem; } + font-size: 0.85rem; +} .lg_number { font-size: 2.3rem; font-weight: bold; display: block; - color: #4f4f4f; } + color: #4f4f4f; +} .stats_area { text-align: center; - color: #a5a5a5; } + color: #a5a5a5; +} .lower_canvas { height: 3.4rem; @@ -60,104 +105,134 @@ HTML, BODY { background-color: #48d338; padding: 15px 10px; margin-left: 0px !important; - margin-right: 0px !important; } + margin-right: 0px !important; +} .lower_canvas SPAN { font-size: 1rem; - color: #fff; } + color: #fff; +} .footer { text-decoration: none; - margin-top: 20px; } + margin-top: 20px; +} .footer A { color: #8d8d8d; - text-decoration: none; } + text-decoration: none; +} .footer A:HOVER { - color: #6d6d6d; } + color: #6d6d6d; +} .badge { color: white; - border-radius: 0.2rem; } + border-radius: 0.2rem; +} .btn-group { - height: 25px; } - .btn-group A { - padding: 0.1rem .75rem; - font-size: 0.8rem; } + height: 25px; +} +.btn-group A { + padding: 0.1rem 0.75rem; + font-size: 0.8rem; +} .card-body .badge { - color: #fff; } + color: #fff; +} .nav-pills .nav-link { - border-radius: 0.2rem; } + border-radius: 0.2rem; +} .form-control { - border-radius: 0.2rem; } + border-radius: 0.2rem; +} .card { background-color: #ffffff; - border: 1px solid rgba(0, 0, 0, 0.125); } + border: 1px solid rgba(0, 0, 0, 0.125); +} .card-body { - overflow: hidden; } + overflow: hidden; +} .card-body H4 A { color: #444444; - text-decoration: none; } + text-decoration: none; +} .chart-container { position: relative; height: 170px; - width: 100%; } + width: 100%; +} .btn-primary { background-color: #3e9bff; border-color: #006fe6; - color: white; } - .btn-primary.dyn-dark { - background-color: #32a825 !important; - border-color: #2c9320 !important; } - .btn-primary.dyn-light { - background-color: #75de69 !important; - border-color: #88e37e !important; } + color: white; +} +.btn-primary.dyn-dark { + background-color: #32a825 !important; + border-color: #2c9320 !important; +} +.btn-primary.dyn-light { + background-color: #75de69 !important; + border-color: #88e37e !important; +} .btn-success { - background-color: #47d337; } - .btn-success.dyn-dark { - background-color: #32a825 !important; - border-color: #2c9320 !important; } - .btn-success.dyn-light { - background-color: #75de69 !important; - border-color: #88e37e !important; } + background-color: #47d337; +} +.btn-success.dyn-dark { + background-color: #32a825 !important; + border-color: #2c9320 !important; +} +.btn-success.dyn-light { + background-color: #75de69 !important; + border-color: #88e37e !important; +} .btn-danger { - background-color: #dd3545; } - .btn-danger.dyn-dark { - background-color: #b61f2d !important; - border-color: #a01b28 !important; } - .btn-danger.dyn-light { - background-color: #e66975 !important; - border-color: #e97f89 !important; } + background-color: #dd3545; +} +.btn-danger.dyn-dark { + background-color: #b61f2d !important; + border-color: #a01b28 !important; +} +.btn-danger.dyn-light { + background-color: #e66975 !important; + border-color: #e97f89 !important; +} .bg-success { - background-color: #47d337 !important; } + background-color: #47d337 !important; +} .bg-danger { - background-color: #dd3545 !important; } + background-color: #dd3545 !important; +} .bg-success .dyn-dark { - background-color: #35b027 !important; } + background-color: #35b027 !important; +} .bg-danger .dyn-dark { - background-color: #bf202f !important; } + background-color: #bf202f !important; +} .nav-pills .nav-link.active, .nav-pills .show > .nav-link { - background-color: #13a00d; } + background-color: #13a00d; +} .nav-pills A { - color: #424242; } + color: #424242; +} .CodeMirror { /* Bootstrap Settings */ @@ -177,23 +252,26 @@ HTML, BODY { border: 1px solid #ccc; border-radius: 4px; box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; /* Code Mirror Settings */ font-family: monospace; position: relative; overflow: hidden; - height: 80vh; } + height: 80vh; +} .CodeMirror-focused { /* Bootstrap Settings */ border-color: #66afe9; outline: 0; box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); - transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; } + transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; +} .switch { font-size: 1rem; - position: relative; } + position: relative; +} .switch input { position: absolute; @@ -204,7 +282,8 @@ HTML, BODY { clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; - padding: 0; } + padding: 0; +} .switch input + label { position: relative; @@ -217,23 +296,26 @@ HTML, BODY { outline: none; user-select: none; vertical-align: middle; - text-indent: calc(calc(calc(2.375rem * .8) * 2) + .5rem); } + text-indent: calc(calc(calc(2.375rem * .8) * 2) + .5rem); +} .switch input + label::before, .switch input + label::after { - content: ''; + content: ""; position: absolute; top: 0; left: 0; width: calc(calc(2.375rem * .8) * 2); bottom: 0; - display: block; } + display: block; +} .switch input + label::before { right: 0; background-color: #dee2e6; border-radius: calc(2.375rem * .8); - transition: 0.2s all; } + transition: 0.2s all; +} .switch input + label::after { top: 2px; @@ -242,120 +324,154 @@ HTML, BODY { height: calc(calc(2.375rem * .8) - calc(2px * 2)); border-radius: 50%; background-color: white; - transition: 0.2s all; } + transition: 0.2s all; +} .switch input:checked + label::before { - background-color: #08d; } + background-color: #08d; +} .switch input:checked + label::after { - margin-left: calc(2.375rem * .8); } + margin-left: calc(2.375rem * .8); +} .switch input:focus + label::before { outline: none; - box-shadow: 0 0 0 0.2rem rgba(0, 136, 221, 0.25); } + box-shadow: 0 0 0 0.2rem rgba(0, 136, 221, 0.25); +} .switch input:disabled + label { color: #868e96; - cursor: not-allowed; } + cursor: not-allowed; +} .switch input:disabled + label::before { - background-color: #e9ecef; } + background-color: #e9ecef; +} .switch.switch-sm { - font-size: 0.875rem; } + font-size: 0.875rem; +} .switch.switch-sm input + label { min-width: calc(calc(1.9375rem * .8) * 2); height: calc(1.9375rem * .8); line-height: calc(1.9375rem * .8); - text-indent: calc(calc(calc(1.9375rem * .8) * 2) + .5rem); } + text-indent: calc(calc(calc(1.9375rem * .8) * 2) + .5rem); +} .switch.switch-sm input + label::before { - width: calc(calc(1.9375rem * .8) * 2); } + width: calc(calc(1.9375rem * .8) * 2); +} .switch.switch-sm input + label::after { width: calc(calc(1.9375rem * .8) - calc(2px * 2)); - height: calc(calc(1.9375rem * .8) - calc(2px * 2)); } + height: calc(calc(1.9375rem * .8) - calc(2px * 2)); +} .switch.switch-sm input:checked + label::after { - margin-left: calc(1.9375rem * .8); } + margin-left: calc(1.9375rem * .8); +} .switch.switch-lg { - font-size: 1.25rem; } + font-size: 1.25rem; +} .switch.switch-lg input + label { min-width: calc(calc(3rem * .8) * 2); height: calc(3rem * .8); line-height: calc(3rem * .8); - text-indent: calc(calc(calc(3rem * .8) * 2) + .5rem); } + text-indent: calc(calc(calc(3rem * .8) * 2) + .5rem); +} .switch.switch-lg input + label::before { - width: calc(calc(3rem * .8) * 2); } + width: calc(calc(3rem * .8) * 2); +} .switch.switch-lg input + label::after { width: calc(calc(3rem * .8) - calc(2px * 2)); - height: calc(calc(3rem * .8) - calc(2px * 2)); } + height: calc(calc(3rem * .8) - calc(2px * 2)); +} .switch.switch-lg input:checked + label::after { - margin-left: calc(3rem * .8); } + margin-left: calc(3rem * .8); +} .switch + .switch { - margin-left: 1rem; } + margin-left: 1rem; +} @keyframes pulse_animation { 0% { - transform: scale(1); } + transform: scale(1); + } 30% { - transform: scale(1); } + transform: scale(1); + } 40% { - transform: scale(1.02); } + transform: scale(1.02); + } 50% { - transform: scale(1); } + transform: scale(1); + } 60% { - transform: scale(1); } + transform: scale(1); + } 70% { - transform: scale(1.05); } + transform: scale(1.05); + } 80% { - transform: scale(1); } + transform: scale(1); + } 100% { - transform: scale(1); } } + transform: scale(1); + } +} .pulse { animation-name: pulse_animation; animation-duration: 1500ms; transform-origin: 70% 70%; animation-iteration-count: infinite; - animation-timing-function: linear; } + animation-timing-function: linear; +} @keyframes glow-grow { 0% { opacity: 0; - transform: scale(1); } + transform: scale(1); + } 80% { - opacity: 1; } + opacity: 1; + } 100% { transform: scale(2); - opacity: 0; } } + opacity: 0; + } +} .pulse-glow { animation-name: glow-grown; animation-duration: 100ms; transform-origin: 70% 30%; animation-iteration-count: infinite; - animation-timing-function: linear; } + animation-timing-function: linear; +} .pulse-glow:before, .pulse-glow:after { position: absolute; - content: ''; + content: ""; height: 0.5rem; width: 1.75rem; top: 1.2rem; right: 2.15rem; border-radius: 0; box-shadow: 0 0 7px #47d337; - animation: glow-grow 2s ease-out infinite; } + animation: glow-grow 2s ease-out infinite; +} .sortable_drag { - background-color: #0000000f; } + background-color: #0000000f; +} .drag_icon { cursor: move; @@ -369,82 +485,346 @@ HTML, BODY { margin-right: 5px; margin-left: -10px; text-align: center; - color: #b1b1b1; } + color: #b1b1b1; +} /* (Optional) Apply a "closed-hand" cursor during drag operation. */ .drag_icon:active { cursor: grabbing; cursor: -moz-grabbing; - cursor: -webkit-grabbing; } + cursor: -webkit-grabbing; +} .switch_btn { float: right; margin: -1px 0px 0px 0px; - display: block; } + display: block; +} +#start_container { + position: absolute; + z-index: 99999; + margin-top: 20px; +} + +#end_container { + position: absolute; + z-index: 99999; + margin-top: 20px; + right: 0; +} + +.pointer { + cursor: pointer; +} + +/*! + * Pikaday + * Copyright © 2014 David Bushell | BSD & MIT license | http://dbushell.com/ + */ +.pika-single { + z-index: 9999; + display: block; + position: relative; + color: #333; + background: #fff; + border: 1px solid #ccc; + border-bottom-color: #bbb; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; +} +.pika-single.is-hidden { + display: none; +} +.pika-single.is-bound { + position: absolute; + box-shadow: 0 5px 15px -5px rgba(0, 0, 0, 0.5); +} + +.pika-single { + *zoom: 1; +} +.pika-single:before, .pika-single:after { + content: " "; + display: table; +} +.pika-single:after { + clear: both; +} + +.pika-lendar { + float: left; + width: 240px; + margin: 8px; +} + +.pika-title { + position: relative; + text-align: center; +} +.pika-title select { + cursor: pointer; + position: absolute; + z-index: 9998; + margin: 0; + left: 0; + top: 5px; + filter: alpha(opacity=0); + opacity: 0; +} + +.pika-label { + display: inline-block; + *display: inline; + position: relative; + z-index: 9999; + overflow: hidden; + margin: 0; + padding: 5px 3px; + font-size: 14px; + line-height: 20px; + font-weight: bold; + color: #333; + background-color: #fff; +} + +.pika-prev, +.pika-next { + display: block; + cursor: pointer; + position: relative; + outline: none; + border: 0; + padding: 0; + width: 20px; + height: 30px; + text-indent: 20px; + white-space: nowrap; + overflow: hidden; + background-color: transparent; + background-position: center center; + background-repeat: no-repeat; + background-size: 75% 75%; + opacity: 0.5; + *position: absolute; + *top: 0; +} +.pika-prev:hover, +.pika-next:hover { + opacity: 1; +} +.pika-prev.is-disabled, +.pika-next.is-disabled { + cursor: default; + opacity: 0.2; +} + +.pika-prev, +.is-rtl .pika-next { + float: left; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAUklEQVR42u3VMQoAIBADQf8Pgj+OD9hG2CtONJB2ymQkKe0HbwAP0xucDiQWARITIDEBEnMgMQ8S8+AqBIl6kKgHiXqQqAeJepBo/z38J/U0uAHlaBkBl9I4GwAAAABJRU5ErkJggg=="); + *left: 0; +} + +.pika-next, +.is-rtl .pika-prev { + float: right; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAU0lEQVR42u3VOwoAMAgE0dwfAnNjU26bYkBCFGwfiL9VVWoO+BJ4Gf3gtsEKKoFBNTCoCAYVwaAiGNQGMUHMkjGbgjk2mIONuXo0nC8XnCf1JXgArVIZAQh5TKYAAAAASUVORK5CYII="); + *right: 0; +} + +.pika-select { + display: inline-block; + *display: inline; +} + +.pika-table { + width: 100%; + border-collapse: collapse; + border-spacing: 0; + border: 0; +} +.pika-table th, +.pika-table td { + width: 14.2857142857%; + padding: 0; +} +.pika-table th { + color: #999; + font-size: 12px; + line-height: 25px; + font-weight: bold; + text-align: center; +} +.pika-table abbr { + border-bottom: none; + cursor: help; +} + +.pika-button { + cursor: pointer; + display: block; + -moz-box-sizing: border-box; + box-sizing: border-box; + outline: none; + border: 0; + margin: 0; + width: 100%; + padding: 5px; + color: #666; + font-size: 12px; + line-height: 15px; + text-align: right; + background: #f5f5f5; +} +.is-today .pika-button { + color: #33aaff; + font-weight: bold; +} +.is-selected .pika-button { + color: #fff; + font-weight: bold; + background: #33aaff; + box-shadow: inset 0 1px 3px #178fe5; + border-radius: 3px; +} +.is-disabled .pika-button, .is-outside-current-month .pika-button { + color: #999; + opacity: 0.3; +} +.is-disabled .pika-button { + pointer-events: none; + cursor: default; +} +.pika-button:hover { + color: #fff; + background: #ff8000; + box-shadow: none; + border-radius: 3px; +} +.pika-button .is-selection-disabled { + pointer-events: none; + cursor: default; +} + +.pika-week { + font-size: 11px; + color: #999; +} + +.is-inrange .pika-button { + background: #D5E9F7; +} + +.is-startrange .pika-button { + color: #fff; + background: #6CB31D; + box-shadow: none; + border-radius: 3px; +} + +.is-endrange .pika-button { + color: #fff; + background: #33aaff; + box-shadow: none; + border-radius: 3px; +} + +/*! + * Statup + * Copyright (C) 2018. Hunter Long and the project contributors + * Written by Hunter Long and the project contributors + * + * https://github.com/hunterlong/statup + * + * The licenses for most software and other practical works are designed + * to take away your freedom to share and change the works. By contrast, + * the GNU General Public License is intended to guarantee your freedom to + * share and change all versions of a program--to make sure it remains free + * software for all its users. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ @media (max-width: 767px) { HTML, BODY { - background-color: #fcfcfc; } + background-color: #fcfcfc; + } .sm-container { margin-top: 40px !important; - padding: 0 !important; } + padding: 0 !important; + } .list-group-item H5 { - font-size: 0.9rem; } + font-size: 0.9rem; + } .container { - padding: 0 !important; } + padding: 0 !important; + } .navbar { margin-left: 0px; margin-top: 0px; width: 100%; - margin-bottom: 0; } + margin-bottom: 0; + } .btn-sm { line-height: 0.9rem; - font-size: 0.65rem; } + font-size: 0.65rem; + } .full-col-12 { padding-left: 0px; - padding-right: 0px; } + padding-right: 0px; + } .card { border: 0; border-radius: 0rem; padding: 0; - background-color: #ffffff; } + background-color: #ffffff; + } .card-body { font-size: 6pt; - padding: 5px 5px; } + padding: 5px 5px; + } .lg_number { - font-size: 7.8vw; } + font-size: 7.8vw; + } .stats_area { margin-top: 1.5rem !important; - margin-bottom: 1.5rem !important; } + margin-bottom: 1.5rem !important; + } .stats_area .col-4 { padding-left: 0; padding-right: 0; - font-size: 0.6rem; } + font-size: 0.6rem; + } .list-group-item { border-top: 1px solid #e4e4e4; - border: 0px; } + border: 0px; + } .list-group-item:first-child { border-top-left-radius: 0; - border-top-right-radius: 0; } + border-top-right-radius: 0; + } .list-group-item:last-child { border-bottom-right-radius: 0; - border-bottom-left-radius: 0; } + border-bottom-left-radius: 0; + } .list-group-item P { - font-size: 0.7rem; } } + font-size: 0.7rem; + } +} /*# sourceMappingURL=base.css.map */ diff --git a/source/js/charts.js b/source/js/charts.js index 5dbb1b6e..1dd33e04 100644 --- a/source/js/charts.js +++ b/source/js/charts.js @@ -1,4 +1,21 @@ -{{ range . }}{{ if .AvgTime }}var ctx_{{.Id}}=document.getElementById("service_{{.Id}}").getContext('2d');var chartdata=new Chart(ctx_{{.Id}},{type:'line',data:{datasets:[{label:'Response Time (Milliseconds)',data:{{safe .GraphData}},backgroundColor:['rgba(47, 206, 30, 0.92)'],borderColor:['rgb(47, 171, 34)'],borderWidth:1}]},options:{maintainAspectRatio:!1,scaleShowValues:!0,layout:{padding:{left:0,right:0,top:0,bottom:-10}},hover:{animationDuration:0,},responsiveAnimationDuration:0,animation:{duration:3500,onComplete:function(){var chartInstance=this.chart,ctx=chartInstance.ctx;var controller=this.chart.controller;var xAxis=controller.scales['x-axis-0'];var yAxis=controller.scales['y-axis-0'];ctx.font=Chart.helpers.fontString(Chart.defaults.global.defaultFontSize,Chart.defaults.global.defaultFontStyle,Chart.defaults.global.defaultFontFamily);ctx.textAlign='center';ctx.textBaseline='bottom';var numTicks=xAxis.ticks.length;var yOffsetStart=xAxis.width/numTicks;var halfBarWidth=(xAxis.width/(numTicks*2));xAxis.ticks.forEach(function(value,index){var xOffset=20;var yOffset=(yOffsetStart*index)+halfBarWidth;ctx.fillStyle='#e2e2e2';ctx.fillText(value,yOffset,xOffset)});this.data.datasets.forEach(function(dataset,i){var meta=chartInstance.controller.getDatasetMeta(i);var hxH=0;var hyH=0;var hxL=0;var hyL=0;var highestNum=0;var lowestnum=999999999999;meta.data.forEach(function(bar,index){var data=dataset.data[index];if(lowestnum>data.y){lowestnum=data.y;hxL=bar._model.x;hyL=bar._model.y} +/* + * Statup + * Copyright (C) 2018. Hunter Long and the project contributors + * Written by Hunter Long and the project contributors + * + * https://github.com/hunterlong/statup + * + * The licenses for most software and other practical works are designed + * to take away your freedom to share and change the works. By contrast, + * the GNU General Public License is intended to guarantee your freedom to + * share and change all versions of a program--to make sure it remains free + * software for all its users. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +{{$d := .Data}}{{ range $i, $s := .Services }}{{ if $s.AvgTime }}var ctx_{{$s.Id}}=document.getElementById("service_{{$s.Id}}").getContext('2d');var chartdata=new Chart(ctx_{{$s.Id}},{type:'line',data:{datasets:[{label:'Response Time (Milliseconds)',data:{{safe (index $d $i)}},backgroundColor:['rgba(47, 206, 30, 0.92)'],borderColor:['rgb(47, 171, 34)'],borderWidth:1}]},options:{maintainAspectRatio:!1,scaleShowValues:!0,layout:{padding:{left:0,right:0,top:0,bottom:-10}},hover:{animationDuration:0,},responsiveAnimationDuration:0,animation:{duration:3500,onComplete:function(){var chartInstance=this.chart,ctx=chartInstance.ctx;var controller=this.chart.controller;var xAxis=controller.scales['x-axis-0'];var yAxis=controller.scales['y-axis-0'];ctx.font=Chart.helpers.fontString(Chart.defaults.global.defaultFontSize,Chart.defaults.global.defaultFontStyle,Chart.defaults.global.defaultFontFamily);ctx.textAlign='center';ctx.textBaseline='bottom';var numTicks=xAxis.ticks.length;var yOffsetStart=xAxis.width/numTicks;var halfBarWidth=(xAxis.width/(numTicks*2));xAxis.ticks.forEach(function(value,index){var xOffset=20;var yOffset=(yOffsetStart*index)+halfBarWidth;ctx.fillStyle='#e2e2e2';ctx.fillText(value,yOffset,xOffset)});this.data.datasets.forEach(function(dataset,i){var meta=chartInstance.controller.getDatasetMeta(i);var hxH=0;var hyH=0;var hxL=0;var hyL=0;var highestNum=0;var lowestnum=999999999999;meta.data.forEach(function(bar,index){var data=dataset.data[index];if(lowestnum>data.y){lowestnum=data.y;hxL=bar._model.x;hyL=bar._model.y} if(data.y>highestNum){highestNum=data.y;hxH=bar._model.x;hyH=bar._model.y}});if(hxH>=820){hxH=820}else if(50>=hxH){hxH=50} if(hxL>=820){hxL=820}else if(70>=hxL){hxL=70} ctx.fillStyle='#ffa7a2';ctx.fillText(highestNum+"ms",hxH-40,hyH+15);ctx.fillStyle='#45d642';ctx.fillText(lowestnum+"ms",hxL,hyL+10);console.log("done service_id_{{.Id}}")})}},legend:{display:!1},tooltips:{"enabled":!1},scales:{yAxes:[{display:!1,ticks:{fontSize:20,display:!1,beginAtZero:!1},gridLines:{display:!1}}],xAxes:[{type:'time',distribution:'series',autoSkip:!1,gridLines:{display:!1},ticks:{stepSize:1,min:0,fontColor:"white",fontSize:20,display:!1,}}]},elements:{point:{radius:0}}}}) diff --git a/source/js/pikaday.js b/source/js/pikaday.js new file mode 100644 index 00000000..58da5a61 --- /dev/null +++ b/source/js/pikaday.js @@ -0,0 +1,1257 @@ +/*! + * Pikaday + * + * Copyright © 2014 David Bushell | BSD & MIT license | https://github.com/dbushell/Pikaday + */ + +(function (root, factory) +{ + 'use strict'; + + var moment; + if (typeof exports === 'object') { + // CommonJS module + // Load moment.js as an optional dependency + try { moment = require('moment'); } catch (e) {} + module.exports = factory(moment); + } else if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(function (req) + { + // Load moment.js as an optional dependency + var id = 'moment'; + try { moment = req(id); } catch (e) {} + return factory(moment); + }); + } else { + root.Pikaday = factory(root.moment); + } +}(this, function (moment) +{ + 'use strict'; + + /** + * feature detection and helper functions + */ + var hasMoment = typeof moment === 'function', + + hasEventListeners = !!window.addEventListener, + + document = window.document, + + sto = window.setTimeout, + + addEvent = function(el, e, callback, capture) + { + if (hasEventListeners) { + el.addEventListener(e, callback, !!capture); + } else { + el.attachEvent('on' + e, callback); + } + }, + + removeEvent = function(el, e, callback, capture) + { + if (hasEventListeners) { + el.removeEventListener(e, callback, !!capture); + } else { + el.detachEvent('on' + e, callback); + } + }, + + trim = function(str) + { + return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g,''); + }, + + hasClass = function(el, cn) + { + return (' ' + el.className + ' ').indexOf(' ' + cn + ' ') !== -1; + }, + + addClass = function(el, cn) + { + if (!hasClass(el, cn)) { + el.className = (el.className === '') ? cn : el.className + ' ' + cn; + } + }, + + removeClass = function(el, cn) + { + el.className = trim((' ' + el.className + ' ').replace(' ' + cn + ' ', ' ')); + }, + + isArray = function(obj) + { + return (/Array/).test(Object.prototype.toString.call(obj)); + }, + + isDate = function(obj) + { + return (/Date/).test(Object.prototype.toString.call(obj)) && !isNaN(obj.getTime()); + }, + + isWeekend = function(date) + { + var day = date.getDay(); + return day === 0 || day === 6; + }, + + isLeapYear = function(year) + { + // solution by Matti Virkkunen: http://stackoverflow.com/a/4881951 + return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0; + }, + + getDaysInMonth = function(year, month) + { + return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]; + }, + + setToStartOfDay = function(date) + { + if (isDate(date)) date.setHours(0,0,0,0); + }, + + compareDates = function(a,b) + { + // weak date comparison (use setToStartOfDay(date) to ensure correct result) + return a.getTime() === b.getTime(); + }, + + extend = function(to, from, overwrite) + { + var prop, hasProp; + for (prop in from) { + hasProp = to[prop] !== undefined; + if (hasProp && typeof from[prop] === 'object' && from[prop] !== null && from[prop].nodeName === undefined) { + if (isDate(from[prop])) { + if (overwrite) { + to[prop] = new Date(from[prop].getTime()); + } + } + else if (isArray(from[prop])) { + if (overwrite) { + to[prop] = from[prop].slice(0); + } + } else { + to[prop] = extend({}, from[prop], overwrite); + } + } else if (overwrite || !hasProp) { + to[prop] = from[prop]; + } + } + return to; + }, + + fireEvent = function(el, eventName, data) + { + var ev; + + if (document.createEvent) { + ev = document.createEvent('HTMLEvents'); + ev.initEvent(eventName, true, false); + ev = extend(ev, data); + el.dispatchEvent(ev); + } else if (document.createEventObject) { + ev = document.createEventObject(); + ev = extend(ev, data); + el.fireEvent('on' + eventName, ev); + } + }, + + adjustCalendar = function(calendar) { + if (calendar.month < 0) { + calendar.year -= Math.ceil(Math.abs(calendar.month)/12); + calendar.month += 12; + } + if (calendar.month > 11) { + calendar.year += Math.floor(Math.abs(calendar.month)/12); + calendar.month -= 12; + } + return calendar; + }, + + /** + * defaults and localisation + */ + defaults = { + + // bind the picker to a form field + field: null, + + // automatically show/hide the picker on `field` focus (default `true` if `field` is set) + bound: undefined, + + // data-attribute on the input field with an aria assistance tekst (only applied when `bound` is set) + ariaLabel: 'Use the arrow keys to pick a date', + + // position of the datepicker, relative to the field (default to bottom & left) + // ('bottom' & 'left' keywords are not used, 'top' & 'right' are modifier on the bottom/left position) + position: 'bottom left', + + // automatically fit in the viewport even if it means repositioning from the position option + reposition: true, + + // the default output format for `.toString()` and `field` value + format: 'YYYY-MM-DD', + + // the toString function which gets passed a current date object and format + // and returns a string + toString: null, + + // used to create date object from current input string + parse: null, + + // the initial date to view when first opened + defaultDate: null, + + // make the `defaultDate` the initial selected value + setDefaultDate: false, + + // first day of week (0: Sunday, 1: Monday etc) + firstDay: 0, + + // the default flag for moment's strict date parsing + formatStrict: false, + + // the minimum/earliest date that can be selected + minDate: null, + // the maximum/latest date that can be selected + maxDate: null, + + // number of years either side, or array of upper/lower range + yearRange: 10, + + // show week numbers at head of row + showWeekNumber: false, + + // Week picker mode + pickWholeWeek: false, + + // used internally (don't config outside) + minYear: 0, + maxYear: 9999, + minMonth: undefined, + maxMonth: undefined, + + startRange: null, + endRange: null, + + isRTL: false, + + // Additional text to append to the year in the calendar title + yearSuffix: '', + + // Render the month after year in the calendar title + showMonthAfterYear: false, + + // Render days of the calendar grid that fall in the next or previous month + showDaysInNextAndPreviousMonths: false, + + // Allows user to select days that fall in the next or previous month + enableSelectionDaysInNextAndPreviousMonths: false, + + // how many months are visible + numberOfMonths: 1, + + // when numberOfMonths is used, this will help you to choose where the main calendar will be (default `left`, can be set to `right`) + // only used for the first display or when a selected date is not visible + mainCalendar: 'left', + + // Specify a DOM element to render the calendar in + container: undefined, + + // Blur field when date is selected + blurFieldOnSelect : true, + + // internationalization + i18n: { + previousMonth : 'Previous Month', + nextMonth : 'Next Month', + months : ['January','February','March','April','May','June','July','August','September','October','November','December'], + weekdays : ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'], + weekdaysShort : ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'] + }, + + // Theme Classname + theme: null, + + // events array + events: [], + + // callback function + onSelect: null, + onOpen: null, + onClose: null, + onDraw: null, + + // Enable keyboard input + keyboardInput: true + }, + + + /** + * templating functions to abstract HTML rendering + */ + renderDayName = function(opts, day, abbr) + { + day += opts.firstDay; + while (day >= 7) { + day -= 7; + } + return abbr ? opts.i18n.weekdaysShort[day] : opts.i18n.weekdays[day]; + }, + + renderDay = function(opts) + { + var arr = []; + var ariaSelected = 'false'; + if (opts.isEmpty) { + if (opts.showDaysInNextAndPreviousMonths) { + arr.push('is-outside-current-month'); + + if(!opts.enableSelectionDaysInNextAndPreviousMonths) { + arr.push('is-selection-disabled'); + } + + } else { + return ''; + } + } + if (opts.isDisabled) { + arr.push('is-disabled'); + } + if (opts.isToday) { + arr.push('is-today'); + } + if (opts.isSelected) { + arr.push('is-selected'); + ariaSelected = 'true'; + } + if (opts.hasEvent) { + arr.push('has-event'); + } + if (opts.isInRange) { + arr.push('is-inrange'); + } + if (opts.isStartRange) { + arr.push('is-startrange'); + } + if (opts.isEndRange) { + arr.push('is-endrange'); + } + return '' + + '' + + ''; + }, + + renderWeek = function (d, m, y) { + // Lifted from http://javascript.about.com/library/blweekyear.htm, lightly modified. + var onejan = new Date(y, 0, 1), + weekNum = Math.ceil((((new Date(y, m, d) - onejan) / 86400000) + onejan.getDay()+1)/7); + return '' + weekNum + ''; + }, + + renderRow = function(days, isRTL, pickWholeWeek, isRowSelected) + { + return '' + (isRTL ? days.reverse() : days).join('') + ''; + }, + + renderBody = function(rows) + { + return '' + rows.join('') + ''; + }, + + renderHead = function(opts) + { + var i, arr = []; + if (opts.showWeekNumber) { + arr.push(''); + } + for (i = 0; i < 7; i++) { + arr.push('' + renderDayName(opts, i, true) + ''); + } + return '' + (opts.isRTL ? arr.reverse() : arr).join('') + ''; + }, + + renderTitle = function(instance, c, year, month, refYear, randId) + { + var i, j, arr, + opts = instance._o, + isMinYear = year === opts.minYear, + isMaxYear = year === opts.maxYear, + html = '
', + monthHtml, + yearHtml, + prev = true, + next = true; + + for (arr = [], i = 0; i < 12; i++) { + arr.push(''); + } + + monthHtml = '
' + opts.i18n.months[month] + '
'; + + if (isArray(opts.yearRange)) { + i = opts.yearRange[0]; + j = opts.yearRange[1] + 1; + } else { + i = year - opts.yearRange; + j = 1 + year + opts.yearRange; + } + + for (arr = []; i < j && i <= opts.maxYear; i++) { + if (i >= opts.minYear) { + arr.push(''); + } + } + yearHtml = '
' + year + opts.yearSuffix + '
'; + + if (opts.showMonthAfterYear) { + html += yearHtml + monthHtml; + } else { + html += monthHtml + yearHtml; + } + + if (isMinYear && (month === 0 || opts.minMonth >= month)) { + prev = false; + } + + if (isMaxYear && (month === 11 || opts.maxMonth <= month)) { + next = false; + } + + if (c === 0) { + html += ''; + } + if (c === (instance._o.numberOfMonths - 1) ) { + html += ''; + } + + return html += '
'; + }, + + renderTable = function(opts, data, randId) + { + return '' + renderHead(opts) + renderBody(data) + '
'; + }, + + + /** + * Pikaday constructor + */ + Pikaday = function(options) + { + var self = this, + opts = self.config(options); + + self._onMouseDown = function(e) + { + if (!self._v) { + return; + } + e = e || window.event; + var target = e.target || e.srcElement; + if (!target) { + return; + } + + if (!hasClass(target, 'is-disabled')) { + if (hasClass(target, 'pika-button') && !hasClass(target, 'is-empty') && !hasClass(target.parentNode, 'is-disabled')) { + self.setDate(new Date(target.getAttribute('data-pika-year'), target.getAttribute('data-pika-month'), target.getAttribute('data-pika-day'))); + if (opts.bound) { + sto(function() { + self.hide(); + if (opts.blurFieldOnSelect && opts.field) { + opts.field.blur(); + } + }, 100); + } + } + else if (hasClass(target, 'pika-prev')) { + self.prevMonth(); + } + else if (hasClass(target, 'pika-next')) { + self.nextMonth(); + } + } + if (!hasClass(target, 'pika-select')) { + // if this is touch event prevent mouse events emulation + if (e.preventDefault) { + e.preventDefault(); + } else { + e.returnValue = false; + return false; + } + } else { + self._c = true; + } + }; + + self._onChange = function(e) + { + e = e || window.event; + var target = e.target || e.srcElement; + if (!target) { + return; + } + if (hasClass(target, 'pika-select-month')) { + self.gotoMonth(target.value); + } + else if (hasClass(target, 'pika-select-year')) { + self.gotoYear(target.value); + } + }; + + self._onKeyChange = function(e) + { + e = e || window.event; + + if (self.isVisible()) { + + switch(e.keyCode){ + case 13: + case 27: + if (opts.field) { + opts.field.blur(); + } + break; + case 37: + e.preventDefault(); + self.adjustDate('subtract', 1); + break; + case 38: + self.adjustDate('subtract', 7); + break; + case 39: + self.adjustDate('add', 1); + break; + case 40: + self.adjustDate('add', 7); + break; + } + } + }; + + self._onInputChange = function(e) + { + var date; + + if (e.firedBy === self) { + return; + } + if (opts.parse) { + date = opts.parse(opts.field.value, opts.format); + } else if (hasMoment) { + date = moment(opts.field.value, opts.format, opts.formatStrict); + date = (date && date.isValid()) ? date.toDate() : null; + } + else { + date = new Date(Date.parse(opts.field.value)); + } + if (isDate(date)) { + self.setDate(date); + } + if (!self._v) { + self.show(); + } + }; + + self._onInputFocus = function() + { + self.show(); + }; + + self._onInputClick = function() + { + self.show(); + }; + + self._onInputBlur = function() + { + // IE allows pika div to gain focus; catch blur the input field + var pEl = document.activeElement; + do { + if (hasClass(pEl, 'pika-single')) { + return; + } + } + while ((pEl = pEl.parentNode)); + + if (!self._c) { + self._b = sto(function() { + self.hide(); + }, 50); + } + self._c = false; + }; + + self._onClick = function(e) + { + e = e || window.event; + var target = e.target || e.srcElement, + pEl = target; + if (!target) { + return; + } + if (!hasEventListeners && hasClass(target, 'pika-select')) { + if (!target.onchange) { + target.setAttribute('onchange', 'return;'); + addEvent(target, 'change', self._onChange); + } + } + do { + if (hasClass(pEl, 'pika-single') || pEl === opts.trigger) { + return; + } + } + while ((pEl = pEl.parentNode)); + if (self._v && target !== opts.trigger && pEl !== opts.trigger) { + self.hide(); + } + }; + + self.el = document.createElement('div'); + self.el.className = 'pika-single' + (opts.isRTL ? ' is-rtl' : '') + (opts.theme ? ' ' + opts.theme : ''); + + addEvent(self.el, 'mousedown', self._onMouseDown, true); + addEvent(self.el, 'touchend', self._onMouseDown, true); + addEvent(self.el, 'change', self._onChange); + + if (opts.keyboardInput) { + addEvent(document, 'keydown', self._onKeyChange); + } + + if (opts.field) { + if (opts.container) { + opts.container.appendChild(self.el); + } else if (opts.bound) { + document.body.appendChild(self.el); + } else { + opts.field.parentNode.insertBefore(self.el, opts.field.nextSibling); + } + addEvent(opts.field, 'change', self._onInputChange); + + if (!opts.defaultDate) { + if (hasMoment && opts.field.value) { + opts.defaultDate = moment(opts.field.value, opts.format).toDate(); + } else { + opts.defaultDate = new Date(Date.parse(opts.field.value)); + } + opts.setDefaultDate = true; + } + } + + var defDate = opts.defaultDate; + + if (isDate(defDate)) { + if (opts.setDefaultDate) { + self.setDate(defDate, true); + } else { + self.gotoDate(defDate); + } + } else { + self.gotoDate(new Date()); + } + + if (opts.bound) { + this.hide(); + self.el.className += ' is-bound'; + addEvent(opts.trigger, 'click', self._onInputClick); + addEvent(opts.trigger, 'focus', self._onInputFocus); + addEvent(opts.trigger, 'blur', self._onInputBlur); + } else { + this.show(); + } + }; + + + /** + * public Pikaday API + */ + Pikaday.prototype = { + + + /** + * configure functionality + */ + config: function(options) + { + if (!this._o) { + this._o = extend({}, defaults, true); + } + + var opts = extend(this._o, options, true); + + opts.isRTL = !!opts.isRTL; + + opts.field = (opts.field && opts.field.nodeName) ? opts.field : null; + + opts.theme = (typeof opts.theme) === 'string' && opts.theme ? opts.theme : null; + + opts.bound = !!(opts.bound !== undefined ? opts.field && opts.bound : opts.field); + + opts.trigger = (opts.trigger && opts.trigger.nodeName) ? opts.trigger : opts.field; + + opts.disableWeekends = !!opts.disableWeekends; + + opts.disableDayFn = (typeof opts.disableDayFn) === 'function' ? opts.disableDayFn : null; + + var nom = parseInt(opts.numberOfMonths, 10) || 1; + opts.numberOfMonths = nom > 4 ? 4 : nom; + + if (!isDate(opts.minDate)) { + opts.minDate = false; + } + if (!isDate(opts.maxDate)) { + opts.maxDate = false; + } + if ((opts.minDate && opts.maxDate) && opts.maxDate < opts.minDate) { + opts.maxDate = opts.minDate = false; + } + if (opts.minDate) { + this.setMinDate(opts.minDate); + } + if (opts.maxDate) { + this.setMaxDate(opts.maxDate); + } + + if (isArray(opts.yearRange)) { + var fallback = new Date().getFullYear() - 10; + opts.yearRange[0] = parseInt(opts.yearRange[0], 10) || fallback; + opts.yearRange[1] = parseInt(opts.yearRange[1], 10) || fallback; + } else { + opts.yearRange = Math.abs(parseInt(opts.yearRange, 10)) || defaults.yearRange; + if (opts.yearRange > 100) { + opts.yearRange = 100; + } + } + + return opts; + }, + + /** + * return a formatted string of the current selection (using Moment.js if available) + */ + toString: function(format) + { + format = format || this._o.format; + if (!isDate(this._d)) { + return ''; + } + if (this._o.toString) { + return this._o.toString(this._d, format); + } + if (hasMoment) { + return moment(this._d).format(format); + } + return this._d.toDateString(); + }, + + /** + * return a Moment.js object of the current selection (if available) + */ + getMoment: function() + { + return hasMoment ? moment(this._d) : null; + }, + + /** + * set the current selection from a Moment.js object (if available) + */ + setMoment: function(date, preventOnSelect) + { + if (hasMoment && moment.isMoment(date)) { + this.setDate(date.toDate(), preventOnSelect); + } + }, + + /** + * return a Date object of the current selection + */ + getDate: function() + { + return isDate(this._d) ? new Date(this._d.getTime()) : null; + }, + + /** + * set the current selection + */ + setDate: function(date, preventOnSelect) + { + if (!date) { + this._d = null; + + if (this._o.field) { + this._o.field.value = ''; + fireEvent(this._o.field, 'change', { firedBy: this }); + } + + return this.draw(); + } + if (typeof date === 'string') { + date = new Date(Date.parse(date)); + } + if (!isDate(date)) { + return; + } + + var min = this._o.minDate, + max = this._o.maxDate; + + if (isDate(min) && date < min) { + date = min; + } else if (isDate(max) && date > max) { + date = max; + } + + this._d = new Date(date.getTime()); + setToStartOfDay(this._d); + this.gotoDate(this._d); + + if (this._o.field) { + this._o.field.value = this.toString(); + fireEvent(this._o.field, 'change', { firedBy: this }); + } + if (!preventOnSelect && typeof this._o.onSelect === 'function') { + this._o.onSelect.call(this, this.getDate()); + } + }, + + /** + * change view to a specific date + */ + gotoDate: function(date) + { + var newCalendar = true; + + if (!isDate(date)) { + return; + } + + if (this.calendars) { + var firstVisibleDate = new Date(this.calendars[0].year, this.calendars[0].month, 1), + lastVisibleDate = new Date(this.calendars[this.calendars.length-1].year, this.calendars[this.calendars.length-1].month, 1), + visibleDate = date.getTime(); + // get the end of the month + lastVisibleDate.setMonth(lastVisibleDate.getMonth()+1); + lastVisibleDate.setDate(lastVisibleDate.getDate()-1); + newCalendar = (visibleDate < firstVisibleDate.getTime() || lastVisibleDate.getTime() < visibleDate); + } + + if (newCalendar) { + this.calendars = [{ + month: date.getMonth(), + year: date.getFullYear() + }]; + if (this._o.mainCalendar === 'right') { + this.calendars[0].month += 1 - this._o.numberOfMonths; + } + } + + this.adjustCalendars(); + }, + + adjustDate: function(sign, days) { + + var day = this.getDate() || new Date(); + var difference = parseInt(days)*24*60*60*1000; + + var newDay; + + if (sign === 'add') { + newDay = new Date(day.valueOf() + difference); + } else if (sign === 'subtract') { + newDay = new Date(day.valueOf() - difference); + } + + this.setDate(newDay); + }, + + adjustCalendars: function() { + this.calendars[0] = adjustCalendar(this.calendars[0]); + for (var c = 1; c < this._o.numberOfMonths; c++) { + this.calendars[c] = adjustCalendar({ + month: this.calendars[0].month + c, + year: this.calendars[0].year + }); + } + this.draw(); + }, + + gotoToday: function() + { + this.gotoDate(new Date()); + }, + + /** + * change view to a specific month (zero-index, e.g. 0: January) + */ + gotoMonth: function(month) + { + if (!isNaN(month)) { + this.calendars[0].month = parseInt(month, 10); + this.adjustCalendars(); + } + }, + + nextMonth: function() + { + this.calendars[0].month++; + this.adjustCalendars(); + }, + + prevMonth: function() + { + this.calendars[0].month--; + this.adjustCalendars(); + }, + + /** + * change view to a specific full year (e.g. "2012") + */ + gotoYear: function(year) + { + if (!isNaN(year)) { + this.calendars[0].year = parseInt(year, 10); + this.adjustCalendars(); + } + }, + + /** + * change the minDate + */ + setMinDate: function(value) + { + if(value instanceof Date) { + setToStartOfDay(value); + this._o.minDate = value; + this._o.minYear = value.getFullYear(); + this._o.minMonth = value.getMonth(); + } else { + this._o.minDate = defaults.minDate; + this._o.minYear = defaults.minYear; + this._o.minMonth = defaults.minMonth; + this._o.startRange = defaults.startRange; + } + + this.draw(); + }, + + /** + * change the maxDate + */ + setMaxDate: function(value) + { + if(value instanceof Date) { + setToStartOfDay(value); + this._o.maxDate = value; + this._o.maxYear = value.getFullYear(); + this._o.maxMonth = value.getMonth(); + } else { + this._o.maxDate = defaults.maxDate; + this._o.maxYear = defaults.maxYear; + this._o.maxMonth = defaults.maxMonth; + this._o.endRange = defaults.endRange; + } + + this.draw(); + }, + + setStartRange: function(value) + { + this._o.startRange = value; + }, + + setEndRange: function(value) + { + this._o.endRange = value; + }, + + /** + * refresh the HTML + */ + draw: function(force) + { + if (!this._v && !force) { + return; + } + var opts = this._o, + minYear = opts.minYear, + maxYear = opts.maxYear, + minMonth = opts.minMonth, + maxMonth = opts.maxMonth, + html = '', + randId; + + if (this._y <= minYear) { + this._y = minYear; + if (!isNaN(minMonth) && this._m < minMonth) { + this._m = minMonth; + } + } + if (this._y >= maxYear) { + this._y = maxYear; + if (!isNaN(maxMonth) && this._m > maxMonth) { + this._m = maxMonth; + } + } + + randId = 'pika-title-' + Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 2); + + for (var c = 0; c < opts.numberOfMonths; c++) { + html += '
' + renderTitle(this, c, this.calendars[c].year, this.calendars[c].month, this.calendars[0].year, randId) + this.render(this.calendars[c].year, this.calendars[c].month, randId) + '
'; + } + + this.el.innerHTML = html; + + if (opts.bound) { + if(opts.field.type !== 'hidden') { + sto(function() { + opts.trigger.focus(); + }, 1); + } + } + + if (typeof this._o.onDraw === 'function') { + this._o.onDraw(this); + } + + if (opts.bound) { + // let the screen reader user know to use arrow keys + opts.field.setAttribute('aria-label', opts.ariaLabel); + } + }, + + adjustPosition: function() + { + var field, pEl, width, height, viewportWidth, viewportHeight, scrollTop, left, top, clientRect, leftAligned, bottomAligned; + + if (this._o.container) return; + + this.el.style.position = 'absolute'; + + field = this._o.trigger; + pEl = field; + width = this.el.offsetWidth; + height = this.el.offsetHeight; + viewportWidth = window.innerWidth || document.documentElement.clientWidth; + viewportHeight = window.innerHeight || document.documentElement.clientHeight; + scrollTop = window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop; + leftAligned = true; + bottomAligned = true; + + if (typeof field.getBoundingClientRect === 'function') { + clientRect = field.getBoundingClientRect(); + left = clientRect.left + window.pageXOffset; + top = clientRect.bottom + window.pageYOffset; + } else { + left = pEl.offsetLeft; + top = pEl.offsetTop + pEl.offsetHeight; + while((pEl = pEl.offsetParent)) { + left += pEl.offsetLeft; + top += pEl.offsetTop; + } + } + + // default position is bottom & left + if ((this._o.reposition && left + width > viewportWidth) || + ( + this._o.position.indexOf('right') > -1 && + left - width + field.offsetWidth > 0 + ) + ) { + left = left - width + field.offsetWidth; + leftAligned = false; + } + if ((this._o.reposition && top + height > viewportHeight + scrollTop) || + ( + this._o.position.indexOf('top') > -1 && + top - height - field.offsetHeight > 0 + ) + ) { + top = top - height - field.offsetHeight; + bottomAligned = false; + } + + this.el.style.left = left + 'px'; + this.el.style.top = top + 'px'; + + addClass(this.el, leftAligned ? 'left-aligned' : 'right-aligned'); + addClass(this.el, bottomAligned ? 'bottom-aligned' : 'top-aligned'); + removeClass(this.el, !leftAligned ? 'left-aligned' : 'right-aligned'); + removeClass(this.el, !bottomAligned ? 'bottom-aligned' : 'top-aligned'); + }, + + /** + * render HTML for a particular month + */ + render: function(year, month, randId) + { + var opts = this._o, + now = new Date(), + days = getDaysInMonth(year, month), + before = new Date(year, month, 1).getDay(), + data = [], + row = []; + setToStartOfDay(now); + if (opts.firstDay > 0) { + before -= opts.firstDay; + if (before < 0) { + before += 7; + } + } + var previousMonth = month === 0 ? 11 : month - 1, + nextMonth = month === 11 ? 0 : month + 1, + yearOfPreviousMonth = month === 0 ? year - 1 : year, + yearOfNextMonth = month === 11 ? year + 1 : year, + daysInPreviousMonth = getDaysInMonth(yearOfPreviousMonth, previousMonth); + var cells = days + before, + after = cells; + while(after > 7) { + after -= 7; + } + cells += 7 - after; + var isWeekSelected = false; + for (var i = 0, r = 0; i < cells; i++) + { + var day = new Date(year, month, 1 + (i - before)), + isSelected = isDate(this._d) ? compareDates(day, this._d) : false, + isToday = compareDates(day, now), + hasEvent = opts.events.indexOf(day.toDateString()) !== -1 ? true : false, + isEmpty = i < before || i >= (days + before), + dayNumber = 1 + (i - before), + monthNumber = month, + yearNumber = year, + isStartRange = opts.startRange && compareDates(opts.startRange, day), + isEndRange = opts.endRange && compareDates(opts.endRange, day), + isInRange = opts.startRange && opts.endRange && opts.startRange < day && day < opts.endRange, + isDisabled = (opts.minDate && day < opts.minDate) || + (opts.maxDate && day > opts.maxDate) || + (opts.disableWeekends && isWeekend(day)) || + (opts.disableDayFn && opts.disableDayFn(day)); + + if (isEmpty) { + if (i < before) { + dayNumber = daysInPreviousMonth + dayNumber; + monthNumber = previousMonth; + yearNumber = yearOfPreviousMonth; + } else { + dayNumber = dayNumber - days; + monthNumber = nextMonth; + yearNumber = yearOfNextMonth; + } + } + + var dayConfig = { + day: dayNumber, + month: monthNumber, + year: yearNumber, + hasEvent: hasEvent, + isSelected: isSelected, + isToday: isToday, + isDisabled: isDisabled, + isEmpty: isEmpty, + isStartRange: isStartRange, + isEndRange: isEndRange, + isInRange: isInRange, + showDaysInNextAndPreviousMonths: opts.showDaysInNextAndPreviousMonths, + enableSelectionDaysInNextAndPreviousMonths: opts.enableSelectionDaysInNextAndPreviousMonths + }; + + if (opts.pickWholeWeek && isSelected) { + isWeekSelected = true; + } + + row.push(renderDay(dayConfig)); + + if (++r === 7) { + if (opts.showWeekNumber) { + row.unshift(renderWeek(i - before, month, year)); + } + data.push(renderRow(row, opts.isRTL, opts.pickWholeWeek, isWeekSelected)); + row = []; + r = 0; + isWeekSelected = false; + } + } + return renderTable(opts, data, randId); + }, + + isVisible: function() + { + return this._v; + }, + + show: function() + { + if (!this.isVisible()) { + this._v = true; + this.draw(); + removeClass(this.el, 'is-hidden'); + if (this._o.bound) { + addEvent(document, 'click', this._onClick); + this.adjustPosition(); + } + if (typeof this._o.onOpen === 'function') { + this._o.onOpen.call(this); + } + } + }, + + hide: function() + { + var v = this._v; + if (v !== false) { + if (this._o.bound) { + removeEvent(document, 'click', this._onClick); + } + this.el.style.position = 'static'; // reset + this.el.style.left = 'auto'; + this.el.style.top = 'auto'; + addClass(this.el, 'is-hidden'); + this._v = false; + if (v !== undefined && typeof this._o.onClose === 'function') { + this._o.onClose.call(this); + } + } + }, + + /** + * GAME OVER + */ + destroy: function() + { + var opts = this._o; + + this.hide(); + removeEvent(this.el, 'mousedown', this._onMouseDown, true); + removeEvent(this.el, 'touchend', this._onMouseDown, true); + removeEvent(this.el, 'change', this._onChange); + if (opts.keyboardInput) { + removeEvent(document, 'keydown', this._onKeyChange); + } + if (opts.field) { + removeEvent(opts.field, 'change', this._onInputChange); + if (opts.bound) { + removeEvent(opts.trigger, 'click', this._onInputClick); + removeEvent(opts.trigger, 'focus', this._onInputFocus); + removeEvent(opts.trigger, 'blur', this._onInputBlur); + } + } + if (this.el.parentNode) { + this.el.parentNode.removeChild(this.el); + } + } + + }; + + return Pikaday; +})); diff --git a/source/scss/base.scss b/source/scss/base.scss index 202c4122..c098579e 100644 --- a/source/scss/base.scss +++ b/source/scss/base.scss @@ -1,3 +1,20 @@ +/*! + * Statup + * Copyright (C) 2018. Hunter Long and the project contributors + * Written by Hunter Long and the project contributors + * + * https://github.com/hunterlong/statup + * + * The licenses for most software and other practical works are designed + * to take away your freedom to share and change the works. By contrast, + * the GNU General Public License is intended to guarantee your freedom to + * share and change all versions of a program--to make sure it remains free + * software for all its users. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + @import 'variables'; @@ -441,4 +458,23 @@ HTML,BODY { display: block; } -@import 'mobile'; +#start_container { + position: absolute; + z-index: 99999; + margin-top: 20px; +} + +#end_container { + position: absolute; + z-index: 99999; + margin-top: 20px; + right: 0; +} + +.pointer { + cursor: pointer; +} + +@import './pikaday'; + +@import './mobile'; diff --git a/source/scss/mobile.scss b/source/scss/mobile.scss index d9b1f8c7..83ff146a 100644 --- a/source/scss/mobile.scss +++ b/source/scss/mobile.scss @@ -1,3 +1,19 @@ +/*! + * Statup + * Copyright (C) 2018. Hunter Long and the project contributors + * Written by Hunter Long and the project contributors + * + * https://github.com/hunterlong/statup + * + * The licenses for most software and other practical works are designed + * to take away your freedom to share and change the works. By contrast, + * the GNU General Public License is intended to guarantee your freedom to + * share and change all versions of a program--to make sure it remains free + * software for all its users. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ @media (max-width: 767px) { diff --git a/source/scss/pikaday.scss b/source/scss/pikaday.scss new file mode 100644 index 00000000..f129aadd --- /dev/null +++ b/source/scss/pikaday.scss @@ -0,0 +1,255 @@ +/*! + * Pikaday + * Copyright © 2014 David Bushell | BSD & MIT license | http://dbushell.com/ + */ + +// Variables +// Declare any of these variables before importing this SCSS file to easily override defaults +// Variables are namespaced with the pd (pikaday) prefix + +// Colours +$pd-text-color: #333 !default; +$pd-title-color: #333 !default; +$pd-title-bg: #fff !default; +$pd-picker-bg: #fff !default; +$pd-picker-border: #ccc !default; +$pd-picker-border-bottom: #bbb !default; +$pd-picker-shadow: rgba(0,0,0,.5) !default; +$pd-th-color: #999 !default; +$pd-day-color: #666 !default; +$pd-day-bg: #f5f5f5 !default; +$pd-day-hover-color: #fff !default; +$pd-day-hover-bg: #ff8000 !default; +$pd-day-today-color: #33aaff !default; +$pd-day-selected-color: #fff !default; +$pd-day-selected-bg: #33aaff !default; +$pd-day-selected-shadow: #178fe5 !default; +$pd-day-disabled-color: #999 !default; +$pd-week-color: #999 !default; + +// Font +$pd-font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !default; + + +.pika-single { + z-index: 9999; + display: block; + position: relative; + color: $pd-text-color; + background: $pd-picker-bg; + border: 1px solid $pd-picker-border; + border-bottom-color: $pd-picker-border-bottom; + font-family: $pd-font-family; + + &.is-hidden { + display: none; + } + + &.is-bound { + position: absolute; + box-shadow: 0 5px 15px -5px $pd-picker-shadow; + } +} + +// clear child float (pika-lendar), using the famous micro clearfix hack +// http://nicolasgallagher.com/micro-clearfix-hack/ +.pika-single { + *zoom: 1; + + &:before, + &:after { + content: " "; + display: table; + } + + &:after { clear: both } +} + +.pika-lendar { + float: left; + width: 240px; + margin: 8px; +} + +.pika-title { + position: relative; + text-align: center; + + select { + cursor: pointer; + position: absolute; + z-index: 9998; + margin: 0; + left: 0; + top: 5px; + filter: alpha(opacity=0); + opacity: 0; + } +} + +.pika-label { + display: inline-block; + *display: inline; + position: relative; + z-index: 9999; + overflow: hidden; + margin: 0; + padding: 5px 3px; + font-size: 14px; + line-height: 20px; + font-weight: bold; + color: $pd-title-color; + background-color: $pd-title-bg; +} + +.pika-prev, +.pika-next { + display: block; + cursor: pointer; + position: relative; + outline: none; + border: 0; + padding: 0; + width: 20px; + height: 30px; + text-indent: 20px; // hide text using text-indent trick, using width value (it's enough) + white-space: nowrap; + overflow: hidden; + background-color: transparent; + background-position: center center; + background-repeat: no-repeat; + background-size: 75% 75%; + opacity: .5; + *position: absolute; + *top: 0; + + &:hover { + opacity: 1; + } + + &.is-disabled { + cursor: default; + opacity: .2; + } +} + +.pika-prev, +.is-rtl .pika-next { + float: left; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAUklEQVR42u3VMQoAIBADQf8Pgj+OD9hG2CtONJB2ymQkKe0HbwAP0xucDiQWARITIDEBEnMgMQ8S8+AqBIl6kKgHiXqQqAeJepBo/z38J/U0uAHlaBkBl9I4GwAAAABJRU5ErkJggg=='); + *left: 0; +} + +.pika-next, +.is-rtl .pika-prev { + float: right; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAU0lEQVR42u3VOwoAMAgE0dwfAnNjU26bYkBCFGwfiL9VVWoO+BJ4Gf3gtsEKKoFBNTCoCAYVwaAiGNQGMUHMkjGbgjk2mIONuXo0nC8XnCf1JXgArVIZAQh5TKYAAAAASUVORK5CYII='); + *right: 0; +} + +.pika-select { + display: inline-block; + *display: inline; +} + +.pika-table { + width: 100%; + border-collapse: collapse; + border-spacing: 0; + border: 0; + + th, + td { + width: 14.285714285714286%; + padding: 0; + } + + th { + color: $pd-th-color; + font-size: 12px; + line-height: 25px; + font-weight: bold; + text-align: center; + } + + abbr { + border-bottom: none; + cursor: help; + } +} + +.pika-button { + cursor: pointer; + display: block; + -moz-box-sizing: border-box; + box-sizing: border-box; + outline: none; + border: 0; + margin: 0; + width: 100%; + padding: 5px; + color: $pd-day-color; + font-size: 12px; + line-height: 15px; + text-align: right; + background: $pd-day-bg; + + .is-today & { + color: $pd-day-today-color; + font-weight: bold; + } + + .is-selected & { + color: $pd-day-selected-color; + font-weight: bold; + background: $pd-day-selected-bg; + box-shadow: inset 0 1px 3px $pd-day-selected-shadow; + border-radius: 3px; + } + + .is-disabled &, + .is-outside-current-month & { + color: $pd-day-disabled-color; + opacity: .3; + } + + .is-disabled & { + pointer-events: none; + cursor: default; + } + + &:hover { + color: $pd-day-hover-color; + background: $pd-day-hover-bg; + box-shadow: none; + border-radius: 3px; + } + + .is-selection-disabled { + pointer-events: none; + cursor: default; + } +} + +.pika-week { + font-size: 11px; + color: $pd-week-color; +} + +.is-inrange .pika-button { + background: #D5E9F7; +} + +.is-startrange .pika-button { + color: #fff; + background: #6CB31D; + box-shadow: none; + border-radius: 3px; +} + +.is-endrange .pika-button { + color: #fff; + background: #33aaff; + box-shadow: none; + border-radius: 3px; +} \ No newline at end of file diff --git a/source/scss/variables.scss b/source/scss/variables.scss index e0c15b0b..a5687691 100644 --- a/source/scss/variables.scss +++ b/source/scss/variables.scss @@ -1,3 +1,20 @@ +/*! + * Statup + * Copyright (C) 2018. Hunter Long and the project contributors + * Written by Hunter Long and the project contributors + * + * https://github.com/hunterlong/statup + * + * The licenses for most software and other practical works are designed + * to take away your freedom to share and change the works. By contrast, + * the GNU General Public License is intended to guarantee your freedom to + * share and change all versions of a program--to make sure it remains free + * software for all its users. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + /* Index Page */ $background-color: #fcfcfc; $max-width: 860px; diff --git a/source/source.go b/source/source.go index 171631a6..aa8852f7 100644 --- a/source/source.go +++ b/source/source.go @@ -158,6 +158,7 @@ func CreateAllAssets(folder string) error { CopyToPublic(ScssBox, folder+"/assets/scss", "base.scss") CopyToPublic(ScssBox, folder+"/assets/scss", "variables.scss") CopyToPublic(ScssBox, folder+"/assets/scss", "mobile.scss") + CopyToPublic(ScssBox, folder+"/assets/scss", "pikaday.scss") CopyToPublic(CssBox, folder+"/assets/css", "bootstrap.min.css") CopyToPublic(CssBox, folder+"/assets/css", "base.css") //CopyToPublic(JsBox, folder+"/assets/js", "bootstrap.min.js") diff --git a/source/tmpl/service.html b/source/tmpl/service.html index 11d73a68..97d932eb 100644 --- a/source/tmpl/service.html +++ b/source/tmpl/service.html @@ -1,3 +1,4 @@ +{{$s := .Service}} @@ -13,7 +14,7 @@ {{end}} - Statup | {{.Name}} Service + Statup | {{$s.Name}} Service @@ -25,14 +26,14 @@
- {{if .Online }} + {{if $s.Online }} ONLINE {{ else }} OFFLINE {{end}} -

{{ .Name }} - {{if .Online }} +

{{ $s.Name }} + {{if $s.Online }} ONLINE {{ else }} OFFLINE @@ -41,28 +42,42 @@
- {{.Online24}}% + {{$s.Online24}}% Online last 24 Hours
- {{.AvgTime}}ms + {{$s.AvgTime}}ms Average Response
- {{.TotalUptime}}% + {{$s.TotalUptime}}% Total Uptime
-
- +
+
- {{ if .LimitedFailures }} +
+ {{FromUnix .Start}} + {{FromUnix .End}} + + + +
+
+
+ + {{if not $s.Online}} +
{{$s.DowntimeText}}
+ {{end}} + + {{ if $s.LimitedFailures }}
- {{ range .LimitedFailures }} + {{ range $s.LimitedFailures }}
{{.ParseError}}
@@ -83,82 +98,82 @@

Edit Service

-
+
- +
- + +
- +
-
+
- + + + + +
-
+ -
+
- +
-
+
- +
-
+
- +
- + 10,000+ will be checked in Microseconds (1 millisecond = 1000 microseconds).
- +
- +
@@ -166,34 +181,33 @@
-
+

Last Response

- +
- +
-
-
+

Service Checkins

-{{ range .Checkins }} +{{ range $s.Checkins }}
Check #{{.Id}} Checked in {{.Ago}}
{{ end }} -
+
@@ -222,15 +236,58 @@ + {{ else }} + {{end}} - + + + diff --git a/types/checkin.go b/types/checkin.go index 749c3749..bcdaf027 100644 --- a/types/checkin.go +++ b/types/checkin.go @@ -1,3 +1,18 @@ +// Statup +// Copyright (C) 2018. Hunter Long and the project contributors +// Written by Hunter Long and the project contributors +// +// https://github.com/hunterlong/statup +// +// The licenses for most software and other practical works are designed +// to take away your freedom to share and change the works. By contrast, +// the GNU General Public License is intended to guarantee your freedom to +// share and change all versions of a program--to make sure it remains free +// software for all its users. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + package types import ( diff --git a/types/core.go b/types/core.go index 43daf799..3b2c160a 100644 --- a/types/core.go +++ b/types/core.go @@ -1,3 +1,18 @@ +// Statup +// Copyright (C) 2018. Hunter Long and the project contributors +// Written by Hunter Long and the project contributors +// +// https://github.com/hunterlong/statup +// +// The licenses for most software and other practical works are designed +// to take away your freedom to share and change the works. By contrast, +// the GNU General Public License is intended to guarantee your freedom to +// share and change all versions of a program--to make sure it remains free +// software for all its users. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + package types import ( diff --git a/types/failure.go b/types/failure.go index 57713984..bddc44d6 100644 --- a/types/failure.go +++ b/types/failure.go @@ -1,3 +1,18 @@ +// Statup +// Copyright (C) 2018. Hunter Long and the project contributors +// Written by Hunter Long and the project contributors +// +// https://github.com/hunterlong/statup +// +// The licenses for most software and other practical works are designed +// to take away your freedom to share and change the works. By contrast, +// the GNU General Public License is intended to guarantee your freedom to +// share and change all versions of a program--to make sure it remains free +// software for all its users. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + package types import ( diff --git a/types/service.go b/types/service.go index 8b5f761c..15f604d7 100644 --- a/types/service.go +++ b/types/service.go @@ -47,6 +47,7 @@ type Service struct { DnsLookup float64 `gorm:"-" json:"dns_lookup_time"` Failures []interface{} `gorm:"-" json:"failures,omitempty"` Checkins []*Checkin `gorm:"-" json:"checkins,omitempty"` + Range [2]time.Time `gorm:"-" json:"-"` } type ServiceInterface interface { diff --git a/types/user.go b/types/user.go index c18738ed..440cc398 100644 --- a/types/user.go +++ b/types/user.go @@ -1,3 +1,18 @@ +// Statup +// Copyright (C) 2018. Hunter Long and the project contributors +// Written by Hunter Long and the project contributors +// +// https://github.com/hunterlong/statup +// +// The licenses for most software and other practical works are designed +// to take away your freedom to share and change the works. By contrast, +// the GNU General Public License is intended to guarantee your freedom to +// share and change all versions of a program--to make sure it remains free +// software for all its users. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + package types import (