mirror of https://github.com/statping/statping
v0.90.65 (#805)
* notifier panic fix * portainer template * remove default host from discord notifier * fix for updating fields * fix for updating fields * fixed notifier panic * fixed notifier panic * test fix * test fix * missing login banner image * dont delete admin if DEMO_MODE * updatess to service on Dashboard * notifier endpoint fixes, timeframe rounding chart data * modal for UI confirmationspull/815/head^2
parent
243b6f019f
commit
5d85c3ce39
|
@ -257,6 +257,7 @@ jobs:
|
|||
API_SECRET: demopassword123
|
||||
DISABLE_LOGS: false
|
||||
ALLOW_REPORTS: true
|
||||
SAMPLE_DATA: true
|
||||
COVERALLS: ${{ secrets.COVERALLS }}
|
||||
DISCORD_URL: ${{ secrets.DISCORD_URL }}
|
||||
EMAIL_HOST: ${{ secrets.EMAIL_HOST }}
|
||||
|
|
|
@ -257,6 +257,7 @@ jobs:
|
|||
API_SECRET: demopassword123
|
||||
DISABLE_LOGS: false
|
||||
ALLOW_REPORTS: true
|
||||
SAMPLE_DATA: true
|
||||
COVERALLS: ${{ secrets.COVERALLS }}
|
||||
DISCORD_URL: ${{ secrets.DISCORD_URL }}
|
||||
EMAIL_HOST: ${{ secrets.EMAIL_HOST }}
|
||||
|
|
|
@ -3,6 +3,7 @@ snap
|
|||
prime
|
||||
stage
|
||||
parts
|
||||
assets_backup
|
||||
certs
|
||||
releases
|
||||
core/rice-box.go
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
# 0.90.65 (09-01-2020)
|
||||
- Fixed issue with dashboard not logging in (notifier panic)
|
||||
- Modified static email templates to github.com/statping/emails
|
||||
- Modified Regenerate API function to keep API_SECRET env
|
||||
- Added DEMO_MODE env variable, if true, 'admin' cannot be deleted
|
||||
- Modified Service sparklines on Dashboard
|
||||
|
||||
# 0.90.64 (08-18-2020)
|
||||
- Modified max-width for container to 1012px, larger UI
|
||||
- Added failure sparklines in the Services list view
|
||||
|
|
|
@ -148,20 +148,20 @@ func InitApp() error {
|
|||
if _, err := core.Select(); err != nil {
|
||||
return err
|
||||
}
|
||||
// init Sentry error monitoring (its useful)
|
||||
utils.SentryInit(core.App.AllowReports.Bool)
|
||||
// init prometheus metrics
|
||||
metrics.InitMetrics()
|
||||
// connect each notifier, added them into database if needed
|
||||
notifiers.InitNotifiers()
|
||||
// select all services in database and store services in a mapping of Service pointers
|
||||
if _, err := services.SelectAllServices(true); err != nil {
|
||||
return err
|
||||
}
|
||||
// start routines for each service checking process
|
||||
services.CheckServices()
|
||||
// connect each notifier, added them into database if needed
|
||||
notifiers.InitNotifiers()
|
||||
// start routine to delete old records (failures, hits)
|
||||
go database.Maintenance()
|
||||
// init Sentry error monitoring (its useful)
|
||||
utils.SentryInit(core.App.AllowReports.Bool)
|
||||
core.App.Setup = true
|
||||
core.App.Started = utils.Now()
|
||||
return nil
|
||||
|
|
|
@ -70,19 +70,19 @@ func (t *TimeVar) ToValues() ([]*TimeValue, error) {
|
|||
}
|
||||
|
||||
// GraphData will return all hits or failures
|
||||
func (g *GroupQuery) GraphData(by By) ([]*TimeValue, error) {
|
||||
g.db = g.db.MultipleSelects(
|
||||
g.db.SelectByTime(g.Group),
|
||||
func (b *GroupQuery) GraphData(by By) ([]*TimeValue, error) {
|
||||
b.db = b.db.MultipleSelects(
|
||||
b.db.SelectByTime(b.Group),
|
||||
by.String(),
|
||||
).Group("timeframe").Order("timeframe", true)
|
||||
|
||||
caller, err := g.ToTimeValue()
|
||||
caller, err := b.ToTimeValue()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if g.FillEmpty {
|
||||
return caller.FillMissing(g.Start, g.End)
|
||||
if b.FillEmpty {
|
||||
return caller.FillMissing(b.Start, b.End)
|
||||
}
|
||||
return caller.ToValues()
|
||||
}
|
||||
|
@ -90,8 +90,8 @@ func (g *GroupQuery) GraphData(by By) ([]*TimeValue, error) {
|
|||
// 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()
|
||||
func (b *GroupQuery) ToTimeValue() (*TimeVar, error) {
|
||||
rows, err := b.db.Rows()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -102,8 +102,8 @@ func (g *GroupQuery) ToTimeValue() (*TimeVar, error) {
|
|||
if err := rows.Scan(&timeframe, &amount); err != nil {
|
||||
log.Error(err, timeframe)
|
||||
}
|
||||
trueTime, _ := g.db.ParseTime(timeframe)
|
||||
newTs := types.FixedTime(trueTime, g.Group)
|
||||
trueTime, _ := b.db.ParseTime(timeframe)
|
||||
newTs := types.FixedTime(trueTime, b.Group)
|
||||
|
||||
tv := &TimeValue{
|
||||
Timeframe: newTs,
|
||||
|
@ -111,7 +111,7 @@ func (g *GroupQuery) ToTimeValue() (*TimeVar, error) {
|
|||
}
|
||||
data = append(data, tv)
|
||||
}
|
||||
return &TimeVar{g, data}, nil
|
||||
return &TimeVar{b, data}, nil
|
||||
}
|
||||
|
||||
func (t *TimeVar) FillMissing(current, end time.Time) ([]*TimeValue, error) {
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
[
|
||||
{
|
||||
"type": 1,
|
||||
"title": "Statping",
|
||||
"restart_policy": "unless-stopped",
|
||||
"description": "Service monitoring with an easy to use status page and mobile app",
|
||||
"logo": "https://assets.statping.com/icon.png",
|
||||
"image": "statping/statping:latest",
|
||||
"platform": "linux",
|
||||
"categories": ["monitoring"],
|
||||
"administrator_only": false,
|
||||
"ports": [
|
||||
"8080:8080/tcp"
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"container": "/app"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -74,7 +74,7 @@ const webpackConfig = merge(commonConfig, {
|
|||
threshold: 10240,
|
||||
minRatio: 0.8
|
||||
}),
|
||||
new webpack.HashedModuleIdsPlugin(),
|
||||
// new webpack.HashedModuleIdsPlugin(),
|
||||
new HtmlPlugin({
|
||||
template: 'public/base.gohtml',
|
||||
filename: 'base.gohtml',
|
||||
|
|
|
@ -7,8 +7,8 @@ const tokenKey = "statping_auth";
|
|||
|
||||
class Api {
|
||||
constructor() {
|
||||
this.version = "0.90.64";
|
||||
this.commit = "130cc3ede7463ec9af8d62abb84992e2a0ef453c";
|
||||
this.version = "0.90.65";
|
||||
this.commit = "5bc10fcc8536a08ce7a099a0b4cbceb2dc9fc35b";
|
||||
}
|
||||
|
||||
async oauth() {
|
||||
|
|
|
@ -85,13 +85,13 @@
|
|||
.chartmarker {
|
||||
padding: 0px;
|
||||
width: 200px;
|
||||
text-align: right;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.chartmarker SPAN {
|
||||
font-size: 4pt;
|
||||
display: block;
|
||||
color: #8b8b8b;
|
||||
color: #b1b1b1;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip {
|
||||
|
|
|
@ -14,6 +14,34 @@ A:HOVER {
|
|||
color: lighten($text-color, 12%) !important;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
display: block;
|
||||
z-index: 10000;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #00000073;
|
||||
}
|
||||
|
||||
.modal {
|
||||
z-index: 999999 !important;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
top: 20%;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: lighten($text-color, 30%) !important;
|
||||
}
|
||||
|
|
|
@ -81,13 +81,21 @@
|
|||
serviceName (service) {
|
||||
return service.name || "Global Message"
|
||||
},
|
||||
async delete(m) {
|
||||
await Api.message_delete(m.id)
|
||||
const messages = await Api.messages()
|
||||
this.$store.commit('setMessages', messages)
|
||||
},
|
||||
async deleteMessage(m) {
|
||||
let c = confirm(`Are you sure you want to delete message '${m.title}'?`)
|
||||
if (c) {
|
||||
await Api.message_delete(m.id)
|
||||
const messages = await Api.messages()
|
||||
this.$store.commit('setMessages', messages)
|
||||
const modal = {
|
||||
visible: true,
|
||||
title: "Delete Announcement",
|
||||
body: `Are you sure you want to delete Announcement ${m.title}?`,
|
||||
btnColor: "btn-danger",
|
||||
btnText: "Delete Announcement",
|
||||
func: () => this.delete(m),
|
||||
}
|
||||
this.$store.commit("setModal", modal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<div class="col-12">
|
||||
|
||||
<div class="card contain-card mb-4">
|
||||
<div class="card-header">{{ $t('top_nav.services') }}
|
||||
<router-link v-if="$store.state.admin" to="/dashboard/create_service" class="btn btn-sm btn-success float-right">
|
||||
|
@ -67,6 +68,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
const Modal = () => import(/* webpackChunkName: "dashboard" */ "@/components/Elements/Modal")
|
||||
const FormGroup = () => import(/* webpackChunkName: "dashboard" */ '@/forms/Group')
|
||||
const ToggleSwitch = () => import(/* webpackChunkName: "dashboard" */ '@/forms/ToggleSwitch')
|
||||
const ServicesList = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/ServicesList')
|
||||
|
@ -76,6 +78,7 @@
|
|||
export default {
|
||||
name: 'DashboardServices',
|
||||
components: {
|
||||
Modal,
|
||||
ServicesList,
|
||||
ToggleSwitch,
|
||||
FormGroup,
|
||||
|
@ -112,13 +115,24 @@
|
|||
this.group = g
|
||||
this.edit = !mode
|
||||
},
|
||||
confirm_delete(service) {
|
||||
|
||||
},
|
||||
async delete(g) {
|
||||
await Api.group_delete(g.id)
|
||||
const groups = await Api.groups()
|
||||
this.$store.commit('setGroups', groups)
|
||||
},
|
||||
async deleteGroup(g) {
|
||||
let c = confirm(`Are you sure you want to delete '${g.name}'?`)
|
||||
if (c) {
|
||||
await Api.group_delete(g.id)
|
||||
const groups = await Api.groups()
|
||||
this.$store.commit('setGroups', groups)
|
||||
}
|
||||
const modal = {
|
||||
visible: true,
|
||||
title: "Delete Group",
|
||||
body: `Are you sure you want to delete group ${g.name}? All services attached will be removed from this group.`,
|
||||
btnColor: "btn-danger",
|
||||
btnText: "Delete Group",
|
||||
func: () => this.delete(g),
|
||||
}
|
||||
this.$store.commit("setModal", modal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,13 +74,21 @@
|
|||
this.user = u
|
||||
this.edit = !mode
|
||||
},
|
||||
async delete(u) {
|
||||
await Api.user_delete(u.id)
|
||||
const users = await Api.users()
|
||||
this.$store.commit('setUsers', users)
|
||||
},
|
||||
async deleteUser(u) {
|
||||
let c = confirm(`Are you sure you want to delete user '${u.username}'?`)
|
||||
if (c) {
|
||||
await Api.user_delete(u.id)
|
||||
const users = await Api.users()
|
||||
this.$store.commit('setUsers', users)
|
||||
const modal = {
|
||||
visible: true,
|
||||
title: "Delete User",
|
||||
body: `Are you sure you want to delete user ${u.username}?`,
|
||||
btnColor: "btn-danger",
|
||||
btnText: "Delete User",
|
||||
func: () => this.delete(u),
|
||||
}
|
||||
this.$store.commit("setModal", modal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -151,14 +151,22 @@ export default {
|
|||
await this.gotoPage(1)
|
||||
},
|
||||
methods: {
|
||||
async delete() {
|
||||
await Api.service_failures_delete(this.service)
|
||||
this.service = await Api.service(this.service.id)
|
||||
this.total = 0
|
||||
await this.load()
|
||||
},
|
||||
async deleteFailures() {
|
||||
const c = confirm('Are you sure you want to delete all failures?')
|
||||
if (c) {
|
||||
await Api.service_failures_delete(this.service)
|
||||
this.service = await Api.service(this.service.id)
|
||||
this.total = 0
|
||||
await this.load()
|
||||
const modal = {
|
||||
visible: true,
|
||||
title: "Delete All Failures",
|
||||
body: `Are you sure you want to delete all Failures for service ${this.service.title}?`,
|
||||
btnColor: "btn-danger",
|
||||
btnText: "Delete Failures",
|
||||
func: () => this.delete(),
|
||||
}
|
||||
this.$store.commit("setModal", modal)
|
||||
},
|
||||
async gotoPage(page) {
|
||||
this.page = page;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="row">
|
||||
<h5 v-if="group.name" class="h5 col-12 mb-3 mt-2 text-dim">
|
||||
<h5 v-if="group.name && group_services" class="h5 col-12 mb-3 mt-2 text-dim">
|
||||
<font-awesome-icon @click="toggle" :icon="expanded ? 'minus' : 'plus'" class="pointer mr-3"/> {{group.name}}
|
||||
<span class="badge badge-success text-uppercase float-right ml-2">{{services_online.length}} online</span>
|
||||
<span v-if="services_online.services_offline > 0" class="badge badge-danger text-uppercase float-right">
|
||||
|
|
|
@ -80,15 +80,23 @@ const FormIncidentUpdates = () => import(/* webpackChunkName: "dashboard" */ '@/
|
|||
|
||||
methods: {
|
||||
|
||||
async delete(i) {
|
||||
this.res = await Api.incident_delete(i)
|
||||
if (this.res.status === "success") {
|
||||
this.incidents = this.incidents.filter(obj => obj.id !== i.id);
|
||||
//await this.loadIncidents()
|
||||
}
|
||||
},
|
||||
async deleteIncident(incident) {
|
||||
let c = confirm(`Are you sure you want to delete '${incident.title}'?`)
|
||||
if (c) {
|
||||
this.res = await Api.incident_delete(incident)
|
||||
if (this.res.status === "success") {
|
||||
this.incidents = this.incidents.filter(obj => obj.id !== incident.id); // this is better in terms of not having to querry the db to get a fresh copy of all updates
|
||||
//await this.loadIncidents()
|
||||
} // TODO: further error checking here... maybe alert user it failed with modal or so
|
||||
}
|
||||
const modal = {
|
||||
visible: true,
|
||||
title: "Delete Incident",
|
||||
body: `Are you sure you want to delete Incident ${incident.title}?`,
|
||||
btnColor: "btn-danger",
|
||||
btnText: "Delete Incident",
|
||||
func: () => this.delete(incident),
|
||||
}
|
||||
this.$store.commit("setModal", modal)
|
||||
},
|
||||
|
||||
async createIncident() {
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="col-5 pr-0">
|
||||
<span class="small text-dim"> {{ hoverbtn }}</span>
|
||||
<span class="small text-dim">{{ hoverbtn }}</span>
|
||||
</div>
|
||||
|
||||
<div class="col-7 pr-2 pl-0">
|
||||
|
@ -121,13 +121,14 @@
|
|||
}
|
||||
},
|
||||
async getUptime() {
|
||||
const start = this.nowSubtract(3 * 86400)
|
||||
this.uptime = await Api.service_uptime(this.service.id, this.toUnix(start), this.toUnix(this.now()))
|
||||
const end = this.endOf("day", this.now())
|
||||
const start = this.beginningOf("day", this.nowSubtract(3 * 86400))
|
||||
this.uptime = await Api.service_uptime(this.service.id, this.toUnix(start), this.toUnix(end))
|
||||
},
|
||||
async loadInfo() {
|
||||
this.set1 = await this.getHits(24 * 7, "6h")
|
||||
this.set1 = await this.getHits(86400 * 7, "12h")
|
||||
this.set1_name = this.calc(this.set1)
|
||||
this.set2 = await this.getHits(24, "1h")
|
||||
this.set2 = await this.getHits(86400, "60m")
|
||||
this.set2_name = this.calc(this.set2)
|
||||
this.loaded = true
|
||||
},
|
||||
|
@ -145,14 +146,13 @@
|
|||
});
|
||||
total = total / data.length
|
||||
},
|
||||
async getHits(hours, group) {
|
||||
const start = this.nowSubtract(3600 * hours)
|
||||
const fetched = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(this.now()), group, false)
|
||||
|
||||
async getHits(seconds, group) {
|
||||
let start = this.nowSubtract(seconds)
|
||||
let end = this.endOf("today")
|
||||
const startEnd = this.startEndParams(start, end, group)
|
||||
const fetched = await Api.service_hits(this.service.id, startEnd.start, startEnd.end, group, true)
|
||||
const data = this.convertToChartData(fetched, 0.001, true)
|
||||
|
||||
return [{name: "Latency", ...data}]
|
||||
|
||||
},
|
||||
calc(s) {
|
||||
let data = s[0].data
|
||||
|
|
|
@ -57,13 +57,13 @@
|
|||
let ts = w.globals.seriesX[seriesIndex][dataPointIndex];
|
||||
const dt = new Date(ts).toLocaleDateString("en-us", timeoptions)
|
||||
let val = series[seriesIndex][dataPointIndex];
|
||||
return `<div class="chartmarker"><span>Average Response Time: </span><span class="font-3">${this.humanTime(val)}</span><span>${dt}</span></div>`
|
||||
return `<div class="chartmarker"><span class="font-3">Average Response Time: ${this.humanTime(val)}</span><span>${dt}</span></div>`
|
||||
},
|
||||
fixed: {
|
||||
enabled: true,
|
||||
position: 'topRight',
|
||||
position: 'bottomLeft',
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
offsetY: -30,
|
||||
},
|
||||
x: {
|
||||
show: true,
|
||||
|
@ -94,7 +94,3 @@
|
|||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
</style>
|
||||
|
|
|
@ -72,12 +72,14 @@
|
|||
<script>
|
||||
import Api from "../../API";
|
||||
import ServiceSparkList from "@/components/Service/ServiceSparkList";
|
||||
import Modal from "@/components/Elements/Modal";
|
||||
const draggable = () => import(/* webpackChunkName: "dashboard" */ 'vuedraggable')
|
||||
const ToggleSwitch = () => import(/* webpackChunkName: "dashboard" */ '../../forms/ToggleSwitch');
|
||||
|
||||
export default {
|
||||
name: 'ServicesList',
|
||||
components: {
|
||||
Modal,
|
||||
ServiceSparkList,
|
||||
ToggleSwitch,
|
||||
draggable
|
||||
|
@ -159,14 +161,25 @@ export default {
|
|||
await Api.services_reorder(data)
|
||||
await this.update()
|
||||
},
|
||||
tester(s) {
|
||||
console.log(s)
|
||||
},
|
||||
async delete(s) {
|
||||
this.loading = true
|
||||
await Api.service_delete(s.id)
|
||||
await this.update()
|
||||
this.loading = false
|
||||
},
|
||||
async deleteService(s) {
|
||||
let c = confirm(`Are you sure you want to delete '${s.name}'?`)
|
||||
if (c) {
|
||||
this.loading = true
|
||||
await Api.service_delete(s.id)
|
||||
await this.update()
|
||||
this.loading = false
|
||||
}
|
||||
const modal = {
|
||||
visible: true,
|
||||
title: "Delete Service",
|
||||
body: `Are you sure you want to delete service ${s.name}? This will also delete all failures, checkins, and incidents for this service.`,
|
||||
btnColor: "btn-danger",
|
||||
btnText: "Delete Service",
|
||||
func: () => this.delete(s),
|
||||
}
|
||||
this.$store.commit("setModal", modal)
|
||||
},
|
||||
serviceGroup(s) {
|
||||
let group = this.$store.getters.groupById(s.group_id)
|
||||
|
|
|
@ -144,14 +144,22 @@ import('codemirror/mode/css/css.js')
|
|||
this.pending = false
|
||||
await this.fetchTheme()
|
||||
},
|
||||
async delete() {
|
||||
this.pending = true
|
||||
const resp = await Api.theme_generate(false)
|
||||
await this.fetchTheme()
|
||||
this.pending = false
|
||||
},
|
||||
async deleteAssets() {
|
||||
this.pending = true
|
||||
let c = confirm('Are you sure you want to delete all local assets?')
|
||||
if (c) {
|
||||
const resp = await Api.theme_generate(false)
|
||||
await this.fetchTheme()
|
||||
}
|
||||
this.pending = false
|
||||
const modal = {
|
||||
visible: true,
|
||||
title: "Delete Local Assets",
|
||||
body: `Are you sure you want to delete all local assets?`,
|
||||
btnColor: "btn-danger",
|
||||
btnText: "Delete",
|
||||
func: () => this.delete(),
|
||||
}
|
||||
this.$store.commit("setModal", modal)
|
||||
},
|
||||
async saveAssets() {
|
||||
this.pending = true
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<div v-if="modal.visible" class="modal d-block" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{modal.title}}</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{modal.body}}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button @click.prevent="close" type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
<button @click.prevent="runFunc" type="button" :class="`btn ${modal.btnColor}`">{{modal.btnText}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Modal",
|
||||
data () {
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
modal() {
|
||||
return this.$store.getters.modal
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
runFunc() {
|
||||
this.$store.getters.modal.func()
|
||||
this.close()
|
||||
},
|
||||
close() {
|
||||
this.$store.commit("setModal", {visible: false})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -157,7 +157,8 @@ export default {
|
|||
},
|
||||
async loadFailures() {
|
||||
this.loaded = false
|
||||
const data = await Api.service_failures_data(this.service.id, this.toUnix(this.parseISO(this.start)), this.toUnix(this.parseISO(this.end)), this.group, true)
|
||||
const startEnd = this.startEndParams(this.parseISO(this.start), this.parseISO(this.end), this.group)
|
||||
const data = await Api.service_failures_data(this.service.id, startEnd.start, startEnd.end, this.group, true)
|
||||
this.loaded = true
|
||||
this.data = [{data: this.convertChartData(data)}]
|
||||
}
|
||||
|
|
|
@ -61,7 +61,6 @@
|
|||
const Analytics = () => import(/* webpackChunkName: "service" */ './Analytics');
|
||||
const ServiceChart = () => import(/* webpackChunkName: "service" */ "./ServiceChart");
|
||||
const ServiceTopStats = () => import(/* webpackChunkName: "service" */ "@/components/Service/ServiceTopStats");
|
||||
const Graphing = () => import(/* webpackChunkName: "service" */ '../../graphing');
|
||||
|
||||
export default {
|
||||
name: 'ServiceBlock',
|
||||
|
|
|
@ -195,17 +195,14 @@
|
|||
methods: {
|
||||
async chartHits(val) {
|
||||
this.ready = false
|
||||
const start = val.start_time
|
||||
const end = this.toUnix(new Date())
|
||||
this.data = await Api.service_hits(this.service.id, start, end, val.interval, false)
|
||||
if (this.data === null && val.interval !== "5m") {
|
||||
await this.chartHits({start_time: val.start_time, interval: "5m"})
|
||||
}
|
||||
this.ping_data = await Api.service_ping(this.service.id, start, end, val.interval, false)
|
||||
const end = this.endOf("hour", this.now())
|
||||
const start = this.beginningOf("hour", this.fromUnix(val.start_time))
|
||||
this.data = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(end), val.interval, false)
|
||||
this.ping_data = await Api.service_ping(this.service.id, this.toUnix(start), this.toUnix(end), val.interval, false)
|
||||
|
||||
this.series = [
|
||||
{name: "Latency", ...this.convertToChartData(this.data)},
|
||||
{name: "Ping", ...this.convertToChartData(this.ping_data)},
|
||||
{name: "Ping", ...this.convertToChartData(this.ping_data)},
|
||||
]
|
||||
this.ready = true
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<div class="form-group row">
|
||||
<label for="password" class="col-4 col-form-label">{{$t('password')}}</label>
|
||||
<div class="col-8">
|
||||
<input @keyup="checkForm" type="password" v-model="password" autocomplete="current-password" name="password" class="form-control" id="password" placeholder="password123">
|
||||
<input @keyup="checkForm" type="password" v-model="password" autocomplete="current-password" name="password" class="form-control" id="password" placeholder="************">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
|
|
|
@ -149,11 +149,11 @@
|
|||
|
||||
<div v-for="(log, i) in notifier.logs.reverse()" class="alert" :class="{'alert-danger': log.error, 'alert-dark': !log.success && !log.error, 'alert-success': log.success && !log.error}">
|
||||
<span class="d-block">
|
||||
Service '{{$store.getters.serviceById(log.service).name}}'
|
||||
Service {{log.service}}
|
||||
{{log.success ? "Success Triggered" : "Failure Triggered"}}
|
||||
</span>
|
||||
|
||||
<div class="bg-white p-3 small mt-2">
|
||||
<div v-if="log.message !== ''" class="bg-white p-3 small mt-2">
|
||||
<code>{{log.message}}</code>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Vue from "vue";
|
||||
const { startOfDay, startOfWeek, endOfMonth, startOfToday, startOfTomorrow, startOfYesterday, endOfYesterday, endOfTomorrow, endOfToday, endOfDay, startOfMonth, lastDayOfMonth, subSeconds, getUnixTime, fromUnixTime, differenceInSeconds, formatDistance, addMonths, addSeconds, isWithinInterval } = require('date-fns')
|
||||
const { startOfDay, startOfHour, startOfWeek, endOfMonth, endOfHour, startOfToday, startOfTomorrow, startOfYesterday, endOfYesterday, endOfTomorrow, endOfToday, endOfDay, startOfMonth, lastDayOfMonth, subSeconds, getUnixTime, fromUnixTime, differenceInSeconds, formatDistance, addMonths, addSeconds, isWithinInterval } = require('date-fns')
|
||||
import formatDistanceToNow from 'date-fns/formatDistanceToNow'
|
||||
import format from 'date-fns/format'
|
||||
import parseISO from 'date-fns/parseISO'
|
||||
|
@ -59,6 +59,8 @@ export default Vue.mixin({
|
|||
},
|
||||
endOf(method, val) {
|
||||
switch (method) {
|
||||
case "hour":
|
||||
return endOfHour(val)
|
||||
case "day":
|
||||
return endOfDay(val)
|
||||
case "today":
|
||||
|
@ -70,10 +72,17 @@ export default Vue.mixin({
|
|||
case "month":
|
||||
return endOfMonth(val)
|
||||
}
|
||||
return roundToNearestMinutes(val)
|
||||
return val
|
||||
},
|
||||
startEndParams(start, end, group) {
|
||||
start = this.beginningOf("hour", start)
|
||||
end = this.endOf("hour", end)
|
||||
return {start: this.toUnix(start), end: this.toUnix(end), group: group}
|
||||
},
|
||||
beginningOf(method, val) {
|
||||
switch (method) {
|
||||
case "hour":
|
||||
return startOfHour(val)
|
||||
case "day":
|
||||
return startOfDay(val)
|
||||
case "today":
|
||||
|
@ -83,11 +92,11 @@ export default Vue.mixin({
|
|||
case "yesterday":
|
||||
return startOfYesterday()
|
||||
case "week":
|
||||
return startOfWeek()
|
||||
return startOfWeek(val)
|
||||
case "month":
|
||||
return startOfMonth(val)
|
||||
}
|
||||
return roundToNearestMinutes(val)
|
||||
return val
|
||||
},
|
||||
isZero(val) {
|
||||
return getUnixTime(parseISO(val)) <= 0
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
<template>
|
||||
<div class="container col-md-7 col-sm-12 mt-md-5">
|
||||
<div v-if="modal" class="modal-backdrop"></div>
|
||||
<Modal/>
|
||||
<TopNav :admin="admin"/>
|
||||
<router-view :admin="admin"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from "@/components/Elements/Modal";
|
||||
const TopNav = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/TopNav')
|
||||
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
components: {
|
||||
Modal,
|
||||
TopNav,
|
||||
},
|
||||
data () {
|
||||
|
@ -20,6 +24,9 @@
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
modal() {
|
||||
return this.$store.getters.modal.visible
|
||||
},
|
||||
admin() {
|
||||
return this.$store.getters.admin
|
||||
},
|
||||
|
|
|
@ -2275,7 +2275,7 @@ OluFxewsEO0QNDrfFb+0gnjYlnGqOFcZjUMXbDdY5oLSPtXohynuTK1qyQ==
|
|||
</div>
|
||||
|
||||
<div class="text-center small text-dim" v-pre>
|
||||
Automatically generated from Statping's Wiki on 2020-08-22 21:27:09.3468 +0000 UTC
|
||||
Automatically generated from Statping's Wiki on 2020-09-02 02:46:04.864615 +0000 UTC
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -23,10 +23,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loaded">
|
||||
<Group v-for="group in groups" v-bind:key="group.id" :group=group />
|
||||
</div>
|
||||
|
||||
<Group v-for="group in groups" v-bind:key="group.id" :group=group />
|
||||
<div class="col-12 full-col-12">
|
||||
<MessageBlock v-for="message in messages" v-bind:key="message.id" :message="message" />
|
||||
</div>
|
||||
|
@ -66,20 +63,18 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
loading_text() {
|
||||
if (this.core == null) {
|
||||
if (!this.$store.getters.core.version) {
|
||||
return "Loading Core"
|
||||
} else if (this.groups == null) {
|
||||
} else if (this.$store.getters.groups.length === 0) {
|
||||
return "Loading Groups"
|
||||
} else if (this.services == null) {
|
||||
} else if (this.$store.getters.services.length === 0) {
|
||||
return "Loading Services"
|
||||
} else if (this.messages == null) {
|
||||
} else if (this.$store.getters.messages == null) {
|
||||
return "Loading Announcements"
|
||||
} else {
|
||||
return "Completed"
|
||||
}
|
||||
},
|
||||
loaded() {
|
||||
return this.core !== null && this.groups !== null && this.services !== null
|
||||
return this.$store.getters.core.version && this.$store.getters.services.length !== 0
|
||||
},
|
||||
core() {
|
||||
return this.$store.getters.core
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="offset-md-3 offset-lg-4 offset-0 col-lg-4 col-md-6 mt-5">
|
||||
|
||||
<div class="offset-1 offset-lg-2 col-lg-8 col-10 mb-4 mb-md-3">
|
||||
<img alt="Statping Login" class="embed-responsive" src="http://0.0.0.0:8585/banner.png">
|
||||
<img alt="Statping Login" class="embed-responsive" src="banner.png">
|
||||
</div>
|
||||
|
||||
<div class="login_container col-12 p-4">
|
||||
|
|
|
@ -189,15 +189,23 @@
|
|||
liClass(id) {
|
||||
return this.tab === id
|
||||
},
|
||||
async renew() {
|
||||
await Api.renewApiKeys()
|
||||
const core = await Api.core()
|
||||
this.$store.commit('setCore', core)
|
||||
this.core = core
|
||||
await this.logout()
|
||||
},
|
||||
async renewApiKeys() {
|
||||
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()
|
||||
const modal = {
|
||||
visible: true,
|
||||
title: "Reset API Key",
|
||||
body: `Are you sure you want to reset the API keys? You will be logged out.`,
|
||||
btnColor: "btn-danger",
|
||||
btnText: "Reset",
|
||||
func: () => this.renew(),
|
||||
}
|
||||
this.$store.commit("setModal", modal)
|
||||
},
|
||||
async logout () {
|
||||
await Api.logout()
|
||||
|
|
|
@ -31,7 +31,15 @@ export default new Vuex.Store({
|
|||
checkins: [],
|
||||
admin: false,
|
||||
user: false,
|
||||
loggedIn: false
|
||||
loggedIn: false,
|
||||
modal: {
|
||||
visible: false,
|
||||
title: "Modal Header",
|
||||
body: "This is the content for the modal body",
|
||||
btnText: "Save Changes",
|
||||
btnColor: "btn-primary",
|
||||
func: null,
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
hasAllData: state => state.hasAllData,
|
||||
|
@ -49,6 +57,7 @@ export default new Vuex.Store({
|
|||
notifiers: state => state.notifiers,
|
||||
checkins: state => state.checkins,
|
||||
loggedIn: state => state.loggedIn,
|
||||
modal: state => state.modal,
|
||||
|
||||
isAdmin: state => state.admin,
|
||||
isUser: state => state.user,
|
||||
|
@ -140,6 +149,9 @@ export default new Vuex.Store({
|
|||
setOAuth(state, oauth) {
|
||||
state.oauth = oauth
|
||||
},
|
||||
setModal(state, modal) {
|
||||
state.modal = modal
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async getAllServices(context) {
|
||||
|
|
1
go.mod
1
go.mod
|
@ -30,6 +30,7 @@ require (
|
|||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.6.3
|
||||
github.com/statping/emails v1.0.0
|
||||
github.com/stretchr/testify v1.5.1
|
||||
github.com/t-tiger/gorm-bulk-insert/v2 v2.0.1
|
||||
github.com/tdewolff/minify/v2 v2.8.0 // indirect
|
||||
|
|
3
go.sum
3
go.sum
|
@ -568,6 +568,9 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM
|
|||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/spf13/viper v1.6.3 h1:pDDu1OyEDTKzpJwdq4TiuLyMsUgRa/BT5cn5O62NoHs=
|
||||
github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw=
|
||||
github.com/statping/emails v1.0.0 h1:90hGweEhr8wIFiy34KCkiFqGJlkug2gAQLVR6oSCFNU=
|
||||
github.com/statping/emails v1.0.0/go.mod h1:xFU85jXaiWQadqHqu/jDrGsAn6WPSk1WgKyTVuFm0TI=
|
||||
github.com/statping/statping v0.90.64/go.mod h1:lbyNPB73IjWtnommV4wSejYfgUT1yLhhqelMjl1ZBb8=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
|
|
|
@ -31,10 +31,12 @@ func apiIndexHandler(r *http.Request) interface{} {
|
|||
}
|
||||
|
||||
func apiRenewHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
core.App.ApiSecret = utils.NewSHA256Hash()
|
||||
err = core.App.Update()
|
||||
if err != nil {
|
||||
newApi := utils.Params.GetString("API_SECRET")
|
||||
if newApi == "" {
|
||||
newApi = utils.NewSHA256Hash()
|
||||
}
|
||||
core.App.ApiSecret = newApi
|
||||
if err := core.App.Update(); err != nil {
|
||||
sendErrorJson(err, w, r)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -13,9 +13,14 @@ import (
|
|||
func apiNotifiersHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var notifs []notifications.Notification
|
||||
for _, n := range services.AllNotifiers() {
|
||||
no := n.Select()
|
||||
notif, _ := notifications.Find(no.Method)
|
||||
notifs = append(notifs, *no.UpdateFields(notif))
|
||||
notif := n.Select()
|
||||
no, err := notifications.Find(notif.Method)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
sendErrorJson(err, w, r)
|
||||
}
|
||||
notif.UpdateFields(no)
|
||||
notifs = append(notifs, *notif)
|
||||
}
|
||||
sort.Sort(notifications.NotificationOrder(notifs))
|
||||
returnJson(notifs, w, r)
|
||||
|
|
|
@ -132,6 +132,7 @@ func Router() *mux.Router {
|
|||
api.Handle("/api/services/{id}/failures", scoped(apiServiceFailuresHandler)).Methods("GET")
|
||||
api.Handle("/api/services/{id}/failures", authenticated(servicesDeleteFailuresHandler, false)).Methods("DELETE")
|
||||
api.Handle("/api/services/{id}/hits", scoped(apiServiceHitsHandler)).Methods("GET")
|
||||
api.Handle("/api/services/{id}/hits", authenticated(apiServiceHitsDeleteHandler, false)).Methods("DELETE")
|
||||
|
||||
// API SERVICE CHART DATA Routes
|
||||
api.Handle("/api/services/{id}/hits_data", cached("30s", "application/json", apiServiceDataHandler)).Methods("GET")
|
||||
|
|
|
@ -237,6 +237,19 @@ func apiServiceTimeDataHandler(w http.ResponseWriter, r *http.Request) {
|
|||
returnJson(uptimeData, w, r)
|
||||
}
|
||||
|
||||
func apiServiceHitsDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
service, err := findService(r)
|
||||
if err != nil {
|
||||
sendErrorJson(err, w, r)
|
||||
return
|
||||
}
|
||||
if err := service.AllHits().DeleteAll(); err != nil {
|
||||
sendErrorJson(err, w, r)
|
||||
return
|
||||
}
|
||||
sendJsonAction(service, "delete", w, r)
|
||||
}
|
||||
|
||||
func apiServiceDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
service, err := findService(r)
|
||||
if err != nil {
|
||||
|
|
|
@ -64,7 +64,7 @@ var AmazonSNS = &amazonSNS{¬ifications.Notification{
|
|||
Type: "text",
|
||||
Title: "SNS Topic ARN",
|
||||
SmallText: "The ARN of the Topic",
|
||||
DbField: "host",
|
||||
DbField: "Host",
|
||||
Placeholder: "arn:aws:sns:us-west-2:123456789012:YourTopic",
|
||||
Required: true,
|
||||
}}},
|
||||
|
|
|
@ -27,16 +27,15 @@ var Discorder = &discord{¬ifications.Notification{
|
|||
Author: "Hunter Long",
|
||||
AuthorUrl: "https://github.com/hunterlong",
|
||||
Delay: time.Duration(5 * time.Second),
|
||||
Host: null.NewNullString("https://discordapp.com/api/webhooks/****/*****"),
|
||||
Icon: "fab fa-discord",
|
||||
SuccessData: null.NewNullString(`{"content": "Your service '{{.Service.Name}}' is currently online!"}`),
|
||||
FailureData: null.NewNullString(`{"content": "Your service '{{.Service.Name}}' is currently failing! Reason: {{.Failure.Issue}}"}`),
|
||||
SuccessData: null.NewNullString(`{"content": "Your service '{{.Service.Name}}' is currently back online and was down for {{.Service.Downtime.Human}}."}`),
|
||||
FailureData: null.NewNullString(`{"content": "Your service '{{.Service.Name}}' is has been failing for {{.Service.Downtime.Human}}! Reason: {{.Failure.Issue}}"}`),
|
||||
DataType: "json",
|
||||
Limits: 60,
|
||||
Form: []notifications.NotificationForm{{
|
||||
Type: "text",
|
||||
Title: "discord webhooker URL",
|
||||
Placeholder: "Insert your Webhook URL here",
|
||||
Placeholder: "https://discordapp.com/api/webhooks/****/*****",
|
||||
DbField: "host",
|
||||
}}},
|
||||
}
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
package notifiers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"github.com/go-mail/mail"
|
||||
"github.com/statping/emails"
|
||||
"github.com/statping/statping/types/core"
|
||||
"github.com/statping/statping/types/failures"
|
||||
"github.com/statping/statping/types/notifications"
|
||||
"github.com/statping/statping/types/notifier"
|
||||
"github.com/statping/statping/types/services"
|
||||
"github.com/statping/statping/utils"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
var _ notifier.Notifier = (*emailer)(nil)
|
||||
|
@ -92,7 +91,7 @@ type emailOutgoing struct {
|
|||
// OnFailure will trigger failing service
|
||||
func (e *emailer) OnFailure(s services.Service, f failures.Failure) (string, error) {
|
||||
subject := fmt.Sprintf("Service %s is Offline", s.Name)
|
||||
tmpl := renderEmail(s, f, emailFailure)
|
||||
tmpl := renderEmail(s, f, emails.Failure)
|
||||
email := &emailOutgoing{
|
||||
To: e.Var2.String,
|
||||
Subject: subject,
|
||||
|
@ -105,7 +104,7 @@ func (e *emailer) OnFailure(s services.Service, f failures.Failure) (string, err
|
|||
// OnSuccess will trigger successful service
|
||||
func (e *emailer) OnSuccess(s services.Service) (string, error) {
|
||||
subject := fmt.Sprintf("Service %s is Back Online", s.Name)
|
||||
tmpl := renderEmail(s, failures.Failure{}, emailSuccess)
|
||||
tmpl := renderEmail(s, failures.Failure{}, emails.Success)
|
||||
email := &emailOutgoing{
|
||||
To: e.Var2.String,
|
||||
Subject: subject,
|
||||
|
@ -116,27 +115,18 @@ func (e *emailer) OnSuccess(s services.Service) (string, error) {
|
|||
}
|
||||
|
||||
func renderEmail(s services.Service, f failures.Failure, emailData string) string {
|
||||
wr := bytes.NewBuffer(nil)
|
||||
tmpl := template.New("email")
|
||||
tmpl, err := tmpl.Parse(emailData)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return emailData
|
||||
}
|
||||
|
||||
data := replacer{
|
||||
Core: *core.App,
|
||||
Service: s,
|
||||
Failure: f,
|
||||
Custom: nil,
|
||||
}
|
||||
|
||||
if err = tmpl.ExecuteTemplate(wr, "email", data); err != nil {
|
||||
output, err := emails.Parse(emailData, data)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return emailData
|
||||
}
|
||||
|
||||
return wr.String()
|
||||
return output
|
||||
}
|
||||
|
||||
// OnTest triggers when this notifier has been saved
|
||||
|
|
|
@ -37,6 +37,8 @@ func InitNotifiers() {
|
|||
Gotify,
|
||||
AmazonSNS,
|
||||
)
|
||||
|
||||
services.UpdateNotifiers()
|
||||
}
|
||||
|
||||
func ReplaceTemplate(tmpl string, data replacer) string {
|
||||
|
@ -56,10 +58,11 @@ func ReplaceTemplate(tmpl string, data replacer) string {
|
|||
|
||||
func Add(notifs ...services.ServiceNotifier) {
|
||||
for _, n := range notifs {
|
||||
services.AddNotifier(n)
|
||||
if err := n.Select().Create(); err != nil {
|
||||
notif := n.Select()
|
||||
if err := notif.Create(); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
services.AddNotifier(n)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ import (
|
|||
"github.com/statping/statping/types/null"
|
||||
"github.com/statping/statping/types/services"
|
||||
"github.com/statping/statping/utils"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
@ -94,10 +93,5 @@ func (s *slack) OnSave() (string, error) {
|
|||
}
|
||||
|
||||
func (s *slack) Valid(values notifications.Values) error {
|
||||
regex := `https\:\/\/hooks\.slack\.com/services/[A-Z0-9]{7,11}/[A-Z0-9]{7,11}/[a-zA-Z0-9]{20,22}`
|
||||
r := regexp.MustCompile(regex)
|
||||
if !r.MatchString(values.Host) {
|
||||
return errors.New("slack webhook does not match with expected regex " + regex)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ func TestSlackNotifier(t *testing.T) {
|
|||
|
||||
t.Run("Load slack", func(t *testing.T) {
|
||||
slacker.Host = null.NewNullString(SLACK_URL)
|
||||
slacker.Delay = time.Duration(100 * time.Millisecond)
|
||||
slacker.Delay = 100 * time.Millisecond
|
||||
slacker.Limits = 3
|
||||
Add(slacker)
|
||||
assert.Equal(t, "Hunter Long", slacker.Author)
|
||||
|
|
|
@ -8,7 +8,7 @@ func (c *Checkin) LastHit() *CheckinHit {
|
|||
|
||||
func (c *Checkin) Hits() []*CheckinHit {
|
||||
var hits []*CheckinHit
|
||||
dbHits.Where("checkin = ?", c.Id).Order("DESC").Find(&hits)
|
||||
dbHits.Where("checkin = ?", c.Id).Order("id DESC").Find(&hits)
|
||||
c.AllHits = hits
|
||||
return hits
|
||||
}
|
||||
|
|
|
@ -48,21 +48,21 @@ func LoadConfigForm(r *http.Request) (*DbConfig, error) {
|
|||
p.Set("ADMIN_EMAIL", email)
|
||||
|
||||
confg := &DbConfig{
|
||||
DbConn: dbConn,
|
||||
DbHost: dbHost,
|
||||
DbUser: dbUser,
|
||||
DbPass: dbPass,
|
||||
DbData: dbDatabase,
|
||||
DbPort: int(dbPort),
|
||||
Project: project,
|
||||
Description: description,
|
||||
Domain: domain,
|
||||
Username: username,
|
||||
Password: password,
|
||||
Email: email,
|
||||
Location: utils.Directory,
|
||||
Language: language,
|
||||
SendReports: reports,
|
||||
DbConn: dbConn,
|
||||
DbHost: dbHost,
|
||||
DbUser: dbUser,
|
||||
DbPass: dbPass,
|
||||
DbData: dbDatabase,
|
||||
DbPort: int(dbPort),
|
||||
Project: project,
|
||||
Description: description,
|
||||
Domain: domain,
|
||||
Username: username,
|
||||
Password: password,
|
||||
Email: email,
|
||||
Location: utils.Directory,
|
||||
Language: language,
|
||||
AllowReports: reports,
|
||||
}
|
||||
|
||||
return confg, nil
|
||||
|
|
|
@ -59,7 +59,7 @@ func LoadConfigs(cfgFile string) (*DbConfig, error) {
|
|||
if db.Language != "" {
|
||||
p.Set("LANGUAGE", db.Language)
|
||||
}
|
||||
if db.SendReports {
|
||||
if db.AllowReports {
|
||||
p.Set("ALLOW_REPORTS", true)
|
||||
}
|
||||
if db.LetsEncryptEmail != "" {
|
||||
|
@ -88,11 +88,12 @@ func LoadConfigs(cfgFile string) (*DbConfig, error) {
|
|||
Location: utils.Directory,
|
||||
SqlFile: p.GetString("SQL_FILE"),
|
||||
Language: p.GetString("LANGUAGE"),
|
||||
SendReports: p.GetBool("ALLOW_REPORTS"),
|
||||
AllowReports: p.GetBool("ALLOW_REPORTS"),
|
||||
LetsEncryptEnable: p.GetBool("LETSENCRYPT_ENABLE"),
|
||||
LetsEncryptHost: p.GetString("LETSENCRYPT_HOST"),
|
||||
LetsEncryptEmail: p.GetString("LETSENCRYPT_EMAIL"),
|
||||
ApiSecret: p.GetString("API_SECRET"),
|
||||
SampleData: p.GetBool("SAMPLE_DATA"),
|
||||
}
|
||||
log.WithFields(utils.ToFields(configs)).Debugln("read config file: " + cfgFile)
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ type DbConfig struct {
|
|||
DbPort int `yaml:"port" json:"-"`
|
||||
ApiSecret string `yaml:"api_secret" json:"-"`
|
||||
Language string `yaml:"language" json:"language"`
|
||||
SendReports bool `yaml:"send_reports" json:"send_reports"`
|
||||
AllowReports bool `yaml:"allow_reports" json:"allow_reports"`
|
||||
Project string `yaml:"-" json:"-"`
|
||||
Description string `yaml:"-" json:"-"`
|
||||
Domain string `yaml:"-" json:"-"`
|
||||
|
@ -26,8 +26,28 @@ type DbConfig struct {
|
|||
SqlFile string `yaml:"sqlfile,omitempty" json:"-"`
|
||||
LetsEncryptHost string `yaml:"letsencrypt_host,omitempty" json:"letsencrypt_host"`
|
||||
LetsEncryptEmail string `yaml:"letsencrypt_email,omitempty" json:"letsencrypt_email"`
|
||||
LetsEncryptEnable bool `yaml:"letsencrypt_enable" json:"letsencrypt_enable"`
|
||||
LetsEncryptEnable bool `yaml:"letsencrypt_enable,omitempty" json:"letsencrypt_enable"`
|
||||
LocalIP string `yaml:"-" json:"-"`
|
||||
|
||||
DisableHTTP bool `yaml:"disable_http" json:"disable_http"`
|
||||
DemoMode bool `yaml:"demo_mode" json:"demo_mode"`
|
||||
DisableLogs bool `yaml:"disable_logs" json:"disable_logs"`
|
||||
UseAssets bool `yaml:"use_assets" json:"use_assets"`
|
||||
BasePath string `yaml:"base_path" json:"base_path"`
|
||||
|
||||
AdminUser string `yaml:"admin_user" json:"admin_user"`
|
||||
AdminPassword string `yaml:"admin_password" json:"admin_password"`
|
||||
AdminEmail string `yaml:"admin_email" json:"admin_email"`
|
||||
|
||||
MaxOpenConnections int `yaml:"db_open_connections" json:"db_open_connections"`
|
||||
MaxIdleConnections int `yaml:"db_idle_connections" json:"db_idle_connections"`
|
||||
MaxLifeConnections int `yaml:"db_max_life_connections" json:"db_max_life_connections"`
|
||||
|
||||
SampleData bool `yaml:"sample_data" json:"sample_data"`
|
||||
UseCDN bool `yaml:"use_cdn" json:"use_cdn"`
|
||||
DisableColors bool `yaml:"disable_colors" json:"disable_colors"`
|
||||
|
||||
PostgresSSLMode string `yaml:"postgres_ssl" json:"postgres_ssl"`
|
||||
|
||||
Db database.Database `yaml:"-" json:"-"`
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ func (i *Incident) BeforeCreate() error {
|
|||
}
|
||||
|
||||
func (i *Incident) AfterFind() {
|
||||
db.Model(i).Related(&i.Updates).Order("DESC")
|
||||
db.Model(i).Related(&i.Updates).Order("id DESC")
|
||||
metrics.Query("incident", "find")
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,15 @@ func (n *Notification) Values() Values {
|
|||
}
|
||||
}
|
||||
|
||||
func All() []*Notification {
|
||||
var n []*Notification
|
||||
q := db.Find(&n)
|
||||
if q.Error() != nil {
|
||||
return nil
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func Find(method string) (*Notification, error) {
|
||||
var n Notification
|
||||
q := db.Where("method = ?", method).Find(&n)
|
||||
|
@ -38,6 +47,7 @@ func (n *Notification) Create() error {
|
|||
var p Notification
|
||||
q := db.Where("method = ?", n.Method).Find(&p)
|
||||
if q.RecordNotFound() {
|
||||
log.Infof("Notifier '%s' was not found, adding into database...\n", n.Method)
|
||||
if err := db.Create(n).Error(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -56,6 +66,9 @@ func (n *Notification) Create() error {
|
|||
}
|
||||
|
||||
func (n *Notification) UpdateFields(notif *Notification) *Notification {
|
||||
if notif == nil {
|
||||
return n
|
||||
}
|
||||
n.Id = notif.Id
|
||||
n.Limits = notif.Limits
|
||||
n.Enabled = notif.Enabled
|
||||
|
|
|
@ -38,12 +38,10 @@ type Notification struct {
|
|||
AuthorUrl string `gorm:"-" json:"author_url"`
|
||||
Icon string `gorm:"-" json:"icon"`
|
||||
Delay time.Duration `gorm:"-" json:"delay,string"`
|
||||
Running chan bool `gorm:"-" json:"-"`
|
||||
|
||||
Form []NotificationForm `gorm:"-" json:"form"`
|
||||
LastSent time.Time `gorm:"-" json:"-"`
|
||||
LastSentCount int `gorm:"-" json:"-"`
|
||||
sentCount int `gorm:"-" json:"-"`
|
||||
Logs []*NotificationLog `gorm:"-" json:"logs,omitempty"`
|
||||
}
|
||||
|
||||
|
@ -59,8 +57,6 @@ func (n *Notification) Logger() *logrus.Logger {
|
|||
return log.WithField("notifier", n.Method).Logger
|
||||
}
|
||||
|
||||
type RunFunc func(interface{}) error
|
||||
|
||||
type Values struct {
|
||||
Host string
|
||||
Port int64
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
package null
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func (s NullString) Value() (driver.Value, error) {
|
||||
return s.String, nil
|
||||
}
|
||||
|
||||
// MarshalJSON for NullInt64
|
||||
func (i NullInt64) MarshalJSON() ([]byte, error) {
|
||||
if !i.Valid {
|
||||
|
@ -32,7 +37,7 @@ func (bb NullBool) MarshalJSON() ([]byte, error) {
|
|||
// MarshalJSON for NullString
|
||||
func (s NullString) MarshalJSON() ([]byte, error) {
|
||||
if !s.Valid {
|
||||
return []byte("null"), nil
|
||||
return json.Marshal(nil)
|
||||
}
|
||||
return json.Marshal(s.String)
|
||||
}
|
||||
|
|
|
@ -11,6 +11,13 @@ func AddNotifier(n ServiceNotifier) {
|
|||
allNotifiers[notif.Method] = n
|
||||
}
|
||||
|
||||
func UpdateNotifiers() {
|
||||
for _, n := range notifications.All() {
|
||||
notifier := allNotifiers[n.Method]
|
||||
notifier.Select().UpdateFields(n)
|
||||
}
|
||||
}
|
||||
|
||||
func sendSuccess(s *Service) {
|
||||
if !s.AllowNotifications.Bool {
|
||||
return
|
||||
|
|
|
@ -14,6 +14,15 @@ func (u *User) Validate() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (u *User) BeforeDelete() error {
|
||||
if utils.Params.GetBool("DEMO_MODE") {
|
||||
if u.Username == "admin" {
|
||||
return errors.New("cannot delete admin in DEMO_MODE")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) BeforeCreate() error {
|
||||
if err := u.Validate(); err != nil {
|
||||
return err
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
package utils
|
||||
|
||||
var (
|
||||
StartTime = Now()
|
||||
)
|
|
@ -192,7 +192,8 @@ func TestHttpRequest(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestConfigLoad(t *testing.T) {
|
||||
InitLogs()
|
||||
err := InitLogs()
|
||||
require.Nil(t, err)
|
||||
InitEnvs()
|
||||
|
||||
s := Params.GetString
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.90.64
|
||||
0.90.65
|
||||
|
|
Loading…
Reference in New Issue