mirror of https://github.com/statping/statping
vue
parent
eb5ecdad16
commit
da34fc179c
|
@ -10,7 +10,7 @@ ADD Makefile go.mod /go/src/github.com/hunterlong/statping/
|
|||
RUN go mod vendor && \
|
||||
make dev-deps
|
||||
ADD . /go/src/github.com/hunterlong/statping
|
||||
RUN cd frontend && yarn install
|
||||
RUN cd frontend && yarn install --network-timeout 1000000
|
||||
RUN make compile install
|
||||
|
||||
# Statping :latest Docker Image
|
||||
|
|
|
@ -86,6 +86,16 @@ func (s *Service) HitsBetween(t1, t2 time.Time, group string, column string) typ
|
|||
}
|
||||
}
|
||||
|
||||
// FailuresBetween returns the gorm database query for a collection of service hits between a time range
|
||||
func (s *Service) FailuresBetween(t1, t2 time.Time, group string, column string) types.Database {
|
||||
selector := Dbtimestamp(group, column)
|
||||
if CoreApp.Config.DbConn == "postgres" {
|
||||
return Database(&Failure{}).Select(selector).Where("service = ? AND created_at BETWEEN ? AND ?", s.Id, t1.UTC().Format(types.TIME), t2.UTC().Format(types.TIME))
|
||||
} else {
|
||||
return Database(&Failure{}).Select(selector).Where("service = ? AND created_at BETWEEN ? AND ?", s.Id, t1.UTC().Format(types.TIME_DAY), t2.UTC().Format(types.TIME_DAY))
|
||||
}
|
||||
}
|
||||
|
||||
// CloseDB will close the database connection if available
|
||||
func CloseDB() {
|
||||
if DbSession != nil {
|
||||
|
|
|
@ -272,7 +272,7 @@ type DateScanObj struct {
|
|||
}
|
||||
|
||||
// GraphDataRaw will return all the hits between 2 times for a Service
|
||||
func GraphDataRaw(service types.ServiceInterface, start, end time.Time, group string, column string) *DateScanObj {
|
||||
func GraphHitsDataRaw(service types.ServiceInterface, start, end time.Time, group string, column string) *DateScanObj {
|
||||
model := service.(*Service).HitsBetween(start, end, group, column)
|
||||
model = model.Order("timeframe asc", false).Group("timeframe")
|
||||
outgoing, err := model.ToChart()
|
||||
|
@ -282,6 +282,23 @@ func GraphDataRaw(service types.ServiceInterface, start, end time.Time, group st
|
|||
return &DateScanObj{outgoing}
|
||||
}
|
||||
|
||||
// GraphDataRaw will return all the hits between 2 times for a Service
|
||||
func GraphFailuresDataRaw(service types.ServiceInterface, start, end time.Time, group string) []types.TimeValue {
|
||||
srv := service.(*Service)
|
||||
|
||||
query := Database(&types.Failure{}).
|
||||
Where("service = ?", srv.Id).
|
||||
Between(start, end).
|
||||
MultipleSelects(types.SelectByTime(group), types.CountAmount()).
|
||||
GroupByTimeframe().Debug()
|
||||
|
||||
outgoing, err := query.ToTimeValue()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
return outgoing
|
||||
}
|
||||
|
||||
// ToString will convert the DateScanObj into a JSON string for the charts to render
|
||||
func (d *DateScanObj) ToString() string {
|
||||
data, err := json.Marshal(d.Array)
|
||||
|
|
|
@ -23,7 +23,7 @@ func (s *Service) SparklineHourResponse(hours int, method string) string {
|
|||
var arr []string
|
||||
end := time.Now().UTC()
|
||||
start := end.Add(time.Duration(-hours) * time.Hour)
|
||||
obj := GraphDataRaw(s, start, end, "hour", method)
|
||||
obj := GraphHitsDataRaw(s, start, end, "hour", method)
|
||||
for _, v := range obj.Array {
|
||||
arr = append(arr, utils.ToString(v.Value))
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
"vue-codemirror": "^4.0.6",
|
||||
"vue-flatpickr-component": "^8.1.5",
|
||||
"vue-moment": "^4.1.0",
|
||||
"vue-observe-visibility": "^0.4.6",
|
||||
"vue-router": "~3.0",
|
||||
"vuedraggable": "^2.23.2",
|
||||
"vuex": "^3.1.2"
|
||||
|
|
|
@ -40,6 +40,10 @@ class Api {
|
|||
return axios.get('/api/services/' + id + '/data?start=' + start + '&end=' + end + '&group=' + group).then(response => (response.data))
|
||||
}
|
||||
|
||||
async service_failures_data(id, start, end, group) {
|
||||
return axios.get('/api/services/' + id + '/failure_data?start=' + start + '&end=' + end + '&group=' + group).then(response => (response.data))
|
||||
}
|
||||
|
||||
async service_heatmap(id, start, end, group) {
|
||||
return axios.get('/api/services/' + id + '/heatmap').then(response => (response.data))
|
||||
}
|
||||
|
@ -61,6 +65,7 @@ class Api {
|
|||
}
|
||||
|
||||
async groups_reorder(data) {
|
||||
window.console.log('/api/reorder/groups', data)
|
||||
return axios.post('/api/reorder/groups', data).then(response => (response.data))
|
||||
}
|
||||
|
||||
|
|
|
@ -3,57 +3,11 @@
|
|||
<div class="col-12">
|
||||
<h1 class="text-black-50">Services
|
||||
<router-link to="/dashboard/create_service" class="btn btn-outline-success mt-1 float-right">
|
||||
<i class="fas fa-plus"></i> Create
|
||||
<font-awesome-icon icon="plus"/> Create
|
||||
</router-link>
|
||||
</h1>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col" class="d-none d-md-table-cell">Status</th>
|
||||
<th scope="col" class="d-none d-md-table-cell">Visibility</th>
|
||||
<th scope="col" class="d-none d-md-table-cell">Group</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<draggable tag="tbody" v-model="servicesList" :key="$store.getters.servicesInOrder.length" class="sortable" handle=".drag_icon">
|
||||
<tr v-for="(service, index) in $store.getters.servicesInOrder" :key="index">
|
||||
<td>
|
||||
<span class="drag_icon d-none d-md-inline">
|
||||
<font-awesome-icon icon="bars" />
|
||||
</span> {{service.name}}
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell">
|
||||
<span class="badge" :class="{'animate-fader': !service.online, 'badge-success': service.online, 'badge-danger': !service.online}">
|
||||
{{service.online ? "ONLINE" : "OFFLINE"}}
|
||||
</span>
|
||||
<ToggleSwitch v-if="service.online" :service="service"/>
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell">
|
||||
<span class="badge" :class="{'badge-primary': service.public, 'badge-secondary': !service.public}">
|
||||
{{service.public ? "PUBLIC" : "PRIVATE"}}
|
||||
</span>
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell">
|
||||
<div v-if="service.group_id !== 0"><span class="badge badge-secondary">{{serviceGroup(service)}}</span></div>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<div class="btn-group">
|
||||
<router-link :to="{path: `/dashboard/edit_service/${service.id}`, params: {service: service} }" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-chart-area"></i> Edit
|
||||
</router-link>
|
||||
<router-link :to="{path: serviceLink(service), params: {service: service} }" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-chart-area"></i> View
|
||||
</router-link>
|
||||
<a @click.prevent="deleteService(service)" href="#" class="btn btn-danger">
|
||||
<font-awesome-icon icon="times" />
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</draggable>
|
||||
</table>
|
||||
<ServicesList/>
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -71,8 +25,10 @@
|
|||
</thead>
|
||||
|
||||
<draggable tag="tbody" v-model="groupsList" class="sortable_groups" handle=".drag_icon">
|
||||
<tr v-for="(group, index) in $store.getters.groupsClean" v-bind:key="index">
|
||||
<td><span class="drag_icon d-none d-md-inline"><font-awesome-icon icon="bars" /></span> {{group.name}}</td>
|
||||
<tr v-for="(group, index) in $store.getters.groupsCleanInOrder" v-bind:key="group.id">
|
||||
<td><span class="drag_icon d-none d-md-inline">
|
||||
<font-awesome-icon icon="bars" /></span> {{group.name}}
|
||||
</td>
|
||||
<td>{{$store.getters.servicesInGroup(group.id).length}}</td>
|
||||
<td>
|
||||
<span v-if="group.public" class="badge badge-primary">PUBLIC</span>
|
||||
|
@ -102,10 +58,12 @@
|
|||
import Api from "../../API";
|
||||
import ToggleSwitch from "../../forms/ToggleSwitch";
|
||||
import draggable from 'vuedraggable'
|
||||
import ServicesList from './ServicesList';
|
||||
|
||||
export default {
|
||||
name: 'DashboardServices',
|
||||
components: {
|
||||
ServicesList,
|
||||
ToggleSwitch,
|
||||
FormGroup,
|
||||
draggable
|
||||
|
@ -117,23 +75,9 @@
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
servicesList: {
|
||||
get() {
|
||||
return this.$store.state.servicesInOrder
|
||||
},
|
||||
async set(value) {
|
||||
let data = [];
|
||||
value.forEach((s, k) => {
|
||||
data.push({service: s.id, order: k + 1})
|
||||
});
|
||||
await Api.services_reorder(data)
|
||||
const services = await Api.services()
|
||||
this.$store.commit('setServices', services)
|
||||
}
|
||||
},
|
||||
groupsList: {
|
||||
get() {
|
||||
return this.$store.state.groupsInOrder
|
||||
return this.$store.getters.groupsCleanInOrder
|
||||
},
|
||||
async set(value) {
|
||||
let data = [];
|
||||
|
@ -165,13 +109,6 @@
|
|||
window.console.log("saving...");
|
||||
window.console.log(this.myViews.array()); // this.myViews.array is not a function
|
||||
},
|
||||
serviceGroup(s) {
|
||||
let group = this.$store.getters.groupById(s.group_id)
|
||||
if (group) {
|
||||
return group.name
|
||||
}
|
||||
return ""
|
||||
},
|
||||
async deleteGroup(g) {
|
||||
let c = confirm(`Are you sure you want to delete '${g.name}'?`)
|
||||
if (c) {
|
||||
|
@ -179,14 +116,6 @@
|
|||
const groups = await Api.groups()
|
||||
this.$store.commit('setGroups', groups)
|
||||
}
|
||||
},
|
||||
async deleteService(s) {
|
||||
let c = confirm(`Are you sure you want to delete '${s.name}'?`)
|
||||
if (c) {
|
||||
await Api.service_delete(s.id)
|
||||
const services = await Api.services()
|
||||
this.$store.commit('setServices', services)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
<template>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col" class="d-none d-md-table-cell">Status</th>
|
||||
<th scope="col" class="d-none d-md-table-cell">Visibility</th>
|
||||
<th scope="col" class="d-none d-md-table-cell">Group</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<draggable tag="tbody" v-model="servicesList" handle=".drag_icon">
|
||||
<tr v-for="(service, index) in $store.getters.servicesInOrder" :key="service.id">
|
||||
<td>
|
||||
<span class="drag_icon d-none d-md-inline">
|
||||
<font-awesome-icon icon="bars" />
|
||||
</span> {{service.name}}
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell">
|
||||
<span class="badge" :class="{'animate-fader': !service.online, 'badge-success': service.online, 'badge-danger': !service.online}">
|
||||
{{service.online ? "ONLINE" : "OFFLINE"}}
|
||||
</span>
|
||||
<ToggleSwitch v-if="service.online" :service="service"/>
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell">
|
||||
<span class="badge" :class="{'badge-primary': service.public, 'badge-secondary': !service.public}">
|
||||
{{service.public ? "PUBLIC" : "PRIVATE"}}
|
||||
</span>
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell">
|
||||
<div v-if="service.group_id !== 0"><span class="badge badge-secondary">{{serviceGroup(service)}}</span></div>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<div class="btn-group">
|
||||
<router-link :to="{path: `/dashboard/edit_service/${service.id}`, params: {service: service} }" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-chart-area"></i> Edit
|
||||
</router-link>
|
||||
<router-link :to="{path: serviceLink(service), params: {service: service} }" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-chart-area"></i> View
|
||||
</router-link>
|
||||
<a @click.prevent="deleteService(service)" href="#" class="btn btn-danger">
|
||||
<font-awesome-icon icon="times" />
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</draggable>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "../../API";
|
||||
import draggable from 'vuedraggable'
|
||||
import ToggleSwitch from '../../forms/ToggleSwitch';
|
||||
|
||||
export default {
|
||||
name: 'ServicesList',
|
||||
components: {
|
||||
ToggleSwitch,
|
||||
draggable
|
||||
},
|
||||
computed: {
|
||||
servicesList: {
|
||||
get () {
|
||||
return this.$store.getters.servicesInOrder
|
||||
},
|
||||
set (value) {
|
||||
this.updateOrder(value)
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async updateOrder(value) {
|
||||
let data = [];
|
||||
value.forEach((s, k) => {
|
||||
data.push({ service: s.id, order: k + 1 })
|
||||
});
|
||||
const reorder = await Api.services_reorder(data)
|
||||
window.console.log('reorder', reorder)
|
||||
const services = await Api.services()
|
||||
this.$store.commit('setServices', services)
|
||||
},
|
||||
async deleteService(s) {
|
||||
let c = confirm(`Are you sure you want to delete '${s.name}'?`)
|
||||
if (c) {
|
||||
await Api.service_delete(s.id)
|
||||
const services = await Api.services()
|
||||
this.$store.commit('setServices', services)
|
||||
}
|
||||
},
|
||||
serviceGroup(s) {
|
||||
let group = this.$store.getters.groupById(s.group_id)
|
||||
if (group) {
|
||||
return group.name
|
||||
}
|
||||
return ""
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
</style>
|
|
@ -6,6 +6,8 @@
|
|||
<a v-for="(service, index) in $store.getters.servicesInGroup(group.id)" v-bind:key="index" class="service_li list-group-item list-group-item-action">
|
||||
{{service.name}}
|
||||
<span class="badge bg-success float-right pulse-glow">{{service.online ? "ONLINE" : "OFFLINE"}}</span>
|
||||
|
||||
<GroupServiceFailures :service="service"/>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
@ -13,20 +15,18 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '../../API';
|
||||
import GroupServiceFailures from './GroupServiceFailures';
|
||||
|
||||
export default {
|
||||
name: 'Group',
|
||||
components: {
|
||||
GroupServiceFailures
|
||||
|
||||
},
|
||||
props: {
|
||||
group: Object
|
||||
},
|
||||
methods: {
|
||||
serviceBadge (s) {
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
<template>
|
||||
<div class="row">
|
||||
|
||||
<span class="bg-danger">o</span>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '../../API';
|
||||
|
||||
export default {
|
||||
name: 'GroupServiceFailures',
|
||||
components: {
|
||||
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
failureData: null
|
||||
}
|
||||
},
|
||||
props: {
|
||||
service: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.lastDaysFailures()
|
||||
},
|
||||
methods: {
|
||||
async lastDaysFailures() {
|
||||
const start = this.nowSubtract((3600 * 24) * 30)
|
||||
this.failureData = await Api.service_failures_data(this.service.id, this.toUnix(start), this.toUnix(this.now()), "day")
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
@keyframes pulse_animation {
|
||||
0% { transform: scale(1); }
|
||||
30% { transform: scale(1); }
|
||||
40% { transform: scale(1.02); }
|
||||
50% { transform: scale(1); }
|
||||
60% { transform: scale(1); }
|
||||
70% { transform: scale(1.05); }
|
||||
80% { transform: scale(1); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation-name: pulse_animation;
|
||||
animation-duration: 1500ms;
|
||||
transform-origin:70% 70%;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
|
||||
|
||||
@keyframes glow-grow {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(1);
|
||||
}
|
||||
80% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.pulse-glow {
|
||||
animation-name: glow-grown;
|
||||
animation-duration: 100ms;
|
||||
transform-origin: 70% 30%;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
|
||||
.pulse-glow:before,
|
||||
.pulse-glow:after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 0.4rem;
|
||||
width: 1.7rem;
|
||||
top: 1.3rem;
|
||||
right: 2.15rem;
|
||||
border-radius: 0;
|
||||
box-shadow: 0 0 6px #47d337;
|
||||
animation: glow-grow 2s ease-out infinite;
|
||||
}
|
||||
</style>
|
|
@ -13,8 +13,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<ServiceChart :service="service"/>
|
||||
<div v-observe-visibility="visibleChart" class="chart-container">
|
||||
<ServiceChart :service="service" :visible="visible"/>
|
||||
</div>
|
||||
|
||||
<div class="row lower_canvas full-col-12 text-white" :class="{'bg-success': service.online, 'bg-danger': !service.online}">
|
||||
|
@ -46,6 +46,11 @@ export default {
|
|||
required: true
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
smallText(s) {
|
||||
if (s.online) {
|
||||
|
@ -53,6 +58,11 @@ export default {
|
|||
} else {
|
||||
return `Offline, last error: ${s.last_failure.issue} ${this.ago(this.parseTime(s.last_failure.created_at))}`
|
||||
}
|
||||
},
|
||||
visibleChart(isVisible, entry) {
|
||||
if (isVisible && !this.visible) {
|
||||
this.visible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<apexchart v-if="ready" width="100%" height="225" type="area" :options="chartOptions" :series="series"></apexchart>
|
||||
<template v-show="showing">
|
||||
<apexchart v-if="ready" width="100%" height="235" type="area" :options="chartOptions" :series="series"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -35,14 +35,16 @@
|
|||
service: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
await this.chartHits()
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ready: false,
|
||||
showing: false,
|
||||
data: [],
|
||||
chartOptions: {
|
||||
chart: {
|
||||
|
@ -120,6 +122,14 @@
|
|||
}]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible: function(newVal, oldVal) {
|
||||
if (newVal && !this.showing) {
|
||||
this.showing = true
|
||||
this.chartHits()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async chartHits() {
|
||||
const start = this.nowSubtract((3600 * 24) * 7)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import VueApexCharts from 'vue-apexcharts'
|
||||
import VueObserveVisibility from 'vue-observe-visibility'
|
||||
|
||||
import App from '@/App.vue'
|
||||
import store from './store'
|
||||
|
@ -12,6 +13,7 @@ import "./icons"
|
|||
Vue.component('apexchart', VueApexCharts)
|
||||
|
||||
Vue.use(VueRouter);
|
||||
Vue.use(VueObserveVisibility);
|
||||
|
||||
Vue.config.productionTip = false
|
||||
new Vue({
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="container col-md-7 col-sm-12 mt-2 sm-container">
|
||||
<div class="container col-md-7 col-sm-12 mt-4 sm-container">
|
||||
|
||||
<Header/>
|
||||
|
||||
|
@ -12,12 +12,11 @@
|
|||
</div>
|
||||
|
||||
<div class="col-12 full-col-12">
|
||||
<div v-for="(service, index) in $store.getters.services" :ref="service.id" v-bind:key="index">
|
||||
<div v-for="(service, index) in $store.getters.servicesInOrder" :ref="service.id" v-bind:key="index">
|
||||
<ServiceBlock :service=service />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
<a v-for="(notifier, index) in $store.getters.notifiers" v-bind:key="`${notifier.method}_${index}`" @click.prevent="changeTab" class="nav-link text-capitalize" v-bind:class="{active: liClass(`v-pills-${notifier.method.toLowerCase()}-tab`)}" v-bind:id="`v-pills-${notifier.method.toLowerCase()}-tab`" data-toggle="pill" v-bind:href="`#v-pills-${notifier.method.toLowerCase()}`" role="tab" v-bind:aria-controls="`v-pills-${notifier.method.toLowerCase()}`" aria-selected="false">
|
||||
<font-awesome-icon :icon="iconName(notifier.icon)" class="mr-2"/> {{notifier.method}}
|
||||
<span v-if="notifier.enabled" class="badge badge-pill float-right mt-1" :class="{'badge-success': !liClass(`v-pills-${notifier.method.toLowerCase()}-tab`), 'badge-light': liClass(`v-pills-${notifier.method.toLowerCase()}-tab`)}">ON</span>
|
||||
<span v-if="notifier.enabled" class="badge badge-pill float-right mt-1" :class="{'badge-success': !liClass(`v-pills-${notifier.method.toLowerCase()}-tab`), 'badge-light': liClass(`v-pills-${notifier.method.toLowerCase()}-tab`), 'text-dark': liClass(`v-pills-${notifier.method.toLowerCase()}-tab`)}">ON</span>
|
||||
</a>
|
||||
|
||||
<h6 class="mt-4 text-muted">Integrations <span class="badge badge-secondary float-right">BETA</span></h6>
|
||||
|
|
|
@ -43,6 +43,7 @@ export default new Vuex.Store({
|
|||
servicesInOrder: state => state.services.sort((a, b) => a.order_id - b.order_id),
|
||||
groupsInOrder: state => state.groups.sort((a, b) => a.order_id - b.order_id),
|
||||
groupsClean: state => state.groups.filter(g => g.name !== '').sort((a, b) => a.order_id - b.order_id),
|
||||
groupsCleanInOrder: state => state.groups.filter(g => g.name !== '').sort((a, b) => a.order_id - b.order_id).sort((a, b) => a.order_id - b.order_id),
|
||||
|
||||
serviceById: (state) => (id) => {
|
||||
return state.services.find(s => s.id == id)
|
||||
|
|
|
@ -120,6 +120,7 @@ func Router() *mux.Router {
|
|||
api.Handle("/api/reorder/services", authenticated(reorderServiceHandler, false)).Methods("POST")
|
||||
api.Handle("/api/services/{id}/running", authenticated(apiServiceRunningHandler, false)).Methods("POST")
|
||||
api.Handle("/api/services/{id}/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", cached("30s", "application/json", apiServicePingDataHandler)).Methods("GET")
|
||||
api.Handle("/api/services/{id}/heatmap", cached("30s", "application/json", apiServiceHeatmapHandler)).Methods("GET")
|
||||
api.Handle("/api/services/{id}", authenticated(apiServiceUpdateHandler, false)).Methods("POST")
|
||||
|
|
|
@ -107,7 +107,7 @@ func servicesViewHandler(w http.ResponseWriter, r *http.Request) {
|
|||
group = "hour"
|
||||
}
|
||||
|
||||
data := core.GraphDataRaw(serv, start, end, group, "latency")
|
||||
data := core.GraphHitsDataRaw(serv, start, end, group, "latency")
|
||||
|
||||
out := struct {
|
||||
Service *core.Service
|
||||
|
@ -216,7 +216,29 @@ func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) {
|
|||
start := time.Unix(startField, 0)
|
||||
end := time.Unix(endField, 0)
|
||||
|
||||
obj := core.GraphDataRaw(service, start, end, grouping, "latency")
|
||||
obj := core.GraphHitsDataRaw(service, start, end, grouping, "latency")
|
||||
returnJson(obj, w, r)
|
||||
}
|
||||
|
||||
func apiServiceFailureDataHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
service := core.SelectService(utils.ToInt(vars["id"]))
|
||||
if service == nil {
|
||||
sendErrorJson(errors.New("service data not found"), w, r)
|
||||
return
|
||||
}
|
||||
fields := parseGet(r)
|
||||
grouping := fields.Get("group")
|
||||
if grouping == "" {
|
||||
grouping = "hour"
|
||||
}
|
||||
startField := utils.ToInt(fields.Get("start"))
|
||||
endField := utils.ToInt(fields.Get("end"))
|
||||
|
||||
start := time.Unix(startField, 0)
|
||||
end := time.Unix(endField, 0)
|
||||
|
||||
obj := core.GraphFailuresDataRaw(service, start, end, grouping)
|
||||
returnJson(obj, w, r)
|
||||
}
|
||||
|
||||
|
@ -235,7 +257,7 @@ func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) {
|
|||
start := time.Unix(startField, 0)
|
||||
end := time.Unix(endField, 0)
|
||||
|
||||
obj := core.GraphDataRaw(service, start, end, grouping, "ping_time")
|
||||
obj := core.GraphHitsDataRaw(service, start, end, grouping, "ping_time")
|
||||
returnJson(obj, w, r)
|
||||
}
|
||||
|
||||
|
|
|
@ -17,9 +17,11 @@ package types
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/jinzhu/gorm"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -108,6 +110,11 @@ type Database interface {
|
|||
Hits() ([]*Hit, error)
|
||||
ToChart() ([]*DateScan, error)
|
||||
|
||||
GroupByTimeframe() Database
|
||||
ToTimeValue() ([]TimeValue, error)
|
||||
|
||||
MultipleSelects(args ...string) Database
|
||||
|
||||
Failurer
|
||||
}
|
||||
|
||||
|
@ -116,6 +123,42 @@ type Failurer interface {
|
|||
Fails() ([]*Failure, error)
|
||||
}
|
||||
|
||||
func sqlTimeframes(increment string) string {
|
||||
switch increment {
|
||||
case "second":
|
||||
return "%Y-%m-%d %H:%M:%S"
|
||||
case "minute":
|
||||
return "%Y-%m-%d %H:%M:00"
|
||||
case "hour":
|
||||
return "%Y-%m-%d %H:00:00"
|
||||
case "day":
|
||||
return "%Y-%m-%d 00:00:00"
|
||||
case "month":
|
||||
return "%Y-%m 00:00:00"
|
||||
case "year":
|
||||
return "%Y"
|
||||
default:
|
||||
return "%Y-%m-%d 00:00:00"
|
||||
}
|
||||
}
|
||||
|
||||
func (it *Db) MultipleSelects(args ...string) Database {
|
||||
joined := strings.Join(args, ", ")
|
||||
return it.Select(joined)
|
||||
}
|
||||
|
||||
func CountAmount() string {
|
||||
return fmt.Sprintf("COUNT(id) as amount")
|
||||
}
|
||||
|
||||
func SelectByTime(increment string) string {
|
||||
return fmt.Sprintf("strftime('%s', created_at, 'utc') as timeframe", sqlTimeframes(increment))
|
||||
}
|
||||
|
||||
func (it *Db) GroupByTimeframe() Database {
|
||||
return it.Group("timeframe")
|
||||
}
|
||||
|
||||
func (it *Db) Failures(id int64) Database {
|
||||
return it.Model(&Failure{}).Where("service = ?", id).Not("method = 'checkin'").Order("id desc")
|
||||
}
|
||||
|
@ -128,6 +171,7 @@ func (it *Db) Fails() ([]*Failure, error) {
|
|||
|
||||
type Db struct {
|
||||
Database *gorm.DB
|
||||
Type string
|
||||
}
|
||||
|
||||
// Openw is a drop-in replacement for Open()
|
||||
|
@ -138,7 +182,10 @@ func Openw(dialect string, args ...interface{}) (db Database, err error) {
|
|||
|
||||
// Wrap wraps gorm.DB in an interface
|
||||
func Wrap(db *gorm.DB) Database {
|
||||
return &Db{db}
|
||||
return &Db{
|
||||
Database: db,
|
||||
Type: db.Dialect().GetName(),
|
||||
}
|
||||
}
|
||||
|
||||
func (it *Db) Close() error {
|
||||
|
@ -457,6 +504,32 @@ type DateScan struct {
|
|||
Value int64 `json:"y"`
|
||||
}
|
||||
|
||||
type TimeValue struct {
|
||||
Timeframe time.Time `json:"timeframe"`
|
||||
Amount int64 `json:"amount"`
|
||||
}
|
||||
|
||||
func (it *Db) ToTimeValue() ([]TimeValue, error) {
|
||||
rows, err := it.Database.Rows()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var data []TimeValue
|
||||
for rows.Next() {
|
||||
var timeframe string
|
||||
var amount int64
|
||||
if err := rows.Scan(&timeframe, &amount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
createdTime, _ := time.Parse(TIME, timeframe)
|
||||
data = append(data, TimeValue{
|
||||
Timeframe: createdTime,
|
||||
Amount: amount,
|
||||
})
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (it *Db) ToChart() ([]*DateScan, error) {
|
||||
rows, err := it.Database.Rows()
|
||||
if err != nil {
|
||||
|
|
Loading…
Reference in New Issue