diff --git a/.gitignore b/.gitignore
index 59dce8cf..5b458bcc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,4 +41,5 @@ tmp
/frontend/cypress/videos/
services.yml
statping.wiki
-assets/
\ No newline at end of file
+assets/
+.vscode/settings.json
diff --git a/frontend/.gitignore b/frontend/.gitignore
index a0dddc6f..b4a193e4 100644
--- a/frontend/.gitignore
+++ b/frontend/.gitignore
@@ -19,3 +19,6 @@ yarn-error.log*
*.njsproj
*.sln
*.sw?
+
+# Package lock file
+package-lock.json
diff --git a/frontend/src/API.js b/frontend/src/API.js
index e6f3fc7d..2646a0e7 100644
--- a/frontend/src/API.js
+++ b/frontend/src/API.js
@@ -282,6 +282,27 @@ class Api {
await axios.all([all])
}
+ async downtimes ({ serviceId, start, end, skip, count, subStatus }) {
+ return axios.get('api/downtimes', {
+ params: { service_id: serviceId, start, end, skip, count, sub_status: subStatus }
+ }).then((response) => response.data);
+ }
+
+ async downtime (id) {
+ return axios.get(`api/downtimes/${id}`).then((response) => response.data);
+ }
+
+ async downtime_create (data) {
+ return axios.post('/api/downtimes', data).then((response) => response.data);
+ }
+
+ async downtime_update ({ id, data }) {
+ return axios.patch(`/api/downtimes/${id}`, data).then((response) => response.data);
+ }
+
+ async downtime_delete (id) {
+ return axios.delete(`/api/downtimes/${id}`).then((response) => response.data);
+ }
}
const api = new Api()
export default api
diff --git a/frontend/src/assets/scss/layout.scss b/frontend/src/assets/scss/layout.scss
index 59b82352..dc6da19b 100644
--- a/frontend/src/assets/scss/layout.scss
+++ b/frontend/src/assets/scss/layout.scss
@@ -85,6 +85,10 @@ A {
.nav-link {
color: $navbar-color;
+
+ &.router-link-exact-active {
+ font-weight: bold;
+ }
}
.form-control {
diff --git a/frontend/src/components/Dashboard/DashboardDowntimes.vue b/frontend/src/components/Dashboard/DashboardDowntimes.vue
new file mode 100644
index 00000000..adf38bdc
--- /dev/null
+++ b/frontend/src/components/Dashboard/DashboardDowntimes.vue
@@ -0,0 +1,176 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Loading Downtimes
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/components/Dashboard/DowntimesList.vue b/frontend/src/components/Dashboard/DowntimesList.vue
new file mode 100644
index 00000000..ecdcf23e
--- /dev/null
+++ b/frontend/src/components/Dashboard/DowntimesList.vue
@@ -0,0 +1,172 @@
+
+
+
+ You currently don't have any downtimes for this services!
+
+
+
+
+
+
+ {{ $t('name') }}
+
+
+ {{ $t('start_time') }}
+
+
+ {{ $t('end_time') }}
+
+
+ {{ $t('status') }}
+
+
+ {{ $t('failures') }}
+
+
+ {{ $t('actions') }}
+
+
+
+
+
+
+ {{ downtime.service.name }}
+
+
+
+
+ {{ niceDateWithYear(downtime.start) }}
+
+
+
+
+ {{ downtime.end ? niceDateWithYear(downtime.end) : 'Ongoing' }}
+
+
+
+
+ {{ downtime.sub_status }}
+
+
+
+
+ {{ downtime.failures }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/components/Dashboard/EditDowntime.vue b/frontend/src/components/Dashboard/EditDowntime.vue
new file mode 100644
index 00000000..4bcc8549
--- /dev/null
+++ b/frontend/src/components/Dashboard/EditDowntime.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+ Loading Downtime
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/components/Dashboard/TopNav.vue b/frontend/src/components/Dashboard/TopNav.vue
index 8136fff8..45a1394b 100644
--- a/frontend/src/components/Dashboard/TopNav.vue
+++ b/frontend/src/components/Dashboard/TopNav.vue
@@ -14,6 +14,9 @@
{{ $t('services') }}
+
+ {{'Downtimes'}}
+
{{ $t('users') }}
diff --git a/frontend/src/components/Elements/Pagination.vue b/frontend/src/components/Elements/Pagination.vue
new file mode 100644
index 00000000..5b4687f4
--- /dev/null
+++ b/frontend/src/components/Elements/Pagination.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/forms/Downtime.vue b/frontend/src/forms/Downtime.vue
new file mode 100644
index 00000000..a23cb788
--- /dev/null
+++ b/frontend/src/forms/Downtime.vue
@@ -0,0 +1,325 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/forms/DowntimeFilters.vue b/frontend/src/forms/DowntimeFilters.vue
new file mode 100644
index 00000000..e8317227
--- /dev/null
+++ b/frontend/src/forms/DowntimeFilters.vue
@@ -0,0 +1,173 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/icons.js b/frontend/src/icons.js
index 91e7c3bc..86024a5c 100644
--- a/frontend/src/icons.js
+++ b/frontend/src/icons.js
@@ -6,4 +6,4 @@ import Vue from "vue";
library.add(fas, fab)
-Vue.component('font-awesome-icon', FontAwesomeIcon)
+Vue.component('FontAwesomeIcon', FontAwesomeIcon);
diff --git a/frontend/src/languages/english.js b/frontend/src/languages/english.js
index db54f907..33d73f4e 100644
--- a/frontend/src/languages/english.js
+++ b/frontend/src/languages/english.js
@@ -140,6 +140,20 @@ const english = {
notify_all: "Notify All Changes",
service_update: "Update Service",
service_create: "Create Service",
+ start_time: 'Start Time',
+ end_time: "End Time",
+ actions: "Actions",
+ services: "Services",
+ downtimes: "Downtimes",
+ downtime_info: "Downtime Info",
+ downtime_status: "Downtime Status",
+ downtime_date_range: "Downtime Date Range",
+ downtime_create: "Create Downtime",
+ downtime_update: "Update Downtime",
+ filters: "Filters",
+ service: "Service",
+ clear: "Clear",
+ search: "Search",
};
export default english;
diff --git a/frontend/src/mixin.js b/frontend/src/mixin.js
index 373239e6..32776900 100644
--- a/frontend/src/mixin.js
+++ b/frontend/src/mixin.js
@@ -256,6 +256,9 @@ export default Vue.mixin({
},
addSeconds(date, amount) {
return addSeconds(date, amount)
- }
+ },
+ niceDateWithYear (val) {
+ return format(parseISO(val), 'do MMM, yyyy h:mma');
+ },
}
});
diff --git a/frontend/src/routes.js b/frontend/src/routes.js
index 2b2a82ad..2cebe256 100644
--- a/frontend/src/routes.js
+++ b/frontend/src/routes.js
@@ -16,6 +16,9 @@ const Checkins = () => import(/* webpackChunkName: "dashboard" */ '@/components/
const Failures = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/Failures')
const NotFound = () => import(/* webpackChunkName: "index" */ '@/pages/NotFound')
const Importer = () => import(/* webpackChunkName: "index" */ '@/components/Dashboard/Importer')
+const DashboardDowntimes = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/DashboardDowntimes')
+const EditDowntime = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/EditDowntime')
+
import VueRouter from "vue-router";
import Api from "./API";
@@ -137,6 +140,27 @@ const routes = [
requiresAuth: true,
title: 'Statping - Service Failures',
}
+ },{
+ path: 'downtimes',
+ component: DashboardDowntimes,
+ meta: {
+ requiresAuth: true,
+ title: 'Statping - Downtimes',
+ }
+ },{
+ path: 'create_downtime',
+ component: EditDowntime,
+ meta: {
+ requiresAuth: true,
+ title: 'Statping - Create Downtime',
+ }
+ },{
+ path: 'edit_downtime/:id',
+ component: EditDowntime,
+ meta: {
+ requiresAuth: true,
+ title: 'Statping - Update Downtime',
+ }
},{
path: 'messages',
component: DashboardMessages,
@@ -228,3 +252,4 @@ router.beforeEach((to, from, next) => {
});
export default router
+
diff --git a/frontend/src/store.js b/frontend/src/store.js
index 66d923e1..c8b328c0 100644
--- a/frontend/src/store.js
+++ b/frontend/src/store.js
@@ -23,6 +23,7 @@ export default new Vuex.Store({
oauth: {},
token: null,
services: [],
+ downtimes: [],
service: null,
groups: [],
messages: [],
@@ -152,12 +153,19 @@ export default new Vuex.Store({
setModal(state, modal) {
state.modal = modal
},
+ setDowntimes (state, downtimes) {
+ state.downtimes = downtimes;
+ }
},
actions: {
async getAllServices(context) {
const services = await Api.services()
context.commit("setServices", services);
},
+ async getDowntimes (context, { payload }) {
+ const { output } = await Api.downtimes(payload);
+ context.commit('setDowntimes', output ?? []);
+ },
async loadCore(context) {
const core = await Api.core()
const token = await Api.token()
diff --git a/handlers/api.go b/handlers/api.go
index a564bd2e..a306dfbb 100644
--- a/handlers/api.go
+++ b/handlers/api.go
@@ -167,7 +167,9 @@ func sendJsonAction(obj interface{}, method string, w http.ResponseWriter, r *ht
case *downtimes.Downtime:
objName = "downtime"
objId = v.Id
-
+ case *DowntimeService:
+ objName = "downtime_with_service"
+ objId = v.Id
default:
objName = fmt.Sprintf("%T", v)
}
diff --git a/handlers/downtimes.go b/handlers/downtimes.go
index 3a451684..c9f26670 100644
--- a/handlers/downtimes.go
+++ b/handlers/downtimes.go
@@ -7,6 +7,7 @@ import (
"github.com/statping/statping/types/services"
"github.com/statping/statping/utils"
"net/http"
+ "net/url"
"time"
)
@@ -21,10 +22,75 @@ func findDowntime(r *http.Request) (*downtimes.Downtime, error) {
return downtime, nil
}
+func convertToMap(query url.Values) map[string]string {
+ vars := make(map[string]string)
+ if query.Get("start") != "" {
+ vars["start"] = query.Get("start")
+ }
+ if query.Get("end") != "" {
+ vars["end"] = query.Get("end")
+ }
+ if query.Get("sub_status") != "" {
+ vars["sub_status"] = query.Get("sub_status")
+ }
+ if query.Get("service_id") != "" {
+ vars["service_id"] = query.Get("service_id")
+ }
+ if query.Get("type") != "" {
+ vars["type"] = query.Get("type")
+ }
+ if query.Get("skip") != "" {
+ vars["skip"] = query.Get("skip")
+ }
+ if query.Get("count") != "" {
+ vars["count"] = query.Get("count")
+ }
+ return vars
+}
+
+type DowntimeService struct {
+ Id int64 `gorm:"primary_key;column:id" json:"id"`
+ Service *services.Service `gorm:"foreignKey:service" json:"service"`
+ ServiceId int64 `gorm:"index;column:service" json:"service_id"`
+ SubStatus string `gorm:"column:sub_status" json:"sub_status"`
+ Failures int `gorm:"column:failures" json:"failures"`
+ Start *time.Time `gorm:"index;column:start" json:"start"`
+ End *time.Time `gorm:"column:end" json:"end"`
+ Type string `gorm:"default:'auto';column:type" json:"type"`
+}
+
+func apiAllDowntimes(w http.ResponseWriter, r *http.Request) {
+ query := r.URL.Query()
+ vars := convertToMap(query)
+ downtime, err := downtimes.FindAll(vars)
+ var downtimeWithService []DowntimeService
+ servicesMap := services.All()
+ if downtime == nil {
+ sendJsonAction(downtimeWithService, "fetch", w, r)
+ return
+ }
+ for _, dtime := range *downtime {
+ var downtimeWithServiceVar DowntimeService
+ downtimeWithServiceVar.Id = dtime.Id
+ downtimeWithServiceVar.ServiceId = dtime.ServiceId
+ downtimeWithServiceVar.SubStatus = dtime.SubStatus
+ downtimeWithServiceVar.Failures = dtime.Failures
+ downtimeWithServiceVar.Start = dtime.Start
+ downtimeWithServiceVar.End = dtime.End
+ downtimeWithServiceVar.Type = dtime.Type
+ downtimeWithServiceVar.Service = servicesMap[dtime.ServiceId]
+ downtimeWithService = append(downtimeWithService, downtimeWithServiceVar)
+ }
+ if err != nil {
+ sendErrorJson(err, w, r)
+ return
+ }
+ sendJsonAction(downtimeWithService, "fetch", w, r)
+}
+
func apiAllDowntimesForServiceHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
serviceId := utils.ToInt(vars["service_id"])
-
ninetyDaysAgo := time.Now().Add(time.Duration(-90*24) * time.Hour)
downtime, err := downtimes.FindByService(serviceId, ninetyDaysAgo, time.Now())
diff --git a/handlers/routes.go b/handlers/routes.go
index d2eb1018..47040c87 100644
--- a/handlers/routes.go
+++ b/handlers/routes.go
@@ -194,6 +194,7 @@ func Router() *mux.Router {
//r.Handle("/checkin/{api}", http.HandlerFunc(checkinHitHandler))
// API DOWNTIME Routes
+ api.Handle("/api/downtimes", authenticated(apiAllDowntimes, false)).Methods("GET")
api.Handle("/api/service/{service_id}/downtimes", authenticated(apiAllDowntimesForServiceHandler, false)).Methods("GET")
api.Handle("/api/downtimes", authenticated(apiCreateDowntimeHandler, false)).Methods("POST")
api.Handle("/api/downtimes/{id}", authenticated(apiDowntimeHandler, false)).Methods("GET")
diff --git a/types/downtimes/database.go b/types/downtimes/database.go
index dc74ee98..95d2feca 100644
--- a/types/downtimes/database.go
+++ b/types/downtimes/database.go
@@ -3,6 +3,7 @@ package downtimes
import (
"fmt"
"github.com/statping/statping/database"
+ "strconv"
"time"
)
@@ -66,6 +67,68 @@ func FindDowntime(timeVar time.Time) []Downtime {
}
+func ConvertToUnixTime(str string) (time.Time, error) {
+ i, err := strconv.ParseInt(str, 10, 64)
+ var t time.Time
+ if err != nil {
+ return t, err
+ }
+ tm := time.Unix(i, 0)
+ return tm, nil
+}
+
+func FindAll(vars map[string]string) (*[]Downtime, error) {
+ var downtime []Downtime
+ var start time.Time
+ var end time.Time
+ st, err1 := vars["start"]
+ en, err2 := vars["end"]
+ startInt, err := strconv.ParseInt(st, 10, 64)
+ endInt, err := strconv.ParseInt(en, 10, 64)
+ if err1 && err2 && (endInt > startInt) {
+ start, err = ConvertToUnixTime(vars["start"])
+ if err != nil {
+ return &downtime, err
+ }
+ end, err = ConvertToUnixTime(vars["end"])
+ if err != nil {
+ return &downtime, err
+ }
+ } else {
+ ninetyDaysAgo := time.Now().Add(time.Duration(-90*24) * time.Hour)
+ start = ninetyDaysAgo
+ end = time.Now()
+ }
+ q := db.Where("start BETWEEN ? AND ?", start, end)
+ if subStatusVar, subStatusErr := vars["sub_status"]; subStatusErr {
+ q = q.Where("sub_status = ?", subStatusVar)
+ }
+ if serviceIdVar, serviceIdErr := vars["service_id"]; serviceIdErr {
+ q = q.Where("service = ?", serviceIdVar)
+ }
+ if typeVar, typeErr := vars["type"]; typeErr {
+ q = q.Where("type = ?", typeVar)
+ }
+ var count int64
+ if countVar, countErr := vars["count"]; countErr {
+ count, err = strconv.ParseInt(countVar, 10, 64)
+ if count > 100 {
+ count = 100
+ }
+ } else {
+ count = 20
+ }
+ var skip int64
+ if skipVar, err6 := vars["skip"]; err6 {
+ skip, err = strconv.ParseInt(skipVar, 10, 64)
+ } else {
+ skip = 0
+ }
+ q = q.Order("start DESC")
+ q = q.Limit((int)(count)).Offset((int)(skip)).Find(&downtime)
+ return &downtime, q.Error()
+}
+
func (c *Downtime) Create() error {
q := db.Create(c)
return q.Error()