mirror of https://github.com/statping/statping
incidents, UX
parent
fe2cdb915a
commit
b3b41f58fa
|
@ -3,6 +3,9 @@
|
|||
- Modified UI to show user the response for a Notifier.
|
||||
- Modified some Notifiers title's
|
||||
- Added more Cypress e2e testing
|
||||
- Modified Incidents form and UX.
|
||||
- Added /api/services/{id}/uptime_data API endpoint to show online/offline durations as a series for charts.
|
||||
- Modified index page to automatically refresh Service details on interval
|
||||
|
||||
# 0.90.24
|
||||
- Fixed login form from not showing
|
||||
|
|
|
@ -56,6 +56,10 @@ class Api {
|
|||
return axios.get('api/services/' + id + '/failure_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).then(response => (response.data))
|
||||
}
|
||||
|
||||
async service_uptime(id) {
|
||||
return axios.get('api/services/' + id + '/uptime_data').then(response => (response.data))
|
||||
}
|
||||
|
||||
async service_heatmap(id, start, end, group) {
|
||||
return axios.get('api/services/' + id + '/heatmap').then(response => (response.data))
|
||||
}
|
||||
|
@ -118,7 +122,7 @@ class Api {
|
|||
}
|
||||
|
||||
async incident_updates(incident) {
|
||||
return axios.post('api/incidents/'+incident.id+'/updates', data).then(response => (response.data))
|
||||
return axios.get('api/incidents/'+incident.id+'/updates').then(response => (response.data))
|
||||
}
|
||||
|
||||
async incident_update_create(update) {
|
||||
|
@ -129,12 +133,12 @@ class Api {
|
|||
return axios.delete('api/incidents/'+update.incident+'/updates/'+update.id).then(response => (response.data))
|
||||
}
|
||||
|
||||
async incidents_service(service) {
|
||||
return axios.get('api/services/'+service.id+'/incidents').then(response => (response.data))
|
||||
async incidents_service(id) {
|
||||
return axios.get('api/services/'+id+'/incidents').then(response => (response.data))
|
||||
}
|
||||
|
||||
async incident_create(service, data) {
|
||||
return axios.post('api/services/'+service.id+'/incidents', data).then(response => (response.data))
|
||||
async incident_create(service_id, data) {
|
||||
return axios.post('api/services/'+service_id+'/incidents', data).then(response => (response.data))
|
||||
}
|
||||
|
||||
async incident_delete(incident) {
|
||||
|
|
|
@ -28,8 +28,8 @@ HTML,BODY {
|
|||
background-color: white;
|
||||
margin: 6px;
|
||||
height: 26px;
|
||||
font-size: 10pt;
|
||||
padding: 3px 7px;
|
||||
font-size: 8pt;
|
||||
padding: 5px 7px;
|
||||
border: 1px solid #a7a7a7;
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
@ -344,10 +344,33 @@ HTML,BODY {
|
|||
background-color: #ffbbbb;
|
||||
}
|
||||
|
||||
.btn-white {
|
||||
background-color: white;
|
||||
border: 1px solid #d8d8d8;
|
||||
color: #767676;
|
||||
}
|
||||
|
||||
.btn-white:hover {
|
||||
background-color: #fcfcfc;
|
||||
border: 1px solid #bdbdbd;
|
||||
color: #767676;
|
||||
}
|
||||
|
||||
.braker {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.10);
|
||||
width: 98%;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.text-dim {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: $service-background;
|
||||
border: $service-border;
|
||||
//box-shadow: 0px 2px 11px 1px rgba(0, 0, 0, 0.13);
|
||||
box-shadow: 0px 3px 6px 1px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="col-12">
|
||||
<div class="card contain-card text-black-50 bg-white mb-4">
|
||||
<div class="card-header">Annoucements</div>
|
||||
<div class="card-body">
|
||||
<div class="card-body pt-0">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
|
@ -3,16 +3,17 @@
|
|||
<div class="card contain-card text-black-50 bg-white mb-4">
|
||||
<div class="card-header">Services
|
||||
<router-link v-if="$store.state.admin" to="/dashboard/create_service" class="btn btn-sm btn-outline-success float-right">
|
||||
<font-awesome-icon icon="plus"/> Create
|
||||
</router-link></div>
|
||||
<div class="card-body">
|
||||
<font-awesome-icon icon="plus"/> Create
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="card-body pt-0">
|
||||
<ServicesList/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card contain-card text-black-50 bg-white mb-4">
|
||||
<div class="card-header">Groups</div>
|
||||
<div class="card-body">
|
||||
<div class="card-body pt-0">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="col-12">
|
||||
<div class="card contain-card text-black-50 bg-white mb-4">
|
||||
<div class="card-header">Users</div>
|
||||
<div class="card-body">
|
||||
<div class="card-body pt-0">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<draggable id="services_list" tag="tbody" v-model="servicesList" handle=".drag_icon">
|
||||
<tr v-for="(service, index) in $store.getters.servicesInOrder" :key="service.id">
|
||||
<tr v-for="(service, index) in servicesList" :key="service.id">
|
||||
<td>
|
||||
<span v-if="$store.state.admin" class="drag_icon d-none d-md-inline">
|
||||
<font-awesome-icon icon="bars" class="mr-3"/>
|
||||
|
|
|
@ -4,8 +4,11 @@
|
|||
<div class="flex-fill service_day" v-for="(d, index) in failureData" :class="{'mini_error': d.amount > 0, 'mini_success': d.amount === 0}"></div>
|
||||
</div>
|
||||
<div class="row mt-2">
|
||||
<div class="col-6 text-left font-2 text-muted">30 Days Ago</div>
|
||||
<div class="col-6 text-right font-2 text-muted">Today</div>
|
||||
<div class="col-4 text-left font-2 text-muted">30 Days Ago</div>
|
||||
<div class="col-4 text-center font-2" :class="{'text-muted': service.online, 'text-danger': !service.online}">
|
||||
{{service_txt}}
|
||||
</div>
|
||||
<div class="col-4 text-right font-2 text-muted">Today</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -28,6 +31,14 @@ export default {
|
|||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
service_txt() {
|
||||
if (!this.service.online) {
|
||||
return `Offline for ${this.ago(this.service.last_success)}`
|
||||
}
|
||||
return `${this.service.online_24_hours}% Uptime`
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.lastDaysFailures()
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
<template>
|
||||
<div class="row">
|
||||
<div v-for="(incident, i) in incidents" class="col-12 mt-4">
|
||||
<h5>Incident: {{incident.title}}<span class="font-2 float-right">{{niceDate(incident.created_at)}}</span></h5>
|
||||
{{incident.description}}
|
||||
<div class="row">
|
||||
<div v-for="(update, i) in incident.updates.reverse()" v-bind:key="update.id" class="col-12 mt-3">
|
||||
<span class="col-2 badge text-uppercase" :class="badgeClass(update.type)">{{update.type}}</span>
|
||||
<span class="col-10">{{update.message}}</span>
|
||||
<span class="col-12 font-1 float-right text-black-50">{{ago(update.created_at)}} ago</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(incident, i) in incidents" class="col-12 mt-4 mb-3">
|
||||
<span class="braker mt-1 mb-3"></span>
|
||||
<h6>Incident: {{incident.title}}<span class="font-2 float-right">{{niceDate(incident.created_at)}}</span></h6>
|
||||
<span class="font-2" v-html="incident.description"></span>
|
||||
|
||||
<UpdatesBlock :incident="incident"/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '../../API';
|
||||
import UpdatesBlock from "@/components/Index/UpdatesBlock";
|
||||
|
||||
export default {
|
||||
name: 'IncidentsBlock',
|
||||
props: {
|
||||
components: {UpdatesBlock},
|
||||
props: {
|
||||
service: {
|
||||
type: Object
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
@ -43,9 +43,13 @@ export default {
|
|||
return "badge-danger"
|
||||
}
|
||||
},
|
||||
async getIncidents() {
|
||||
this.incidents = await Api.incidents_service(this.service)
|
||||
}
|
||||
async getIncidents() {
|
||||
this.incidents = await Api.incidents_service(this.service.id)
|
||||
},
|
||||
async incident_updates(incident) {
|
||||
await Api.incident_updates(incident).then((d) => {return d})
|
||||
return o
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<div class="row">
|
||||
<div v-for="(update, i) in updates" v-bind:key="i" class="col-12 mt-3">
|
||||
<span class="col-2 badge text-uppercase" :class="badgeClass(update.type)">{{update.type}}</span>
|
||||
<span class="col-10">{{update.message}}</span>
|
||||
<span class="col-12 font-1 float-right text-black-50">{{ago(update.created_at)}} ago</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '../../API';
|
||||
|
||||
export default {
|
||||
name: 'UpdatesBlock',
|
||||
props: {
|
||||
incident: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
updates: null,
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.getIncidentUpdates()
|
||||
},
|
||||
methods: {
|
||||
badgeClass(val) {
|
||||
switch (val.toLowerCase()) {
|
||||
case "resolved":
|
||||
return "badge-success"
|
||||
case "update":
|
||||
return "badge-info"
|
||||
case "investigating":
|
||||
return "badge-danger"
|
||||
}
|
||||
},
|
||||
async getIncidentUpdates() {
|
||||
this.updates = await Api.incident_updates(this.incident)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
</style>
|
|
@ -63,6 +63,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '../../API';
|
||||
import Analytics from './Analytics';
|
||||
import ServiceChart from "./ServiceChart";
|
||||
import ServiceTopStats from "@/components/Service/ServiceTopStats";
|
||||
|
@ -72,13 +73,14 @@ export default {
|
|||
name: 'ServiceBlock',
|
||||
components: { Analytics, ServiceTopStats, ServiceChart},
|
||||
props: {
|
||||
service: {
|
||||
in_service: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
timer_func: null,
|
||||
expanded: false,
|
||||
visible: false,
|
||||
dropDownMenu: false,
|
||||
|
@ -115,9 +117,21 @@ export default {
|
|||
subtitle: "Last 7 Days",
|
||||
value: 0,
|
||||
}
|
||||
}
|
||||
},
|
||||
track_service: null,
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.timer_func)
|
||||
},
|
||||
computed: {
|
||||
service() {
|
||||
return this.track_service
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.track_service = this.in_service
|
||||
},
|
||||
methods: {
|
||||
async showMoreStats() {
|
||||
this.expanded = !this.expanded;
|
||||
|
@ -142,6 +156,7 @@ export default {
|
|||
this.stats.low_ping.value = this.humanTime(pingData.low);
|
||||
},
|
||||
smallText(s) {
|
||||
const incidents = s.incidents
|
||||
if (s.online) {
|
||||
return `Online, last checked ${this.ago(s.last_success)}`
|
||||
} else {
|
||||
|
@ -155,6 +170,13 @@ export default {
|
|||
visibleChart(isVisible, entry) {
|
||||
if (isVisible && !this.visible) {
|
||||
this.visible = true
|
||||
|
||||
if (!this.timer_func) {
|
||||
this.timer_func = setInterval(async () => {
|
||||
this.track_service = await Api.service(this.service.id)
|
||||
window.console.log(this.track_service.name)
|
||||
}, this.track_service.check_interval * 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,10 +9,48 @@
|
|||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-3 p-md-1 pt-md-3 pb-md-1">
|
||||
<div class="card-body p-3 p-md-1 pt-md-1 pb-md-1">
|
||||
|
||||
<transition name="fade">
|
||||
<div v-if="loaded && service.online" class="col-12 pb-2">
|
||||
<div v-if="loaded" class="col-12 pb-2">
|
||||
|
||||
<div v-if="false" class="row mb-4 align-content-center">
|
||||
|
||||
<div v-if="!service.online" class="col-3 text-left">
|
||||
<span class="text-danger font-5 font-weight-bold">okko</span>
|
||||
<span class="font-2 d-block">Current Downtime</span>
|
||||
</div>
|
||||
|
||||
<div v-if="service.online" class="col-3 text-left">
|
||||
<span class="text-success font-5 font-weight-bold">
|
||||
{{service.online_24_hours.toString()}}%
|
||||
</span>
|
||||
<span class="font-2 d-block">Total Uptime</span>
|
||||
</div>
|
||||
|
||||
<div v-if="service.online" class="col-3 text-left">
|
||||
<span class="text-success font-5 font-weight-bold">
|
||||
0
|
||||
</span>
|
||||
<span class="font-2 d-block">Downtime Today</span>
|
||||
</div>
|
||||
|
||||
<div v-if="service.online" class="col-3 text-left">
|
||||
<span class="text-success font-5 font-weight-bold">
|
||||
{{(uptime.uptime / 10000).toFixed(0).toString()}}
|
||||
</span>
|
||||
<span class="font-2 d-block">Uptime Duration</span>
|
||||
</div>
|
||||
|
||||
<div class="col-3 text-left">
|
||||
<span class="text-danger font-5 font-weight-bold">
|
||||
{{service.failures_24_hours}}
|
||||
</span>
|
||||
<span class="font-2 d-block">Failures last 24 hours</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<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"/>
|
||||
|
@ -22,33 +60,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="false" class="row mt-4 pt-1 mb-3 align-content-center">
|
||||
|
||||
<StatsGen :service="service"
|
||||
title="Since Yesterday"
|
||||
:start="this.toUnix(this.nowSubtract(86400 * 2))"
|
||||
:end="this.toUnix(this.nowSubtract(86400))"
|
||||
group="24h" expression="latencyPercent"/>
|
||||
|
||||
<StatsGen :service="service"
|
||||
title="7 Day Change"
|
||||
:start="this.toUnix(this.nowSubtract(86400 * 7))"
|
||||
:end="this.toUnix(this.now())"
|
||||
group="24h" expression="latencyPercent"/>
|
||||
|
||||
<StatsGen :service="service"
|
||||
title="Max Latency"
|
||||
:start="this.toUnix(this.nowSubtract(86400 * 2))"
|
||||
:end="this.toUnix(this.nowSubtract(86400))"
|
||||
group="24h" expression="latencyPercent"/>
|
||||
|
||||
<StatsGen :service="service"
|
||||
title="Uptime"
|
||||
:start="this.toUnix(this.nowSubtract(86400 * 2))"
|
||||
:end="this.toUnix(this.nowSubtract(86400))"
|
||||
group="24h" expression="latencyPercent"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
@ -57,33 +68,24 @@
|
|||
<div class="row">
|
||||
|
||||
<div class="col-3">
|
||||
<button @click.prevent="Tab('incident')" class="btn btn-block btn-outline-secondary incident" :class="{'text-white btn-secondary': openTab==='incident'}" >Incidents</button>
|
||||
<router-link :to="{path: `/dashboard/service/${service.id}/incidents`, params: {id: service.id} }" class="btn btn-block btn-white incident">
|
||||
Incidents
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<button @click.prevent="Tab('checkin')" class="btn btn-block btn-outline-secondary checkin" :class="{'text-white btn-secondary': openTab==='checkin'}" >Checkins</button>
|
||||
<router-link :to="{path: `/dashboard/service/${service.id}/checkins`, params: {id: service.id} }" class="btn btn-block btn-white checkins">
|
||||
Checkins
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<button @click.prevent="Tab('failures')" class="btn btn-block btn-outline-secondary failures" :disabled="service.stats.failures === 0" :class="{'text-white btn-secondary': openTab==='failures'}">
|
||||
Failures <span class="badge badge-danger float-right mt-1">{{service.stats.failures}}</span></button>
|
||||
<router-link :to="{path: `/dashboard/service/${service.id}/failures`, params: {id: service.id} }" class="btn btn-block btn-white failures">
|
||||
Failures <span class="badge badge-danger float-right mt-1">{{service.stats.failures}}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="col-3 pt-2">
|
||||
<span class="text-black-50 float-right">{{service.online_7_days}}% Uptime</span>
|
||||
</div>
|
||||
|
||||
<div v-if="openTab === 'incident'" class="col-12 mt-4">
|
||||
<FormIncident :service="service" />
|
||||
</div>
|
||||
|
||||
<div v-if="openTab === 'checkin'" class="col-12 mt-4">
|
||||
<Checkin :service="service" />
|
||||
</div>
|
||||
|
||||
<div v-if="openTab === 'failures'" class="col-12 mt-4">
|
||||
<button @click.prevent="deleteFailures" class="btn btn-block btn-outline-secondary delete_failures" :disabled="service.stats.failures === 0">Delete Failures</button>
|
||||
|
||||
<ServiceFailures :service="service"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -122,6 +124,7 @@
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
uptime: null,
|
||||
openTab: "",
|
||||
set1: [],
|
||||
set2: [],
|
||||
|
@ -139,9 +142,13 @@
|
|||
async setVisible(isVisible, entry) {
|
||||
if (isVisible && !this.visible) {
|
||||
await this.loadInfo()
|
||||
await this.getUptime()
|
||||
this.visible = true
|
||||
}
|
||||
},
|
||||
async getUptime() {
|
||||
this.uptime = await Api.service_uptime(this.service.id)
|
||||
},
|
||||
async loadInfo() {
|
||||
this.set1 = await this.getHits(24 * 7, "6h")
|
||||
this.set1_name = this.calc(this.set1)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="col-3 text-center">
|
||||
<div class="col-3 text-left">
|
||||
<span class="text-success font-5 font-weight-bold">{{value}}</span>
|
||||
<span class="font-2 d-block">{{title}}</span>
|
||||
</div>
|
||||
|
@ -34,6 +34,9 @@
|
|||
expression: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
in_value: {
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
@ -42,6 +45,9 @@
|
|||
}
|
||||
},
|
||||
async mounted() {
|
||||
if (this.in_value) {
|
||||
this.value = this.in_value
|
||||
}
|
||||
await this.latencyYesterday();
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -1,61 +1,6 @@
|
|||
<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-3">
|
||||
<div v-for="(update, i) in incident.updates" class="alert alert-light" role="alert">
|
||||
<span class="badge badge-pill badge-info text-uppercase">{{update.type}}</span>
|
||||
<span class="float-right font-2">{{ago(update.created_at)}} ago</span>
|
||||
<span class="d-block mt-2">{{update.message}}
|
||||
<button @click="delete_update(update)" type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<FormIncidentUpdates :incident="incident"/>
|
||||
|
||||
<span class="font-2 mt-3">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>
|
||||
|
||||
|
@ -89,29 +34,7 @@
|
|||
await this.loadIncidents()
|
||||
},
|
||||
methods: {
|
||||
async delete_update(update) {
|
||||
await Api.incident_update_delete(update)
|
||||
this.incidents = await Api.incidents_service(this.service)
|
||||
},
|
||||
async loadIncidents() {
|
||||
this.incidents = await Api.incidents_service(this.service)
|
||||
},
|
||||
async createIncident() {
|
||||
await Api.incident_create(this.service, this.incident)
|
||||
await this.loadIncidents()
|
||||
this.incident = {
|
||||
title: "",
|
||||
description: "",
|
||||
service: this.service.id,
|
||||
}
|
||||
},
|
||||
async deleteIncident(incident) {
|
||||
let c = confirm(`Are you sure you want to delete '${incident.title}'?`)
|
||||
if (c) {
|
||||
await Api.incident_delete(incident)
|
||||
await this.loadIncidents()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,6 +1,22 @@
|
|||
<template>
|
||||
<form class="row" @submit.prevent="createIncidentUpdate">
|
||||
<div class="card-body bg-light pt-3">
|
||||
|
||||
<div v-if="updates.length===0" class="alert alert-link text-danger">
|
||||
No updates found, create a new Incident Update below.
|
||||
</div>
|
||||
|
||||
<div v-for="(update, i) in updates">
|
||||
<div class="alert alert-light" role="alert">
|
||||
<span class="badge badge-pill badge-info text-uppercase">{{update.type}}</span>
|
||||
<span class="float-right font-2">{{ago(update.created_at)}} ago</span>
|
||||
<span class="d-block mt-2">{{update.message}}
|
||||
<button @click="delete_update(update)" type="button" class="close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<form class="row" @submit.prevent="createIncidentUpdate">
|
||||
<div class="col-3">
|
||||
<select v-model="incident_update.type" class="form-control">
|
||||
<option value="Investigating">Investigating</option>
|
||||
|
@ -21,6 +37,7 @@
|
|||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -35,12 +52,13 @@
|
|||
},
|
||||
props: {
|
||||
incident: {
|
||||
type: Object
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
updates: [],
|
||||
updates: this.incident.updates,
|
||||
incident_update: {
|
||||
incident: this.incident.id,
|
||||
message: "",
|
||||
|
@ -48,16 +66,23 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
await this.loadUpdates()
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadUpdates()
|
||||
},
|
||||
methods: {
|
||||
async delete_update(update) {
|
||||
await Api.incident_update_delete(update)
|
||||
await this.loadUpdates()
|
||||
},
|
||||
methods: {
|
||||
async loadUpdates() {
|
||||
this.updates = await Api.incident_updates(this.incident)
|
||||
},
|
||||
async createIncidentUpdate() {
|
||||
await Api.incident_update_create(this.incident_update)
|
||||
await this.loadUpdates()
|
||||
},
|
||||
async loadUpdates() {
|
||||
this.updates = await Api.incident_updates(this.incident)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -156,10 +156,10 @@
|
|||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">Enable Notifications</label>
|
||||
<div class="col-8 mt-1">
|
||||
<span @click="service.allow_notifications = !!service.allow_notifications" class="switch float-left">
|
||||
<input v-model="service.allow_notifications" type="checkbox" name="allow_notifications-option" class="switch" id="switch-notifications" v-bind:checked="service.allow_notifications">
|
||||
<label for="switch-notifications">Allow notifications to be sent for this service</label>
|
||||
</span>
|
||||
<span @click="service.allow_notifications = !!service.allow_notifications" class="switch float-left">
|
||||
<input v-model="service.allow_notifications" type="checkbox" name="allow_notifications-option" class="switch" id="switch-notifications" v-bind:checked="service.allow_notifications">
|
||||
<label for="switch-notifications">Allow notifications to be sent for this service</label>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="service.allow_notifications" class="form-group row">
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<div class="form-group">
|
||||
<label>Database Connection</label>
|
||||
<select @change="canSubmit" v-model="setup.db_connection" id="db_connection" class="form-control">
|
||||
<option value="sqlite">Sqlite</option>
|
||||
<option value="sqlite">SQLite</option>
|
||||
<option value="postgres">Postgres</option>
|
||||
<option value="mysql">MySQL</option>
|
||||
</select>
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
<template>
|
||||
<div class="col-12">
|
||||
<h2>{{service.name}} Checkins</h2>
|
||||
<p class="mb-3">Tell your service to send a routine HTTP request to a Statping Checkin.</p>
|
||||
<div v-for="(checkin, i) in checkins" class="col-12 alert alert-light" role="alert">
|
||||
<span class="badge badge-pill badge-info text-uppercase">{{checkin.name}}</span>
|
||||
<span class="float-right font-2">Last checkin {{ago(checkin.last_hit)}}</span>
|
||||
<span class="float-right font-2 mr-3">Check Every {{checkin.interval}} seconds</span>
|
||||
<span class="float-right font-2 mr-3">Grace Period {{checkin.grace}} seconds</span>
|
||||
<span class="d-block mt-2">
|
||||
<input type="text" class="form-control" :value="`${core.domain}/checkin/${checkin.api_key}`" readonly>
|
||||
<span class="small">Send a GET request to this URL every {{checkin.interval}} seconds
|
||||
<button @click="deleteCheckin(checkin)" type="button" class="btn btn-danger btn-xs float-right mt-1">Delete</button>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col-12 alert alert-light">
|
||||
<form @submit.prevent="saveCheckin">
|
||||
<div class="form-group row">
|
||||
<div class="col-5">
|
||||
<label for="checkin_interval" class="col-form-label">Checkin Name</label>
|
||||
<input v-model="checkin.name" type="text" name="name" class="form-control" id="checkin_name" placeholder="New Checkin">
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<label for="checkin_interval" class="col-form-label">Interval</label>
|
||||
<input v-model="checkin.interval" type="number" name="interval" class="form-control" id="checkin_interval" placeholder="60">
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<label for="grace_period" class="col-form-label">Grace Period</label>
|
||||
<input v-model="checkin.grace" type="number" name="grace" class="form-control" id="grace_period" placeholder="10">
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="col-form-label"></label>
|
||||
<button @click.prevent="saveCheckin" type="submit" id="submit" class="btn btn-primary d-block mt-2">Save Checkin</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "../API";
|
||||
|
||||
export default {
|
||||
name: 'Checkins',
|
||||
data() {
|
||||
return {
|
||||
service: {},
|
||||
ready: false,
|
||||
checkin: {
|
||||
name: "",
|
||||
interval: 60,
|
||||
grace: 60,
|
||||
service_id: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
checkins() {
|
||||
return this.$store.getters.serviceCheckins(this.service.id)
|
||||
},
|
||||
core() {
|
||||
return this.$store.getters.core
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
if (this.$route.params) {
|
||||
const id = this.$route.params.id
|
||||
this.service = await Api.service(id)
|
||||
this.checkin.service_id = this.service.id
|
||||
this.ready = true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fixInts() {
|
||||
const c = this.checkin
|
||||
this.checkin.interval = parseInt(c.interval)
|
||||
this.checkin.grace = parseInt(c.grace)
|
||||
return this.checkin
|
||||
},
|
||||
async saveCheckin() {
|
||||
const c = this.fixInts()
|
||||
await Api.checkin_create(c)
|
||||
await this.updateCheckins()
|
||||
},
|
||||
async deleteCheckin(checkin) {
|
||||
await Api.checkin_delete(checkin)
|
||||
await this.updateCheckins()
|
||||
},
|
||||
async updateCheckins() {
|
||||
const checkins = await Api.checkins()
|
||||
this.$store.commit('setCheckins', checkins)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
.sm {
|
||||
font-size: 8pt;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,93 @@
|
|||
<template>
|
||||
<div class="col-12">
|
||||
<h2>{{service.name}} Failures
|
||||
<span class="btn btn-outline-danger float-right">Delete All</span></h2>
|
||||
<div class="list-group mt-3 mb-4">
|
||||
|
||||
<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>{{niceDate(failure.created_at)}}</small>
|
||||
</div>
|
||||
<p class="mb-1">{{failure.issue}}</p>
|
||||
</div>
|
||||
|
||||
<nav v-if="total > 4" class="mt-3">
|
||||
<ul class="pagination justify-content-center">
|
||||
<li class="page-item" :class="{'disabled': page===1}">
|
||||
<a @click.prevent="gotoPage(page-1)" :disabled="page===1" class="page-link" href="#" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
<span class="sr-only">Previous</span>
|
||||
</a>
|
||||
</li>
|
||||
<li v-for="n in maxPages" class="page-item" :class="{'active': page === n}">
|
||||
<a @click.prevent="gotoPage(n)" class="page-link" href="#">{{n}}</a>
|
||||
</li>
|
||||
<li class="page-item" :class="{'disabled': page===Math.floor(total / limit)}">
|
||||
<a @click.prevent="gotoPage(page+1)" :disabled="page===Math.floor(total / limit)" class="page-link" href="#" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
<span class="sr-only">Next</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="text-center">
|
||||
<span class="text-black-50">{{total}} Total</span>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "../API";
|
||||
|
||||
export default {
|
||||
name: 'Failures',
|
||||
data() {
|
||||
return {
|
||||
service: {},
|
||||
failures: [],
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
total: 0,
|
||||
page: 1
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pages() {
|
||||
return Math.floor(this.total / this.limit)
|
||||
},
|
||||
maxPages() {
|
||||
const p = Math.floor(this.total / this.limit)
|
||||
if (p > 16) {
|
||||
return 16
|
||||
} else {
|
||||
return p
|
||||
}
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.service = await Api.service(this.$route.params.id)
|
||||
this.total = this.service.stats.failures
|
||||
await this.gotoPage(1)
|
||||
},
|
||||
methods: {
|
||||
async gotoPage(page) {
|
||||
this.page = page;
|
||||
|
||||
this.offset = (page-1) * this.limit;
|
||||
|
||||
window.console.log('page', this.page, this.limit, this.offset);
|
||||
|
||||
this.failures = await Api.service_failures(this.service.id, 0, 9999999999, this.limit, this.offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
.sm {
|
||||
font-size: 8pt;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,109 @@
|
|||
<template>
|
||||
<div class="col-12">
|
||||
<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>
|
||||
|
||||
<FormIncidentUpdates :incident="incident"/>
|
||||
|
||||
<span class="font-2 p-2 pl-3">Created: {{niceDate(incident.created_at)}} | Last Update: {{niceDate(incident.updated_at)}}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card contain-card text-black-50 bg-white">
|
||||
<div class="card-header">Create Incident</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 FormIncidentUpdates from "@/forms/IncidentUpdates";
|
||||
|
||||
export default {
|
||||
name: 'Incidents',
|
||||
components: {FormIncidentUpdates},
|
||||
data() {
|
||||
return {
|
||||
service_id: 0,
|
||||
ready: false,
|
||||
incidents: [],
|
||||
incident: {
|
||||
title: "",
|
||||
description: "",
|
||||
service: 0,
|
||||
}
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
theID() {
|
||||
return this.$route.params.id
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadIncidents()
|
||||
},
|
||||
methods: {
|
||||
async getIncidents() {
|
||||
return await Api.incidents_service(this.theID)
|
||||
},
|
||||
async deleteIncident(incident) {
|
||||
let c = confirm(`Are you sure you want to delete '${incident.title}'?`)
|
||||
if (c) {
|
||||
await Api.incident_delete(incident)
|
||||
await this.loadIncidents()
|
||||
}
|
||||
},
|
||||
async loadIncidents() {
|
||||
this.incidents = await Api.incidents_service(this.service_id)
|
||||
},
|
||||
async createIncident() {
|
||||
await Api.incident_create(this.theID, this.incident)
|
||||
await this.loadIncidents()
|
||||
this.incident = {
|
||||
title: "",
|
||||
description: "",
|
||||
service: this.service_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
.sm {
|
||||
font-size: 8pt;
|
||||
}
|
||||
</style>
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<div class="col-12 full-col-12">
|
||||
<div v-for="(service, index) in services" :ref="service.id" v-bind:key="index">
|
||||
<ServiceBlock :service=service />
|
||||
<ServiceBlock :in_service=service />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div v-if="service" class="container col-md-7 col-sm-12 mt-md-5 bg-light">
|
||||
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
|
||||
|
||||
<div class="col-12 mb-4">
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
|||
</span>
|
||||
|
||||
<h4 class="mt-2">
|
||||
<router-link to="/" class="text-black-50 text-decoration-none">{{$store.getters.core.name}}</router-link> - <span class="text-muted">{{service.name}}</span>
|
||||
<router-link to="/" class="text-black-50 text-decoration-none">{{core.name}}</router-link> - <span class="text-muted">{{service.name}}</span>
|
||||
<span class="badge float-right d-none d-md-block" :class="{'bg-success': service.online, 'bg-danger': !service.online}">
|
||||
{{service.online ? "ONLINE" : "OFFLINE"}}
|
||||
</span>
|
||||
|
@ -37,6 +37,11 @@
|
|||
<ServiceHeatmap :service="service"/>
|
||||
</div>
|
||||
|
||||
<div v-if="load_timedata" class="col-12">
|
||||
|
||||
<apexchart width="100%" height="420" type="rangeBar" :options="timeRangeOptions" :series="rangeSeries"></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>
|
||||
|
@ -132,7 +137,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
id: this.$route.params.id,
|
||||
id: 0,
|
||||
tab: "failures",
|
||||
authenticated: false,
|
||||
ready: true,
|
||||
|
@ -141,6 +146,8 @@ export default {
|
|||
failures: [],
|
||||
start_time: this.nowSubtract(84600 * 30),
|
||||
end_time: new Date(),
|
||||
timedata: [],
|
||||
load_timedata: false,
|
||||
dailyRangeOpts: {
|
||||
chart: {
|
||||
height: 500,
|
||||
|
@ -148,6 +155,45 @@ export default {
|
|||
type: "area",
|
||||
}
|
||||
},
|
||||
timeRangeOptions: {
|
||||
chart: {
|
||||
height: 200,
|
||||
type: 'rangeBar'
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: true,
|
||||
distributed: true,
|
||||
dataLabels: {
|
||||
hideOverflowingLabels: false
|
||||
}
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
formatter: (val, opts) => {
|
||||
var label = opts.w.globals.labels[opts.dataPointIndex]
|
||||
var a = this.parseISO(val[0])
|
||||
var b = this.parseISO(val[1])
|
||||
return label
|
||||
},
|
||||
style: {
|
||||
colors: ['#f3f4f5', '#fff']
|
||||
}
|
||||
},
|
||||
xaxis: {
|
||||
type: 'datetime'
|
||||
},
|
||||
yaxis: {
|
||||
show: false
|
||||
},
|
||||
grid: {
|
||||
row: {
|
||||
colors: ['#f3f4f5', '#fff'],
|
||||
opacity: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
chartOptions: {
|
||||
chart: {
|
||||
events: {
|
||||
|
@ -268,24 +314,65 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
service () {
|
||||
return this.$store.getters.serviceByAll(this.id)
|
||||
}
|
||||
service () {
|
||||
return this.$store.getters.serviceByAll(this.id)
|
||||
},
|
||||
core () {
|
||||
return this.$store.getters.core
|
||||
},
|
||||
uptime_data() {
|
||||
const data = this.timedata.series.filter(g => g.online)
|
||||
const offData = this.timedata.series.filter(g => !g.online)
|
||||
let arr = [];
|
||||
data.forEach((d) => {
|
||||
arr.push({
|
||||
name: "Online", data: {
|
||||
x: 'Online',
|
||||
y: [
|
||||
new Date(d.start).getTime(),
|
||||
new Date(d.end).getTime()
|
||||
],
|
||||
fillColor: '#0db407'
|
||||
}
|
||||
})
|
||||
})
|
||||
offData.forEach((d) => {
|
||||
arr.push({
|
||||
name: "offline", data: {
|
||||
x: 'Offline',
|
||||
y: [
|
||||
new Date(d.start).getTime(),
|
||||
new Date(d.end).getTime()
|
||||
],
|
||||
fillColor: '#b40707'
|
||||
}
|
||||
})
|
||||
})
|
||||
return arr
|
||||
},
|
||||
rangeSeries() {
|
||||
return [{data: this.time_chart_data}]
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
service: function(n, o) {
|
||||
this.chartHits()
|
||||
}
|
||||
service: function(n, o) {
|
||||
this.chartHits()
|
||||
this.fetchUptime()
|
||||
},
|
||||
load_timedata: function(n, o) {
|
||||
this.chartHits()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
||||
},
|
||||
mounted() {
|
||||
|
||||
async created() {
|
||||
this.id = this.$route.params.id;
|
||||
},
|
||||
methods: {
|
||||
async fetchUptime() {
|
||||
this.timedata = await Api.service_uptime(this.id)
|
||||
this.load_timedata = true
|
||||
},
|
||||
async get() {
|
||||
const s = store.getters.serviceByAll(this.id)
|
||||
const s = this.$store.getters.serviceByAll(this.id)
|
||||
window.console.log("service: ", s)
|
||||
this.getService(this.service)
|
||||
this.messages = this.$store.getters.serviceMessages(this.service.id)
|
||||
|
@ -295,13 +382,12 @@ export default {
|
|||
const end = this.isBetween(message.end_on, new Date())
|
||||
return start && end
|
||||
},
|
||||
async getService(s) {
|
||||
async getService() {
|
||||
await this.chartHits()
|
||||
await this.serviceFailures()
|
||||
},
|
||||
async serviceFailures() {
|
||||
let tt = this.startEndTimes()
|
||||
|
||||
this.failures = await Api.service_failures(this.service.id, tt.start, tt.end)
|
||||
},
|
||||
async chartHits(start=0, end=99999999999, group="30m") {
|
||||
|
|
|
@ -13,6 +13,9 @@ import VueRouter from "vue-router";
|
|||
import Setup from "./forms/Setup";
|
||||
|
||||
import Api from "./API";
|
||||
import Incidents from "@/pages/Incidents";
|
||||
import Checkins from "@/pages/Checkins";
|
||||
import Failures from "@/pages/Failures";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
|
@ -59,9 +62,27 @@ const routes = [
|
|||
},{
|
||||
path: 'edit_service/:id',
|
||||
component: EditService,
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
}
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
}
|
||||
},{
|
||||
path: 'service/:id/incidents',
|
||||
component: Incidents,
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
}
|
||||
},{
|
||||
path: 'service/:id/checkins',
|
||||
component: Checkins,
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
}
|
||||
},{
|
||||
path: 'service/:id/failures',
|
||||
component: Failures,
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
}
|
||||
},{
|
||||
path: 'messages',
|
||||
component: DashboardMessages,
|
||||
|
|
|
@ -111,6 +111,7 @@ func Router() *mux.Router {
|
|||
api.Handle("/api/services/{id}/hits_data", cached("30s", "application/json", apiServiceDataHandler)).Methods("GET")
|
||||
api.Handle("/api/services/{id}/failure_data", cached("30s", "application/json", apiServiceFailureDataHandler)).Methods("GET")
|
||||
api.Handle("/api/services/{id}/ping_data", cached("30s", "application/json", apiServicePingDataHandler)).Methods("GET")
|
||||
api.Handle("/api/services/{id}/uptime_data", http.HandlerFunc(apiServiceTimeDataHandler)).Methods("GET")
|
||||
//api.Handle("/api/services/{id}/heatmap", cached("30s", "application/json", apiServiceHeatmapHandler)).Methods("GET")
|
||||
|
||||
// API INCIDENTS Routes
|
||||
|
@ -120,7 +121,7 @@ func Router() *mux.Router {
|
|||
api.Handle("/api/incidents/{id}", authenticated(apiDeleteIncidentHandler, false)).Methods("DELETE")
|
||||
|
||||
// API INCIDENTS UPDATES Routes
|
||||
api.Handle("/api/incidents/{id}/updates", authenticated(apiIncidentUpdatesHandler, false)).Methods("GET")
|
||||
api.Handle("/api/incidents/{id}/updates", http.HandlerFunc(apiIncidentUpdatesHandler)).Methods("GET")
|
||||
api.Handle("/api/incidents/{id}/updates", authenticated(apiCreateIncidentUpdateHandler, false)).Methods("POST")
|
||||
api.Handle("/api/incidents/{id}/updates/{uid}", authenticated(apiDeleteIncidentUpdateHandler, false)).Methods("DELETE")
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ import (
|
|||
"github.com/statping/statping/types/services"
|
||||
"github.com/statping/statping/utils"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
type serviceOrder struct {
|
||||
|
@ -176,6 +178,140 @@ func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) {
|
|||
returnJson(objs, w, r)
|
||||
}
|
||||
|
||||
func apiServiceTimeDataHandler(w http.ResponseWriter, r *http.Request) {
|
||||
service, err := serviceByID(r)
|
||||
if err != nil {
|
||||
sendErrorJson(errors.New("service data not found"), w, r)
|
||||
return
|
||||
}
|
||||
|
||||
allFailures := service.AllFailures()
|
||||
allHits := service.AllHits()
|
||||
|
||||
tMap := make(map[time.Time]bool)
|
||||
for _, v := range allHits.List() {
|
||||
tMap[v.CreatedAt] = true
|
||||
}
|
||||
for _, v := range allFailures.List() {
|
||||
tMap[v.CreatedAt] = false
|
||||
}
|
||||
|
||||
var servs []ser
|
||||
for t, v := range tMap {
|
||||
s := ser{
|
||||
Time: t,
|
||||
Online: v,
|
||||
}
|
||||
servs = append(servs, s)
|
||||
}
|
||||
|
||||
sort.Sort(ByTime(servs))
|
||||
|
||||
var allTimes []series
|
||||
online := servs[0].Online
|
||||
thisTime := servs[0].Time
|
||||
for i := 0; i < len(servs); i++ {
|
||||
v := servs[i]
|
||||
|
||||
if v.Online != online {
|
||||
s := series{
|
||||
Start: thisTime,
|
||||
End: v.Time,
|
||||
Duration: v.Time.Sub(thisTime).Milliseconds(),
|
||||
Online: online,
|
||||
}
|
||||
allTimes = append(allTimes, s)
|
||||
thisTime = v.Time
|
||||
online = v.Online
|
||||
}
|
||||
}
|
||||
|
||||
first := servs[0].Time
|
||||
last := servs[len(servs)-1].Time
|
||||
|
||||
if !service.Online {
|
||||
s := series{
|
||||
Start: allTimes[len(allTimes)-1].End,
|
||||
End: utils.Now(),
|
||||
Duration: utils.Now().Sub(last).Milliseconds(),
|
||||
Online: service.Online,
|
||||
}
|
||||
allTimes = append(allTimes, s)
|
||||
} else {
|
||||
l := allTimes[len(allTimes)-1]
|
||||
allTimes[len(allTimes)-1] = series{
|
||||
Start: l.Start,
|
||||
End: utils.Now(),
|
||||
Duration: utils.Now().Sub(l.Start).Milliseconds(),
|
||||
Online: true,
|
||||
}
|
||||
}
|
||||
|
||||
jj := uptimeSeries{
|
||||
Start: first,
|
||||
End: last,
|
||||
Uptime: addDurations(allTimes, true),
|
||||
Downtime: addDurations(allTimes, false),
|
||||
Series: allTimes,
|
||||
}
|
||||
|
||||
returnJson(jj, w, r)
|
||||
}
|
||||
|
||||
type ByTime []ser
|
||||
|
||||
func (a ByTime) Len() int { return len(a) }
|
||||
func (a ByTime) Less(i, j int) bool { return a[i].Time.Before(a[j].Time) }
|
||||
func (a ByTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
|
||||
func addDurations(s []series, on bool) int64 {
|
||||
var dur int64
|
||||
for _, v := range s {
|
||||
if v.Online == on {
|
||||
dur += v.Duration
|
||||
}
|
||||
}
|
||||
return dur
|
||||
}
|
||||
|
||||
type ser struct {
|
||||
Time time.Time
|
||||
Online bool
|
||||
}
|
||||
|
||||
func findNextFailure(m map[time.Time]bool, after time.Time, online bool) time.Time {
|
||||
for k, v := range m {
|
||||
if k.After(after) && v == online {
|
||||
return k
|
||||
}
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
//func calculateDuration(m map[time.Time]bool, on bool) time.Duration {
|
||||
// var t time.Duration
|
||||
// for t, v := range m {
|
||||
// if v == on {
|
||||
// t.
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
type uptimeSeries struct {
|
||||
Start time.Time `json:"start"`
|
||||
End time.Time `json:"end"`
|
||||
Uptime int64 `json:"uptime"`
|
||||
Downtime int64 `json:"downtime"`
|
||||
Series []series `json:"series"`
|
||||
}
|
||||
|
||||
type series struct {
|
||||
Start time.Time `json:"start"`
|
||||
End time.Time `json:"end"`
|
||||
Duration int64 `json:"duration"`
|
||||
Online bool `json:"online"`
|
||||
}
|
||||
|
||||
func apiServiceDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
service, err := serviceByID(r)
|
||||
if err != nil {
|
||||
|
|
|
@ -26,7 +26,7 @@ type Service struct {
|
|||
Domain string `gorm:"column:domain" json:"domain" private:"true" scope:"user,admin"`
|
||||
Expected null.NullString `gorm:"column:expected" json:"expected" scope:"user,admin"`
|
||||
ExpectedStatus int `gorm:"default:200;column:expected_status" json:"expected_status" scope:"user,admin"`
|
||||
Interval int `gorm:"default:30;column:check_interval" json:"check_interval" scope:"user,admin"`
|
||||
Interval int `gorm:"default:30;column:check_interval" json:"check_interval"`
|
||||
Type string `gorm:"column:check_type" json:"type" scope:"user,admin"`
|
||||
Method string `gorm:"column:method" json:"method" scope:"user,admin"`
|
||||
PostData null.NullString `gorm:"column:post_data" json:"post_data" scope:"user,admin"`
|
||||
|
|
Loading…
Reference in New Issue