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 UI to show user the response for a Notifier.
|
||||||
- Modified some Notifiers title's
|
- Modified some Notifiers title's
|
||||||
- Added more Cypress e2e testing
|
- 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
|
# 0.90.24
|
||||||
- Fixed login form from not showing
|
- 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))
|
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) {
|
async service_heatmap(id, start, end, group) {
|
||||||
return axios.get('api/services/' + id + '/heatmap').then(response => (response.data))
|
return axios.get('api/services/' + id + '/heatmap').then(response => (response.data))
|
||||||
}
|
}
|
||||||
|
@ -118,7 +122,7 @@ class Api {
|
||||||
}
|
}
|
||||||
|
|
||||||
async incident_updates(incident) {
|
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) {
|
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))
|
return axios.delete('api/incidents/'+update.incident+'/updates/'+update.id).then(response => (response.data))
|
||||||
}
|
}
|
||||||
|
|
||||||
async incidents_service(service) {
|
async incidents_service(id) {
|
||||||
return axios.get('api/services/'+service.id+'/incidents').then(response => (response.data))
|
return axios.get('api/services/'+id+'/incidents').then(response => (response.data))
|
||||||
}
|
}
|
||||||
|
|
||||||
async incident_create(service, data) {
|
async incident_create(service_id, data) {
|
||||||
return axios.post('api/services/'+service.id+'/incidents', data).then(response => (response.data))
|
return axios.post('api/services/'+service_id+'/incidents', data).then(response => (response.data))
|
||||||
}
|
}
|
||||||
|
|
||||||
async incident_delete(incident) {
|
async incident_delete(incident) {
|
||||||
|
|
|
@ -28,8 +28,8 @@ HTML,BODY {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
margin: 6px;
|
margin: 6px;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
font-size: 10pt;
|
font-size: 8pt;
|
||||||
padding: 3px 7px;
|
padding: 5px 7px;
|
||||||
border: 1px solid #a7a7a7;
|
border: 1px solid #a7a7a7;
|
||||||
border-radius: 4px !important;
|
border-radius: 4px !important;
|
||||||
}
|
}
|
||||||
|
@ -344,10 +344,33 @@ HTML,BODY {
|
||||||
background-color: #ffbbbb;
|
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 {
|
.card {
|
||||||
background-color: $service-background;
|
background-color: $service-background;
|
||||||
border: $service-border;
|
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 {
|
.card-body {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card contain-card text-black-50 bg-white mb-4">
|
<div class="card contain-card text-black-50 bg-white mb-4">
|
||||||
<div class="card-header">Annoucements</div>
|
<div class="card-header">Annoucements</div>
|
||||||
<div class="card-body">
|
<div class="card-body pt-0">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -4,15 +4,16 @@
|
||||||
<div class="card-header">Services
|
<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">
|
<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
|
<font-awesome-icon icon="plus"/> Create
|
||||||
</router-link></div>
|
</router-link>
|
||||||
<div class="card-body">
|
</div>
|
||||||
|
<div class="card-body pt-0">
|
||||||
<ServicesList/>
|
<ServicesList/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card contain-card text-black-50 bg-white mb-4">
|
<div class="card contain-card text-black-50 bg-white mb-4">
|
||||||
<div class="card-header">Groups</div>
|
<div class="card-header">Groups</div>
|
||||||
<div class="card-body">
|
<div class="card-body pt-0">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card contain-card text-black-50 bg-white mb-4">
|
<div class="card contain-card text-black-50 bg-white mb-4">
|
||||||
<div class="card-header">Users</div>
|
<div class="card-header">Users</div>
|
||||||
<div class="card-body">
|
<div class="card-body pt-0">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<draggable id="services_list" tag="tbody" v-model="servicesList" handle=".drag_icon">
|
<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>
|
<td>
|
||||||
<span v-if="$store.state.admin" class="drag_icon d-none d-md-inline">
|
<span v-if="$store.state.admin" class="drag_icon d-none d-md-inline">
|
||||||
<font-awesome-icon icon="bars" class="mr-3"/>
|
<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 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>
|
||||||
<div class="row mt-2">
|
<div class="row mt-2">
|
||||||
<div class="col-6 text-left font-2 text-muted">30 Days Ago</div>
|
<div class="col-4 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-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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -28,6 +31,14 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
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 () {
|
mounted () {
|
||||||
this.lastDaysFailures()
|
this.lastDaysFailures()
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div v-for="(incident, i) in incidents" class="col-12 mt-4">
|
<div v-for="(incident, i) in incidents" class="col-12 mt-4 mb-3">
|
||||||
<h5>Incident: {{incident.title}}<span class="font-2 float-right">{{niceDate(incident.created_at)}}</span></h5>
|
<span class="braker mt-1 mb-3"></span>
|
||||||
{{incident.description}}
|
<h6>Incident: {{incident.title}}<span class="font-2 float-right">{{niceDate(incident.created_at)}}</span></h6>
|
||||||
<div class="row">
|
<span class="font-2" v-html="incident.description"></span>
|
||||||
<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>
|
<UpdatesBlock :incident="incident"/>
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Api from '../../API';
|
import Api from '../../API';
|
||||||
|
import UpdatesBlock from "@/components/Index/UpdatesBlock";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'IncidentsBlock',
|
name: 'IncidentsBlock',
|
||||||
|
components: {UpdatesBlock},
|
||||||
props: {
|
props: {
|
||||||
service: {
|
service: {
|
||||||
type: Object
|
type: Object,
|
||||||
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
@ -44,7 +44,11 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getIncidents() {
|
async getIncidents() {
|
||||||
this.incidents = await Api.incidents_service(this.service)
|
this.incidents = await Api.incidents_service(this.service.id)
|
||||||
|
},
|
||||||
|
async incident_updates(incident) {
|
||||||
|
await Api.incident_updates(incident).then((d) => {return d})
|
||||||
|
return o
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Api from '../../API';
|
||||||
import Analytics from './Analytics';
|
import Analytics from './Analytics';
|
||||||
import ServiceChart from "./ServiceChart";
|
import ServiceChart from "./ServiceChart";
|
||||||
import ServiceTopStats from "@/components/Service/ServiceTopStats";
|
import ServiceTopStats from "@/components/Service/ServiceTopStats";
|
||||||
|
@ -72,13 +73,14 @@ export default {
|
||||||
name: 'ServiceBlock',
|
name: 'ServiceBlock',
|
||||||
components: { Analytics, ServiceTopStats, ServiceChart},
|
components: { Analytics, ServiceTopStats, ServiceChart},
|
||||||
props: {
|
props: {
|
||||||
service: {
|
in_service: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
timer_func: null,
|
||||||
expanded: false,
|
expanded: false,
|
||||||
visible: false,
|
visible: false,
|
||||||
dropDownMenu: false,
|
dropDownMenu: false,
|
||||||
|
@ -115,8 +117,20 @@ export default {
|
||||||
subtitle: "Last 7 Days",
|
subtitle: "Last 7 Days",
|
||||||
value: 0,
|
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: {
|
methods: {
|
||||||
async showMoreStats() {
|
async showMoreStats() {
|
||||||
|
@ -142,6 +156,7 @@ export default {
|
||||||
this.stats.low_ping.value = this.humanTime(pingData.low);
|
this.stats.low_ping.value = this.humanTime(pingData.low);
|
||||||
},
|
},
|
||||||
smallText(s) {
|
smallText(s) {
|
||||||
|
const incidents = s.incidents
|
||||||
if (s.online) {
|
if (s.online) {
|
||||||
return `Online, last checked ${this.ago(s.last_success)}`
|
return `Online, last checked ${this.ago(s.last_success)}`
|
||||||
} else {
|
} else {
|
||||||
|
@ -155,6 +170,13 @@ export default {
|
||||||
visibleChart(isVisible, entry) {
|
visibleChart(isVisible, entry) {
|
||||||
if (isVisible && !this.visible) {
|
if (isVisible && !this.visible) {
|
||||||
this.visible = true
|
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>
|
</h4>
|
||||||
</div>
|
</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">
|
<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="row">
|
||||||
<div class="col-md-6 col-sm-12 mt-2 mt-md-0 mb-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"/>
|
<ServiceSparkLine :title="set2_name" subtitle="Latency Last 24 Hours" :series="set2"/>
|
||||||
|
@ -22,33 +60,6 @@
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
|
@ -57,33 +68,24 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
||||||
<div class="col-3">
|
<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>
|
||||||
<div class="col-3">
|
<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>
|
||||||
<div class="col-3">
|
<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'}">
|
<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></button>
|
Failures <span class="badge badge-danger float-right mt-1">{{service.stats.failures}}</span>
|
||||||
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-3 pt-2">
|
<div class="col-3 pt-2">
|
||||||
<span class="text-black-50 float-right">{{service.online_7_days}}% Uptime</span>
|
<span class="text-black-50 float-right">{{service.online_7_days}}% Uptime</span>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -122,6 +124,7 @@
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
uptime: null,
|
||||||
openTab: "",
|
openTab: "",
|
||||||
set1: [],
|
set1: [],
|
||||||
set2: [],
|
set2: [],
|
||||||
|
@ -139,9 +142,13 @@
|
||||||
async setVisible(isVisible, entry) {
|
async setVisible(isVisible, entry) {
|
||||||
if (isVisible && !this.visible) {
|
if (isVisible && !this.visible) {
|
||||||
await this.loadInfo()
|
await this.loadInfo()
|
||||||
|
await this.getUptime()
|
||||||
this.visible = true
|
this.visible = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async getUptime() {
|
||||||
|
this.uptime = await Api.service_uptime(this.service.id)
|
||||||
|
},
|
||||||
async loadInfo() {
|
async loadInfo() {
|
||||||
this.set1 = await this.getHits(24 * 7, "6h")
|
this.set1 = await this.getHits(24 * 7, "6h")
|
||||||
this.set1_name = this.calc(this.set1)
|
this.set1_name = this.calc(this.set1)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<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="text-success font-5 font-weight-bold">{{value}}</span>
|
||||||
<span class="font-2 d-block">{{title}}</span>
|
<span class="font-2 d-block">{{title}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -34,6 +34,9 @@
|
||||||
expression: {
|
expression: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
in_value: {
|
||||||
|
required: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
@ -42,6 +45,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
if (this.in_value) {
|
||||||
|
this.value = this.in_value
|
||||||
|
}
|
||||||
await this.latencyYesterday();
|
await this.latencyYesterday();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -1,61 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -89,29 +34,7 @@
|
||||||
await this.loadIncidents()
|
await this.loadIncidents()
|
||||||
},
|
},
|
||||||
methods: {
|
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>
|
</script>
|
||||||
|
|
|
@ -1,6 +1,22 @@
|
||||||
<template>
|
<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">
|
<div class="col-3">
|
||||||
<select v-model="incident_update.type" class="form-control">
|
<select v-model="incident_update.type" class="form-control">
|
||||||
<option value="Investigating">Investigating</option>
|
<option value="Investigating">Investigating</option>
|
||||||
|
@ -21,6 +37,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -35,12 +52,13 @@
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
incident: {
|
incident: {
|
||||||
type: Object
|
type: Object,
|
||||||
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
updates: [],
|
updates: this.incident.updates,
|
||||||
incident_update: {
|
incident_update: {
|
||||||
incident: this.incident.id,
|
incident: this.incident.id,
|
||||||
message: "",
|
message: "",
|
||||||
|
@ -48,16 +66,23 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted () {
|
beforeRouteUpdate (to, from, next) {
|
||||||
|
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
await this.loadUpdates()
|
await this.loadUpdates()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async loadUpdates() {
|
async delete_update(update) {
|
||||||
this.updates = await Api.incident_updates(this.incident)
|
await Api.incident_update_delete(update)
|
||||||
|
await this.loadUpdates()
|
||||||
},
|
},
|
||||||
async createIncidentUpdate() {
|
async createIncidentUpdate() {
|
||||||
await Api.incident_update_create(this.incident_update)
|
await Api.incident_update_create(this.incident_update)
|
||||||
await this.loadUpdates()
|
await this.loadUpdates()
|
||||||
|
},
|
||||||
|
async loadUpdates() {
|
||||||
|
this.updates = await Api.incident_updates(this.incident)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Database Connection</label>
|
<label>Database Connection</label>
|
||||||
<select @change="canSubmit" v-model="setup.db_connection" id="db_connection" class="form-control">
|
<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="postgres">Postgres</option>
|
||||||
<option value="mysql">MySQL</option>
|
<option value="mysql">MySQL</option>
|
||||||
</select>
|
</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 class="col-12 full-col-12">
|
||||||
<div v-for="(service, index) in services" :ref="service.id" v-bind:key="index">
|
<div v-for="(service, index) in services" :ref="service.id" v-bind:key="index">
|
||||||
<ServiceBlock :service=service />
|
<ServiceBlock :in_service=service />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<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">
|
<div class="col-12 mb-4">
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<h4 class="mt-2">
|
<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}">
|
<span class="badge float-right d-none d-md-block" :class="{'bg-success': service.online, 'bg-danger': !service.online}">
|
||||||
{{service.online ? "ONLINE" : "OFFLINE"}}
|
{{service.online ? "ONLINE" : "OFFLINE"}}
|
||||||
</span>
|
</span>
|
||||||
|
@ -37,6 +37,11 @@
|
||||||
<ServiceHeatmap :service="service"/>
|
<ServiceHeatmap :service="service"/>
|
||||||
</div>
|
</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">
|
<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='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>
|
<a @click="tab='incidents'" class="flex-sm-fill text-sm-center nav-link">Incidents</a>
|
||||||
|
@ -132,7 +137,7 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
id: this.$route.params.id,
|
id: 0,
|
||||||
tab: "failures",
|
tab: "failures",
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
ready: true,
|
ready: true,
|
||||||
|
@ -141,6 +146,8 @@ export default {
|
||||||
failures: [],
|
failures: [],
|
||||||
start_time: this.nowSubtract(84600 * 30),
|
start_time: this.nowSubtract(84600 * 30),
|
||||||
end_time: new Date(),
|
end_time: new Date(),
|
||||||
|
timedata: [],
|
||||||
|
load_timedata: false,
|
||||||
dailyRangeOpts: {
|
dailyRangeOpts: {
|
||||||
chart: {
|
chart: {
|
||||||
height: 500,
|
height: 500,
|
||||||
|
@ -148,6 +155,45 @@ export default {
|
||||||
type: "area",
|
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: {
|
chartOptions: {
|
||||||
chart: {
|
chart: {
|
||||||
events: {
|
events: {
|
||||||
|
@ -270,22 +316,63 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
service () {
|
service () {
|
||||||
return this.$store.getters.serviceByAll(this.id)
|
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: {
|
watch: {
|
||||||
service: function(n, o) {
|
service: function(n, o) {
|
||||||
this.chartHits()
|
this.chartHits()
|
||||||
|
this.fetchUptime()
|
||||||
|
},
|
||||||
|
load_timedata: function(n, o) {
|
||||||
|
this.chartHits()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
async created() {
|
||||||
|
this.id = this.$route.params.id;
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async fetchUptime() {
|
||||||
|
this.timedata = await Api.service_uptime(this.id)
|
||||||
|
this.load_timedata = true
|
||||||
|
},
|
||||||
async get() {
|
async get() {
|
||||||
const s = store.getters.serviceByAll(this.id)
|
const s = this.$store.getters.serviceByAll(this.id)
|
||||||
window.console.log("service: ", s)
|
window.console.log("service: ", s)
|
||||||
this.getService(this.service)
|
this.getService(this.service)
|
||||||
this.messages = this.$store.getters.serviceMessages(this.service.id)
|
this.messages = this.$store.getters.serviceMessages(this.service.id)
|
||||||
|
@ -295,13 +382,12 @@ export default {
|
||||||
const end = this.isBetween(message.end_on, new Date())
|
const end = this.isBetween(message.end_on, new Date())
|
||||||
return start && end
|
return start && end
|
||||||
},
|
},
|
||||||
async getService(s) {
|
async getService() {
|
||||||
await this.chartHits()
|
await this.chartHits()
|
||||||
await this.serviceFailures()
|
await this.serviceFailures()
|
||||||
},
|
},
|
||||||
async serviceFailures() {
|
async serviceFailures() {
|
||||||
let tt = this.startEndTimes()
|
let tt = this.startEndTimes()
|
||||||
|
|
||||||
this.failures = await Api.service_failures(this.service.id, tt.start, tt.end)
|
this.failures = await Api.service_failures(this.service.id, tt.start, tt.end)
|
||||||
},
|
},
|
||||||
async chartHits(start=0, end=99999999999, group="30m") {
|
async chartHits(start=0, end=99999999999, group="30m") {
|
||||||
|
|
|
@ -13,6 +13,9 @@ import VueRouter from "vue-router";
|
||||||
import Setup from "./forms/Setup";
|
import Setup from "./forms/Setup";
|
||||||
|
|
||||||
import Api from "./API";
|
import Api from "./API";
|
||||||
|
import Incidents from "@/pages/Incidents";
|
||||||
|
import Checkins from "@/pages/Checkins";
|
||||||
|
import Failures from "@/pages/Failures";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
|
@ -62,6 +65,24 @@ const routes = [
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true
|
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',
|
path: 'messages',
|
||||||
component: DashboardMessages,
|
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}/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}/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}/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.Handle("/api/services/{id}/heatmap", cached("30s", "application/json", apiServiceHeatmapHandler)).Methods("GET")
|
||||||
|
|
||||||
// API INCIDENTS Routes
|
// API INCIDENTS Routes
|
||||||
|
@ -120,7 +121,7 @@ func Router() *mux.Router {
|
||||||
api.Handle("/api/incidents/{id}", authenticated(apiDeleteIncidentHandler, false)).Methods("DELETE")
|
api.Handle("/api/incidents/{id}", authenticated(apiDeleteIncidentHandler, false)).Methods("DELETE")
|
||||||
|
|
||||||
// API INCIDENTS UPDATES Routes
|
// 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", authenticated(apiCreateIncidentUpdateHandler, false)).Methods("POST")
|
||||||
api.Handle("/api/incidents/{id}/updates/{uid}", authenticated(apiDeleteIncidentUpdateHandler, false)).Methods("DELETE")
|
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/types/services"
|
||||||
"github.com/statping/statping/utils"
|
"github.com/statping/statping/utils"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type serviceOrder struct {
|
type serviceOrder struct {
|
||||||
|
@ -176,6 +178,140 @@ func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
returnJson(objs, w, r)
|
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) {
|
func apiServiceDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
service, err := serviceByID(r)
|
service, err := serviceByID(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -26,7 +26,7 @@ type Service struct {
|
||||||
Domain string `gorm:"column:domain" json:"domain" private:"true" scope:"user,admin"`
|
Domain string `gorm:"column:domain" json:"domain" private:"true" scope:"user,admin"`
|
||||||
Expected null.NullString `gorm:"column:expected" json:"expected" 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"`
|
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"`
|
Type string `gorm:"column:check_type" json:"type" scope:"user,admin"`
|
||||||
Method string `gorm:"column:method" json:"method" 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"`
|
PostData null.NullString `gorm:"column:post_data" json:"post_data" scope:"user,admin"`
|
||||||
|
|
Loading…
Reference in New Issue