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 && \
|
RUN go mod vendor && \
|
||||||
make dev-deps
|
make dev-deps
|
||||||
ADD . /go/src/github.com/hunterlong/statping
|
ADD . /go/src/github.com/hunterlong/statping
|
||||||
RUN cd frontend && yarn install
|
RUN cd frontend && yarn install --network-timeout 1000000
|
||||||
RUN make compile install
|
RUN make compile install
|
||||||
|
|
||||||
# Statping :latest Docker Image
|
# 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
|
// CloseDB will close the database connection if available
|
||||||
func CloseDB() {
|
func CloseDB() {
|
||||||
if DbSession != nil {
|
if DbSession != nil {
|
||||||
|
|
|
@ -272,7 +272,7 @@ type DateScanObj struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GraphDataRaw will return all the hits between 2 times for a Service
|
// 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 := service.(*Service).HitsBetween(start, end, group, column)
|
||||||
model = model.Order("timeframe asc", false).Group("timeframe")
|
model = model.Order("timeframe asc", false).Group("timeframe")
|
||||||
outgoing, err := model.ToChart()
|
outgoing, err := model.ToChart()
|
||||||
|
@ -282,6 +282,23 @@ func GraphDataRaw(service types.ServiceInterface, start, end time.Time, group st
|
||||||
return &DateScanObj{outgoing}
|
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
|
// ToString will convert the DateScanObj into a JSON string for the charts to render
|
||||||
func (d *DateScanObj) ToString() string {
|
func (d *DateScanObj) ToString() string {
|
||||||
data, err := json.Marshal(d.Array)
|
data, err := json.Marshal(d.Array)
|
||||||
|
|
|
@ -23,7 +23,7 @@ func (s *Service) SparklineHourResponse(hours int, method string) string {
|
||||||
var arr []string
|
var arr []string
|
||||||
end := time.Now().UTC()
|
end := time.Now().UTC()
|
||||||
start := end.Add(time.Duration(-hours) * time.Hour)
|
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 {
|
for _, v := range obj.Array {
|
||||||
arr = append(arr, utils.ToString(v.Value))
|
arr = append(arr, utils.ToString(v.Value))
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
"vue-codemirror": "^4.0.6",
|
"vue-codemirror": "^4.0.6",
|
||||||
"vue-flatpickr-component": "^8.1.5",
|
"vue-flatpickr-component": "^8.1.5",
|
||||||
"vue-moment": "^4.1.0",
|
"vue-moment": "^4.1.0",
|
||||||
|
"vue-observe-visibility": "^0.4.6",
|
||||||
"vue-router": "~3.0",
|
"vue-router": "~3.0",
|
||||||
"vuedraggable": "^2.23.2",
|
"vuedraggable": "^2.23.2",
|
||||||
"vuex": "^3.1.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))
|
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) {
|
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))
|
||||||
}
|
}
|
||||||
|
@ -61,6 +65,7 @@ class Api {
|
||||||
}
|
}
|
||||||
|
|
||||||
async groups_reorder(data) {
|
async groups_reorder(data) {
|
||||||
|
window.console.log('/api/reorder/groups', data)
|
||||||
return axios.post('/api/reorder/groups', data).then(response => (response.data))
|
return axios.post('/api/reorder/groups', data).then(response => (response.data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,57 +3,11 @@
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h1 class="text-black-50">Services
|
<h1 class="text-black-50">Services
|
||||||
<router-link to="/dashboard/create_service" class="btn btn-outline-success mt-1 float-right">
|
<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>
|
</router-link>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<table class="table">
|
<ServicesList/>
|
||||||
<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>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -71,8 +25,10 @@
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<draggable tag="tbody" v-model="groupsList" class="sortable_groups" handle=".drag_icon">
|
<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">
|
<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><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>{{$store.getters.servicesInGroup(group.id).length}}</td>
|
||||||
<td>
|
<td>
|
||||||
<span v-if="group.public" class="badge badge-primary">PUBLIC</span>
|
<span v-if="group.public" class="badge badge-primary">PUBLIC</span>
|
||||||
|
@ -102,10 +58,12 @@
|
||||||
import Api from "../../API";
|
import Api from "../../API";
|
||||||
import ToggleSwitch from "../../forms/ToggleSwitch";
|
import ToggleSwitch from "../../forms/ToggleSwitch";
|
||||||
import draggable from 'vuedraggable'
|
import draggable from 'vuedraggable'
|
||||||
|
import ServicesList from './ServicesList';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DashboardServices',
|
name: 'DashboardServices',
|
||||||
components: {
|
components: {
|
||||||
|
ServicesList,
|
||||||
ToggleSwitch,
|
ToggleSwitch,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
draggable
|
draggable
|
||||||
|
@ -117,23 +75,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
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: {
|
groupsList: {
|
||||||
get() {
|
get() {
|
||||||
return this.$store.state.groupsInOrder
|
return this.$store.getters.groupsCleanInOrder
|
||||||
},
|
},
|
||||||
async set(value) {
|
async set(value) {
|
||||||
let data = [];
|
let data = [];
|
||||||
|
@ -165,13 +109,6 @@
|
||||||
window.console.log("saving...");
|
window.console.log("saving...");
|
||||||
window.console.log(this.myViews.array()); // this.myViews.array is not a function
|
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) {
|
async deleteGroup(g) {
|
||||||
let c = confirm(`Are you sure you want to delete '${g.name}'?`)
|
let c = confirm(`Are you sure you want to delete '${g.name}'?`)
|
||||||
if (c) {
|
if (c) {
|
||||||
|
@ -179,14 +116,6 @@
|
||||||
const groups = await Api.groups()
|
const groups = await Api.groups()
|
||||||
this.$store.commit('setGroups', 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">
|
<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}}
|
{{service.name}}
|
||||||
<span class="badge bg-success float-right pulse-glow">{{service.online ? "ONLINE" : "OFFLINE"}}</span>
|
<span class="badge bg-success float-right pulse-glow">{{service.online ? "ONLINE" : "OFFLINE"}}</span>
|
||||||
|
|
||||||
|
<GroupServiceFailures :service="service"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,20 +15,18 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Api from '../../API';
|
||||||
|
import GroupServiceFailures from './GroupServiceFailures';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Group',
|
name: 'Group',
|
||||||
components: {
|
components: {
|
||||||
|
GroupServiceFailures
|
||||||
|
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
group: Object
|
group: Object
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
serviceBadge (s) {
|
|
||||||
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</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>
|
</div>
|
||||||
|
|
||||||
<div class="chart-container">
|
<div v-observe-visibility="visibleChart" class="chart-container">
|
||||||
<ServiceChart :service="service"/>
|
<ServiceChart :service="service" :visible="visible"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row lower_canvas full-col-12 text-white" :class="{'bg-success': service.online, 'bg-danger': !service.online}">
|
<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
|
required: true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
visible: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
smallText(s) {
|
smallText(s) {
|
||||||
if (s.online) {
|
if (s.online) {
|
||||||
|
@ -53,6 +58,11 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
return `Offline, last error: ${s.last_failure.issue} ${this.ago(this.parseTime(s.last_failure.created_at))}`
|
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>
|
<template v-show="showing">
|
||||||
<apexchart v-if="ready" width="100%" height="225" type="area" :options="chartOptions" :series="series"></apexchart>
|
<apexchart v-if="ready" width="100%" height="235" type="area" :options="chartOptions" :series="series"/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -35,14 +35,16 @@
|
||||||
service: {
|
service: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
|
||||||
await this.chartHits()
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
ready: false,
|
ready: false,
|
||||||
|
showing: false,
|
||||||
data: [],
|
data: [],
|
||||||
chartOptions: {
|
chartOptions: {
|
||||||
chart: {
|
chart: {
|
||||||
|
@ -120,6 +122,14 @@
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
visible: function(newVal, oldVal) {
|
||||||
|
if (newVal && !this.showing) {
|
||||||
|
this.showing = true
|
||||||
|
this.chartHits()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async chartHits() {
|
async chartHits() {
|
||||||
const start = this.nowSubtract((3600 * 24) * 7)
|
const start = this.nowSubtract((3600 * 24) * 7)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import VueRouter from 'vue-router'
|
import VueRouter from 'vue-router'
|
||||||
import VueApexCharts from 'vue-apexcharts'
|
import VueApexCharts from 'vue-apexcharts'
|
||||||
|
import VueObserveVisibility from 'vue-observe-visibility'
|
||||||
|
|
||||||
import App from '@/App.vue'
|
import App from '@/App.vue'
|
||||||
import store from './store'
|
import store from './store'
|
||||||
|
@ -12,6 +13,7 @@ import "./icons"
|
||||||
Vue.component('apexchart', VueApexCharts)
|
Vue.component('apexchart', VueApexCharts)
|
||||||
|
|
||||||
Vue.use(VueRouter);
|
Vue.use(VueRouter);
|
||||||
|
Vue.use(VueObserveVisibility);
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
new Vue({
|
new Vue({
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<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/>
|
<Header/>
|
||||||
|
|
||||||
|
@ -12,12 +12,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 full-col-12">
|
<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 />
|
<ServiceBlock :service=service />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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">
|
<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}}
|
<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>
|
</a>
|
||||||
|
|
||||||
<h6 class="mt-4 text-muted">Integrations <span class="badge badge-secondary float-right">BETA</span></h6>
|
<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),
|
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),
|
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),
|
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) => {
|
serviceById: (state) => (id) => {
|
||||||
return state.services.find(s => s.id == 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/reorder/services", authenticated(reorderServiceHandler, false)).Methods("POST")
|
||||||
api.Handle("/api/services/{id}/running", authenticated(apiServiceRunningHandler, 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}/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}/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}/heatmap", cached("30s", "application/json", apiServiceHeatmapHandler)).Methods("GET")
|
||||||
api.Handle("/api/services/{id}", authenticated(apiServiceUpdateHandler, false)).Methods("POST")
|
api.Handle("/api/services/{id}", authenticated(apiServiceUpdateHandler, false)).Methods("POST")
|
||||||
|
|
|
@ -107,7 +107,7 @@ func servicesViewHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
group = "hour"
|
group = "hour"
|
||||||
}
|
}
|
||||||
|
|
||||||
data := core.GraphDataRaw(serv, start, end, group, "latency")
|
data := core.GraphHitsDataRaw(serv, start, end, group, "latency")
|
||||||
|
|
||||||
out := struct {
|
out := struct {
|
||||||
Service *core.Service
|
Service *core.Service
|
||||||
|
@ -216,7 +216,29 @@ func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
start := time.Unix(startField, 0)
|
start := time.Unix(startField, 0)
|
||||||
end := time.Unix(endField, 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)
|
returnJson(obj, w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,7 +257,7 @@ func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
start := time.Unix(startField, 0)
|
start := time.Unix(startField, 0)
|
||||||
end := time.Unix(endField, 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)
|
returnJson(obj, w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,9 +17,11 @@ package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -108,6 +110,11 @@ type Database interface {
|
||||||
Hits() ([]*Hit, error)
|
Hits() ([]*Hit, error)
|
||||||
ToChart() ([]*DateScan, error)
|
ToChart() ([]*DateScan, error)
|
||||||
|
|
||||||
|
GroupByTimeframe() Database
|
||||||
|
ToTimeValue() ([]TimeValue, error)
|
||||||
|
|
||||||
|
MultipleSelects(args ...string) Database
|
||||||
|
|
||||||
Failurer
|
Failurer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,6 +123,42 @@ type Failurer interface {
|
||||||
Fails() ([]*Failure, error)
|
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 {
|
func (it *Db) Failures(id int64) Database {
|
||||||
return it.Model(&Failure{}).Where("service = ?", id).Not("method = 'checkin'").Order("id desc")
|
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 {
|
type Db struct {
|
||||||
Database *gorm.DB
|
Database *gorm.DB
|
||||||
|
Type string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Openw is a drop-in replacement for Open()
|
// 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
|
// Wrap wraps gorm.DB in an interface
|
||||||
func Wrap(db *gorm.DB) Database {
|
func Wrap(db *gorm.DB) Database {
|
||||||
return &Db{db}
|
return &Db{
|
||||||
|
Database: db,
|
||||||
|
Type: db.Dialect().GetName(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (it *Db) Close() error {
|
func (it *Db) Close() error {
|
||||||
|
@ -457,6 +504,32 @@ type DateScan struct {
|
||||||
Value int64 `json:"y"`
|
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) {
|
func (it *Db) ToChart() ([]*DateScan, error) {
|
||||||
rows, err := it.Database.Rows()
|
rows, err := it.Database.Rows()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Reference in New Issue