service date range

pull/78/head
Hunter Long 2018-09-18 15:02:27 -07:00
parent baa4ac49ce
commit 4598d0688e
29 changed files with 2491 additions and 187 deletions

View File

@ -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

View File

@ -1,3 +1,18 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> 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 <http://www.gnu.org/licenses/>.
// +build debug
// Statup

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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'")

View File

@ -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) {

View File

@ -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 {

View File

@ -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) {

View File

@ -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) {

View File

@ -1,3 +1,18 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> 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 <http://www.gnu.org/licenses/>.
package notifiers
import (

View File

@ -1,3 +1,18 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> 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 <http://www.gnu.org/licenses/>.
package notifiers
import (

View File

@ -1,3 +1,18 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> 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 <http://www.gnu.org/licenses/>.
package notifiers
import (

View File

@ -1,3 +1,18 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> 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 <http://www.gnu.org/licenses/>.
package notifiers
import (

View File

@ -1,3 +1,18 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> 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 <http://www.gnu.org/licenses/>.
package notifiers
import (

View File

@ -1,3 +1,36 @@
@charset "UTF-8";
/*!
* Statup
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> 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 <http://www.gnu.org/licenses/>.
*/
/*!
* Statup
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> 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 <http://www.gnu.org/licenses/>.
*/
/* 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 <info@socialeck.com> 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 <http://www.gnu.org/licenses/>.
*/
@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 */

View File

@ -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 <info@socialeck.com> 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 <http://www.gnu.org/licenses/>.
*/
{{$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}}}})

1257
source/js/pikaday.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,20 @@
/*!
* Statup
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> 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 <http://www.gnu.org/licenses/>.
*/
@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';

View File

@ -1,3 +1,19 @@
/*!
* Statup
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> 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 <http://www.gnu.org/licenses/>.
*/
@media (max-width: 767px) {

255
source/scss/pikaday.scss Normal file
View File

@ -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;
}

View File

@ -1,3 +1,20 @@
/*!
* Statup
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> 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 <http://www.gnu.org/licenses/>.
*/
/* Index Page */
$background-color: #fcfcfc;
$max-width: 860px;

View File

@ -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")

View File

@ -1,3 +1,4 @@
{{$s := .Service}}
<!doctype html>
<html lang="en">
<head>
@ -13,7 +14,7 @@
<script src="/js/Chart.bundle.min.js"></script>
{{end}}
<title>Statup | {{.Name}} Service</title>
<title>Statup | {{$s.Name}} Service</title>
</head>
<body>
@ -25,14 +26,14 @@
<div class="col-12 mb-4">
{{if .Online }}
{{if $s.Online }}
<span class="mt-3 mb-3 text-white d-md-none btn bg-success d-block d-md-none">ONLINE</span>
{{ else }}
<span class="mt-3 mb-3 text-white d-md-none btn bg-danger d-block d-md-none">OFFLINE</span>
{{end}}
<h4 class="mt-2">{{ .Name }}
{{if .Online }}
<h4 class="mt-2">{{ $s.Name }}
{{if $s.Online }}
<span class="badge bg-success float-right d-none d-md-block">ONLINE</span>
{{ else }}
<span class="badge bg-danger float-right d-none d-md-block">OFFLINE</span>
@ -41,28 +42,42 @@
<div class="row stats_area mt-5 mb-5">
<div class="col-4">
<span class="lg_number">{{.Online24}}%</span>
<span class="lg_number">{{$s.Online24}}%</span>
Online last 24 Hours
</div>
<div class="col-4">
<span class="lg_number">{{.AvgTime}}ms</span>
<span class="lg_number">{{$s.AvgTime}}ms</span>
Average Response
</div>
<div class="col-4">
<span class="lg_number">{{.TotalUptime}}%</span>
<span class="lg_number">{{$s.TotalUptime}}%</span>
Total Uptime
</div>
</div>
<div class="chart-container">
<canvas id="service_{{ .Id }}"></canvas>
<div class="chart-container" style="height: 250px">
<canvas id="service_{{ $s.Id }}"></canvas>
</div>
{{ if .LimitedFailures }}
<form id="service_date_form" class="row mt-2 mb-3">
<span id="start_date" class="text-muted small float-left pointer">{{FromUnix .Start}}</span>
<span id="end_date" class="text-muted small float-right pointer" style="position: absolute;right: 0;">{{FromUnix .End}}</span>
<input type="hidden" name="start" class="form-control" id="service_start" spellcheck="false">
<input type="hidden" name="end" class="form-control" id="service_end" spellcheck="false">
<button type="submit" class="btn btn-light btn-block btn-sm mt-2">Set Timeframe</button>
<div id="start_container"></div>
<div id="end_container"></div>
</form>
{{if not $s.Online}}
<div class="col-12 small text-center mt-3 text-muted">{{$s.DowntimeText}}</div>
{{end}}
{{ if $s.LimitedFailures }}
<div class="list-group mt-3 mb-4">
{{ range .LimitedFailures }}
{{ range $s.LimitedFailures }}
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{.ParseError}}</h5>
@ -83,82 +98,82 @@
<h3>Edit Service</h3>
<form action="/service/{{.Id}}" method="POST">
<form action="/service/{{$s.Id}}" method="POST">
<div class="form-group row">
<label for="service_name" class="col-sm-4 col-form-label">Service Name</label>
<div class="col-sm-8">
<input type="text" name="name" class="form-control" id="service_name" value="{{.Name}}" placeholder="Name" required spellcheck="false">
<input type="text" name="name" class="form-control" id="service_name" value="{{$s.Name}}" placeholder="Name" required spellcheck="false">
</div>
</div>
<div class="form-group row">
<label for="service_type" class="col-sm-4 col-form-label">Service Check Type</label>
<div class="col-sm-8">
<select name="check_type" class="form-control" id="service_type" value="{{.Type}}">
<option value="http" {{if eq .Type "http"}}selected{{end}}>HTTP Service</option>
<option value="tcp" {{if eq .Type "tcp"}}selected{{end}}>TCP Service</option>
<select name="check_type" class="form-control" id="service_type" value="{{$s.Type}}">
<option value="http" {{if eq $s.Type "http"}}selected{{end}}>HTTP Service</option>
<option value="tcp" {{if eq $s.Type "tcp"}}selected{{end}}>TCP Service</option>
</select>
</div>
</div>
<div class="form-group row">
<label for="service_url" class="col-sm-4 col-form-label">Application Endpoint (URL)</label>
<div class="col-sm-8">
<input type="text" name="domain" class="form-control" id="service_url" value="{{.Domain}}" placeholder="https://google.com" required autocapitalize="false" spellcheck="false">
<input type="text" name="domain" class="form-control" id="service_url" value="{{$s.Domain}}" placeholder="https://google.com" required autocapitalize="false" spellcheck="false">
</div>
</div>
<div class="form-group row{{if eq .Type "tcp"}} d-none{{end}}">
<div class="form-group row{{if eq $s.Type "tcp"}} d-none{{end}}">
<label for="service_check_type" class="col-sm-4 col-form-label">Service Check Type</label>
<div class="col-sm-8">
<select name="method" class="form-control" id="service_check_type" value="{{.Method}}">
<option value="GET" {{if eq .Method "GET"}}selected{{end}}>GET</option>
<option value="POST" {{if eq .Method "POST"}}selected{{end}}>POST</option>
<option value="DELETE" {{if eq .Method "DELETE"}}selected{{end}}>DELETE</option>
<option value="PATCH" {{if eq .Method "PATCH"}}selected{{end}}>PATCH</option>
<option value="PUT" {{if eq .Method "PUT"}}selected{{end}}>PUT</option>
<select name="method" class="form-control" id="service_check_type" value="{{$s.Method}}">
<option value="GET" {{if eq $s.Method "GET"}}selected{{end}}>GET</option>
<option value="POST" {{if eq $s.Method "POST"}}selected{{end}}>POST</option>
<option value="DELETE" {{if eq $s.Method "DELETE"}}selected{{end}}>DELETE</option>
<option value="PATCH" {{if eq $s.Method "PATCH"}}selected{{end}}>PATCH</option>
<option value="PUT" {{if eq $s.Method "PUT"}}selected{{end}}>PUT</option>
</select>
</div>
</div>
<div class="form-group row{{if ne .Method "POST"}} d-none{{end}}">
<div class="form-group row{{if ne $s.Method "POST"}} d-none{{end}}">
<label for="post_data" class="col-sm-4 col-form-label">Optional Post Data (JSON)</label>
<div class="col-sm-8">
<textarea name="post_data" class="form-control" id="post_data" rows="3" autocapitalize="false" spellcheck="false">{{.PostData}}</textarea>
<textarea name="post_data" class="form-control" id="post_data" rows="3" autocapitalize="false" spellcheck="false">{{$s.PostData}}</textarea>
<small id="emailHelp" class="form-text text-muted">You can insert <a target="_blank" href="https://regex101.com/r/I5bbj9/1">Regex</a> to validate the response</small>
</div>
</div>
<div class="form-group row{{if eq .Type "tcp"}} d-none{{end}}">
<div class="form-group row{{if eq $s.Type "tcp"}} d-none{{end}}">
<label for="service_response" class="col-sm-4 col-form-label">Expected Response (Regex)</label>
<div class="col-sm-8">
<textarea name="expected" class="form-control" id="service_response" rows="3" autocapitalize="false" spellcheck="false">{{.Expected}}</textarea>
<textarea name="expected" class="form-control" id="service_response" rows="3" autocapitalize="false" spellcheck="false">{{$s.Expected}}</textarea>
</div>
</div>
<div class="form-group row{{if eq .Type "tcp"}} d-none{{end}}">
<div class="form-group row{{if eq $s.Type "tcp"}} d-none{{end}}">
<label for="service_response_code" class="col-sm-4 col-form-label">Expected Status Code</label>
<div class="col-sm-8">
<input type="number" name="expected_status" class="form-control" value="{{.ExpectedStatus}}" id="service_response_code">
<input type="number" name="expected_status" class="form-control" value="{{$s.ExpectedStatus}}" id="service_response_code">
</div>
</div>
<div class="form-group row{{if eq .Type "http"}} d-none{{end}}">
<div class="form-group row{{if eq $s.Type "http"}} d-none{{end}}">
<label for="service_port" class="col-sm-4 col-form-label">TCP Port</label>
<div class="col-sm-8">
<input type="number" name="port" class="form-control" value="{{.Port}}" id="service_port" placeholder="8080">
<input type="number" name="port" class="form-control" value="{{$s.Port}}" id="service_port" placeholder="8080">
</div>
</div>
<div class="form-group row">
<label for="service_interval" class="col-sm-4 col-form-label">Check Interval (Seconds)</label>
<div class="col-sm-8">
<input type="number" name="interval" class="form-control" value="{{.Interval}}" min="1" id="service_interval" required>
<input type="number" name="interval" class="form-control" value="{{$s.Interval}}" min="1" id="service_interval" required>
<small id="emailHelp" class="form-text text-muted">10,000+ will be checked in Microseconds (1 millisecond = 1000 microseconds).</small>
</div>
</div>
<div class="form-group row">
<label for="service_timeout" class="col-sm-4 col-form-label">Timeout in Seconds</label>
<div class="col-sm-8">
<input type="number" name="timeout" class="form-control" value="{{.Timeout}}" id="service_timeout" min="1">
<input type="number" name="timeout" class="form-control" value="{{$s.Timeout}}" id="service_timeout" min="1">
</div>
</div>
<div class="form-group row">
<label for="order" class="col-sm-4 col-form-label">List Order</label>
<div class="col-sm-8">
<input type="number" name="order" class="form-control" min="0" value="{{.Order}}" id="order">
<input type="number" name="order" class="form-control" min="0" value="{{$s.Order}}" id="order">
</div>
</div>
<div class="form-group row">
@ -166,34 +181,33 @@
<button type="submit" class="btn btn-success btn-block">Update Service</button>
</div>
<div class="col-6">
<a href="/service/{{ .Id }}/delete_failures" class="btn btn-danger btn-block confirm-btn">Delete All Failures</a>
<a href="/service/{{ $s.Id }}/delete_failures" class="btn btn-danger btn-block confirm-btn">Delete All Failures</a>
</div>
</div>
</form>
</div>
<div class="col-12 mt-4{{if eq .Type "tcp"}} d-none{{end}}">
<div class="col-12 mt-4{{if eq $s.Type "tcp"}} d-none{{end}}">
<h3>Last Response</h3>
<textarea rows="8" class="form-control" readonly>{{ .LastResponse }}</textarea>
<textarea rows="8" class="form-control" readonly>{{ $s.LastResponse }}</textarea>
<div class="form-group row mt-2">
<label for="last_status_code" class="col-sm-3 col-form-label">HTTP Status Code</label>
<div class="col-sm-2">
<input type="text" id="last_status_code" class="form-control" value="{{ .LastStatusCode }}" readonly>
<input type="text" id="last_status_code" class="form-control" value="{{ $s.LastStatusCode }}" readonly>
</div>
</div>
</div>
<div class="col-12 mt-4{{if eq .Type "tcp"}} d-none{{end}}">
<div class="col-12 mt-4{{if eq $s.Type "tcp"}} d-none{{end}}">
<h3>Service Checkins</h3>
{{ range .Checkins }}
{{ range $s.Checkins }}
<h5>Check #{{.Id}} <span class="badge online_badge float-right">Checked in {{.Ago}}</span></h5>
<input type="text" class="form-control" value="https://domainhere.com/api/checkin/{{.Api}}">
{{ end }}
<form action="/service/{{.Id}}/checkin" method="POST">
<form action="/service/{{$s.Id}}/checkin" method="POST">
<div class="form-group row">
<label for="service_name" class="col-sm-4 col-form-label">Check Interval (in seconds)</label>
<div class="col-md-6 col-sm-12">
@ -222,15 +236,58 @@
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.bundle.min.js"></script>
<script src="https://assets.statup.io/pikaday.js"></script>
<script src="https://assets.statup.io/main.js"></script>
{{ else }}
<script src="/js/jquery-3.3.1.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
<script src="/js/Chart.bundle.min.js"></script>
<script src="/js/pikaday.js"></script>
<script src="/js/main.js"></script>
{{end}}
<script src="/charts/{{.Id}}.js"></script>
<script>
var startPick = new Pikaday({
field: $('#service_start')[0],
bound: false,
trigger: $("#start_date"),
container: $("#start_container")[0],
maxDate: new Date(),
onSelect: function(date) {
$('#service_start')[0].value = Math.round(date.getTime() / 1000);
this.hide();
}
});
var endPick = new Pikaday({
field: $('#service_end')[0],
bound: false,
trigger: $("#end_date"),
container: $("#end_container")[0],
maxDate: new Date(),
onSelect: function(date) {
$('#service_end')[0].value = Math.round(date.getTime() / 1000);
this.hide();
}
});
startPick.setDate(new Date({{.Start}}*1000));
endPick.setDate(new Date({{.End}}*1000));
startPick.hide()
endPick.hide()
$("#start_date").click(function(e) {
startPick.show()
})
$("#end_date").click(function(e) {
endPick.show()
})
</script>
<script src="/charts/{{$s.Id}}.js?start={{.Start}}&end={{.End}}"></script>
</body>
</html>

View File

@ -1,3 +1,18 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> 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 <http://www.gnu.org/licenses/>.
package types
import (

View File

@ -1,3 +1,18 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> 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 <http://www.gnu.org/licenses/>.
package types
import (

View File

@ -1,3 +1,18 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> 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 <http://www.gnu.org/licenses/>.
package types
import (

View File

@ -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 {

View File

@ -1,3 +1,18 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> 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 <http://www.gnu.org/licenses/>.
package types
import (