mirror of https://github.com/statping/statping
incidents vue
parent
89c103279b
commit
d8a02b31a2
39
Dockerfile
39
Dockerfile
|
@ -1,43 +1,12 @@
|
|||
FROM node:10.17.0 AS frontend
|
||||
RUN npm install yarn -g
|
||||
WORKDIR /statping
|
||||
COPY ./frontend/package.json .
|
||||
COPY ./frontend/yarn.lock .
|
||||
RUN yarn install --pure-lockfile --network-timeout 1000000
|
||||
COPY ./frontend .
|
||||
RUN yarn build && rm -rf node_modules && yarn cache clean
|
||||
# Compiles webpacked Vue production build for frontend at /statping/dist
|
||||
|
||||
# Statping Golang BACKEND building from source
|
||||
# Creates "/go/bin/statping" and "/usr/local/bin/sass" for copying
|
||||
FROM golang:1.14-alpine AS backend
|
||||
LABEL maintainer="Hunter Long (https://github.com/hunterlong)"
|
||||
ARG VERSION
|
||||
RUN apk add --update --no-cache libstdc++ gcc g++ make git ca-certificates linux-headers wget curl jq
|
||||
RUN curl -L -s https://assets.statping.com/sass -o /usr/local/bin/sass && \
|
||||
chmod +x /usr/local/bin/sass
|
||||
WORKDIR /go/src/github.com/statping/statping
|
||||
ADD go.mod go.sum ./
|
||||
RUN go mod download
|
||||
ENV GO111MODULE on
|
||||
RUN go get github.com/stretchr/testify/assert && \
|
||||
go get github.com/stretchr/testify/require && \
|
||||
go get github.com/GeertJohan/go.rice/rice && \
|
||||
go get github.com/cortesi/modd/cmd/modd && \
|
||||
go get github.com/crazy-max/xgo
|
||||
COPY . .
|
||||
COPY --from=frontend /statping/dist/ ./source/dist/
|
||||
RUN make clean generate embed build
|
||||
RUN chmod a+x statping && mv statping /go/bin/statping
|
||||
|
||||
FROM statping/statping:base AS base
|
||||
|
||||
# Statping main Docker image that contains all required libraries
|
||||
FROM alpine:latest
|
||||
RUN apk --no-cache add libgcc libstdc++ curl jq
|
||||
|
||||
COPY --from=backend /go/bin/statping /usr/local/bin/
|
||||
COPY --from=backend /usr/local/bin/sass /usr/local/bin/
|
||||
COPY --from=backend /usr/local/share/ca-certificates /usr/local/share/
|
||||
COPY --from=base /go/bin/statping /usr/local/bin/
|
||||
COPY --from=base /usr/local/bin/sass /usr/local/bin/
|
||||
COPY --from=base /usr/local/share/ca-certificates /usr/local/share/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ COPY ./frontend/package.json .
|
|||
COPY ./frontend/yarn.lock .
|
||||
RUN yarn install --pure-lockfile --network-timeout 1000000
|
||||
COPY ./frontend .
|
||||
RUN yarn build && rm -rf node_modules && yarn cache clean
|
||||
RUN yarn build && yarn cache clean
|
||||
|
||||
|
||||
# Statping Golang BACKEND building from source
|
||||
|
@ -26,6 +26,9 @@ RUN go get github.com/stretchr/testify/assert && \
|
|||
go get github.com/cortesi/modd/cmd/modd && \
|
||||
go get github.com/crazy-max/xgo
|
||||
COPY . .
|
||||
COPY --from=frontend /statping/dist/ ./source/dist/
|
||||
COPY --from=frontend /statping/ ./frontend/
|
||||
RUN make clean generate embed build
|
||||
RUN chmod a+x statping && mv statping /go/bin/statping
|
||||
# /go/bin/statping - statping binary
|
||||
# /usr/local/bin/sass - sass binary
|
||||
# /statping - Vue frontend (from frontend)
|
||||
|
|
21
Makefile
21
Makefile
|
@ -20,9 +20,6 @@ up:
|
|||
down:
|
||||
docker-compose -f docker-compose.yml -f dev/docker-compose.full.yml down --volumes --remove-orphans
|
||||
|
||||
test: clean
|
||||
go test -v -p=1 -ldflags="-X main.VERSION=dev" -coverprofile=coverage.out ./...
|
||||
|
||||
lite: clean
|
||||
docker build -t hunterlong/statping:dev -f dev/Dockerfile.dev .
|
||||
docker-compose -f dev/docker-compose.lite.yml down
|
||||
|
@ -30,6 +27,9 @@ lite: clean
|
|||
|
||||
reup: down clean compose-build-full up
|
||||
|
||||
test: clean
|
||||
go test -v -p=1 -ldflags="-X main.VERSION=dev" -coverprofile=coverage.out ./...
|
||||
|
||||
yarn-serve:
|
||||
cd frontend && yarn serve
|
||||
|
||||
|
@ -61,20 +61,23 @@ compose-build-full: docker-base
|
|||
docker-compose -f docker-compose.yml -f dev/docker-compose.full.yml build --parallel --build-arg VERSION=${VERSION}
|
||||
|
||||
docker-base:
|
||||
docker build -t hunterlong/statping:base -f Dockerfile.base --no-cache --build-arg VERSION=${VERSION} .
|
||||
docker build -t statping/statping:base -f Dockerfile.base --no-cache --build-arg VERSION=${VERSION} .
|
||||
|
||||
docker-latest: docker-base
|
||||
docker build -t hunterlong/statping:latest --build-arg VERSION=${VERSION} .
|
||||
docker build -t statping/statping:latest --build-arg VERSION=${VERSION} .
|
||||
|
||||
docker-vue:
|
||||
docker build -t hunterlong/statping:vue --build-arg VERSION=${VERSION} .
|
||||
docker build -t statping/statping:vue --build-arg VERSION=${VERSION} .
|
||||
|
||||
docker-test:
|
||||
docker-compose -f docker-compose.test.yml up --remove-orphans
|
||||
|
||||
push-base: docker-base
|
||||
docker push hunterlong/statping:base
|
||||
docker push statping/statping:base
|
||||
|
||||
push-vue: clean docker-base docker-vue
|
||||
docker push hunterlong/statping:base
|
||||
docker push hunterlong/statping:vue
|
||||
docker push statping/statping:base
|
||||
docker push statping/statping:vue
|
||||
|
||||
modd:
|
||||
modd -f ./dev/modd.conf
|
||||
|
|
|
@ -41,16 +41,16 @@ func StartMaintenceRoutine() {
|
|||
// databaseMaintence will automatically delete old records from 'failures' and 'hits'
|
||||
// this function is currently set to delete records 7+ days old every 60 minutes
|
||||
func databaseMaintence(dur time.Duration) {
|
||||
deleteAfter := time.Now().UTC().Add(dur)
|
||||
//deleteAfter := time.Now().UTC().Add(dur)
|
||||
|
||||
time.Sleep(20 * types.Second)
|
||||
|
||||
for range time.Tick(maintenceDuration) {
|
||||
log.Infof("Deleting failures older than %s", dur.String())
|
||||
DeleteAllSince("failures", deleteAfter)
|
||||
//DeleteAllSince("failures", deleteAfter)
|
||||
|
||||
log.Infof("Deleting hits older than %s", dur.String())
|
||||
DeleteAllSince("hits", deleteAfter)
|
||||
//DeleteAllSince("hits", deleteAfter)
|
||||
|
||||
maintenceDuration = types.Hour
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,38 @@
|
|||
version: '2.4'
|
||||
|
||||
services:
|
||||
|
||||
sut:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile.base
|
||||
links:
|
||||
- postgres
|
||||
depends_on:
|
||||
- postgres
|
||||
entrypoint: make test
|
||||
environment:
|
||||
DB_CONN: postgres
|
||||
DB_USER: root
|
||||
DB_PASS: password123
|
||||
DB_HOST: postgres
|
||||
DB_DATABASE: statping
|
||||
API_KEY: exampleapikey
|
||||
API_SECRET: exampleapisecret
|
||||
NAME: Statping Testing
|
||||
DOMAIN: http://localhost:8080
|
||||
DESCRIPTION: This is a TESTING environment
|
||||
ADMIN_USER: admin
|
||||
ADMIN_PASS: admin
|
||||
|
||||
postgres:
|
||||
image: postgres
|
||||
environment:
|
||||
POSTGRES_PASSWORD: password123
|
||||
POSTGRES_DB: statping
|
||||
POSTGRES_USER: root
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U root"]
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 20
|
|
@ -7,7 +7,8 @@
|
|||
"build": "rm -rf dist && cross-env NODE_ENV=production webpack --mode production",
|
||||
"dev": "cross-env NODE_ENV=development webpack-dev-server --host 0.0.0.0 --port 8888 --progress",
|
||||
"lint": "vue-cli-service lint",
|
||||
"test": "cross-env NODE_ENV=development mochapack --webpack-config webpack.config.js --require test/setup.js test/**/*.spec.js"
|
||||
"test": "cross-env NODE_ENV=development mochapack --webpack-config webpack.config.js --require test/setup.js test/**/*.spec.js",
|
||||
"backend-test": "newman run -e ../dev/postman_environment.json --delay-request 500 ../dev/postman.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free-solid": "^5.1.0-3",
|
||||
|
|
|
@ -40,6 +40,10 @@ class Api {
|
|||
return axios.get('/api/services/' + id + '/hits_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).then(response => (response.data))
|
||||
}
|
||||
|
||||
async service_ping(id, start, end, group, fill=true) {
|
||||
return axios.get('/api/services/' + id + '/ping_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).then(response => (response.data))
|
||||
}
|
||||
|
||||
async service_failures_data(id, start, end, group, fill=true) {
|
||||
return axios.get('/api/services/' + id + '/failure_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).then(response => (response.data))
|
||||
}
|
||||
|
@ -97,6 +101,22 @@ class Api {
|
|||
return axios.delete('/api/users/' + id).then(response => (response.data))
|
||||
}
|
||||
|
||||
async incident_update_create(incident, data) {
|
||||
return axios.post('/api/incidents/'+incident.id+'/updates', data).then(response => (response.data))
|
||||
}
|
||||
|
||||
async incidents_service(service) {
|
||||
return axios.get('/api/services/'+service.id+'/incidents').then(response => (response.data))
|
||||
}
|
||||
|
||||
async incident_create(data) {
|
||||
return axios.post('/api/incidents', data).then(response => (response.data))
|
||||
}
|
||||
|
||||
async incident_delete(incident) {
|
||||
return axios.delete('/api/incidents/'+incident.id).then(response => (response.data))
|
||||
}
|
||||
|
||||
async messages() {
|
||||
return axios.get('/api/messages').then(response => (response.data))
|
||||
}
|
||||
|
|
|
@ -4,23 +4,47 @@ HTML,BODY {
|
|||
|
||||
.index-chart {
|
||||
height: $service-card-height;
|
||||
/*Animation*/
|
||||
-webkit-transition: height 0.3s ease;
|
||||
-moz-transition: height 0.3s ease;
|
||||
-o-transition: height 0.3s ease;
|
||||
-ms-transition: height 0.3s ease;
|
||||
transition: height 0.3s ease;
|
||||
}
|
||||
|
||||
@-o-keyframes fadeIt {
|
||||
0% { background-color: #f5f5f5; }
|
||||
50% { background-color: #f2f2f2; }
|
||||
100% { background-color: #f5f5f5; }
|
||||
}
|
||||
@keyframes fadeIt {
|
||||
0% { background-color: #f5f5f5; }
|
||||
50% { background-color: #f2f2f2; }
|
||||
100% { background-color: #f5f5f5; }
|
||||
}
|
||||
|
||||
.backgroundAnimated {
|
||||
background-image:none !important;
|
||||
-o-animation: fadeIt 1s ease-in-out;
|
||||
animation: fadeIt 1s ease-in-out;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.loader {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 35px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.sub-service-card {
|
||||
border: 1px solid #dcdcdc87;
|
||||
padding-top: 7px;
|
||||
padding-bottom: 10px;
|
||||
height: 155px;
|
||||
min-height: 70px;
|
||||
}
|
||||
|
||||
.expanded-service {
|
||||
height: 89vh;
|
||||
/*Animation*/
|
||||
-webkit-transition: height 2s ease;
|
||||
-moz-transition: height 2s ease;
|
||||
-o-transition: height 2s ease;
|
||||
-ms-transition: height 2s ease;
|
||||
transition: height 2s ease;
|
||||
height: 550px;
|
||||
|
||||
.stats_area {
|
||||
display: none;
|
||||
|
@ -140,7 +164,7 @@ HTML,BODY {
|
|||
|
||||
.stats_area {
|
||||
text-align: center;
|
||||
color: #a5a5a5;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.lower_canvas {
|
||||
|
@ -171,39 +195,43 @@ HTML,BODY {
|
|||
}
|
||||
|
||||
.font-0 {
|
||||
font-size: 5pt;
|
||||
font-size: 0.35rem;
|
||||
}
|
||||
|
||||
.font-1 {
|
||||
font-size: 7pt;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.font-2 {
|
||||
font-size: 9pt;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.font-3 {
|
||||
font-size: 11pt;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.font-4 {
|
||||
font-size: 14pt;
|
||||
font-size: 1.7rem;
|
||||
}
|
||||
|
||||
.font-5 {
|
||||
font-size: 17pt;
|
||||
font-size: 2.3rem;
|
||||
}
|
||||
|
||||
.font-6 {
|
||||
font-size: 24pt;
|
||||
font-size: 2.8rem;
|
||||
}
|
||||
|
||||
.font-7 {
|
||||
font-size: 31pt;
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.font-8 {
|
||||
font-size: 38pt;
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: $subtitle-color;
|
||||
}
|
||||
|
||||
.badge {
|
||||
|
@ -261,7 +289,6 @@ HTML,BODY {
|
|||
|
||||
.card-body H4 A {
|
||||
color: $service-title;
|
||||
font-size: $service-title-size;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,10 +4,19 @@
|
|||
background-color: $sm-background-color;
|
||||
}
|
||||
|
||||
|
||||
.index-chart {
|
||||
height: 380px;
|
||||
}
|
||||
|
||||
.online_list {
|
||||
box-shadow: $mobile-card-shadow;
|
||||
}
|
||||
|
||||
.expanded-service {
|
||||
height: 810px;
|
||||
}
|
||||
|
||||
.sm-container {
|
||||
margin-top: 0px !important;
|
||||
padding: 0 !important;
|
||||
|
@ -71,8 +80,7 @@
|
|||
border-radius: $sm-border-radius;
|
||||
padding: $sm-padding;
|
||||
background-color: $sm-service-background;
|
||||
box-shadow: 0px 3px 10px 2px;
|
||||
color: #b5b5b5;
|
||||
box-shadow: $mobile-card-shadow;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
|
@ -92,7 +100,6 @@
|
|||
.stats_area .col-4 {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.navbar-item {
|
||||
|
@ -121,4 +128,44 @@
|
|||
.service-chart-container {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.font-0 {
|
||||
font-size: 0.35rem;
|
||||
}
|
||||
|
||||
.font-1 {
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.font-2 {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.font-3 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.font-4 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.font-5 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.font-6 {
|
||||
font-size: 1.9rem;
|
||||
}
|
||||
|
||||
.font-7 {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
.font-8 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ $background-color: #fcfcfc;
|
|||
$max-width: 860px;
|
||||
$title-color: #464646;
|
||||
$description-color: #939393;
|
||||
$subtitle-color: #747474;
|
||||
$mobile-card-shadow: 2px 3px 10px #b7b7b7;
|
||||
|
||||
/* Status Container */
|
||||
$service-background: #ffffff;
|
||||
|
@ -12,7 +14,7 @@ $service-title-size: 1.8rem;
|
|||
$service-stats-color: #4f4f4f;
|
||||
$service-description-color: #fff;
|
||||
$service-stats-size: 2.3rem;
|
||||
$service-card-height: 480px !important;
|
||||
$service-card-height: 480px;
|
||||
|
||||
/* Button Colors */
|
||||
$success-color: #47d337;
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<h3>Cache</h3>
|
||||
<div v-if="!cache && cache.length !== 0" class="alert alert-danger">
|
||||
There are no cached files
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
<template>
|
||||
<div class="col-12 mt-3">
|
||||
<div class="col-12 mt-4 mt-md-3">
|
||||
|
||||
<div class="row stats_area mb-5">
|
||||
<div class="col-4">
|
||||
<span class="lg_number">{{$store.getters.services.length}}</span>
|
||||
Total Services
|
||||
<span class="font-6 font-weight-bold d-block">{{$store.getters.services.length}}</span>
|
||||
<span class="font-2">Total Services</span>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<span class="lg_number">{{failuresLast24Hours()}}</span>
|
||||
Failures last 24 Hours
|
||||
<span class="font-6 font-weight-bold d-block">{{failuresLast24Hours()}}</span>
|
||||
<span class="font-2">Failures last 24 Hours</span>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<span class="lg_number">{{$store.getters.onlineServices(true).length}}</span>
|
||||
Online Services
|
||||
<span class="font-6 font-weight-bold d-block">{{$store.getters.onlineServices(true).length}}</span>
|
||||
<span class="font-2">Online Services</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<td class="d-none d-md-table-cell">
|
||||
<router-link :to="serviceLink(message.service)">{{service(message.service)}}</router-link>
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell">{{toLocal(message.start_on)}}</td>
|
||||
<td class="d-none d-md-table-cell">{{niceDate(message.start_on)}}</td>
|
||||
<td class="text-right">
|
||||
<div class="btn-group">
|
||||
<a @click.prevent="editMessage(message, edit)" href="#" class="btn btn-outline-secondary"><i class="fas fa-exclamation-triangle"></i> Edit</a>
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card contain-card text-black-50 bg-white mb-4">
|
||||
<div class="card-header">Groups</div>
|
||||
<div class="card-body">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="col-12">
|
||||
<div class="card contain-card text-black-50 bg-white mb-5">
|
||||
<div class="card contain-card text-black-50 bg-white mb-4">
|
||||
<div class="card-header">Users</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-striped">
|
||||
|
@ -18,7 +18,7 @@
|
|||
<td>{{user.username}}</td>
|
||||
<td v-if="user.admin"><span class="badge badge-danger">ADMIN</span></td>
|
||||
<td v-if="!user.admin"><span class="badge badge-primary">USER</span></td>
|
||||
<td class="d-none d-md-table-cell">{{toLocal(user.updated_at)}}</td>
|
||||
<td class="d-none d-md-table-cell">{{niceDate(user.updated_at)}}</td>
|
||||
<td class="text-right">
|
||||
<div class="btn-group">
|
||||
<a @click.prevent="editUser(user, edit)" href="" class="btn btn-outline-secondary"><font-awesome-icon icon="user" /> Edit</a>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="list-group online_list mb-4">
|
||||
|
||||
<a v-for="(service, index) in $store.getters.servicesInGroup(group.id)" v-bind:key="index" class="service_li list-group-item list-group-item-action">
|
||||
<router-link class="no-decoration" :to="serviceLink(service)">{{service.name}}</router-link>
|
||||
<router-link class="no-decoration font-3" :to="serviceLink(service)">{{service.name}}</router-link>
|
||||
<span class="badge float-right" :class="{'bg-success': service.online, 'bg-danger': !service.online }">{{service.online ? "ONLINE" : "OFFLINE"}}</span>
|
||||
|
||||
<GroupServiceFailures :service="service"/>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div>
|
||||
<h1 class="col-12 text-center pt-4 mt-4 header-title">{{$store.getters.core.name}}</h1>
|
||||
<h5 class="col-12 text-center mb-5 header-desc">{{$store.getters.core.description}}</h5>
|
||||
<h1 class="col-12 text-center pt-4 mt-4 mb-3 header-title font-6">{{$store.getters.core.name}}</h1>
|
||||
<h5 class="col-12 text-center mb-5 header-desc font-3">{{$store.getters.core.description}}</h5>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
<template>
|
||||
<div class="col-6 mt-4">
|
||||
<div class="col-12 sub-service-card">
|
||||
<div class="col-8 float-left p-0 mt-1 mb-3">
|
||||
<span class="font-5 d-block">{{title}}</span>
|
||||
<span class="text-muted font-3 d-block font-weight-bold">{{subtitle}}</span>
|
||||
<div class="col-12 col-md-6 mt-2 mt-md-4">
|
||||
<div class="col-12 pt-2 sub-service-card">
|
||||
<div class="col-8 float-left p-0">
|
||||
<span class="font-4 d-block text-muted">{{func.title}}</span>
|
||||
<span class="font-2 d-block subtitle">{{func.subtitle}}</span>
|
||||
</div>
|
||||
<div class="col-4 float-right text-right mt-2 p-0">
|
||||
<span class="text-success font-5 font-weight-bold">{{value}}</span>
|
||||
<span class="text-success font-4 font-weight-bold">{{func.value}}</span>
|
||||
</div>
|
||||
|
||||
<MiniSparkLine :series="[{name: 'okokokok', data:[{x: '2019-01-01', y: 120},{x: '2019-01-02', y: 160},{x: '2019-01-03', y: 240},{x: '2019-01-04', y: 45}]}]"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -23,30 +21,24 @@
|
|||
name: 'Analytics',
|
||||
components: { MiniSparkLine, ServiceSparkLine },
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
func: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
value: 0,
|
||||
title: "",
|
||||
subtitle: "",
|
||||
chart: [],
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.latencyYesterday();
|
||||
this.value = this.func.value;
|
||||
this.title = this.func.title;
|
||||
this.subtitle = this.func.subtitle;
|
||||
this.chart = this.convertToChartData(this.func.chart);
|
||||
},
|
||||
async latencyYesterday() {
|
||||
const todayTime = await Api.service_hits(this.service.id, this.toUnix(this.nowSubtract(86400)), this.toUnix(new Date()), this.group, false)
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<template>
|
||||
<div class="row stats_area mt-5 mb-4">
|
||||
okok
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Incidents',
|
||||
props: {
|
||||
service: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
</style>
|
|
@ -1,28 +1,34 @@
|
|||
<template>
|
||||
<div class="mb-md-4 mb-5">
|
||||
<div class="mb-md-4 mb-4">
|
||||
<div class="card index-chart" :class="{'expanded-service': expanded}">
|
||||
<div class="card-body">
|
||||
<div class="col-12">
|
||||
<h4 class="mt-3">
|
||||
<router-link :to="serviceLink(service)" class="d-inline-block text-truncate" style="max-width: 65vw;" :in_service="service">{{service.name}}</router-link>
|
||||
<h4 class="mt-2">
|
||||
<router-link :to="serviceLink(service)" class="d-inline-block text-truncate font-4" style="max-width: 65vw;" :in_service="service">{{service.name}}</router-link>
|
||||
<span class="badge float-right" :class="{'bg-success': service.online, 'bg-danger': !service.online}">{{service.online ? "ONLINE" : "OFFLINE"}}</span>
|
||||
</h4>
|
||||
|
||||
<ServiceTopStats :service="service"/>
|
||||
|
||||
<div v-if="expanded" class="row">
|
||||
<Analytics title="Last Failure" level="100" value="35%" subtitle="417 Days ago"/>
|
||||
<Analytics title="Total Failures" level="100" value="35%" subtitle="417 Days ago"/>
|
||||
<Analytics title="Highest Latency" level="100" value="450ms" subtitle="417 Days ago"/>
|
||||
<Analytics title="Lowest Latency" level="100" value="120ms" subtitle="417 Days ago"/>
|
||||
<Analytics title="Total Uptime" level="100" value="35%" subtitle="850ms"/>
|
||||
<Analytics title="Total Downtime" level="100" value="35%" subtitle="32ms"/>
|
||||
<Analytics title="Last Failure" :func="stats.total_failures"/>
|
||||
<Analytics title="Total Failures" :func="stats.total_failures"/>
|
||||
<Analytics title="Highest Latency" :func="stats.high_latency"/>
|
||||
<Analytics title="Lowest Latency" :func="stats.lowest_latency"/>
|
||||
<Analytics title="Total Uptime" :func="stats.high_ping"/>
|
||||
<Analytics title="Total Downtime" :func="stats.low_ping"/>
|
||||
|
||||
<div class="col-12">
|
||||
<router-link :to="serviceLink(service)" class="btn btn-block btn-outline-success mt-4" :class="{'btn-outline-success': service.online, 'btn-outline-danger': !service.online}">
|
||||
View More Details
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!expanded" v-observe-visibility="visibleChart" class="chart-container">
|
||||
<div v-show="!expanded" v-observe-visibility="visibleChart" class="chart-container">
|
||||
<ServiceChart :service="service" :visible="visible"/>
|
||||
</div>
|
||||
|
||||
|
@ -36,16 +42,15 @@
|
|||
<a v-for="(timeframe, i) in timeframes" @click="timeframe.picked = true" class="dropdown-item" href="#">{{timeframe.text}}</a>
|
||||
</div>
|
||||
|
||||
<span class="d-none float-right d-md-inline">
|
||||
<span class="d-none float-left d-md-inline">
|
||||
{{smallText(service)}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 col-6 float-right">
|
||||
<router-link :to="serviceLink(service)" class="d-none btn btn-sm float-right dyn-dark text-white" :class="{'bg-success': service.online, 'bg-danger': !service.online}">
|
||||
View Service</router-link>
|
||||
<button @click="expanded = !expanded" class="btn btn-sm float-right dyn-dark text-white" :class="{'bg-success': service.online, 'bg-danger': !service.online}">View Service</button>
|
||||
<button v-if="!expanded" @click="showMoreStats" class="btn btn-sm float-right dyn-dark text-white" :class="{'bg-success': service.online, 'bg-danger': !service.online}">View Service</button>
|
||||
<button v-if="expanded" @click="expanded = false" class="btn btn-sm float-right dyn-dark text-white" :class="{'btn-outline-success': service.online, 'bg-danger': !service.online}">Hide</button>
|
||||
</div>
|
||||
|
||||
<div v-if="expanded" class="row">
|
||||
|
@ -61,6 +66,7 @@
|
|||
import Analytics from './Analytics';
|
||||
import ServiceChart from "./ServiceChart";
|
||||
import ServiceTopStats from "@/components/Service/ServiceTopStats";
|
||||
import Graphing from '../../graphing'
|
||||
|
||||
export default {
|
||||
name: 'ServiceBlock',
|
||||
|
@ -82,17 +88,66 @@ export default {
|
|||
{value: "3", text: "3 Hours" },
|
||||
{value: "1m", text: "1 Month" },
|
||||
{value: "3", text: "Last 3 Months" },
|
||||
]
|
||||
],
|
||||
stats: {
|
||||
total_failures: {
|
||||
title: "Total Failures",
|
||||
subtitle: "Last 7 Days",
|
||||
value: 0,
|
||||
},
|
||||
high_latency: {
|
||||
title: "Highest Latency",
|
||||
subtitle: "Last 7 Days",
|
||||
value: 0,
|
||||
},
|
||||
lowest_latency: {
|
||||
title: "Lowest Latency",
|
||||
subtitle: "Last 7 Days",
|
||||
value: 0,
|
||||
},
|
||||
high_ping: {
|
||||
title: "Highest Ping",
|
||||
subtitle: "Last 7 Days",
|
||||
value: 0,
|
||||
},
|
||||
low_ping: {
|
||||
title: "Lowest Ping",
|
||||
subtitle: "Last 7 Days",
|
||||
value: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async showMoreStats() {
|
||||
this.expanded = !this.expanded;
|
||||
|
||||
const failData = await Graphing.failures(this.service, 7)
|
||||
this.stats.total_failures.chart = failData.data;
|
||||
this.stats.total_failures.value = failData.total;
|
||||
|
||||
const hitsData = await Graphing.hits(this.service, 7)
|
||||
|
||||
this.stats.high_latency.chart = hitsData.chart;
|
||||
this.stats.high_latency.value = this.humanTime(hitsData.high);
|
||||
|
||||
this.stats.lowest_latency.chart = hitsData.chart;
|
||||
this.stats.lowest_latency.value = this.humanTime(hitsData.low);
|
||||
|
||||
const pingData = await Graphing.pings(this.service, 7)
|
||||
this.stats.high_ping.chart = pingData.chart;
|
||||
this.stats.high_ping.value = this.humanTime(pingData.high);
|
||||
|
||||
this.stats.low_ping.chart = pingData.chart;
|
||||
this.stats.low_ping.value = this.humanTime(pingData.low);
|
||||
},
|
||||
smallText(s) {
|
||||
if (s.online) {
|
||||
return `Online, last checked ${this.ago(this.parseTime(s.last_success))}`
|
||||
return `Online, last checked ${this.ago(s.last_success)}`
|
||||
} else {
|
||||
const last = s.last_failure
|
||||
if (last) {
|
||||
return `Offline, last error: ${last} ${this.ago(this.parseTime(last.created_at))}`
|
||||
return `Offline, last error: ${last} ${this.ago(last.created_at)}`
|
||||
}
|
||||
return `Offline`
|
||||
}
|
||||
|
|
|
@ -105,16 +105,16 @@
|
|||
tooltip: {
|
||||
theme: false,
|
||||
enabled: true,
|
||||
custom: function({series, seriesIndex, dataPointIndex, w}) {
|
||||
custom: ({series, seriesIndex, dataPointIndex, w}) => {
|
||||
let ts = w.globals.seriesX[seriesIndex][dataPointIndex];
|
||||
const dt = new Date(ts).toLocaleDateString("en-us", timeoptions)
|
||||
let val = series[seriesIndex][dataPointIndex];
|
||||
if (val >= 1000) {
|
||||
val = (val * 0.1).toFixed(0) + " milliseconds"
|
||||
} else {
|
||||
val = (val * 0.01).toFixed(0) + " microseconds"
|
||||
}
|
||||
return `<div class="chartmarker"><span>Average Response Time: </span><span class="font-3">${val}</span><span>${dt}</span></div>`
|
||||
let humanVal = this.humanTime(val);
|
||||
return `<div class="chartmarker">
|
||||
<span>Average Response Time: </span>
|
||||
<span class="font-3">${humanVal}</span>
|
||||
<span>${dt}</span>
|
||||
</div>`
|
||||
},
|
||||
fixed: {
|
||||
enabled: true,
|
||||
|
@ -170,14 +170,14 @@
|
|||
methods: {
|
||||
async chartHits(group) {
|
||||
const start = this.nowSubtract(84600 * 3)
|
||||
this.data = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(new Date()), group, false)
|
||||
this.data = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(new Date()), group, true)
|
||||
|
||||
if (this.data.length === 0 && group !== "1h") {
|
||||
await this.chartHits("1h")
|
||||
}
|
||||
this.series = [{
|
||||
name: this.service.name,
|
||||
...this.convertToChartData(this.data, 0.01)
|
||||
...this.convertToChartData(this.data)
|
||||
}]
|
||||
this.ready = true
|
||||
}
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
<div v-for="(failure, index) in failures" :key="index" class="mb-2 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">{{failure.issue}}</h5>
|
||||
<small>{{toLocal(failure.created_at)}}</small>
|
||||
<small>{{niceDate(failure.created_at)}}</small>
|
||||
</div>
|
||||
<p class="mb-1">{{failure.issue}}</p>
|
||||
</div>
|
||||
|
||||
<nav aria-label="Page navigation example">
|
||||
<nav aria-label="page navigation example">
|
||||
<ul class="pagination">
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="#" aria-label="Previous">
|
||||
|
@ -49,6 +49,7 @@ export default {
|
|||
failures: [],
|
||||
limit: 15,
|
||||
offset: 0,
|
||||
total: this.service.stats.failures
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
|
@ -61,10 +62,6 @@ export default {
|
|||
} else {
|
||||
return `Offline, last error: ${s.last_failure.issue} ${this.ago(s.last_failure.created_at)}`
|
||||
}
|
||||
},
|
||||
ago(t1) {
|
||||
const tm = this.parseTime(t1)
|
||||
return this.duration(this.$moment().utc(), tm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -110,7 +110,7 @@
|
|||
|
||||
let dataArr = []
|
||||
data.forEach((d) => {
|
||||
dataArr.push({x: this.parseTime(d.timeframe).getUTCDate(), y: d.amount});
|
||||
dataArr.push({x: this.parseISO(d.timeframe), y: d.amount});
|
||||
});
|
||||
|
||||
let date = new Date(dataArr[0].x);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template v-if="service">
|
||||
<div class="col-12 card mb-4" style="min-height: 360px" :class="{'offline-card': !service.online}">
|
||||
<div class="col-12 card mb-4" style="min-height: 280px" :class="{'offline-card': !service.online}">
|
||||
<div class="card-body p-3 p-md-1 pt-md-3 pb-md-1">
|
||||
<h4 class="card-title mb-4"><router-link :to="serviceLink(service)">{{service.name}}</router-link>
|
||||
<span class="badge float-right" :class="{'badge-success': service.online, 'badge-danger': !service.online}">
|
||||
|
@ -8,11 +8,11 @@
|
|||
</h4>
|
||||
|
||||
<transition name="fade">
|
||||
<div v-if="loaded && service.online" class="row">
|
||||
<div class="col-md-6 col-sm-12 mt-2 mt-md-0">
|
||||
<div v-if="loaded && service.online" class="row pb-3">
|
||||
<div class="col-md-6 col-sm-12 mt-2 mt-md-0 mb-3">
|
||||
<ServiceSparkLine :title="set2_name" subtitle="Latency Last 24 Hours" :series="set2"/>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12 mt-4 mt-md-0">
|
||||
<div class="col-md-6 col-sm-12 mt-4 mt-md-0 mb-3">
|
||||
<ServiceSparkLine :title="set1_name" subtitle="Latency Last 7 Days" :series="set1"/>
|
||||
</div>
|
||||
|
||||
|
@ -42,8 +42,33 @@
|
|||
:end="this.toUnix(this.nowSubtract(86400))"
|
||||
group="24h" expression="latencyPercent"/>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<button @click.prevent="openTab='incident'" class="btn btn-block btn-outline-secondary">Create Incident</button>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<button @click.prevent="openTab='message'" class="btn btn-block btn-outline-secondary">Create Announcement</button>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<button @click.prevent="openTab='failures'" class="btn btn-block btn-outline-secondary" :disabled="!service.failures">
|
||||
Failures <span class="badge badge-danger float-right mt-1">{{service.stats.failures}}</span></button>
|
||||
</div>
|
||||
|
||||
<div v-if="openTab === 'incident'" class="col-12 mt-4">
|
||||
<FormIncident :service="service" />
|
||||
</div>
|
||||
|
||||
<div v-if="openTab === 'message'" class="col-12 mt-4">
|
||||
<FormMessage :service="service"/>
|
||||
</div>
|
||||
|
||||
<div v-if="openTab === 'failures'" class="col-12 mt-4">
|
||||
<ServiceFailures :service="service"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
</div>
|
||||
|
||||
<span v-for="(failure, index) in failures" v-bind:key="index" class="alert alert-light">
|
||||
|
@ -55,6 +80,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import FormIncident from '../../forms/Incident';
|
||||
import FormMessage from '../../forms/Message';
|
||||
import ServiceFailures from './ServiceFailures';
|
||||
import ServiceSparkLine from "./ServiceSparkLine";
|
||||
import Api from "../../API";
|
||||
import StatsGen from "./StatsGen";
|
||||
|
@ -62,7 +90,10 @@
|
|||
export default {
|
||||
name: 'ServiceInfo',
|
||||
components: {
|
||||
StatsGen,
|
||||
ServiceFailures,
|
||||
FormIncident,
|
||||
FormMessage,
|
||||
StatsGen,
|
||||
ServiceSparkLine
|
||||
},
|
||||
props: {
|
||||
|
@ -73,6 +104,7 @@
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
openTab: "",
|
||||
set1: [],
|
||||
set2: [],
|
||||
loaded: false,
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
fixed: {
|
||||
enabled: true,
|
||||
position: 'topRight',
|
||||
offsetX: -5,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
},
|
||||
x: {
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
<template>
|
||||
<div class="row stats_area mt-5 mb-4">
|
||||
<div class="col-4">
|
||||
<span class="lg_number">{{service.avg_response}}ms</span>
|
||||
Average Response
|
||||
<span class="font-5 d-block font-weight-bold">{{humanTime(service.avg_response)}}</span>
|
||||
<span class="font-1 subtitle">Average Response</span>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<span class="lg_number">{{service.online_24_hours}}%</span>
|
||||
Uptime last 24 Hours
|
||||
<span class="font-5 d-block font-weight-bold">{{service.online_24_hours}}%</span>
|
||||
<span class="font-1 subtitle">Uptime last 24 Hours</span>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<span class="lg_number">{{service.online_7_days}}%</span>
|
||||
Uptime last 7 Days
|
||||
<span class="font-5 d-block font-weight-bold">{{service.online_7_days}}%</span>
|
||||
<span class="font-1 subtitle">Uptime last 7 Days</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
<template>
|
||||
<div>
|
||||
|
||||
<div v-for="(incident, i) in incidents" class="card contain-card text-black-50 bg-white mb-4">
|
||||
<div class="card-header">Incident: {{incident.title}}
|
||||
<button @click="deleteIncident(incident)" class="btn btn-sm btn-danger float-right">
|
||||
<font-awesome-icon icon="times" /> Delete
|
||||
</button></div>
|
||||
<div class="card-body bg-light pt-1">
|
||||
|
||||
<FormIncidentUpdates :incident="incident"/>
|
||||
|
||||
<span class="font-2">Created: {{niceDate(incident.created_at)}} | Last Update: {{niceDate(incident.updated_at)}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card contain-card text-black-50 bg-white mb-5">
|
||||
<div class="card-header">Create Incident for {{service.name}}</div>
|
||||
<div class="card-body">
|
||||
<form @submit.prevent="createIncident">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">Title</label>
|
||||
<div class="col-sm-8">
|
||||
<input v-model="incident.title" type="text" name="title" class="form-control" id="title" placeholder="Incident Title" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">Description</label>
|
||||
<div class="col-sm-8">
|
||||
<textarea v-model="incident.description" rows="5" name="description" class="form-control" id="description" required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-12">
|
||||
<button @click.prevent="createIncident"
|
||||
:disabled="!incident.title || !incident.description"
|
||||
type="submit" class="btn btn-block btn-primary">
|
||||
Create Incident
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-danger d-none" id="alerter" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "../API";
|
||||
import flatPickr from 'vue-flatpickr-component';
|
||||
import 'flatpickr/dist/flatpickr.css';
|
||||
import FormIncidentUpdates from './IncidentUpdates';
|
||||
|
||||
export default {
|
||||
name: 'FormIncident',
|
||||
components: {
|
||||
FormIncidentUpdates
|
||||
},
|
||||
props: {
|
||||
service: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
incident: {
|
||||
title: "",
|
||||
description: "",
|
||||
service: this.service.id,
|
||||
},
|
||||
incidents: [],
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
this.incidents = await Api.incidents_service(this.service)
|
||||
},
|
||||
methods: {
|
||||
async createIncident() {
|
||||
await Api.incident_create(this.incident)
|
||||
const incidents = await Api.incidents()
|
||||
this.$store.commit('setIncidents', incidents)
|
||||
this.incident = {}
|
||||
},
|
||||
async deleteIncident(incident) {
|
||||
let c = confirm(`Are you sure you want to delete '${incident.title}'?`)
|
||||
if (c) {
|
||||
await Api.incident_delete(incident)
|
||||
this.incidents = await Api.incidents_service(this.service)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
</style>
|
|
@ -0,0 +1,96 @@
|
|||
<template>
|
||||
<div>
|
||||
|
||||
<div v-for="(update, i) in updates" class="col-12 bg-active card pt-2 pb-2 mt-3 pl-3 pr-3">
|
||||
<span class="font-4">
|
||||
<font-awesome-icon v-if="update.type === 'Resolved'" icon="check-circle" class="mr-2"/>
|
||||
<font-awesome-icon v-if="update.type === 'Update'" icon="asterisk" class="mr-2"/>
|
||||
<font-awesome-icon v-if="update.type === 'Investigating'" icon="lightbulb" class="mr-2"/>
|
||||
<font-awesome-icon v-if="update.type === 'Unknown'" icon="question" class="mr-2"/>
|
||||
|
||||
{{update.type}}
|
||||
</span>
|
||||
<span class="font-3 mt-3">{{update.message}}</span>
|
||||
</div>
|
||||
|
||||
<div class="col-12 bg-active card pt-2 pb-2 mt-3 pl-3 pr-3">
|
||||
|
||||
<form @submit.prevent="createIncidentUpdate">
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">Update Type</label>
|
||||
<div class="col-sm-8">
|
||||
<select v-model="incident_update.type" class="form-control">
|
||||
<option value="Investigating">Investigating</option>
|
||||
<option value="Update">Update</option>
|
||||
<option value="Unknown">Unknown</option>
|
||||
<option value="Resolved">Resolved</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">New Update</label>
|
||||
<div class="col-sm-8">
|
||||
<textarea v-model="incident_update.message" rows="5" name="description" class="form-control" id="description" required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-12">
|
||||
<button @click.prevent="createIncidentUpdate"
|
||||
:disabled="!incident.title || !incident.description"
|
||||
type="submit" class="btn btn-block btn-primary">
|
||||
Add Update
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "../API";
|
||||
import flatPickr from 'vue-flatpickr-component';
|
||||
import 'flatpickr/dist/flatpickr.css';
|
||||
|
||||
export default {
|
||||
name: 'FormIncidentUpdates',
|
||||
components: {
|
||||
|
||||
},
|
||||
props: {
|
||||
incident: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
updates: this.incident.updates,
|
||||
incident_update: {
|
||||
incident: this.incident,
|
||||
message: "",
|
||||
type: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
this.updates = await Api.incident_updates(this.incident)
|
||||
},
|
||||
methods: {
|
||||
async createIncidentUpdate(incident) {
|
||||
await Api.incident_update_create(incident, this.incident_update)
|
||||
const updates = await Api.incident_updates()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
</style>
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="card contain-card text-black-50 bg-white mb-5">
|
||||
<div class="card-header">{{message.id ? `Update ${message.title}` : "Create Message"}}
|
||||
<div class="card-header">{{message.id ? `Update ${message.title}` : "Create Annoucement"}}
|
||||
|
||||
<transition name="slide-fade">
|
||||
<button @click="removeEdit" v-if="message.id" class="btn btn-sm float-right btn-danger btn-sm">Close</button>
|
||||
|
@ -12,7 +12,7 @@
|
|||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">Title</label>
|
||||
<div class="col-sm-8">
|
||||
<input v-model="message.title" type="text" name="title" class="form-control" id="title" placeholder="Message Title" required>
|
||||
<input v-model="message.title" type="text" name="title" class="form-control" id="title" placeholder="Announcement Title" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -24,7 +24,7 @@
|
|||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">Message Date Range</label>
|
||||
<label class="col-sm-4 col-form-label">Announcement Date Range</label>
|
||||
<div class="col-sm-4">
|
||||
<flatPickr v-model="message.start_on" @on-change="startChange" :config="config" type="text" name="start_on" class="form-control form-control-plaintext" id="start_on" value="0001-01-01T00:00:00Z" required />
|
||||
</div>
|
||||
|
@ -33,7 +33,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div v-show="this.service === null" class="form-group row">
|
||||
<label for="service_id" class="col-sm-4 col-form-label">Service</label>
|
||||
<div class="col-sm-8">
|
||||
<select v-model="message.service_id" class="form-control" name="service" id="service_id">
|
||||
|
@ -104,6 +104,9 @@
|
|||
in_message: {
|
||||
type: Object
|
||||
},
|
||||
service: {
|
||||
type: Object
|
||||
},
|
||||
edit: {
|
||||
type: Function
|
||||
}
|
||||
|
@ -135,7 +138,12 @@
|
|||
this.message = this.in_message
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
mounted () {
|
||||
if (this.service) {
|
||||
this.service_id = this.service.id
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
startChange(e) {
|
||||
window.console.log(e)
|
||||
},
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<template>
|
||||
<div class="card text-black-50 bg-white mb-5">
|
||||
<div class="card-header text-capitalize">{{notifier.title}}</div>
|
||||
<div class="card-body">
|
||||
<form @submit.prevent="saveNotifier">
|
||||
|
||||
<div v-if="error" class="alert alert-danger col-12" role="alert">{{error}}</div>
|
||||
|
@ -7,7 +10,6 @@
|
|||
<i class="fa fa-smile-beam"></i> The {{notifier.method}} notifier is working correctly!
|
||||
</div>
|
||||
|
||||
<h4 class="text-capitalize">{{notifier.title}}</h4>
|
||||
<p class="small text-muted" v-html="notifier.description"/>
|
||||
|
||||
<div v-for="(form, index) in notifier.form" v-bind:key="index" class="form-group">
|
||||
|
@ -50,11 +52,12 @@
|
|||
|
||||
</div>
|
||||
|
||||
<span class="d-block small text-center mt-5 mb-5">
|
||||
</form>
|
||||
</div>
|
||||
<span class="d-block small text-center mb-3">
|
||||
<span class="text-capitalize">{{notifier.title}}</span> Notifier created by <a :href="notifier.author_url" target="_blank">{{notifier.author}}</a>
|
||||
</span>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -79,7 +79,8 @@
|
|||
},
|
||||
watch: {
|
||||
in_user() {
|
||||
const u = this.in_user
|
||||
let u = this.in_user
|
||||
u.password = null
|
||||
this.user = u
|
||||
}
|
||||
},
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
import axios from 'axios'
|
||||
import API from './API'
|
||||
import subSeconds from 'date-fns/subSeconds'
|
||||
import getUnixTime from 'date-fns/getUnixTime'
|
||||
|
||||
class Graphing {
|
||||
constructor () {
|
||||
|
||||
}
|
||||
|
||||
subtract(seconds) {
|
||||
return getUnixTime(subSeconds(new Date(), seconds))
|
||||
}
|
||||
|
||||
now() {
|
||||
return getUnixTime(new Date())
|
||||
}
|
||||
|
||||
async hits(service, days, group='24h') {
|
||||
const query = await API.service_hits(service.id, this.subtract(86400 * days), this.now(), group)
|
||||
let total = 0;
|
||||
let high = 0;
|
||||
let low = 99999999;
|
||||
query.map((d) => {
|
||||
if (high <= d.amount) {
|
||||
high = d.amount
|
||||
}
|
||||
if (low >= d.amount && d.amount !== 0) {
|
||||
low = d.amount
|
||||
}
|
||||
total += d.amount;
|
||||
});
|
||||
const average = total / query.length;
|
||||
return {chart: query, average: average, total: total, high: high, low: low}
|
||||
}
|
||||
|
||||
async pings(service, days, group='24h') {
|
||||
const query = await API.service_ping(service.id, this.subtract(86400 * days), this.now(), group)
|
||||
let total = 0;
|
||||
let high = 0;
|
||||
let low = 99999999;
|
||||
query.map((d) => {
|
||||
if (high <= d.amount) {
|
||||
high = d.amount
|
||||
}
|
||||
if (low >= d.amount && d.amount !== 0) {
|
||||
low = d.amount
|
||||
}
|
||||
total += d.amount;
|
||||
});
|
||||
const average = total / query.length;
|
||||
return {chart: query, average: average, total: total, high: high, low: low}
|
||||
}
|
||||
|
||||
async failures(service, days, group='24h') {
|
||||
const query = await API.service_failures_data(service.id, this.subtract(86400 * days), this.now(), group)
|
||||
let total = 0;
|
||||
let high = 0;
|
||||
let lowest = 99999999;
|
||||
query.map((d) => {
|
||||
if (d.amount >= high) {
|
||||
high = d.amount
|
||||
}
|
||||
if (lowest >= d.amount) {
|
||||
lowest = d.amount
|
||||
}
|
||||
total += d.amount;
|
||||
});
|
||||
const average = total / query.length;
|
||||
return {data: query, average: average, total: total, high: high, low: lowest}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const graphing = new Graphing()
|
||||
export default graphing
|
|
@ -16,7 +16,7 @@ export default Vue.mixin({
|
|||
return new Date.UTC(val)
|
||||
},
|
||||
ago(t1) {
|
||||
return formatDistanceToNow(t1)
|
||||
return formatDistanceToNow(parseISO(t1))
|
||||
},
|
||||
daysInMonth(t1) {
|
||||
return lastDayOfMonth(t1)
|
||||
|
@ -29,17 +29,10 @@ export default Vue.mixin({
|
|||
},
|
||||
niceDate(val) {
|
||||
return format(parseISO(val), "EEEE, MMM do h:mma")
|
||||
},
|
||||
parseTime(val) {
|
||||
return parseISO(val)
|
||||
},
|
||||
parseISO(v) {
|
||||
return parseISO(v)
|
||||
},
|
||||
toLocal(val, suf = 'at') {
|
||||
const t = this.parseTime(val)
|
||||
return format(t, `EEEE, MMM do h:mma`)
|
||||
},
|
||||
toUnix(val) {
|
||||
return getUnixTime(val)
|
||||
},
|
||||
|
@ -47,7 +40,7 @@ export default Vue.mixin({
|
|||
return fromUnixTime(val)
|
||||
},
|
||||
isBetween(t1, t2) {
|
||||
return differenceInSeconds(t1, t2) >= 0
|
||||
return differenceInSeconds(parseISO(t1), parseISO(t2)) >= 0
|
||||
},
|
||||
hour() {
|
||||
return 3600
|
||||
|
@ -116,6 +109,12 @@ export default Vue.mixin({
|
|||
})
|
||||
return {data: newSet}
|
||||
},
|
||||
humanTime(val) {
|
||||
if (val >= 10000) {
|
||||
return Math.floor(val / 10000) + "ms"
|
||||
}
|
||||
return Math.floor(val / 1000) + "μs"
|
||||
},
|
||||
lastDayOfMonth(month) {
|
||||
return new Date(Date.UTC(new Date().getUTCFullYear(), month + 1, 0))
|
||||
},
|
||||
|
|
|
@ -3,6 +3,18 @@
|
|||
|
||||
<Header/>
|
||||
|
||||
<div v-show="$store.getters.groups.length === 0">
|
||||
<div class="col-12">
|
||||
<div class="col-12 loader backgroundAnimated"></div>
|
||||
<div class="col-12 mt-3 border-0 loader backgroundAnimated" style="min-height: 200px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4">
|
||||
<div class="col-12 loader backgroundAnimated"></div>
|
||||
<div class="col-12 mt-3 border-0 loader backgroundAnimated" style="min-height: 200px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-for="(group, index) in $store.getters.groupsInOrder" v-bind:key="index">
|
||||
<Group :group=group />
|
||||
</div>
|
||||
|
|
|
@ -37,10 +37,6 @@
|
|||
<ServiceHeatmap :service="service"/>
|
||||
</div>
|
||||
|
||||
<!-- <div v-if="series" class="service-chart-container">-->
|
||||
<!-- <apexchart width="100%" height="300" type="range" :options="dailyRangeOpts" :series="series"></apexchart>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<nav v-if="service.failures" class="nav nav-pills flex-column flex-sm-row mt-3" id="service_tabs">
|
||||
<a @click="tab='failures'" class="flex-sm-fill text-sm-center nav-link active">Failures</a>
|
||||
<a @click="tab='incidents'" class="flex-sm-fill text-sm-center nav-link">Incidents</a>
|
||||
|
@ -295,8 +291,8 @@ export default {
|
|||
this.messages = this.$store.getters.serviceMessages(this.service.id)
|
||||
},
|
||||
messageInRange(message) {
|
||||
const start = this.isBetween(this.now(), this.parseTime(message.start_on))
|
||||
const end = this.isBetween(this.parseTime(message.end_on), this.now())
|
||||
const start = this.isBetween(new Date(), message.start_on)
|
||||
const end = this.isBetween(message.end_on, new Date())
|
||||
return start && end
|
||||
},
|
||||
async getService(s) {
|
||||
|
@ -327,7 +323,7 @@ export default {
|
|||
this.ready = true
|
||||
},
|
||||
startEndTimes() {
|
||||
const start = this.toUnix(this.parseTime(this.service.stats.first_hit))
|
||||
const start = this.toUnix(this.service.stats.first_hit)
|
||||
const end = this.toUnix(new Date())
|
||||
return {start, end}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ export default new Vuex.Store({
|
|||
services: state => state.services,
|
||||
groups: state => state.groups,
|
||||
messages: state => state.messages,
|
||||
incidents: state => state.incidents,
|
||||
users: state => state.users,
|
||||
notifiers: state => state.notifiers,
|
||||
|
||||
|
@ -100,6 +101,9 @@ export default new Vuex.Store({
|
|||
setMessages (state, messages) {
|
||||
state.messages = messages
|
||||
},
|
||||
setIncidents (state, incidents) {
|
||||
state.incidents = incidents
|
||||
},
|
||||
setUsers (state, users) {
|
||||
state.users = users
|
||||
},
|
||||
|
@ -121,6 +125,8 @@ export default new Vuex.Store({
|
|||
context.commit("setServices", services);
|
||||
const messages = await Api.messages()
|
||||
context.commit("setMessages", messages)
|
||||
const incidents = await Api.incidents()
|
||||
context.commit("setIncidents", incidents)
|
||||
context.commit("setHasPublicData", true)
|
||||
// if (core.logged_in) {
|
||||
// const notifiers = await Api.notifiers()
|
||||
|
|
|
@ -100,6 +100,7 @@ func TestSetupRoutes(t *testing.T) {
|
|||
}
|
||||
return nil
|
||||
},
|
||||
AfterTest: StopServices,
|
||||
}}
|
||||
|
||||
for _, v := range tests {
|
||||
|
@ -284,3 +285,10 @@ func SetTestENV() error {
|
|||
func UnsetTestENV() error {
|
||||
return os.Setenv("GO_ENV", "production")
|
||||
}
|
||||
|
||||
func StopServices() error {
|
||||
for _, s := range services.All() {
|
||||
s.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -13,6 +13,32 @@ func apiAllIncidentsHandler(w http.ResponseWriter, r *http.Request) {
|
|||
returnJson(inc, w, r)
|
||||
}
|
||||
|
||||
func apiServiceIncidentsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
incids := incidents.FindByService(utils.ToInt(vars["id"]))
|
||||
returnJson(incids, w, r)
|
||||
}
|
||||
|
||||
func apiCreateIncidentUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
var update *incidents.IncidentUpdate
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
err := decoder.Decode(&update)
|
||||
if err != nil {
|
||||
sendErrorJson(err, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
update.IncidentId = utils.ToInt(vars["id"])
|
||||
|
||||
err = update.Create()
|
||||
if err != nil {
|
||||
sendErrorJson(err, w, r)
|
||||
return
|
||||
}
|
||||
sendJsonAction(update, "create", w, r)
|
||||
}
|
||||
|
||||
func apiCreateIncidentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var incident *incidents.Incident
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
|
@ -62,3 +88,18 @@ func apiDeleteIncidentHandler(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
sendJsonAction(incident, "delete", w, r)
|
||||
}
|
||||
|
||||
func apiDeleteIncidentUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
update, err := incidents.FindUpdate(utils.ToInt(vars["uid"]))
|
||||
if err != nil {
|
||||
sendErrorJson(err, w, r)
|
||||
return
|
||||
}
|
||||
err = update.Delete()
|
||||
if err != nil {
|
||||
sendErrorJson(err, w, r)
|
||||
return
|
||||
}
|
||||
sendJsonAction(update, "delete", w, r)
|
||||
}
|
||||
|
|
|
@ -131,10 +131,14 @@ func Router() *mux.Router {
|
|||
//api.Handle("/api/services/{id}/heatmap", cached("30s", "application/json", apiServiceHeatmapHandler)).Methods("GET")
|
||||
|
||||
// API INCIDENTS Routes
|
||||
api.Handle("/api/incidents", readOnly(apiAllIncidentsHandler, false)).Methods("GET")
|
||||
api.Handle("/api/incidents", authenticated(apiCreateIncidentHandler, false)).Methods("POST")
|
||||
api.Handle("/api/incidents/:id", authenticated(apiIncidentUpdateHandler, false)).Methods("POST")
|
||||
api.Handle("/api/incidents/:id", authenticated(apiDeleteIncidentHandler, false)).Methods("DELETE")
|
||||
api.Handle("/api/services/{id}/incidents", http.HandlerFunc(apiServiceIncidentsHandler)).Methods("GET")
|
||||
api.Handle("/api/services/{id}/incidents", authenticated(apiCreateIncidentHandler, false)).Methods("POST")
|
||||
api.Handle("/api/incidents/{id}", authenticated(apiIncidentUpdateHandler, false)).Methods("POST")
|
||||
api.Handle("/api/incidents/{id}", authenticated(apiDeleteIncidentHandler, false)).Methods("DELETE")
|
||||
|
||||
// API INCIDENTS UPDATES Routes
|
||||
api.Handle("/api/incidents/{id}/updates", authenticated(apiCreateIncidentUpdateHandler, false)).Methods("POST")
|
||||
api.Handle("/api/incidents/{id}/updates/{uid}", authenticated(apiDeleteIncidentUpdateHandler, false)).Methods("DELETE")
|
||||
|
||||
// API USER Routes
|
||||
api.Handle("/api/users", authenticated(apiAllUsersHandler, false)).Methods("GET")
|
||||
|
|
|
@ -104,7 +104,7 @@ func TestApiServiceRoutes(t *testing.T) {
|
|||
Name: "Statping Reorder Services",
|
||||
URL: "/api/services/reorder",
|
||||
Method: "POST",
|
||||
Body: `[{"service":1,"order":1},{"service":5,"order":2},{"service":2,"order":3},{"service":3,"order":4},{"service":4,"order":5}]`,
|
||||
Body: `[{"service":1,"order":1},{"service":4,"order":2},{"service":2,"order":3},{"service":3,"order":4}]`,
|
||||
ExpectedStatus: 200,
|
||||
HttpHeaders: []string{"Content-Type=application/json"},
|
||||
},
|
||||
|
@ -114,18 +114,18 @@ func TestApiServiceRoutes(t *testing.T) {
|
|||
HttpHeaders: []string{"Content-Type=application/json"},
|
||||
Method: "POST",
|
||||
Body: `{
|
||||
"name": "New Service",
|
||||
"domain": "https://statping.com",
|
||||
"expected": "",
|
||||
"expected_status": 200,
|
||||
"check_interval": 30,
|
||||
"type": "http",
|
||||
"method": "GET",
|
||||
"post_data": "",
|
||||
"port": 0,
|
||||
"timeout": 30,
|
||||
"order_id": 0
|
||||
}`,
|
||||
"name": "New Service",
|
||||
"domain": "https://statping.com",
|
||||
"expected": "",
|
||||
"expected_status": 200,
|
||||
"check_interval": 30,
|
||||
"type": "http",
|
||||
"method": "GET",
|
||||
"post_data": "",
|
||||
"port": 0,
|
||||
"timeout": 30,
|
||||
"order_id": 0
|
||||
}`,
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContains: []string{`"status":"success","type":"service","method":"create"`},
|
||||
FuncTest: func() error {
|
||||
|
@ -142,18 +142,18 @@ func TestApiServiceRoutes(t *testing.T) {
|
|||
HttpHeaders: []string{"Content-Type=application/json"},
|
||||
Method: "POST",
|
||||
Body: `{
|
||||
"name": "Updated New Service",
|
||||
"domain": "https://google.com",
|
||||
"expected": "",
|
||||
"expected_status": 200,
|
||||
"check_interval": 60,
|
||||
"type": "http",
|
||||
"method": "GET",
|
||||
"post_data": "",
|
||||
"port": 0,
|
||||
"timeout": 10,
|
||||
"order_id": 0
|
||||
}`,
|
||||
"name": "Updated New Service",
|
||||
"domain": "https://google.com",
|
||||
"expected": "",
|
||||
"expected_status": 200,
|
||||
"check_interval": 60,
|
||||
"type": "http",
|
||||
"method": "GET",
|
||||
"post_data": "",
|
||||
"port": 0,
|
||||
"timeout": 10,
|
||||
"order_id": 0
|
||||
}`,
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContains: []string{`"status":"success"`, `"name":"Updated New Service"`, `"method":"update"`},
|
||||
},
|
||||
|
|
|
@ -2,28 +2,37 @@ package handlers
|
|||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestApiUsersRoutes(t *testing.T) {
|
||||
form := url.Values{}
|
||||
form.Add("username", "adminupdated")
|
||||
form.Add("password", "password12345")
|
||||
|
||||
badForm := url.Values{}
|
||||
badForm.Add("username", "adminupdated")
|
||||
badForm.Add("password", "wrongpassword")
|
||||
|
||||
tests := []HTTPTest{
|
||||
{
|
||||
Name: "Statping All Users",
|
||||
URL: "/api/users",
|
||||
Method: "GET",
|
||||
ExpectedStatus: 200,
|
||||
ResponseLen: 4,
|
||||
ResponseLen: 1,
|
||||
}, {
|
||||
Name: "Statping Create User",
|
||||
URL: "/api/users",
|
||||
HttpHeaders: []string{"Content-Type=application/json"},
|
||||
Method: "POST",
|
||||
Body: `{
|
||||
"username": "adminuser2",
|
||||
"email": "info@adminemail.com",
|
||||
"password": "passsword123",
|
||||
"admin": true
|
||||
}`,
|
||||
"username": "adminuser2",
|
||||
"email": "info@adminemail.com",
|
||||
"password": "passsword123",
|
||||
"admin": true
|
||||
}`,
|
||||
ExpectedStatus: 200,
|
||||
}, {
|
||||
Name: "Statping View User",
|
||||
|
@ -35,35 +44,33 @@ func TestApiUsersRoutes(t *testing.T) {
|
|||
URL: "/api/users/1",
|
||||
Method: "POST",
|
||||
Body: `{
|
||||
"username": "adminupdated",
|
||||
"email": "info@email.com",
|
||||
"password": "password12345",
|
||||
"admin": true
|
||||
}`,
|
||||
"username": "adminupdated",
|
||||
"email": "info@email.com",
|
||||
"password": "password12345",
|
||||
"admin": true
|
||||
}`,
|
||||
ExpectedStatus: 200,
|
||||
}, {
|
||||
Name: "Statping Delete User",
|
||||
URL: "/api/users/1",
|
||||
URL: "/api/users/2",
|
||||
Method: "DELETE",
|
||||
ExpectedStatus: 200,
|
||||
}, {
|
||||
Name: "Statping Login User",
|
||||
URL: "/api/login",
|
||||
Method: "POST",
|
||||
Body: `{
|
||||
"username": "admin",
|
||||
"password": "admin"
|
||||
}`, ExpectedContains: []string{`"token"`},
|
||||
ExpectedStatus: 200,
|
||||
Name: "Statping Login User",
|
||||
URL: "/api/login",
|
||||
Method: "POST",
|
||||
Body: form.Encode(),
|
||||
ExpectedContains: []string{`"token"`},
|
||||
ExpectedStatus: 200,
|
||||
HttpHeaders: []string{"Content-Type=application/x-www-form-urlencoded"},
|
||||
}, {
|
||||
Name: "Statping Bad Login User",
|
||||
URL: "/api/login",
|
||||
Method: "POST",
|
||||
Body: `{
|
||||
"username": "admin",
|
||||
"password": "wrongpassword"
|
||||
}`, ExpectedContains: []string{`"token"`},
|
||||
ExpectedStatus: 200,
|
||||
Name: "Statping Bad Login User",
|
||||
URL: "/api/login",
|
||||
Method: "POST",
|
||||
Body: badForm.Encode(),
|
||||
ExpectedContains: []string{`incorrect authentication`},
|
||||
ExpectedStatus: 200,
|
||||
HttpHeaders: []string{"Content-Type=application/x-www-form-urlencoded"},
|
||||
}}
|
||||
|
||||
for _, v := range tests {
|
||||
|
|
|
@ -62,7 +62,7 @@ func TestInit(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestFind(t *testing.T) {
|
||||
Append(example)
|
||||
appendList(example)
|
||||
itemer, err := Find(example.Method)
|
||||
require.Nil(t, err)
|
||||
|
||||
|
|
|
@ -85,7 +85,9 @@ func (d *DbConfig) DropDatabase() error {
|
|||
}
|
||||
|
||||
func (d *DbConfig) Close() {
|
||||
d.Db.Close()
|
||||
if d.Db != nil {
|
||||
d.Db.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDatabase will CREATE TABLES for each of the Statping elements
|
||||
|
|
|
@ -59,22 +59,21 @@ func (h Hitters) DeleteAll() error {
|
|||
return q.Error()
|
||||
}
|
||||
|
||||
func (h Hitters) Sum() float64 {
|
||||
result := struct {
|
||||
amount float64
|
||||
}{0}
|
||||
func (h Hitters) Sum() int64 {
|
||||
var r IntResult
|
||||
|
||||
h.db.Select("AVG(latency) as amount").Scan(&result)
|
||||
return result.amount
|
||||
h.db.Select("CAST(SUM(latency) as INT) as amount").Scan(&r)
|
||||
return r.Amount
|
||||
}
|
||||
|
||||
func (h Hitters) Avg() float64 {
|
||||
result := struct {
|
||||
amount float64
|
||||
}{0}
|
||||
type IntResult struct {
|
||||
Amount int64
|
||||
}
|
||||
|
||||
h.db.Select("AVG(latency) as amount").Scan(&result)
|
||||
return result.amount
|
||||
func (h Hitters) Avg() int64 {
|
||||
var r IntResult
|
||||
h.db.Select("CAST(AVG(latency) as INT) as amount").Scan(&r)
|
||||
return r.Amount
|
||||
}
|
||||
|
||||
func AllHits(obj ColumnIDInterfacer) Hitters {
|
||||
|
|
|
@ -2,10 +2,14 @@ package incidents
|
|||
|
||||
import "github.com/statping/statping/database"
|
||||
|
||||
var db database.Database
|
||||
var (
|
||||
db database.Database
|
||||
dbUpdate database.Database
|
||||
)
|
||||
|
||||
func SetDB(database database.Database) {
|
||||
db = database.Model(&Incident{})
|
||||
dbUpdate = database.Model(&IncidentUpdate{})
|
||||
}
|
||||
|
||||
func Find(id int64) (*Incident, error) {
|
||||
|
@ -14,6 +18,23 @@ func Find(id int64) (*Incident, error) {
|
|||
return &incident, q.Error()
|
||||
}
|
||||
|
||||
func FindUpdate(id int64) (*IncidentUpdate, error) {
|
||||
var update IncidentUpdate
|
||||
q := dbUpdate.Where("id = ?", id).Find(&update)
|
||||
return &update, q.Error()
|
||||
}
|
||||
|
||||
func FindByService(id int64) []*Incident {
|
||||
var incidents []*Incident
|
||||
db.Where("service = ?", id).Find(&incidents)
|
||||
for _, i := range incidents {
|
||||
var updates []*IncidentUpdate
|
||||
dbUpdate.Where("incident = ?", id).Find(&updates)
|
||||
i.AllUpdates = updates
|
||||
}
|
||||
return incidents
|
||||
}
|
||||
|
||||
func All() []*Incident {
|
||||
var incidents []*Incident
|
||||
db.Find(&incidents)
|
||||
|
|
|
@ -38,7 +38,7 @@ func humanMicro(val int64) string {
|
|||
if val < 10000 {
|
||||
return fmt.Sprintf("%d μs", val)
|
||||
}
|
||||
return fmt.Sprintf("%v ms", float64(val)*0.001)
|
||||
return fmt.Sprintf("%0.0f ms", float64(val)*0.001)
|
||||
}
|
||||
|
||||
// IsRunning returns true if the service go routine is running
|
||||
|
@ -70,8 +70,6 @@ func SelectAllServices(start bool) (map[int64]*Service, error) {
|
|||
|
||||
for _, s := range all() {
|
||||
|
||||
allServices[s.Id] = s
|
||||
|
||||
if start {
|
||||
CheckinProcess(s)
|
||||
}
|
||||
|
@ -85,6 +83,8 @@ func SelectAllServices(start bool) (map[int64]*Service, error) {
|
|||
|
||||
// collect initial service stats
|
||||
s.UpdateStats()
|
||||
|
||||
allServices[s.Id] = s
|
||||
}
|
||||
|
||||
return allServices, nil
|
||||
|
@ -163,7 +163,7 @@ func (s *Service) UpdateStats() *Service {
|
|||
}
|
||||
|
||||
// AvgTime will return the average amount of time for a service to response back successfully
|
||||
func (s *Service) AvgTime() float64 {
|
||||
func (s *Service) AvgTime() int64 {
|
||||
return s.AllHits().Avg()
|
||||
}
|
||||
|
||||
|
@ -176,18 +176,22 @@ func (s *Service) OnlineDaysPercent(days int) float32 {
|
|||
// OnlineSince accepts a time since parameter to return the percent of a service's uptime.
|
||||
func (s *Service) OnlineSince(ago time.Time) float32 {
|
||||
failed := s.FailuresSince(ago)
|
||||
failsList := failed.List()
|
||||
if len(failsList) == 0 {
|
||||
failsList := failed.Count()
|
||||
|
||||
total := s.HitsSince(ago)
|
||||
hitsList := total.Count()
|
||||
|
||||
if failsList == 0 {
|
||||
s.Online24Hours = 100.00
|
||||
return s.Online24Hours
|
||||
}
|
||||
total := s.HitsSince(ago)
|
||||
hitsList := total.List()
|
||||
if len(hitsList) == 0 {
|
||||
|
||||
if hitsList == 0 {
|
||||
s.Online24Hours = 0
|
||||
return s.Online24Hours
|
||||
}
|
||||
avg := float64(len(hitsList)) / float64(len(failsList)) * 100
|
||||
|
||||
avg := (float64(failsList) / float64(hitsList)) * 100
|
||||
avg = 100 - avg
|
||||
if avg < 0 {
|
||||
avg = 0
|
||||
|
|
|
@ -239,7 +239,7 @@ func recordSuccess(s *Service) {
|
|||
log.Error(err)
|
||||
}
|
||||
log.WithFields(utils.ToFields(hit, s)).Infoln(
|
||||
fmt.Sprintf("Service #%d '%v' Successful Response: %v | Lookup in: %v | Online: %v | Interval: %d seconds", s.Id, s.Name, humanMicro(hit.Latency), humanMicro(hit.PingTime), s.Online, s.Interval))
|
||||
fmt.Sprintf("Service #%d '%v' Successful Response: %s | Lookup in: %s | Online: %v | Interval: %d seconds", s.Id, s.Name, humanMicro(hit.Latency), humanMicro(hit.PingTime), s.Online, s.Interval))
|
||||
s.LastLookupTime = hit.PingTime
|
||||
s.LastLatency = hit.Latency
|
||||
//notifiers.OnSuccess(s)
|
||||
|
|
|
@ -34,22 +34,22 @@ var example = &Service{
|
|||
|
||||
var hit1 = &hits.Hit{
|
||||
Service: 1,
|
||||
Latency: 0.1234,
|
||||
PingTime: 0.01234,
|
||||
Latency: 123456,
|
||||
PingTime: 123456,
|
||||
CreatedAt: utils.Now().Add(-120 * time.Second),
|
||||
}
|
||||
|
||||
var hit2 = &hits.Hit{
|
||||
Service: 1,
|
||||
Latency: 0.2345,
|
||||
PingTime: 0.02345,
|
||||
Latency: 123456,
|
||||
PingTime: 123456,
|
||||
CreatedAt: utils.Now().Add(-60 * time.Second),
|
||||
}
|
||||
|
||||
var hit3 = &hits.Hit{
|
||||
Service: 1,
|
||||
Latency: 0.3456,
|
||||
PingTime: 0.03456,
|
||||
Latency: 123456,
|
||||
PingTime: 123456,
|
||||
CreatedAt: utils.Now().Add(-30 * time.Second),
|
||||
}
|
||||
|
||||
|
@ -65,7 +65,7 @@ var fail1 = &failures.Failure{
|
|||
Issue: "example not found",
|
||||
ErrorCode: 404,
|
||||
Service: 1,
|
||||
PingTime: 0.0123,
|
||||
PingTime: 123456,
|
||||
CreatedAt: utils.Now().Add(-160 * time.Second),
|
||||
}
|
||||
|
||||
|
@ -73,7 +73,7 @@ var fail2 = &failures.Failure{
|
|||
Issue: "example 2 not found",
|
||||
ErrorCode: 500,
|
||||
Service: 1,
|
||||
PingTime: 0.0123,
|
||||
PingTime: 123456,
|
||||
CreatedAt: utils.Now().Add(-5 * time.Second),
|
||||
}
|
||||
|
||||
|
@ -155,6 +155,13 @@ func TestService_Duration(t *testing.T) {
|
|||
assert.Equal(t, float64(30), item.Duration().Seconds())
|
||||
}
|
||||
|
||||
func TestService_CountHits(t *testing.T) {
|
||||
item, err := Find(1)
|
||||
require.Nil(t, err)
|
||||
count := item.AllHits().Count()
|
||||
assert.NotZero(t, count)
|
||||
}
|
||||
|
||||
func TestService_AvgTime(t *testing.T) {
|
||||
item, err := Find(1)
|
||||
require.Nil(t, err)
|
||||
|
|
|
@ -45,7 +45,7 @@ type Service struct {
|
|||
PingTime int64 `gorm:"-" json:"ping_time"`
|
||||
Online24Hours float32 `gorm:"-" json:"online_24_hours"`
|
||||
Online7Days float32 `gorm:"-" json:"online_7_days"`
|
||||
AvgResponse float64 `gorm:"-" json:"avg_response"`
|
||||
AvgResponse int64 `gorm:"-" json:"avg_response"`
|
||||
FailuresLast24Hours int `gorm:"-" json:"failures_24_hours"`
|
||||
Running chan bool `gorm:"-" json:"-"`
|
||||
Checkpoint time.Time `gorm:"-" json:"-"`
|
||||
|
@ -71,11 +71,9 @@ type Service struct {
|
|||
}
|
||||
|
||||
type Stats struct {
|
||||
Failures int `gorm:"-" json:"failures"`
|
||||
Hits int `gorm:"-" json:"hits"`
|
||||
LastLookupTime int64 `gorm:"-" json:"last_lookup"`
|
||||
LastLatency int64 `gorm:"-" json:"last_latency"`
|
||||
FirstHit time.Time `gorm:"-" json:"first_hit"`
|
||||
Failures int `gorm:"-" json:"failures"`
|
||||
Hits int `gorm:"-" json:"hits"`
|
||||
FirstHit time.Time `gorm:"-" json:"first_hit"`
|
||||
}
|
||||
|
||||
// BeforeCreate for Service will set CreatedAt to UTC
|
||||
|
|
Loading…
Reference in New Issue