better loading, updated service chart

pull/792/head
hunterlong 2020-08-19 19:50:52 -07:00
parent 2319cd36d1
commit 8b54ceb16f
17 changed files with 235 additions and 226 deletions

View File

@ -7,6 +7,7 @@
- Added Help page that is generated from Statping's Wiki repo on build - 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 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 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) # 0.90.63 (08-17-2020)
- Modified build process to use xgo for all arch builds - Modified build process to use xgo for all arch builds

View File

@ -87,6 +87,9 @@ func (g *GroupQuery) GraphData(by By) ([]*TimeValue, error) {
return caller.ToValues() 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) { func (g *GroupQuery) ToTimeValue() (*TimeVar, error) {
rows, err := g.db.Rows() rows, err := g.db.Rows()
if err != nil { if err != nil {

View File

@ -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 { func (it *Db) FormatTime(t time.Time) string {
switch it.Type { switch it.Type {
case "mysql":
return t.Format("2006-01-02 15:04:05")
case "postgres": case "postgres":
return t.Format("2006-01-02 15:04:05.999999999") return t.Format("2006-01-02 15:04:05.999999999")
default: 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 { func (it *Db) SelectByTime(increment time.Duration) string {
seconds := int64(increment.Seconds()) seconds := int64(increment.Seconds())
switch it.Type { 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) 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
}

View File

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

View File

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

View File

@ -1,9 +1,9 @@
<template> <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> <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 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> <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 }"> <span class="badge text-uppercase float-right" :class="{'bg-success': service.online, 'bg-danger': !service.online }">
{{service.online ? $t('online') : $t('offline')}} {{service.online ? $t('online') : $t('offline')}}
@ -30,11 +30,15 @@ export default {
GroupServiceFailures GroupServiceFailures
}, },
props: { props: {
group: Object group: {
type: Object,
required: true,
}
}, },
computed: {
services() {
return this.$store.getters.servicesInGroup(this.group.id)
}
}
} }
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -1,5 +1,12 @@
<template> <template>
<div> <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="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}"> <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> <span v-if="d.amount !== 0" class="d-none d-md-block text-center small"></span>
@ -18,6 +25,8 @@
</div> </div>
<div class="daily-failures small text-right text-dim">{{hover_text}}</div> <div class="daily-failures small text-right text-dim">{{hover_text}}</div>
</div> </div>
</transition>
</div>
</template> </template>
<script> <script>
@ -31,7 +40,9 @@ export default {
data() { data() {
return { return {
failureData: [], failureData: [],
hover_text: "" hover_text: "",
loaded: false,
visible: false,
} }
}, },
props: { props: {
@ -46,9 +57,15 @@ export default {
} }
}, },
mounted () { mounted () {
this.lastDaysFailures()
}, },
methods: { methods: {
visibleChart(isVisible, entry) {
if (isVisible && !this.visible) {
this.visible = true
this.lastDaysFailures().then(() => this.loaded = true)
}
},
mouseout() { mouseout() {
this.hover_text = "" this.hover_text = ""
}, },

View File

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

View File

@ -13,7 +13,7 @@
</div> </div>
</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"/> <ServiceChart :service="service" :visible="visible" :chart_timeframe="chartTimeframe"/>
</div> </div>
@ -67,18 +67,12 @@ export default {
name: 'ServiceBlock', name: 'ServiceBlock',
components: { Analytics, ServiceTopStats, ServiceChart}, components: { Analytics, ServiceTopStats, ServiceChart},
props: { props: {
in_service: { service: {
type: Object, type: Object,
required: true required: true
}, },
}, },
watch: {
},
computed: { computed: {
service() {
return this.track_service
},
timeframepick() { timeframepick() {
return this.timeframes.find(s => s.value === this.timeframe_val) return this.timeframes.find(s => s.value === this.timeframe_val)
}, },
@ -151,14 +145,13 @@ export default {
value: 0, value: 0,
} }
}, },
track_service: null,
} }
}, },
beforeDestroy() { beforeDestroy() {
// clearInterval(this.timer_func) // clearInterval(this.timer_func)
}, },
async created() { created() {
this.track_service = this.in_service
}, },
methods: { methods: {
disabled_interval(interval) { disabled_interval(interval) {
@ -189,29 +182,7 @@ export default {
}, },
async setService() { async setService() {
await this.$store.commit('setService', this.service) 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) { visibleChart(isVisible, entry) {
if (isVisible && !this.visible) { if (isVisible && !this.visible) {

View File

@ -47,8 +47,14 @@
return { return {
ready: false, ready: false,
showing: false, showing: false,
data: [], data: null,
chartOptions: { ping_data: null,
series: null,
}
},
computed: {
chartOptions() {
return {
noData: { noData: {
text: 'Loading...' text: 'Loading...'
}, },
@ -58,9 +64,20 @@
type: "area", type: "area",
animations: { animations: {
enabled: true, enabled: true,
initialAnimation: { easing: 'easeinout',
enabled: true 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: { selection: {
enabled: false enabled: false
@ -112,13 +129,13 @@
custom: ({series, seriesIndex, dataPointIndex, w}) => { custom: ({series, seriesIndex, dataPointIndex, w}) => {
let ts = w.globals.seriesX[seriesIndex][dataPointIndex]; let ts = w.globals.seriesX[seriesIndex][dataPointIndex];
const dt = new Date(ts).toLocaleDateString("en-us", timeoptions) const dt = new Date(ts).toLocaleDateString("en-us", timeoptions)
let val = series[seriesIndex][dataPointIndex]; let val = series[0][dataPointIndex];
if (val >= 10000) { let pingVal = series[1][dataPointIndex];
val = Math.round(val / 1000) + " ms" return `<div class="chartmarker">
} else { <span>Average Response Time: ${this.humanTime(val)}/${this.chart_timeframe.interval}</span>
val = val + " μs" <span>Average Ping: ${this.humanTime(pingVal)}/${this.chart_timeframe.interval}</span>
} <span>${dt}</span>
return `<div class="chartmarker"><span>Average Response Time: </span><span class="font-3">${val}</span><span>${dt}</span></div>` </div>`
}, },
fixed: { fixed: {
enabled: true, enabled: true,
@ -130,7 +147,9 @@
show: false, show: false,
}, },
y: { y: {
formatter: (value) => { return value + " %" }, formatter: (value) => {
return value + " %"
},
}, },
}, },
legend: { legend: {
@ -147,20 +166,17 @@
show: false show: false
}, },
fill: { fill: {
colors: [this.service.online ? "#48d338" : "#dd3545"], colors: this.service.online ? ["#3dc82f", "#48d338"] : ["#c60f20", "#dd3545"],
opacity: 1, opacity: 1,
type: 'solid' type: 'solid',
}, },
stroke: { stroke: {
show: false, show: false,
curve: 'smooth', curve: 'smooth',
lineCap: 'butt', lineCap: 'butt',
colors: [this.service.online ? "#3aa82d" : "#dd3545"], colors: this.service.online ? ["#38bc2a", "#48d338"] : ["#c60f20", "#dd3545"],
}
} }
},
series: [{
data: []
}]
} }
}, },
watch: { watch: {
@ -185,10 +201,12 @@
if (this.data === null && val.interval !== "5m") { if (this.data === null && val.interval !== "5m") {
await this.chartHits({start_time: val.start_time, interval: "5m"}) await this.chartHits({start_time: val.start_time, interval: "5m"})
} }
this.series = [{ this.ping_data = await Api.service_ping(this.service.id, start, end, val.interval, false)
name: this.service.name,
...this.convertToChartData(this.data) this.series = [
}] {name: "Latency", ...this.convertToChartData(this.data)},
{name: "Ping", ...this.convertToChartData(this.ping_data)},
]
this.ready = true this.ready = true
} }
} }

View File

@ -37,7 +37,10 @@ Sentry.init({
integrations: [new Integrations.Vue({Vue, attachProps: true, logErrors: true})], 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({ new Vue({
router, router,
store, store,

View File

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

View File

@ -3,6 +3,15 @@
<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 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"> <div v-for="service in services_no_group" v-bind:key="service.id" class="list-group online_list mb-4">
<div class="service_li list-group-item list-group-item-action"> <div class="service_li list-group-item list-group-item-action">
@ -14,7 +23,7 @@
</div> </div>
</div> </div>
<div> <div v-if="loaded">
<Group v-for="group in groups" v-bind:key="group.id" :group=group /> <Group v-for="group in groups" v-bind:key="group.id" :group=group />
</div> </div>
@ -24,7 +33,7 @@
<div class="col-12 full-col-12"> <div class="col-12 full-col-12">
<div v-for="service in services" :ref="service.id" v-bind:key="service.id"> <div v-for="service in services" :ref="service.id" v-bind:key="service.id">
<ServiceBlock :in_service=service /> <ServiceBlock :service="service" />
</div> </div>
</div> </div>
@ -52,10 +61,29 @@ export default {
}, },
data() { data() {
return { return {
logged_in: false logged_in: false,
} }
}, },
computed: { 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() { messages() {
return this.$store.getters.messages.filter(m => this.inRange(m) && m.service === 0) return this.$store.getters.messages.filter(m => this.inRange(m) && m.service === 0)
}, },

View File

@ -173,10 +173,6 @@
}, },
methods: { methods: {
async update() { 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() this.cache = await Api.cache()
await this.getGithub() await this.getGithub()
}, },
@ -194,19 +190,24 @@
return this.tab === id return this.tab === id
}, },
async renewApiKeys() { 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) { if (r === true) {
await Api.renewApiKeys() await Api.renewApiKeys()
const core = await Api.core() const core = await Api.core()
this.$store.commit('setCore', core) this.$store.commit('setCore', core)
this.core = 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> </script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

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

View File

@ -79,7 +79,7 @@ func (m *mobilePush) OnFailure(s services.Service, f failures.Failure) (string,
func (m *mobilePush) OnSuccess(s services.Service) (string, error) { func (m *mobilePush) OnSuccess(s services.Service) (string, error) {
data := dataJson(s, failures.Failure{}) data := dataJson(s, failures.Failure{})
msg := &pushArray{ 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", Title: "Service Online",
Data: data, Data: data,
Platform: 2, Platform: 2,

View File

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