better loading, updated service chart

pull/792/head
hunterlong 4 years ago
parent 2319cd36d1
commit 8b54ceb16f

@ -7,6 +7,7 @@
- Added Help page that is generated from Statping's Wiki repo on build
- Modified Service Group failures on index page to show 90 days of failures
- Modified Service view page, updated Latency and Ping charts, added failures below
- Modified Service chart on index page to show ping data along with latency
# 0.90.63 (08-17-2020)
- Modified build process to use xgo for all arch builds

@ -87,6 +87,9 @@ func (g *GroupQuery) GraphData(by By) ([]*TimeValue, error) {
return caller.ToValues()
}
// ToTimeValue will format the SQL rows into a JSON format for the API.
// [{"timestamp": "2006-01-02T15:04:05Z", "amount": 468293}]
// TODO redo this entire function, use better SQL query to group by time
func (g *GroupQuery) ToTimeValue() (*TimeVar, error) {
rows, err := g.db.Rows()
if err != nil {

@ -19,10 +19,9 @@ func (it *Db) ParseTime(t string) (time.Time, error) {
}
}
// FormatTime returns the timestamp in the same format as the DATETIME column in database
func (it *Db) FormatTime(t time.Time) string {
switch it.Type {
case "mysql":
return t.Format("2006-01-02 15:04:05")
case "postgres":
return t.Format("2006-01-02 15:04:05.999999999")
default:
@ -30,6 +29,7 @@ func (it *Db) FormatTime(t time.Time) string {
}
}
// SelectByTime returns an SQL query that will group "created_at" column by x seconds and returns as "timeframe"
func (it *Db) SelectByTime(increment time.Duration) string {
seconds := int64(increment.Seconds())
switch it.Type {
@ -41,33 +41,3 @@ func (it *Db) SelectByTime(increment time.Duration) string {
return fmt.Sprintf("datetime((strftime('%%s', created_at) / %d) * %d, 'unixepoch') as timeframe", seconds, seconds)
}
}
func (it *Db) correctTimestamp(increment string) string {
var timestamper string
switch increment {
case "second":
timestamper = "%Y-%m-%d %H:%M:%S"
case "minute":
timestamper = "%Y-%m-%d %H:%M:00"
case "hour":
timestamper = "%Y-%m-%d %H:00:00"
case "day":
timestamper = "%Y-%m-%d 00:00:00"
case "month":
timestamper = "%Y-%m-01 00:00:00"
case "year":
timestamper = "%Y-01-01 00:00:00"
default:
timestamper = "%Y-%m-%d 00:00:00"
}
switch it.Type {
case "mysql":
case "second":
timestamper = "%Y-%m-%d %H:%i:%S"
case "minute":
timestamper = "%Y-%m-%d %H:%i:00"
}
return timestamper
}

@ -1,6 +1,6 @@
<template>
<div id="app">
<router-view :loaded="loaded"/>
<router-view/>
<Footer v-if="$route.path !== '/setup'"/>
</div>
</template>

@ -65,7 +65,3 @@
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

@ -1,9 +1,9 @@
<template>
<div class="col-12 full-col-12">
<div v-if="services.length > 0" class="col-12 full-col-12">
<h4 v-if="group.name !== 'Empty Group'" class="group_header mb-3 mt-4">{{group.name}}</h4>
<div class="list-group online_list mb-4">
<div v-for="(service, index) in $store.getters.servicesInGroup(group.id)" v-bind:key="index" class="service_li list-group-item list-group-item-action">
<div v-for="(service, index) in services" v-bind:key="index" class="service_li list-group-item list-group-item-action">
<router-link class="no-decoration font-3" :to="serviceLink(service)">{{service.name}}</router-link>
<span class="badge text-uppercase float-right" :class="{'bg-success': service.online, 'bg-danger': !service.online }">
{{service.online ? $t('online') : $t('offline')}}
@ -30,11 +30,15 @@ export default {
GroupServiceFailures
},
props: {
group: Object
group: {
type: Object,
required: true,
}
},
computed: {
services() {
return this.$store.getters.servicesInGroup(this.group.id)
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

@ -1,5 +1,12 @@
<template>
<div>
<div v-observe-visibility="{callback: visibleChart, once: true}" v-if="!loaded" class="row">
<div class="col-12 text-center mt-3">
<font-awesome-icon icon="circle-notch" class="text-dim" size="1x" spin/>
</div>
</div>
<transition name="fade">
<div v-if="loaded">
<div class="d-flex mt-3">
<div class="flex-fill service_day" v-for="(d, index) in failureData" @mouseover="mouseover(d)" @mouseout="mouseout" :class="{'day-error': d.amount > 0, 'day-success': d.amount === 0}">
<span v-if="d.amount !== 0" class="d-none d-md-block text-center small"></span>
@ -17,6 +24,8 @@
</div>
</div>
<div class="daily-failures small text-right text-dim">{{hover_text}}</div>
</div>
</transition>
</div>
</template>
@ -31,7 +40,9 @@ export default {
data() {
return {
failureData: [],
hover_text: ""
hover_text: "",
loaded: false,
visible: false,
}
},
props: {
@ -45,10 +56,16 @@ export default {
return this.smallText(this.service)
}
},
mounted () {
this.lastDaysFailures()
mounted () {
},
methods: {
visibleChart(isVisible, entry) {
if (isVisible && !this.visible) {
this.visible = true
this.lastDaysFailures().then(() => this.loaded = true)
}
},
mouseout() {
this.hover_text = ""
},

@ -15,7 +15,3 @@ export default {
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

@ -13,7 +13,7 @@
</div>
</div>
<div v-show="!expanded" v-observe-visibility="visibleChart" class="chart-container">
<div v-show="!expanded" v-observe-visibility="{callback: visibleChart, throttle: 200}" class="chart-container">
<ServiceChart :service="service" :visible="visible" :chart_timeframe="chartTimeframe"/>
</div>
@ -67,18 +67,12 @@ export default {
name: 'ServiceBlock',
components: { Analytics, ServiceTopStats, ServiceChart},
props: {
in_service: {
service: {
type: Object,
required: true
},
},
watch: {
},
computed: {
service() {
return this.track_service
},
timeframepick() {
return this.timeframes.find(s => s.value === this.timeframe_val)
},
@ -124,7 +118,7 @@ export default {
{value: "4320m", text: "3/day", set: 10 },
{value: "10080m", text: "7/day", set: 11 },
],
stats: {
stats: {
total_failures: {
title: "Total Failures",
subtitle: "Last 7 Days",
@ -151,14 +145,13 @@ export default {
value: 0,
}
},
track_service: null,
}
},
beforeDestroy() {
// clearInterval(this.timer_func)
},
async created() {
this.track_service = this.in_service
created() {
},
methods: {
disabled_interval(interval) {
@ -189,30 +182,8 @@ export default {
},
async setService() {
await this.$store.commit('setService', this.service)
this.$router.push('/service/'+this.service.id, {props: {in_service: this.service}})
this.$router.push('/service/'+this.service.id, {props: {service: this.service}})
},
async showMoreStats() {
this.expanded = !this.expanded;
const failData = await Graphing.failures(this.service, 7)
this.stats.total_failures.chart = failData.data;
this.stats.total_failures.value = failData.total;
const hitsData = await Graphing.hits(this.service, 7)
this.stats.high_latency.chart = hitsData.chart;
this.stats.high_latency.value = this.humanTime(hitsData.high);
this.stats.lowest_latency.chart = hitsData.chart;
this.stats.lowest_latency.value = this.humanTime(hitsData.low);
const pingData = await Graphing.pings(this.service, 7)
this.stats.high_ping.chart = pingData.chart;
this.stats.high_ping.value = this.humanTime(pingData.high);
this.stats.low_ping.chart = pingData.chart;
this.stats.low_ping.value = this.humanTime(pingData.low);
},
visibleChart(isVisible, entry) {
if (isVisible && !this.visible) {
this.visible = true
@ -226,4 +197,4 @@ export default {
}
}
}
</script>
</script>

@ -47,122 +47,138 @@
return {
ready: false,
showing: false,
data: [],
chartOptions: {
noData: {
text: 'Loading...'
},
chart: {
height: "100%",
width: "100%",
type: "area",
animations: {
enabled: true,
initialAnimation: {
enabled: true
}
},
selection: {
enabled: false
},
zoom: {
enabled: false
},
toolbar: {
show: false
},
},
grid: {
show: false,
padding: {
top: 0,
right: 0,
bottom: 0,
left: -10,
}
},
dropShadow: {
enabled: false,
},
xaxis: {
type: "datetime",
labels: {
show: false
},
tooltip: {
enabled: false
}
},
yaxis: {
labels: {
show: false
},
},
markers: {
size: 0,
strokeWidth: 0,
hover: {
size: undefined,
sizeOffset: 0
}
},
tooltip: {
theme: false,
enabled: true,
custom: ({series, seriesIndex, dataPointIndex, w}) => {
let ts = w.globals.seriesX[seriesIndex][dataPointIndex];
const dt = new Date(ts).toLocaleDateString("en-us", timeoptions)
let val = series[seriesIndex][dataPointIndex];
if (val >= 10000) {
val = Math.round(val / 1000) + " ms"
} else {
val = val + " μs"
}
return `<div class="chartmarker"><span>Average Response Time: </span><span class="font-3">${val}</span><span>${dt}</span></div>`
},
fixed: {
enabled: true,
position: 'topRight',
offsetX: -30,
offsetY: 0,
},
x: {
show: false,
},
y: {
formatter: (value) => { return value + " %" },
},
},
legend: {
show: false,
},
dataLabels: {
enabled: false
},
floating: true,
axisTicks: {
show: false
},
axisBorder: {
show: false
},
fill: {
colors: [this.service.online ? "#48d338" : "#dd3545"],
opacity: 1,
type: 'solid'
},
stroke: {
show: false,
curve: 'smooth',
lineCap: 'butt',
colors: [this.service.online ? "#3aa82d" : "#dd3545"],
}
},
series: [{
data: []
}]
data: null,
ping_data: null,
series: null,
}
},
computed: {
chartOptions() {
return {
noData: {
text: 'Loading...'
},
chart: {
height: "100%",
width: "100%",
type: "area",
animations: {
enabled: true,
easing: 'easeinout',
speed: 800,
animateGradually: {
enabled: false,
delay: 400,
},
dynamicAnimation: {
enabled: true,
speed: 500
},
hover: {
animationDuration: 0, // duration of animations when hovering an item
},
responsiveAnimationDuration: 0,
},
selection: {
enabled: false
},
zoom: {
enabled: false
},
toolbar: {
show: false
},
},
grid: {
show: false,
padding: {
top: 0,
right: 0,
bottom: 0,
left: -10,
}
},
dropShadow: {
enabled: false,
},
xaxis: {
type: "datetime",
labels: {
show: false
},
tooltip: {
enabled: false
}
},
yaxis: {
labels: {
show: false
},
},
markers: {
size: 0,
strokeWidth: 0,
hover: {
size: undefined,
sizeOffset: 0
}
},
tooltip: {
theme: false,
enabled: true,
custom: ({series, seriesIndex, dataPointIndex, w}) => {
let ts = w.globals.seriesX[seriesIndex][dataPointIndex];
const dt = new Date(ts).toLocaleDateString("en-us", timeoptions)
let val = series[0][dataPointIndex];
let pingVal = series[1][dataPointIndex];
return `<div class="chartmarker">
<span>Average Response Time: ${this.humanTime(val)}/${this.chart_timeframe.interval}</span>
<span>Average Ping: ${this.humanTime(pingVal)}/${this.chart_timeframe.interval}</span>
<span>${dt}</span>
</div>`
},
fixed: {
enabled: true,
position: 'topRight',
offsetX: -30,
offsetY: 0,
},
x: {
show: false,
},
y: {
formatter: (value) => {
return value + " %"
},
},
},
legend: {
show: false,
},
dataLabels: {
enabled: false
},
floating: true,
axisTicks: {
show: false
},
axisBorder: {
show: false
},
fill: {
colors: this.service.online ? ["#3dc82f", "#48d338"] : ["#c60f20", "#dd3545"],
opacity: 1,
type: 'solid',
},
stroke: {
show: false,
curve: 'smooth',
lineCap: 'butt',
colors: this.service.online ? ["#38bc2a", "#48d338"] : ["#c60f20", "#dd3545"],
}
}
}
},
watch: {
visible: function(newVal, oldVal) {
if (newVal && !this.showing) {
@ -185,11 +201,13 @@
if (this.data === null && val.interval !== "5m") {
await this.chartHits({start_time: val.start_time, interval: "5m"})
}
this.series = [{
name: this.service.name,
...this.convertToChartData(this.data)
}]
this.ready = true
this.ping_data = await Api.service_ping(this.service.id, start, end, val.interval, false)
this.series = [
{name: "Latency", ...this.convertToChartData(this.data)},
{name: "Ping", ...this.convertToChartData(this.ping_data)},
]
this.ready = true
}
}
}

@ -37,7 +37,10 @@ Sentry.init({
integrations: [new Integrations.Vue({Vue, attachProps: true, logErrors: true})],
});
Vue.config.productionTip = false
Vue.config.productionTip = process.env.NODE_ENV !== 'production'
Vue.config.devtools = process.env.NODE_ENV !== 'production'
Vue.config.performance = process.env.NODE_ENV !== 'production'
new Vue({
router,
store,

@ -57,7 +57,6 @@ export default Vue.mixin({
return getUnixTime(parseISO(val)) <= 0
},
smallText(s) {
const incidents = s.incidents
if (s.online) {
return `Online, checked ${this.ago(s.last_success)} ago`
} else {

@ -1,7 +1,16 @@
<template>
<div class="container col-md-7 col-sm-12 sm-container">
<Header/>
<Header/>
<div v-if="!loaded" class="row mt-5 mb-5">
<div class="col-12 mt-5 mb-2 text-center">
<font-awesome-icon icon="circle-notch" class="text-dim" size="3x" spin/>
</div>
<div class="col-12 text-center mt-3 mb-3">
<span class="text-dim">{{loading_text}}</span>
</div>
</div>
<div class="col-12 full-col-12">
<div v-for="service in services_no_group" v-bind:key="service.id" class="list-group online_list mb-4">
@ -14,9 +23,9 @@
</div>
</div>
<div>
<Group v-for="group in groups" v-bind:key="group.id" :group=group />
</div>
<div v-if="loaded">
<Group v-for="group in groups" v-bind:key="group.id" :group=group />
</div>
<div class="col-12 full-col-12">
<MessageBlock v-for="message in messages" v-bind:key="message.id" :message="message" />
@ -24,7 +33,7 @@
<div class="col-12 full-col-12">
<div v-for="service in services" :ref="service.id" v-bind:key="service.id">
<ServiceBlock :in_service=service />
<ServiceBlock :service="service" />
</div>
</div>
@ -52,10 +61,29 @@ export default {
},
data() {
return {
logged_in: false
logged_in: false,
}
},
computed: {
loading_text() {
if (this.core == null) {
return "Loading Core"
} else if (this.groups == null) {
return "Loading Groups"
} else if (this.services == null) {
return "Loading Services"
} else if (this.messages == null) {
return "Loading Announcements"
} else {
return "Completed"
}
},
loaded() {
return this.core !== null && this.groups !== null && this.services !== null
},
core() {
return this.$store.getters.core
},
messages() {
return this.$store.getters.messages.filter(m => this.inRange(m) && m.service === 0)
},

@ -173,10 +173,6 @@
},
methods: {
async update() {
const c = await Api.core()
this.$store.commit('setCore', c)
const n = await Api.notifiers()
this.$store.commit('setNotifiers', n)
this.cache = await Api.cache()
await this.getGithub()
},
@ -194,19 +190,24 @@
return this.tab === id
},
async renewApiKeys() {
let r = confirm("Are you sure you want to reset the API keys?");
let r = confirm("Are you sure you want to reset the API keys? You will be logged out.");
if (r === true) {
await Api.renewApiKeys()
const core = await Api.core()
this.$store.commit('setCore', core)
this.core = core
await this.logout()
}
},
async logout () {
await Api.logout()
this.$store.commit('setHasAllData', false)
this.$store.commit('setToken', null)
this.$store.commit('setAdmin', false)
this.$store.commit('setUser', false)
// this.$cookies.remove("statping_auth")
await this.$router.push('/logout')
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

@ -2,6 +2,9 @@ module.exports = {
baseUrl: '/',
assetsDir: 'assets',
filenameHashing: false,
productionTip: process.env.NODE_ENV !== 'production',
devtools: process.env.NODE_ENV !== 'production',
performance: process.env.NODE_ENV !== 'production',
devServer: {
disableHostCheck: true,
proxyTable: {

@ -79,7 +79,7 @@ func (m *mobilePush) OnFailure(s services.Service, f failures.Failure) (string,
func (m *mobilePush) OnSuccess(s services.Service) (string, error) {
data := dataJson(s, failures.Failure{})
msg := &pushArray{
Message: fmt.Sprintf("%s is currently online!", s.Name),
Message: fmt.Sprintf("%s is back online and was down for %s", s.Name, s.Downtime().Human()),
Title: "Service Online",
Data: data,
Platform: 2,

@ -19,7 +19,6 @@ var (
)
func TestMobileNotifier(t *testing.T) {
t.SkipNow()
err := utils.InitLogs()
require.Nil(t, err)

Loading…
Cancel
Save