pull/429/head
hunterlong 2020-02-19 21:28:39 -08:00
parent b8dbad85fe
commit 7762ca40d7
61 changed files with 2608 additions and 1225 deletions

View File

@ -280,7 +280,6 @@ func recordFailure(s *Service, issue string) {
s.CreateFailure(fail) s.CreateFailure(fail)
s.Online = false s.Online = false
s.SuccessNotified = false s.SuccessNotified = false
s.UpdateNotify = CoreApp.UpdateNotify.Bool
s.DownText = s.DowntimeText() s.DownText = s.DowntimeText()
notifier.OnFailure(s.Service, fail) notifier.OnFailure(s.Service, fail)
} }

View File

@ -40,7 +40,7 @@ func OnFailure(s *types.Service, f *types.Failure) {
} }
// check if User wants to receive every Status Change // check if User wants to receive every Status Change
if s.UpdateNotify { if s.UpdateNotify.Bool {
// send only if User hasn't been already notified about the Downtime // send only if User hasn't been already notified about the Downtime
if !s.UserNotified { if !s.UserNotified {
s.UserNotified = true s.UserNotified = true
@ -69,7 +69,7 @@ func OnSuccess(s *types.Service) {
} }
// check if User wants to receive every Status Change // check if User wants to receive every Status Change
if s.UpdateNotify && s.UserNotified { if s.UpdateNotify.Bool && s.UserNotified {
s.UserNotified = false s.UserNotified = false
} }

View File

@ -10,12 +10,14 @@ const environment = require('./dev.env');
const webpackConfig = merge(commonConfig, { const webpackConfig = merge(commonConfig, {
mode: 'development', mode: 'development',
devtool: 'cheap-module-eval-source-map', devtool: 'inline-cheap-module-source-map',
output: { output: {
path: helpers.root('dist'), path: helpers.root('dist'),
publicPath: '/', publicPath: '/',
filename: 'js/[name].bundle.js', filename: 'js/[name].bundle.js',
chunkFilename: 'js/[name].chunk.js' chunkFilename: 'js/[name].chunk.js',
devtoolModuleFilenameTemplate: '[absolute-resource-path]',
devtoolFallbackModuleFilenameTemplate: '[absolute-resource-path]?[hash]'
}, },
optimization: { optimization: {
runtimeChunk: 'single', runtimeChunk: 'single',

View File

@ -6,7 +6,8 @@
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "rm -rf dist && cross-env NODE_ENV=production webpack --mode production", "build": "rm -rf dist && cross-env NODE_ENV=production webpack --mode production",
"dev": "cross-env NODE_ENV=development webpack-dev-server --host 0.0.0.0 --port 8888 --progress", "dev": "cross-env NODE_ENV=development webpack-dev-server --host 0.0.0.0 --port 8888 --progress",
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint",
"test": "cross-env NODE_ENV=development mochapack --webpack-config webpack.config.js --require test/setup.js test/**/*.spec.js"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free-solid": "^5.1.0-3", "@fortawesome/fontawesome-free-solid": "^5.1.0-3",
@ -18,7 +19,7 @@
"axios": "^0.19.1", "axios": "^0.19.1",
"codemirror-colorpicker": "^1.9.66", "codemirror-colorpicker": "^1.9.66",
"core-js": "^3.4.4", "core-js": "^3.4.4",
"moment": "^2.24.0", "date-fns": "^2.9.0",
"querystring": "^0.2.0", "querystring": "^0.2.0",
"vue": "^2.6.10", "vue": "^2.6.10",
"vue-apexcharts": "^1.5.2", "vue-apexcharts": "^1.5.2",
@ -40,6 +41,7 @@
"@babel/preset-env": "~7.8.3", "@babel/preset-env": "~7.8.3",
"@vue/babel-preset-app": "^4.1.2", "@vue/babel-preset-app": "^4.1.2",
"@vue/cli-plugin-babel": "^4.1.0", "@vue/cli-plugin-babel": "^4.1.0",
"@vue/test-utils": "^1.0.0-beta.31",
"babel-eslint": "~10.0", "babel-eslint": "~10.0",
"babel-loader": "~8.0", "babel-loader": "~8.0",
"compression-webpack-plugin": "~2.0", "compression-webpack-plugin": "~2.0",
@ -55,10 +57,15 @@
"eslint-plugin-promise": "~3.5", "eslint-plugin-promise": "~3.5",
"eslint-plugin-standard": "~3.0", "eslint-plugin-standard": "~3.0",
"eslint-plugin-vue": "~5.1", "eslint-plugin-vue": "~5.1",
"expect": "^25.1.0",
"file-loader": "^5.0.2", "file-loader": "^5.0.2",
"friendly-errors-webpack-plugin": "~1.7", "friendly-errors-webpack-plugin": "~1.7",
"html-webpack-plugin": "^4.0.0-beta.11", "html-webpack-plugin": "^4.0.0-beta.11",
"jsdom": "^16.2.0",
"jsdom-global": "^3.0.2",
"mini-css-extract-plugin": "~0.5", "mini-css-extract-plugin": "~0.5",
"mocha": "^7.0.1",
"mochapack": "^1.1.13",
"node-sass": "^4.13.1", "node-sass": "^4.13.1",
"optimize-css-assets-webpack-plugin": "~5.0", "optimize-css-assets-webpack-plugin": "~5.0",
"sass-loader": "^8.0.2", "sass-loader": "^8.0.2",

View File

@ -10,11 +10,20 @@
<base href="{{BasePath}}"> <base href="{{BasePath}}">
{{if USE_CDN}} {{if USE_CDN}}
<link rel="shortcut icon" type="image/x-icon" href="https://assets.statping.com/favicon.ico"> <link rel="shortcut icon" type="image/x-icon" href="https://assets.statping.com/favicon.ico">
<link rel="stylesheet" href="https://assets.statping.com/css/bootstrap.min.css">
<link rel="stylesheet" href="https://assets.statping.com/css/base.css">
<link rel="stylesheet" href="https://assets.statping.com/font/all.css">
{{else}} {{else}}
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico"> <link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
{{if USING_ASSETS}}
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="font/all.css">
{{else}}
<% _.each(htmlWebpackPlugin.tags.headTags, function(headTag) { %> <% _.each(htmlWebpackPlugin.tags.headTags, function(headTag) { %>
<%= headTag %> <% }) %> <%= headTag %> <% }) %>
{{end}} {{end}}
{{end}}
</head> </head>
<body> <body>
<noscript> <noscript>

226
frontend/src/API.js Normal file
View File

@ -0,0 +1,226 @@
import axios from 'axios'
const qs = require('querystring')
const tokenKey = "statping_user";
class Api {
constructor() {
}
async core() {
return axios.get('/api').then(response => (response.data))
}
async core_save(obj) {
return axios.post('/api/core', obj).then(response => (response.data))
}
async setup_save(data) {
return axios.post('/api/setup', qs.stringify(data)).then(response => (response.data))
}
async services() {
return axios.get('/api/services').then(response => (response.data))
}
async service(id) {
return axios.get('/api/services/' + id).then(response => (response.data))
}
async service_create(data) {
return axios.post('/api/services', data).then(response => (response.data))
}
async service_update(data) {
return axios.post('/api/services/' + data.id, data).then(response => (response.data))
}
async service_hits(id, start, end, group) {
return axios.get('/api/services/' + id + '/data?start=' + start + '&end=' + end + '&group=' + group).then(response => (response.data))
}
async service_heatmap(id, start, end, group) {
return axios.get('/api/services/' + id + '/heatmap').then(response => (response.data))
}
async service_failures(id, start, end, limit = 999, offset = 0) {
return axios.get('/api/services/' + id + '/failures?start=' + start + '&end=' + end + '&limit=' + limit).then(response => (response.data))
}
async service_delete(id) {
return axios.delete('/api/services/' + id).then(response => (response.data))
}
async services_reorder(data) {
return axios.post('/api/reorder/services', data).then(response => (response.data))
}
async groups() {
return axios.get('/api/groups').then(response => (response.data))
}
async groups_reorder(data) {
return axios.post('/api/reorder/groups', data).then(response => (response.data))
}
async group_delete(id) {
return axios.delete('/api/groups/' + id).then(response => (response.data))
}
async group_create(data) {
return axios.post('/api/groups', data).then(response => (response.data))
}
async group_update(data) {
return axios.post('/api/groups/' + data.id, data).then(response => (response.data))
}
async users() {
return axios.get('/api/users').then(response => (response.data))
}
async user_create(data) {
return axios.post('/api/users', data).then(response => (response.data))
}
async user_update(data) {
return axios.post('/api/users/' + data.id, data).then(response => (response.data))
}
async user_delete(id) {
return axios.delete('/api/users/' + id).then(response => (response.data))
}
async messages() {
return axios.get('/api/messages').then(response => (response.data))
}
async message_create(data) {
return axios.post('/api/messages', data).then(response => (response.data))
}
async message_update(data) {
return axios.post('/api/messages/' + data.id, data).then(response => (response.data))
}
async message_delete(id) {
return axios.delete('/api/messages/' + id).then(response => (response.data))
}
async group(id) {
return axios.get('/api/groups/' + id).then(response => (response.data))
}
async notifiers() {
return axios.get('/api/notifiers').then(response => (response.data))
}
async notifier_save(data) {
return axios.post('/api/notifier/' + data.method, data).then(response => (response.data))
}
async notifier_test(data) {
return axios.post('/api/notifier/' + data.method + '/test', data).then(response => (response.data))
}
async integrations() {
return axios.get('/api/integrations').then(response => (response.data))
}
async integration(name) {
return axios.get('/api/integrations/' + name).then(response => (response.data))
}
async integration_save(data) {
return axios.post('/api/integrations/' + data.name, data).then(response => (response.data))
}
async renewApiKeys() {
return axios.get('/api/renew').then(response => (response.data))
}
async cache() {
return axios.get('/api/cache').then(response => (response.data))
}
async clearCache() {
return axios.get('/api/clear_cache').then(response => (response.data))
}
async logs() {
return axios.get('/api/logs').then(response => (response.data))
}
async logs_last() {
return axios.get('/api/logs/last').then(response => (response.data))
}
async theme() {
return axios.get('/api/theme').then(response => (response.data))
}
async theme_generate(create = true) {
if (create) {
return axios.get('/api/theme/create').then(response => (response.data))
} else {
return axios.delete('/api/theme').then(response => (response.data))
}
}
async theme_save(data) {
return axios.post('/api/theme', data).then(response => (response.data))
}
async login(username, password) {
const f = {username: username, password: password}
return axios.post('/api/login', qs.stringify(f))
.then(response => (response.data))
}
async logout() {
await axios.get('/api/logout').then(response => (response.data))
return localStorage.removeItem(tokenKey)
}
saveToken(username, token) {
const user = {username: username, token: token}
localStorage.setItem(tokenKey, JSON.stringify(user));
return user
}
async scss_base() {
return await axios({
url: '/scss/base.scss',
method: 'GET',
responseType: 'blob'
}).then((response) => {
const reader = new window.FileReader();
return reader.readAsText(response.data)
})
}
token() {
const tk = localStorage.getItem(tokenKey)
if (!tk) {
return {};
}
return JSON.parse(tk);
}
authToken() {
let user = JSON.parse(localStorage.getItem(tokenKey));
if (user && user.token) {
return {'Authorization': 'Bearer ' + user.token};
} else {
return {};
}
}
async allActions(...all) {
await axios.all([all])
}
}
const api = new Api()
export default api

View File

@ -6,7 +6,7 @@
</template> </template>
<script> <script>
import Api from './components/API'; import Api from './API';
import Footer from "./components/Index/Footer"; import Footer from "./components/Index/Footer";
export default { export default {

View File

@ -1,226 +0,0 @@
import axios from 'axios'
const qs = require('querystring')
const tokenKey = "statping_user";
class Api {
constructor() {
}
async core () {
return axios.get('/api').then(response => (response.data))
}
async core_save (obj) {
return axios.post('/api/core', obj).then(response => (response.data))
}
async setup_save (data) {
return axios.post('/api/setup', qs.stringify(data)).then(response => (response.data))
}
async services () {
return axios.get('/api/services').then(response => (response.data))
}
async service (id) {
return axios.get('/api/services/'+id).then(response => (response.data))
}
async service_create (data) {
return axios.post('/api/services', data).then(response => (response.data))
}
async service_update (data) {
return axios.post('/api/services/'+data.id, data).then(response => (response.data))
}
async service_hits (id, start, end, group) {
return axios.get('/api/services/'+id+'/data?start=' + start + '&end=' + end + '&group=' + group).then(response => (response.data))
}
async service_heatmap (id, start, end, group) {
return axios.get('/api/services/'+id+'/heatmap?start=' + start + '&end=' + end + '&group=' + group).then(response => (response.data))
}
async service_failures (id, start, end, limit=999, offset=0) {
return axios.get('/api/services/'+id+'/failures?start=' + start + '&end=' + end + '&limit=' + limit).then(response => (response.data))
}
async service_delete (id) {
return axios.delete('/api/services/'+id).then(response => (response.data))
}
async services_reorder (data) {
return axios.post('/api/reorder/services', data).then(response => (response.data))
}
async groups () {
return axios.get('/api/groups').then(response => (response.data))
}
async groups_reorder (data) {
return axios.post('/api/reorder/groups', data).then(response => (response.data))
}
async group_delete (id) {
return axios.delete('/api/groups/'+id).then(response => (response.data))
}
async group_create (data) {
return axios.post('/api/groups', data).then(response => (response.data))
}
async group_update (data) {
return axios.post('/api/groups/'+data.id, data).then(response => (response.data))
}
async users () {
return axios.get('/api/users').then(response => (response.data))
}
async user_create (data) {
return axios.post('/api/users', data).then(response => (response.data))
}
async user_update (data) {
return axios.post('/api/users/'+data.id, data).then(response => (response.data))
}
async user_delete (id) {
return axios.delete('/api/users/'+id).then(response => (response.data))
}
async messages () {
return axios.get('/api/messages').then(response => (response.data))
}
async message_create (data) {
return axios.post('/api/messages', data).then(response => (response.data))
}
async message_update (data) {
return axios.post('/api/messages/'+data.id, data).then(response => (response.data))
}
async message_delete (id) {
return axios.delete('/api/messages/'+id).then(response => (response.data))
}
async group (id) {
return axios.get('/api/groups/'+id).then(response => (response.data))
}
async notifiers () {
return axios.get('/api/notifiers').then(response => (response.data))
}
async notifier_save (data) {
return axios.post('/api/notifier/'+data.method, data).then(response => (response.data))
}
async notifier_test (data) {
return axios.post('/api/notifier/'+data.method+'/test', data).then(response => (response.data))
}
async integrations () {
return axios.get('/api/integrations').then(response => (response.data))
}
async integration (name) {
return axios.get('/api/integrations/'+name).then(response => (response.data))
}
async integration_save (data) {
return axios.post('/api/integrations/'+data.name, data).then(response => (response.data))
}
async renewApiKeys () {
return axios.get('/api/renew').then(response => (response.data))
}
async cache () {
return axios.get('/api/cache').then(response => (response.data))
}
async clearCache () {
return axios.get('/api/clear_cache').then(response => (response.data))
}
async logs () {
return axios.get('/api/logs').then(response => (response.data))
}
async logs_last () {
return axios.get('/api/logs/last').then(response => (response.data))
}
async theme () {
return axios.get('/api/theme').then(response => (response.data))
}
async theme_generate (create=true) {
if (create) {
return axios.get('/api/theme/create').then(response => (response.data))
} else {
return axios.delete('/api/theme').then(response => (response.data))
}
}
async theme_save (data) {
return axios.post('/api/theme', data).then(response => (response.data))
}
async login (username, password) {
const f = {username: username, password: password}
return axios.post('/api/login', qs.stringify(f))
.then(response => (response.data))
}
async logout () {
await axios.get('/api/logout').then(response => (response.data))
return localStorage.removeItem(tokenKey)
}
saveToken (username, token) {
const user = {username: username, token: token}
localStorage.setItem(tokenKey, JSON.stringify(user));
return user
}
async scss_base () {
return await axios({
url: '/scss/base.scss',
method: 'GET',
responseType: 'blob'
}).then((response) => {
const reader = new window.FileReader();
return reader.readAsText(response.data)
})
}
token () {
const tk = localStorage.getItem(tokenKey)
if (!tk) {
return {};
}
return JSON.parse(tk);
}
authToken () {
let user = JSON.parse(localStorage.getItem(tokenKey));
if (user && user.token) {
return { 'Authorization': 'Bearer ' + user.token };
} else {
return {};
}
}
async allActions (...all) {
await axios.all([all])
}
}
const api = new Api()
export default api

View File

@ -0,0 +1,56 @@
<template>
<div>
<h3>Cache</h3>
<div v-if="!cache && cache.length !== 0" class="alert alert-danger">
There are no cached files
</div>
<table class="table">
<thead>
<tr>
<th scope="col">URL</th>
<th scope="col">Size</th>
<th scope="col">Expiration</th>
</tr>
</thead>
<tbody>
<tr v-for="(cache, index) in cache">
<td>{{cache.url}}</td>
<td>{{cache.size}}</td>
<td>{{expireTime(cache.expiration)}}</td>
</tr>
</tbody>
</table>
<button @click.prevent="clearCache" class="btn btn-danger btn-block">Clear Cache</button>
</div>
</template>
<script>
import Api from "../../API";
export default {
name: 'Cache',
data() {
return {
cache: [],
}
},
async mounted() {
this.cache = await Api.cache()
},
methods: {
expireTime(ex) {
return this.toLocal(ex)
},
async clearCache() {
await Api.clearCache()
this.cache = []
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -23,33 +23,24 @@
</template> </template>
<script> <script>
import ServiceInfo from "./ServiceInfo"; import ServiceInfo from "../Service/ServiceInfo";
export default { export default {
name: 'DashboardIndex', name: 'DashboardIndex',
components: { components: {
ServiceInfo ServiceInfo
}, },
data () { methods: {
return { failuresLast24Hours() {
let total = 0;
this.$store.getters.services.map((s) => {
total += s.failures_24_hours
})
return total
},
} }
},
computed: {
},
async created() {
},
methods: {
failuresLast24Hours() {
let total = 0;
this.$store.getters.services.map((s) => { total += s.failures_24_hours })
return total
},
} }
}
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -37,7 +37,7 @@
</template> </template>
<script> <script>
import Api from "../API" import Api from "../../API"
import FormMessage from "../../forms/Message"; import FormMessage from "../../forms/Message";
export default { export default {

View File

@ -99,97 +99,97 @@
<script> <script>
import FormGroup from "../../forms/Group"; import FormGroup from "../../forms/Group";
import Api from "../../components/API"; import Api from "../../API";
import ToggleSwitch from "../../forms/ToggleSwitch"; import ToggleSwitch from "../../forms/ToggleSwitch";
import draggable from 'vuedraggable' import draggable from 'vuedraggable'
export default { export default {
name: 'DashboardServices', name: 'DashboardServices',
components: { components: {
ToggleSwitch, ToggleSwitch,
FormGroup, FormGroup,
draggable draggable
},
data () {
return {
edit: false,
group: {}
}
},
computed: {
servicesList: {
get() {
return this.$store.state.servicesInOrder
}, },
async set(value) { data() {
let data = []; return {
value.forEach((s, k) => { edit: false,
data.push({service: s.id, order: k+1}) group: {}
}); }
await Api.services_reorder(data)
const services = await Api.services()
this.$store.commit('setServices', services)
}
},
groupsList: {
get() {
return this.$store.state.groupsInOrder
}, },
async set(value) { computed: {
let data = []; servicesList: {
value.forEach((s, k) => { get() {
data.push({group: s.id, order: k+1}) return this.$store.state.servicesInOrder
}); },
await Api.groups_reorder(data) async set(value) {
const groups = await Api.groups() let data = [];
this.$store.commit('setGroups', groups) value.forEach((s, k) => {
} data.push({service: s.id, order: k + 1})
} });
}, await Api.services_reorder(data)
beforeMount() { const services = await Api.services()
this.$store.commit('setServices', services)
}
},
groupsList: {
get() {
return this.$store.state.groupsInOrder
},
async set(value) {
let data = [];
value.forEach((s, k) => {
data.push({group: s.id, order: k + 1})
});
await Api.groups_reorder(data)
const groups = await Api.groups()
this.$store.commit('setGroups', groups)
}
}
},
beforeMount() {
}, },
methods: { methods: {
editChange(v) { editChange(v) {
this.group = {} this.group = {}
this.edit = v this.edit = v
}, },
editGroup(g, mode) { editGroup(g, mode) {
this.group = g this.group = g
this.edit = !mode this.edit = !mode
}, },
reordered_services() { reordered_services() {
}, },
saveUpdatedOrder: function (e) { saveUpdatedOrder: function (e) {
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) { serviceGroup(s) {
let group = this.$store.getters.groupById(s.group_id) let group = this.$store.getters.groupById(s.group_id)
if (group) { if (group) {
return group.name return group.name
}
return ""
},
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)
}
},
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)
}
}
} }
return ""
},
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)
}
},
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)
}
}
} }
}
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -32,7 +32,7 @@
</template> </template>
<script> <script>
import Api from "../API" import Api from "../../API"
import FormUser from "../../forms/User"; import FormUser from "../../forms/User";
export default { export default {

View File

@ -1,16 +1,16 @@
<template> <template>
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<FormService :in_service="service"/> <FormService :in_service="service"/>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import FormGroup from "../../forms/Group"; import FormGroup from "../../forms/Group";
import Api from "../../components/API"; import Api from "../../API";
import ToggleSwitch from "../../forms/ToggleSwitch"; import ToggleSwitch from "../../forms/ToggleSwitch";
import draggable from 'vuedraggable' import draggable from 'vuedraggable'
import FormService from "../../forms/Service"; import FormService from "../../forms/Service";

View File

@ -44,7 +44,7 @@
</template> </template>
<script> <script>
import Api from "../API"; import Api from "../../API";
// require component // require component
import { codemirror } from 'vue-codemirror' import { codemirror } from 'vue-codemirror'
@ -136,7 +136,6 @@
this.error = null this.error = null
} }
this.pending = false this.pending = false
window.console.log(resp)
await this.fetchTheme() await this.fetchTheme()
}, },
changeTab (v) { changeTab (v) {

View File

@ -41,7 +41,7 @@
</template> </template>
<script> <script>
import Api from "../API" import Api from "../../API"
export default { export default {
name: 'TopNav', name: 'TopNav',

View File

@ -8,20 +8,7 @@
<span class="badge float-right" :class="{'bg-success': service.online, 'bg-danger': !service.online}">{{service.online ? "ONLINE" : "OFFLINE"}}</span> <span class="badge float-right" :class="{'bg-success': service.online, 'bg-danger': !service.online}">{{service.online ? "ONLINE" : "OFFLINE"}}</span>
</h4> </h4>
<div class="row stats_area mt-5"> <ServiceTopStats :service="service"/>
<div class="col-4">
<span class="lg_number">{{service.avg_response}}ms</span>
Average Response
</div>
<div class="col-4">
<span class="lg_number">{{service.online_24_hours}}%</span>
Uptime last 24 Hours
</div>
<div class="col-4">
<span class="lg_number">{{service.online_7_days}}%</span>
Uptime last 7 Days
</div>
</div>
</div> </div>
</div> </div>
@ -48,29 +35,26 @@
<script> <script>
import ServiceChart from "./ServiceChart"; import ServiceChart from "./ServiceChart";
import ServiceTopStats from "@/components/Service/ServiceTopStats";
export default { export default {
name: 'ServiceBlock', name: 'ServiceBlock',
components: {ServiceChart}, components: {ServiceTopStats, ServiceChart},
props: { props: {
service: { service: {
type: Object, type: Object,
required: true required: true
},
}, },
}, methods: {
methods: {
smallText(s) { smallText(s) {
if (s.online) { if (s.online) {
return `Online, last checked ${this.ago(s.last_success)}` return `Online, last checked ${this.ago(this.parseTime(s.last_success))}`
} else { } else {
return `Offline, last error: ${s.last_failure.issue} ${this.ago(s.last_failure.created_at)}` return `Offline, last error: ${s.last_failure.issue} ${this.ago(this.parseTime(s.last_failure.created_at))}`
} }
}, }
ago(t1) { }
const tm = this.parseTime(t1)
return this.duration(this.$moment().utc(), tm)
}
}
} }
</script> </script>

View File

@ -3,7 +3,7 @@
</template> </template>
<script> <script>
import Api from "../../components/API" import Api from "../../API"
const axisOptions = { const axisOptions = {
labels: { labels: {
@ -30,108 +30,108 @@
}; };
export default { export default {
name: 'ServiceChart', name: 'ServiceChart',
props: { props: {
service: { service: {
type: Object, type: Object,
required: true required: true
}
},
async created() {
await this.chartHits()
},
data () {
return {
ready: false,
data: [],
chartOptions: {
chart: {
height: 210,
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,
} }
}, },
xaxis: { async created() {
type: "datetime", await this.chartHits()
...axisOptions },
}, data() {
yaxis: { return {
...axisOptions ready: false,
}, data: [],
tooltip: { chartOptions: {
enabled: false, chart: {
marker: { height: 210,
show: false, width: "100%",
}, type: "area",
x: { animations: {
show: false, enabled: true,
} initialAnimation: {
}, enabled: true
legend: { }
show: false, },
}, selection: {
dataLabels: { enabled: false
enabled: false },
}, zoom: {
floating: true, enabled: false
axisTicks: { },
show: false toolbar: {
}, show: false
axisBorder: { },
show: false },
}, grid: {
fill: { show: false,
colors: [this.service.online ? "#48d338" : "#dd3545"], padding: {
opacity: 1, top: 0,
type: 'solid' right: 0,
}, bottom: 0,
stroke: { left: -10,
show: false, }
curve: 'smooth', },
lineCap: 'butt', xaxis: {
colors: [this.service.online ? "#3aa82d" : "#dd3545"], type: "datetime",
...axisOptions
},
yaxis: {
...axisOptions
},
tooltip: {
enabled: false,
marker: {
show: false,
},
x: {
show: false,
}
},
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: []
}]
} }
}, },
series: [{
data: []
}]
}
},
methods: { methods: {
async chartHits() { async chartHits() {
const start = this.ago((3600 * 24) * 7) const start = this.nowSubtract((3600 * 24) * 7)
this.data = await Api.service_hits(this.service.id, start, this.now(), "hour") this.data = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(new Date()), "hour")
this.series = [{ this.series = [{
name: this.service.name, name: this.service.name,
...this.data ...this.data
}] }]
this.ready = true this.ready = true
} }
}, }
} }
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -33,7 +33,7 @@
<script> <script>
import ServiceChart from "./ServiceChart"; import ServiceChart from "./ServiceChart";
import Api from "../API"; import Api from "../../API";
export default { export default {
name: 'ServiceFailures', name: 'ServiceFailures',

View File

@ -0,0 +1,84 @@
<template>
<apexchart v-if="ready" width="100%" height="300" type="heatmap" :options="chartOptions" :series="series"></apexchart>
</template>
<script>
import Api from "../../API"
export default {
name: 'ServiceHeatmap',
props: {
service: {
type: Object,
required: true
}
},
async created() {
await this.chartHeatmap()
},
data() {
return {
ready: false,
data: [],
chartOptions: {
chart: {
height: "100%",
width: "100%",
type: 'heatmap',
toolbar: {
show: false
}
},
dataLabels: {
enabled: false,
},
enableShades: true,
shadeIntensity: 0.5,
colors: ["#d53a3b"],
series: [{data: [{}]}],
yaxis: {
labels: {
formatter: (value) => {
return value
},
},
},
tooltip: {
enabled: true,
x: {
show: false,
},
y: {
formatter: function(val, opts) { return val+" Failures" },
title: {
formatter: (seriesName) => seriesName,
},
},
}
},
series: [{
data: []
}]
}
},
methods: {
async chartHeatmap() {
const start = this.nowSubtract((3600 * 24) * 7)
const data = await Api.service_heatmap(this.service.id, this.toUnix(start), this.toUnix(new Date()), "hour")
let dataArr = []
data.forEach(function(d) {
let date = new Date(d.date);
dataArr.push({name: date.toLocaleString('en-us', { month: 'long' }), data: d.data});
});
this.series = dataArr
this.ready = true
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -16,15 +16,16 @@
</div> </div>
</div> </div>
<span v-for="(failure, index) in failures" v-bind:key="index" class="alert alert-light"> <span v-for="(failure, index) in failures" v-bind:key="index" class="alert alert-light">
Failed {{duration(current(), failure.created_at)}}<br> Failed {{ago(parseTime(failure.created_at))}}<br>
{{failure.issue}} {{failure.issue}}
</span> </span>
</div> </div>
</template> </template>
<script> <script>
import ServiceSparkLine from "./ServiceSparkLine"; import ServiceSparkLine from "./ServiceSparkLine";
import Api from "../API"; import Api from "../../API";
export default { export default {
name: 'ServiceInfo', name: 'ServiceInfo',
@ -37,7 +38,7 @@
required: true required: true
} }
}, },
data () { data() {
return { return {
set1: [], set1: [],
set2: [], set2: [],
@ -47,7 +48,7 @@
failures: null failures: null
} }
}, },
async mounted () { async mounted() {
this.set1 = await this.getHits(24 * 2, "hour") this.set1 = await this.getHits(24 * 2, "hour")
this.set1_name = this.calc(this.set1) this.set1_name = this.calc(this.set1)
this.set2 = await this.getHits(24 * 7, "hour") this.set2 = await this.getHits(24 * 7, "hour")
@ -55,19 +56,19 @@
this.loaded = true this.loaded = true
}, },
methods: { methods: {
async getHits (hours, group) { async getHits(hours, group) {
const start = this.ago(3600 * hours) const start = this.nowSubtract(3600 * hours)
if (!this.service.online) { if (!this.service.online) {
this.failures = await Api.service_failures(this.service.id, this.now()-360, this.now(), 5) this.failures = await Api.service_failures(this.service.id, this.toUnix(start), this.toUnix(this.now()), 5)
return [ { name: "None", data: [] } ] return [{name: "None", data: []}]
} }
const data = await Api.service_hits(this.service.id, start, this.now(), group) const data = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(this.now()), group)
if (!data) { if (!data) {
return [ { name: "None", data: [] } ] return [{name: "None", data: []}]
} }
return [ { name: "Latency", data: data.data } ] return [{name: "Latency", data: data.data}]
}, },
calc (s) { calc(s) {
let data = s[0].data let data = s[0].data
if (data.length > 1) { if (data.length > 1) {
let total = 0 let total = 0
@ -75,7 +76,7 @@
total += f.y total += f.y
}); });
total = total / data.length total = total / data.length
return total.toFixed(0) + "ms Average" return total.toFixed(0) + "ms"
} else { } else {
return "Offline" return "Offline"
} }

View File

@ -0,0 +1,32 @@
<template>
<div class="row stats_area mt-5 mb-4">
<div class="col-4">
<span class="lg_number">{{service.avg_response}}ms</span>
Average Response
</div>
<div class="col-4">
<span class="lg_number">{{service.online_24_hours}}%</span>
Uptime last 24 Hours
</div>
<div class="col-4">
<span class="lg_number">{{service.online_7_days}}%</span>
Uptime last 7 Days
</div>
</div>
</template>
<script>
export default {
name: 'ServiceTopStats',
props: {
service: {
type: Object,
required: true
},
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -1,14 +0,0 @@
class Time {
now () {
return new Date();
}
utc () {
return new Date().getUTCDate();
}
utcToLocal (utc) {
let u = new Date().setUTCDate(utc)
return u.toLocaleString()
}
}
const time = new Time()
export default time

View File

@ -1,5 +1,5 @@
<template> <template>
<form @submit="saveCheckin"> <form @submit.prevent="saveCheckin">
<div class="form-group row"> <div class="form-group row">
<div class="col-md-3"> <div class="col-md-3">
<label for="checkin_interval" class="col-form-label">Checkin Name</label> <label for="checkin_interval" class="col-form-label">Checkin Name</label>
@ -14,46 +14,45 @@
<input v-model="checkin.grace" type="number" name="grace" class="form-control" id="grace_period" placeholder="10"> <input v-model="checkin.grace" type="number" name="grace" class="form-control" id="grace_period" placeholder="10">
</div> </div>
<div class="col-3"> <div class="col-3">
<button @click="saveCheckin" type="submit" id="submit" class="btn btn-success d-block" style="margin-top: 14px;">Save Checkin</button> <button @click.prevent="saveCheckin" type="submit" id="submit" class="btn btn-success d-block" style="margin-top: 14px;">Save Checkin</button>
</div> </div>
</div> </div>
</form> </form>
</template> </template>
<script> <script>
import Api from "../components/API"; import Api from "../API";
export default { export default {
name: 'Checkin', name: 'Checkin',
props: { props: {
service: { service: {
type: Object, type: Object,
required: true required: true
} }
}, },
data () { data() {
return { return {
checkin: { checkin: {
name: "", name: "",
interval: 60, interval: 60,
grace: 60, grace: 60,
service: this.service.id service: this.service.id
} }
} }
}, },
mounted() { mounted() {
}, },
methods: { methods: {
async saveCheckin(e) { async saveCheckin() {
e.preventDefault(); const data = {name: this.group.name, public: this.group.public}
const data = {name: this.group.name, public: this.group.public} await Api.group_create(data)
await Api.group_create(data) const groups = await Api.groups()
const groups = await Api.groups() this.$store.commit('setGroups', groups)
this.$store.commit('setGroups', groups) },
}, }
} }
}
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -1,5 +1,5 @@
<template> <template>
<form @submit="saveSettings"> <form @submit.prevent="saveSettings">
<div class="form-group"> <div class="form-group">
<label>Project Name</label> <label>Project Name</label>
<input v-model="core.name" type="text" class="form-control" placeholder="Great Uptime"> <input v-model="core.name" type="text" class="form-control" placeholder="Great Uptime">
@ -68,20 +68,9 @@
</select> </select>
</div> </div>
<div class="form-group"> <button @click.prevent="saveSettings" type="submit" class="btn btn-primary btn-block">Save Settings</button>
<div class="col-12">
<label class="d-none d-sm-block">Send Updates only</label>
<span class="switch">
<input v-model="core.update_notify" @change="core.update_notify = !!core.update_notify" type="checkbox" class="switch" id="switch-update_notify" v-bind:checked="core.update_notify">
<label for="switch-update_notify" class="mt-2 mt-sm-0"></label>
<small class="form-text text-muted">Enabling this will send only notifications when the status of a services changes.</small>
</span>
</div>
</div>
<button @click="saveSettings" type="submit" class="btn btn-primary btn-block">Save Settings</button> <div class="form-group row mt-5">
<div class="form-group row mt-3">
<label class="col-sm-3 col-form-label">API Key</label> <label class="col-sm-3 col-form-label">API Key</label>
<div class="col-sm-9"> <div class="col-sm-9">
<input v-model="core.api_key" @focus="$event.target.select()" type="text" class="form-control select-input" readonly> <input v-model="core.api_key" @focus="$event.target.select()" type="text" class="form-control select-input" readonly>
@ -102,43 +91,44 @@
</template> </template>
<script> <script>
import Api from '../components/API' import Api from '../API'
export default { export default {
name: 'CoreSettings', name: 'CoreSettings',
data () { data() {
return { return {
core: this.$store.getters.core, core: this.$store.getters.core,
} }
}, },
async mounted () { async mounted() {
}, },
methods: { methods: {
async saveSettings (e) { async saveSettings() {
e.preventDefault() const c = this.core
const c = this.core const coreForm = {
const coreForm = {name: c.name, description: c.description, domain: c.domain, name: c.name, description: c.description, domain: c.domain,
timezone: c.timezone, using_cdn: c.using_cdn, footer: c.footer, update_notify: c.update_notify} timezone: c.timezone, using_cdn: c.using_cdn, footer: c.footer, update_notify: c.update_notify
await Api.core_save(coreForm) }
const core = await Api.core() await Api.core_save(coreForm)
this.$store.commit('setCore', core) const core = await Api.core()
this.core = core this.$store.commit('setCore', core)
}, this.core = core
async renewApiKeys () { },
let r = confirm("Are you sure you want to reset the API keys?"); async renewApiKeys() {
if (r === true) { let r = confirm("Are you sure you want to reset the API keys?");
await Api.renewApiKeys() if (r === true) {
const core = await Api.core() await Api.renewApiKeys()
this.$store.commit('setCore', core) const core = await Api.core()
this.core = core this.$store.commit('setCore', core)
} this.core = core
}, }
selectAll() { },
this.$refs.input.select(); selectAll() {
this.$refs.input.select();
}
} }
} }
}
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -38,65 +38,65 @@
</template> </template>
<script> <script>
import Api from "../components/API"; import Api from "../API";
export default { export default {
name: 'FormGroup', name: 'FormGroup',
props: { props: {
in_group: { in_group: {
type: Object type: Object
}, },
edit: { edit: {
type: Function type: Function
} }
}, },
data () { data() {
return { return {
loading: false, loading: false,
group: { group: {
name: "", name: "",
public: true public: true
}
}
},
watch: {
in_group() {
this.group = this.in_group
}
},
methods: {
removeEdit() {
this.group = {}
this.edit(false)
},
async saveGroup(e) {
e.preventDefault();
this.loading = true
if (this.in_group) {
await this.updateGroup()
} else {
await this.createGroup()
}
this.loading = false
},
async createGroup() {
const g = this.group
const data = {name: g.name, public: g.public}
await Api.group_create(data)
const groups = await Api.groups()
this.$store.commit('setGroups', groups)
this.group = {}
},
async updateGroup() {
const g = this.group
const data = {id: g.id, name: g.name, public: g.public}
await Api.group_update(data)
const groups = await Api.groups()
this.$store.commit('setGroups', groups)
this.edit(false)
}
} }
}
},
watch: {
in_group() {
this.group = this.in_group
}
},
methods: {
removeEdit() {
this.group = {}
this.edit(false)
},
async saveGroup(e) {
e.preventDefault();
this.loading = true
if (this.in_group) {
await this.updateGroup()
} else {
await this.createGroup()
}
this.loading = false
},
async createGroup() {
const g = this.group
const data = {name: g.name, public: g.public}
await Api.group_create(data)
const groups = await Api.groups()
this.$store.commit('setGroups', groups)
this.group = {}
},
async updateGroup() {
const g = this.group
const data = {id: g.id, name: g.name, public: g.public}
await Api.group_update(data)
const groups = await Api.groups()
this.$store.commit('setGroups', groups)
this.edit(false)
}
} }
}
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -59,45 +59,50 @@
</template> </template>
<script> <script>
import Api from "../components/API"; import Api from "../API";
export default { export default {
name: 'FormIntegration', name: 'FormIntegration',
props: { props: {
integration: { integration: {
type: Object type: Object
} }
},
data () {
return {
out: {},
services: []
}
},
watch: {
},
methods: {
async addService(s) {
const data = {name: s.name, type: s.type, domain: s.domain, port: s.port, check_interval: s.check_interval, timeout: s.timeout}
const out = await Api.service_create(data)
const services = await Api.services()
this.$store.commit('setServices', services)
s.added = true
}, },
async updateIntegration() { data() {
const i = this.integration return {
const data = {name: i.name, enabled: i.enabled, fields: i.fields} out: {},
this.out = data services: []
const out = await Api.integration_save(data) }
if (out != null) { },
this.services = out watch: {},
} methods: {
const integrations = await Api.integrations() async addService(s) {
this.$store.commit('setIntegrations', integrations) const data = {
} name: s.name,
type: s.type,
domain: s.domain,
port: s.port,
check_interval: s.check_interval,
timeout: s.timeout
}
const out = await Api.service_create(data)
const services = await Api.services()
this.$store.commit('setServices', services)
s.added = true
},
async updateIntegration() {
const i = this.integration
const data = {name: i.name, enabled: i.enabled, fields: i.fields}
this.out = data
const out = await Api.integration_save(data)
if (out != null) {
this.services = out
}
const integrations = await Api.integrations()
this.$store.commit('setIntegrations', integrations)
}
}
} }
}
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -26,43 +26,43 @@
</template> </template>
<script> <script>
import Api from "../components/API"; import Api from "../API";
export default { export default {
name: 'FormLogin', name: 'FormLogin',
data () { data() {
return { return {
username: "", username: "",
password: "", password: "",
auth: {}, auth: {},
loading: false, loading: false,
error: false, error: false,
disabled: true disabled: true
}
},
methods: {
checkForm() {
if (!this.username || !this.password) {
this.disabled = true
} else {
this.disabled = false
} }
}, },
async login () { methods: {
this.loading = true checkForm() {
this.error = false if (!this.username || !this.password) {
const auth = await Api.login(this.username, this.password) this.disabled = true
if (auth.error) { } else {
this.error = true this.disabled = false
} else if (auth.token) { }
this.auth = Api.saveToken(this.username, auth.token) },
await this.$store.dispatch('loadAdmin') async login() {
this.$router.push('/dashboard') this.loading = true
this.error = false
const auth = await Api.login(this.username, this.password)
if (auth.error) {
this.error = true
} else if (auth.token) {
this.auth = Api.saveToken(this.username, auth.token)
await this.$store.dispatch('loadAdmin')
this.$router.push('/dashboard')
}
this.loading = false
} }
this.loading = false
} }
} }
}
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -24,10 +24,10 @@
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-4 col-form-label">Message Date Range</label> <label class="col-sm-4 col-form-label">Message Date Range</label>
<div class="col-sm-4"> <div class="col-sm-4">
<flatPickr v-model="message.start_on" :config="config" type="text" name="start_on" class="form-control form-control-plaintext" id="start_on" value="0001-01-01T00:00:00Z" required /> <flatPickr v-model="message.start_on" @on-change="startChange" :config="config" type="text" name="start_on" class="form-control form-control-plaintext" id="start_on" value="0001-01-01T00:00:00Z" required />
</div> </div>
<div class="col-sm-4"> <div class="col-sm-4">
<flatPickr v-model="message.end_on" :config="config" type="text" name="end_on" class="form-control form-control-plaintext" id="end_on" value="0001-01-01T00:00:00Z" required /> <flatPickr v-model="message.end_on" @on-change="endChange" :config="config" type="text" name="end_on" class="form-control form-control-plaintext" id="end_on" value="0001-01-01T00:00:00Z" required />
</div> </div>
</div> </div>
@ -90,7 +90,7 @@
</template> </template>
<script> <script>
import Api from "../components/API"; import Api from "../API";
import flatPickr from 'vue-flatpickr-component'; import flatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css'; import 'flatpickr/dist/flatpickr.css';
@ -135,6 +135,12 @@
} }
}, },
methods: { methods: {
startChange(e) {
window.console.log(e)
},
endChange(e) {
window.console.log(e)
},
removeEdit() { removeEdit() {
this.message = {} this.message = {}
this.edit(false) this.edit(false)

View File

@ -1,5 +1,5 @@
<template> <template>
<form @submit="saveNotifier"> <form @submit.prevent="saveNotifier">
<div v-if="error" class="alert alert-danger col-12" role="alert">{{error}}</div> <div v-if="error" class="alert alert-danger col-12" role="alert">{{error}}</div>
@ -38,13 +38,13 @@
</div> </div>
<div class="col-12 col-sm-4 mb-2 mb-sm-0 mt-2 mt-sm-0"> <div class="col-12 col-sm-4 mb-2 mb-sm-0 mt-2 mt-sm-0">
<button @click="saveNotifier" type="submit" class="btn btn-block text-capitalize" :class="{'btn-primary': !saved, 'btn-success': saved}"> <button @click.prevent="saveNotifier" type="submit" class="btn btn-block text-capitalize" :class="{'btn-primary': !saved, 'btn-success': saved}">
<i class="fa fa-check-circle"></i> {{loading ? "Loading..." : saved ? "Saved" : "Save"}} <i class="fa fa-check-circle"></i> {{loading ? "Loading..." : saved ? "Saved" : "Save"}}
</button> </button>
</div> </div>
<div class="col-12 col-sm-12 mt-3"> <div class="col-12 col-sm-12 mt-3">
<button @click="testNotifier" class="btn btn-secondary btn-block text-capitalize col-12 float-right"><i class="fa fa-vial"></i> <button @click.prevent="testNotifier" class="btn btn-secondary btn-block text-capitalize col-12 float-right"><i class="fa fa-vial"></i>
{{loading ? "Loading..." : "Test Notifier"}}</button> {{loading ? "Loading..." : "Test Notifier"}}</button>
</div> </div>
@ -57,67 +57,65 @@
</template> </template>
<script> <script>
import Api from "../components/API"; import Api from "../API";
export default { export default {
name: 'Notifier', name: 'Notifier',
props: { props: {
notifier: { notifier: {
type: Object, type: Object,
required: true required: true
} }
}, },
data () { data() {
return { return {
loading: false, loading: false,
error: null, error: null,
saved: false, saved: false,
ok: false, ok: false,
} }
}, },
mounted() { mounted() {
},
methods: {
async saveNotifier(e) {
e.preventDefault();
this.loading = true
let form = {}
this.notifier.form.forEach((f) => {
form[f.field] = this.notifier[f.field]
});
form.enabled = this.notifier.enabled
form.limits = parseInt(this.notifier.limits)
form.method = this.notifier.method
await Api.notifier_save(form)
const notifiers = await Api.notifiers()
this.$store.commit('setNotifiers', notifiers)
this.saved = true
this.loading = false
setTimeout(() => {
this.saved = false
}, 2000)
}, },
async testNotifier(e) { methods: {
e.preventDefault(); async saveNotifier() {
this.ok = false this.loading = true
this.loading = true let form = {}
let form = {} this.notifier.form.forEach((f) => {
this.notifier.form.forEach((f) => { form[f.field] = this.notifier[f.field]
form[f.field] = this.notifier[f.field] });
}); form.enabled = this.notifier.enabled
form.enabled = this.notifier.enabled form.limits = parseInt(this.notifier.limits)
form.limits = parseInt(this.notifier.limits) form.method = this.notifier.method
form.method = this.notifier.method await Api.notifier_save(form)
const tested = await Api.notifier_test(form) const notifiers = await Api.notifiers()
if (tested === 'ok') { await this.$store.commit('setNotifiers', notifiers)
this.ok = true this.saved = true
} else { this.loading = false
this.error = tested setTimeout(() => {
} this.saved = false
this.loading = false }, 2000)
}, },
} async testNotifier() {
this.ok = false
this.loading = true
let form = {}
this.notifier.form.forEach((f) => {
form[f.field] = this.notifier[f.field]
});
form.enabled = this.notifier.enabled
form.limits = parseInt(this.notifier.limits)
form.method = this.notifier.method
const tested = await Api.notifier_test(form)
if (tested === 'ok') {
this.ok = true
} else {
this.error = tested
}
this.loading = false
},
}
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<form @submit="saveService"> <form @submit.prevent="saveService">
<h4 class="mb-5 text-muted">Basic Information</h4> <h4 class="mb-5 text-muted">Basic Information</h4>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-4 col-form-label">Service Name</label> <label class="col-sm-4 col-form-label">Service Name</label>
@ -12,10 +12,10 @@
<label for="service_type" class="col-sm-4 col-form-label">Service Type</label> <label for="service_type" class="col-sm-4 col-form-label">Service Type</label>
<div class="col-sm-8"> <div class="col-sm-8">
<select v-model="service.type" class="form-control" id="service_type" > <select v-model="service.type" class="form-control" id="service_type" >
<option value="http" >HTTP Service</option> <option value="http">HTTP Service</option>
<option value="tcp" >TCP Service</option> <option value="tcp">TCP Service</option>
<option value="udp" >UDP Service</option> <option value="udp">UDP Service</option>
<option value="icmp" >ICMP Ping</option> <option value="icmp">ICMP Ping</option>
</select> </select>
<small class="form-text text-muted">Use HTTP if you are checking a website or use TCP if you are checking a server</small> <small class="form-text text-muted">Use HTTP if you are checking a website or use TCP if you are checking a server</small>
</div> </div>
@ -111,15 +111,8 @@
<small class="form-text text-muted">Use text for the service URL rather than the service number.</small> <small class="form-text text-muted">Use text for the service URL rather than the service number.</small>
</div> </div>
</div> </div>
<div class="form-group row">
<label for="order" class="col-sm-4 col-form-label">List Order</label>
<div class="col-sm-8">
<input v-model="service.order" type="number" name="order" class="form-control" min="0" id="order">
<small class="form-text text-muted">You can also drag and drop services to reorder on the Services tab.</small>
</div>
</div>
<div v-if="service.type.match(/^(http)$/)" class="form-group row"> <div v-if="service.type.match(/^(http)$/)" class="form-group row">
<label for="order" class="col-sm-4 col-form-label">Verify SSL</label> <label class="col-sm-4 col-form-label">Verify SSL</label>
<div class="col-8 mt-1"> <div class="col-8 mt-1">
<span @click="service.verify_ssl = !!service.verify_ssl" class="switch float-left"> <span @click="service.verify_ssl = !!service.verify_ssl" class="switch float-left">
<input v-model="service.verify_ssl" type="checkbox" name="verify_ssl-option" class="switch" id="switch-verify-ssl" v-bind:checked="service.verify_ssl"> <input v-model="service.verify_ssl" type="checkbox" name="verify_ssl-option" class="switch" id="switch-verify-ssl" v-bind:checked="service.verify_ssl">
@ -128,7 +121,7 @@
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="order" class="col-sm-4 col-form-label">Notifications</label> <label class="col-sm-4 col-form-label">Notifications</label>
<div class="col-8 mt-1"> <div class="col-8 mt-1">
<span @click="service.allow_notifications = !!service.allow_notifications" class="switch float-left"> <span @click="service.allow_notifications = !!service.allow_notifications" class="switch float-left">
<input v-model="service.allow_notifications" type="checkbox" name="allow_notifications-option" class="switch" id="switch-notifications" v-bind:checked="service.allow_notifications"> <input v-model="service.allow_notifications" type="checkbox" name="allow_notifications-option" class="switch" id="switch-notifications" v-bind:checked="service.allow_notifications">
@ -136,8 +129,17 @@
</span> </span>
</div> </div>
</div> </div>
<div v-if="service.allow_notifications" class="form-group row">
<label class="col-sm-4 col-form-label">Notify All Changes</label>
<div class="col-8 mt-1">
<span @click="service.notify_all_changes = !!service.notify_all_changes" class="switch float-left">
<input v-model="service.notify_all_changes" type="checkbox" name="notify_all-option" class="switch" id="notify_all" v-bind:checked="service.notify_all_changes">
<label for="notify_all">Continuously notify when service is failing.</label>
</span>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<label for="order" class="col-sm-4 col-form-label">Visible</label> <label class="col-sm-4 col-form-label">Visible</label>
<div class="col-8 mt-1"> <div class="col-8 mt-1">
<span @click="service.public = !!service.public" class="switch float-left"> <span @click="service.public = !!service.public" class="switch float-left">
<input v-model="service.public" type="checkbox" name="public-option" class="switch" id="switch-public" v-bind:checked="service.public"> <input v-model="service.public" type="checkbox" name="public-option" class="switch" id="switch-public" v-bind:checked="service.public">
@ -157,7 +159,7 @@
</template> </template>
<script> <script>
import Api from "../components/API"; import Api from "../API";
export default { export default {
name: 'FormService', name: 'FormService',
@ -181,6 +183,7 @@
order: 1, order: 1,
verify_ssl: true, verify_ssl: true,
allow_notifications: true, allow_notifications: true,
notify_all_changes: true,
public: true, public: true,
}, },
groups: [], groups: [],
@ -203,8 +206,7 @@
} }
}, },
methods: { methods: {
async saveService(e) { async saveService() {
e.preventDefault()
let s = this.service let s = this.service
delete s.failures delete s.failures
delete s.created_at delete s.created_at

View File

@ -1,7 +1,7 @@
<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-2 sm-container">
<div class="col-12 col-md-8 offset-md-2 mb-4"> <div class="col-12 col-md-8 offset-md-2 mb-4">
<img class="col-12 mt-5 mt-md-0" src="/public/banner.png"> <img class="col-12 mt-5 mt-md-0" src="/banner.png">
</div> </div>
<div class="col-12"> <div class="col-12">
@ -96,7 +96,7 @@
</template> </template>
<script> <script>
import Api from "../components/API"; import Api from "../API";
import Index from "../pages/Index"; import Index from "../pages/Index";
export default { export default {

View File

@ -5,39 +5,39 @@
<script> <script>
export default { export default {
name: 'ToggleSwitch', name: 'ToggleSwitch',
props: { props: {
service: { service: {
type: Object, type: Object,
required: true required: true
} }
}, },
data () { data() {
return { return {
icon: "toggle-on", icon: "toggle-on",
running: true running: true
} }
}, },
mounted() { mounted() {
if (this.service.online) { if (this.service.online) {
this.running = true this.running = true
this.icon = "toggle-on" this.icon = "toggle-on"
} else { } else {
this.running = false this.running = false
this.icon = "toggle-off" this.icon = "toggle-off"
} }
}, },
methods: { methods: {
toggleChecking() { toggleChecking() {
if (this.running) { if (this.running) {
this.icon = "toggle-off" this.icon = "toggle-off"
} else { } else {
this.icon = "toggle-on" this.icon = "toggle-on"
}
this.running = !this.running
},
} }
this.running = !this.running
},
} }
}
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -55,7 +55,7 @@
</template> </template>
<script> <script>
import Api from "../components/API"; import Api from "../API";
export default { export default {
name: 'FormUser', name: 'FormUser',

View File

@ -12,7 +12,6 @@ import "./icons"
Vue.component('apexchart', VueApexCharts) Vue.component('apexchart', VueApexCharts)
Vue.use(VueRouter); Vue.use(VueRouter);
Vue.use(require('vue-moment'));
Vue.config.productionTip = false Vue.config.productionTip = false
new Vue({ new Vue({

View File

@ -1,45 +1,51 @@
import Vue from "vue"; import Vue from "vue";
const { zonedTimeToUtc, utcToZonedTime, subSeconds, parse, parseISO, getUnixTime, fromUnixTime, format, differenceInSeconds, formatDistanceToNow, formatDistance } = require('date-fns')
export default Vue.mixin({ export default Vue.mixin({
methods: { methods: {
now() { now() {
return Math.round(new Date().getTime() / 1000) return new Date()
}, },
current() { current() {
return new Date() return parse(new Date())
}, },
ago(seconds) { utc(val) {
return this.now() - seconds return fromUnixTime(this.toUnix(val) + val.getTimezoneOffset() * 60 * 1000)
},
ago(t1) {
return formatDistanceToNow(t1)
},
nowSubtract(seconds) {
return subSeconds(new Date(), seconds)
}, },
duration(t1, t2) { duration(t1, t2) {
const val = (this.toUnix(t1) - this.toUnix(t2)) return formatDistance(t1, t2)
if (val <= 59) {
return this.$moment.duration(val, 'seconds').get('seconds') + " seconds ago"
}
return this.$moment.duration(val, 'seconds').humanize();
}, },
niceDate(val) { niceDate(val) {
return this.parseTime(val).format('LLLL') return this.parseTime(val).format('LLLL')
}, },
parseTime(val) { parseTime(val) {
return this.$moment(val, this.$moment.ISO_8601, true) return parseISO(val)
}, },
toLocal(val, suf='at') { toLocal(val, suf = 'at') {
return this.parseTime(val).local().format(`dddd, MMM Do \\${suf} h:mma`) const t = this.parseTime(val)
return format(t, `EEEE, MMM do h:mma`)
},
toUnix(val) {
return getUnixTime(val)
}, },
toUnix(val) {
return this.$moment(val).utc().unix().valueOf()
},
fromUnix(val) { fromUnix(val) {
return this.$moment.unix(val).utc() return fromUnixTime(val)
},
isBetween(t1, t2) {
return differenceInSeconds(parseISO(t1), parseISO(t2)) > 0
},
hour() {
return 3600
},
day() {
return 3600 * 24
}, },
isBetween(t1, t2) {
const now = this.$moment(t1).utc().valueOf()
const sub = this.$moment(t2).utc().valueOf()
return (now - sub) > 0
},
hour(){ return 3600 },
day() { return 3600 * 24 },
serviceLink(service) { serviceLink(service) {
if (!service) { if (!service) {
return "" return ""
@ -53,10 +59,10 @@ export default Vue.mixin({
isInt(n) { isInt(n) {
return n % 1 === 0; return n % 1 === 0;
}, },
loggedIn() { loggedIn() {
const core = this.$store.getters.core const core = this.$store.getters.core
return core.logged_in === true return core.logged_in === true
}, },
iconName(name) { iconName(name) {
switch (name) { switch (name) {
case "fas fa-terminal": case "fas fa-terminal":

View File

@ -6,7 +6,7 @@
</template> </template>
<script> <script>
import Api from "../components/API" import Api from "../API"
import TopNav from "../components/Dashboard/TopNav"; import TopNav from "../components/Dashboard/TopNav";
export default { export default {

View File

@ -22,7 +22,7 @@
</template> </template>
<script> <script>
import Api from '../components/API'; import Api from '../API';
import Group from '../components/Index/Group'; import Group from '../components/Index/Group';
import Header from '../components/Index/Header'; import Header from '../components/Index/Header';
import MessageBlock from '../components/Index/MessageBlock'; import MessageBlock from '../components/Index/MessageBlock';
@ -30,36 +30,36 @@ import ServiceBlock from '../components/Service/ServiceBlock';
export default { export default {
name: 'Index', name: 'Index',
components: { components: {
ServiceBlock, ServiceBlock,
MessageBlock, MessageBlock,
Group, Group,
Header Header
}, },
data () { data() {
return { return {
logged_in: false logged_in: false
} }
}, },
async created() { async created() {
this.logged_in = this.loggedIn() this.logged_in = this.loggedIn()
}, },
async mounted() { async mounted() {
}, },
methods: { methods: {
inRange(message) { inRange(message) {
const start = this.isBetween(new Date(), message.start_on) const start = this.isBetween(new Date(), message.start_on)
const end = this.isBetween(message.end_on, new Date()) const end = this.isBetween(message.end_on, new Date())
return start && end return start && end
}, },
clickService(s) { clickService(s) {
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.s.scrollTop = 0; this.$refs.s.scrollTop = 0;
}); });
}
} }
}
} }
</script> </script>

View File

@ -12,7 +12,7 @@
</template> </template>
<script> <script>
import Api from "../components/API"; import Api from "../API";
import FormLogin from '../forms/Login'; import FormLogin from '../forms/Login';
export default { export default {

View File

@ -3,61 +3,55 @@
<p v-if="logs.length === 0" class="text-monospace sm"> <p v-if="logs.length === 0" class="text-monospace sm">
Loading Logs... Loading Logs...
</p> </p>
<p v-for="(log, index) in logs.reverse()" class="text-monospace sm">{{log}}</p> <p v-for="(log, index) in logs" class="text-monospace sm">{{log}}</p>
</div> </div>
</template> </template>
<script> <script>
import Api from "../components/API"; import Api from "../API";
export default { export default {
name: 'Logs', name: 'Logs',
components: { components: {},
data() {
}, return {
data () { logs: [],
return { last: "",
logs: [], t: null
last: "", }
t: null },
} async created() {
}, await this.getLogs()
created() { if (!this.t) {
if (!this.t) { this.t = setInterval(async () => {
this.t = setInterval(() => { await this.lastLog()
this.lastLog() }, 650)
}, 650) }
} },
},
async mounted() {
await this.getLogs()
},
beforeDestroy() { beforeDestroy() {
clearInterval(this.t) clearInterval(this.t)
}, },
methods: { methods: {
cleanLog(l) { cleanLog(l) {
const splitLog = l.split(": ") const splitLog = l.split(": ")
const last = splitLog.slice(1); const last = splitLog.slice(1);
return last.join(": ") return last.join(": ")
}, },
async getLogs() { async getLogs() {
const logs = await Api.logs() const logs = await Api.logs()
this.logs = logs.reverse() const l = logs.reverse()
this.last = this.cleanLog(this.logs[this.logs.length-1]) this.last = this.cleanLog(l[l.length - 1])
}, this.logs = l
async lastLog() { },
const log = await Api.logs_last() async lastLog() {
const cleanLast = this.cleanLog(log) const log = await Api.logs_last()
const cleanLast = this.cleanLog(log)
if (this.last !== cleanLast) { if (this.last !== cleanLast) {
this.last = cleanLast this.last = cleanLast
this.logs.reverse().push(log) this.logs.reverse().push(log)
} }
}
} }
}
} }
</script> </script>

View File

@ -7,50 +7,37 @@
{{service.online ? "ONLINE" : "OFFLINE"}} {{service.online ? "ONLINE" : "OFFLINE"}}
</span> </span>
<h4 class="mt-2"><router-link to="/">{{$store.getters.core.name}}</router-link> - {{service.name}} <h4 class="mt-2">
<router-link to="/">{{$store.getters.core.name}}</router-link> - {{service.name}}
<span class="badge float-right d-none d-md-block" :class="{'bg-success': service.online, 'bg-danger': !service.online}"> <span class="badge float-right d-none d-md-block" :class="{'bg-success': service.online, 'bg-danger': !service.online}">
{{service.online ? "ONLINE" : "OFFLINE"}} {{service.online ? "ONLINE" : "OFFLINE"}}
</span> </span>
</h4> </h4>
<div class="row stats_area mt-5 mb-5"> <ServiceTopStats :service="service"/>
<div class="col-4">
<span class="lg_number">{{service.online_24_hours}}%</span>
Online last 24 Hours
</div>
<div class="col-4">
<span class="lg_number">31ms</span>
Average Response
</div>
<div class="col-4">
<span class="lg_number">85.70%</span>
Total Uptime
</div>
</div>
<div v-for="(message, index) in messages" v-if="messageInRange(message)"> <div v-for="(message, index) in messages" v-if="messageInRange(message)">
<MessageBlock :message="message"/> <MessageBlock :message="message"/>
</div> </div>
<div class="row mt-5 mb-4">
<span class="col-6 font-2">
<flatPickr v-model="start_time" type="text" name="start_time" class="form-control form-control-plaintext" id="start_time" value="0001-01-01T00:00:00Z" required />
</span>
<span class="col-6 font-2">
<flatPickr v-model="end_time" type="text" name="end_time" class="form-control form-control-plaintext" id="end_time" value="0001-01-01T00:00:00Z" required />
</span>
</div>
<div v-if="series" class="service-chart-container"> <div v-if="series" class="service-chart-container">
<apexchart width="100%" height="420" type="area" :options="chartOptions" :series="series"></apexchart> <apexchart width="100%" height="420" type="area" :options="chartOptions" :series="series"></apexchart>
</div> </div>
<div v-if="series" class="service-chart-heatmap"> <div class="service-chart-heatmap mt-3 mb-4">
<apexchart width="100%" height="215" type="heatmap" :options="chartOptions" :series="series"></apexchart> <ServiceHeatmap :service="service"/>
</div> </div>
<form id="service_date_form" class="col-12 mt-2 mb-3"> <nav v-if="service.failures" class="nav nav-pills flex-column flex-sm-row mt-3" id="service_tabs">
<input type="text" class="d-none" name="start" id="service_start" data-input>
<span data-toggle title="toggle" id="start_date" class="text-muted small float-left pointer mt-2">Thu, 09 Jan 2020 to Thu, 16 Jan 2020</span>
<button type="submit" class="btn btn-light btn-sm mt-2">Set Timeframe</button>
<input type="text" class="d-none" name="end" id="service_end" data-input>
<div id="start_container"></div>
<div id="end_container"></div>
</form>
<nav class="nav nav-pills flex-column flex-sm-row mt-3" id="service_tabs">
<a @click="tab='failures'" class="flex-sm-fill text-sm-center nav-link active">Failures</a> <a @click="tab='failures'" class="flex-sm-fill text-sm-center nav-link active">Failures</a>
<a @click="tab='incidents'" class="flex-sm-fill text-sm-center nav-link">Incidents</a> <a @click="tab='incidents'" class="flex-sm-fill text-sm-center nav-link">Incidents</a>
<a @click="tab='checkins'" v-if="$store.getters.token" class="flex-sm-fill text-sm-center nav-link">Checkins</a> <a @click="tab='checkins'" v-if="$store.getters.token" class="flex-sm-fill text-sm-center nav-link">Checkins</a>
@ -58,7 +45,7 @@
</nav> </nav>
<div class="tab-content"> <div v-if="service.failures" class="tab-content">
<div class="tab-pane fade active show"> <div class="tab-pane fade active show">
<ServiceFailures :service="service"/> <ServiceFailures :service="service"/>
</div> </div>
@ -98,10 +85,14 @@
</template> </template>
<script> <script>
import Api from "../components/API" import Api from "../API"
import MessageBlock from '../components/Index/MessageBlock'; import MessageBlock from '../components/Index/MessageBlock';
import ServiceFailures from '../components/Service/ServiceFailures'; import ServiceFailures from '../components/Service/ServiceFailures';
import Checkin from "../forms/Checkin"; import Checkin from "../forms/Checkin";
import ServiceHeatmap from "@/components/Service/ServiceHeatmap";
import ServiceTopStats from "@/components/Service/ServiceTopStats";
import flatPickr from 'vue-flatpickr-component';
import 'flatpickr/dist/flatpickr.css';
const axisOptions = { const axisOptions = {
labels: { labels: {
@ -128,143 +119,145 @@
}; };
export default { export default {
name: 'Service', name: 'Service',
components: { components: {
ServiceFailures, ServiceTopStats,
MessageBlock, ServiceHeatmap,
Checkin ServiceFailures,
}, MessageBlock,
data () { Checkin,
return { flatPickr
id: null, },
tab: "failures", data() {
service: {}, return {
authenticated: false, id: null,
ready: false, tab: "failures",
data: null, service: {},
messages: [], authenticated: false,
failures: [], ready: false,
chartOptions: { data: null,
chart: { messages: [],
height: 500, failures: [],
width: "100%", start_time: "",
type: "area", end_time: "",
animations: { chartOptions: {
enabled: true, chart: {
initialAnimation: { height: 500,
enabled: true width: "100%",
} type: "area",
}, animations: {
selection: { enabled: true,
enabled: false initialAnimation: {
}, enabled: true
zoom: { }
enabled: false },
}, selection: {
toolbar: { enabled: false
show: false },
}, zoom: {
}, enabled: false
grid: { },
show: true, toolbar: {
padding: { show: false
top: 0, },
right: 0, },
bottom: 0, grid: {
left: 0, show: true,
} padding: {
}, top: 0,
xaxis: { right: 0,
type: "datetime", bottom: 0,
...axisOptions left: 0,
}, }
yaxis: { },
...axisOptions xaxis: {
}, type: "datetime",
tooltip: { ...axisOptions
enabled: false, },
marker: { yaxis: {
show: false, ...axisOptions
}, },
x: { tooltip: {
show: false, enabled: false,
} marker: {
}, show: false,
legend: { },
show: false, x: {
}, show: false,
dataLabels: { }
enabled: false },
}, legend: {
floating: true, show: false,
axisTicks: { },
show: false dataLabels: {
}, enabled: false
axisBorder: { },
show: false floating: true,
}, axisTicks: {
fill: { show: false
colors: ["#48d338"], },
opacity: 1, axisBorder: {
type: 'solid' show: false
}, },
stroke: { fill: {
show: true, colors: ["#48d338"],
curve: 'smooth', opacity: 1,
lineCap: 'butt', type: 'solid'
colors: ["#3aa82d"], },
stroke: {
show: true,
curve: 'smooth',
lineCap: 'butt',
colors: ["#3aa82d"],
}
},
series: [{
data: []
}],
heatmap_data: [],
config: {
enableTime: true
},
} }
}, },
series: [{
data: []
}],
heatmap_data: []
}
},
async mounted() { async mounted() {
const id = this.$attrs.id const id = this.$attrs.id
this.id = id this.id = id
let service; let service;
if (this.isInt(id)) { if (this.isInt(id)) {
service = this.$store.getters.serviceById(id) service = this.$store.getters.serviceById(id)
} else { } else {
service = this.$store.getters.serviceByPermalink(id) service = this.$store.getters.serviceByPermalink(id)
} }
this.service = service this.service = service
this.getService(service) this.getService(service)
this.messages = this.$store.getters.serviceMessages(service.id) this.messages = this.$store.getters.serviceMessages(service.id)
}, },
methods: { methods: {
messageInRange(message) { messageInRange(message) {
const start = this.isBetween(new Date(), message.start_on) const start = this.isBetween(new Date(), message.start_on)
const end = this.isBetween(message.end_on, new Date()) const end = this.isBetween(message.end_on, new Date())
return start && end return start && end
}, },
async getService(s) { async getService(s) {
await this.chartHits() await this.chartHits()
await this.heatmapData() await this.serviceFailures()
await this.serviceFailures() },
}, async serviceFailures() {
async serviceFailures() { this.failures = await Api.service_failures(this.service.id, this.now() - 3600, this.now(), 15)
this.failures = await Api.service_failures(this.service.id, this.now() - 3600, this.now(), 15) },
}, async chartHits() {
async chartHits() { const start = this.nowSubtract((3600 * 24) * 7)
this.data = await Api.service_hits(this.service.id, 0, 99999999999, "hour") this.start_time = start
this.series = [{ this.end_time = new Date()
name: this.service.name, this.data = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(new Date()), "hour")
...this.data this.series = [{
}] name: this.service.name,
this.ready = true ...this.data
}, }]
async heatmapData() { this.ready = true
this.data = await Api.service_heatmap(this.service.id, 0, 99999999999, "hour") }
this.series = [{ }
name: this.service.name,
...this.data
}]
this.ready = true
}
}
} }
</script> </script>

View File

@ -42,7 +42,7 @@
<div class="row align-content-center"> <div class="row align-content-center">
<img class="rounded text-center" width="300" height="300" :src="qrcode"> <img class="rounded text-center" width="300" height="300" :src="qrcode">
</div> </div>
<a class="btn btn-sm btn-primary" href=statping://setup?domain&#61;https://demo.statping.com&amp;api&#61;6b05b48f4b3a1460f3864c31b26cab6a27dbaff9>Open in Statping App</a> <a class="btn btn-sm btn-primary" :href="qrurl">Open in Statping App</a>
<a href="settings/export" class="btn btn-sm btn-secondary">Export Settings</a> <a href="settings/export" class="btn btn-sm btn-secondary">Export Settings</a>
</div> </div>
</div> </div>
@ -53,32 +53,11 @@
</div> </div>
<div class="tab-pane fade" v-bind:class="{active: liClass('v-pills-style-tab'), show: liClass('v-pills-style-tab')}" id="v-pills-style" role="tabpanel" aria-labelledby="v-pills-style-tab"> <div class="tab-pane fade" v-bind:class="{active: liClass('v-pills-style-tab'), show: liClass('v-pills-style-tab')}" id="v-pills-style" role="tabpanel" aria-labelledby="v-pills-style-tab">
<ThemeEditor :core="core"/> <ThemeEditor :core="core"/>
</div> </div>
<div class="tab-pane fade" v-bind:class="{active: liClass('v-pills-cache-tab'), show: liClass('v-pills-cache-tab')}" id="v-pills-cache" role="tabpanel" aria-labelledby="v-pills-cache-tab"> <div class="tab-pane fade" v-bind:class="{active: liClass('v-pills-cache-tab'), show: liClass('v-pills-cache-tab')}" id="v-pills-cache" role="tabpanel" aria-labelledby="v-pills-cache-tab">
<h3>Cache</h3> <Cache/>
<table class="table">
<thead>
<tr>
<th scope="col">URL</th>
<th scope="col">Size</th>
<th scope="col">Expiration</th>
</tr>
</thead>
<tbody>
<tr v-for="(cache, index) in cache">
<td>{{cache.url}}</td>
<td>{{cache.size}}</td>
<td>{{expireTime(cache.expiration)}}</td>
</tr>
</tbody>
</table>
<a @click.prevent="clearCache" href="#" class="btn btn-danger btn-block">Clear Cache</a>
</div> </div>
<div v-for="(notifier, index) in $store.getters.notifiers" v-bind:key="`${notifier.title}_${index}`" class="tab-pane fade" v-bind:class="{active: liClass(`v-pills-${notifier.method.toLowerCase()}-tab`), show: liClass(`v-pills-${notifier.method.toLowerCase()}-tab`)}" v-bind:id="`v-pills-${notifier.method.toLowerCase()}-tab`" role="tabpanel" v-bind:aria-labelledby="`v-pills-${notifier.method.toLowerCase()}-tab`"> <div v-for="(notifier, index) in $store.getters.notifiers" v-bind:key="`${notifier.title}_${index}`" class="tab-pane fade" v-bind:class="{active: liClass(`v-pills-${notifier.method.toLowerCase()}-tab`), show: liClass(`v-pills-${notifier.method.toLowerCase()}-tab`)}" v-bind:id="`v-pills-${notifier.method.toLowerCase()}-tab`" role="tabpanel" v-bind:aria-labelledby="`v-pills-${notifier.method.toLowerCase()}-tab`">
@ -97,54 +76,50 @@
</template> </template>
<script> <script>
import Api from '../components/API'; import Api from '../API';
import CoreSettings from '../forms/CoreSettings'; import CoreSettings from '../forms/CoreSettings';
import FormIntegration from '../forms/Integration'; import FormIntegration from '../forms/Integration';
import Notifier from "../forms/Notifier"; import Notifier from "../forms/Notifier";
import ThemeEditor from "../components/Dashboard/ThemeEditor"; import ThemeEditor from "../components/Dashboard/ThemeEditor";
import Cache from "@/components/Dashboard/Cache";
export default { export default {
name: 'Settings', name: 'Settings',
components: { components: {
ThemeEditor, Cache,
FormIntegration, ThemeEditor,
Notifier, FormIntegration,
CoreSettings Notifier,
}, CoreSettings
data () { },
return { data() {
tab: "v-pills-home-tab", return {
qrcode: "", tab: "v-pills-home-tab",
core: this.$store.getters.core, qrcode: "",
cache: [], qrurl: "",
} core: this.$store.getters.core
}, }
async mounted () { },
async mounted() {
this.cache = await Api.cache() this.cache = await Api.cache()
}, },
async created() { async created() {
const qrurl = `statping://setup?domain=${core.domain}&api=${core.api_secret}` const c = this.$store.getters.core
this.qrcode = "https://chart.googleapis.com/chart?chs=500x500&cht=qr&chl=" + encodeURI(qrurl) this.qrurl = `statping://setup?domain=${c.domain}&api=${c.api_secret}`
}, this.qrcode = "https://chart.googleapis.com/chart?chs=500x500&cht=qr&chl=" + encodeURI(this.qrurl)
beforeMount() { },
beforeMount() {
}, },
methods: { methods: {
changeTab (e) { changeTab(e) {
this.tab = e.target.id this.tab = e.target.id
}, },
liClass (id) { liClass(id) {
return this.tab === id return this.tab === id
}, }
expireTime(ex) { }
return this.toLocal(ex)
},
async clearCache () {
await Api.clearCache()
this.cache = await Api.cache()
}
} }
}
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -13,7 +13,7 @@ import Service from "./pages/Service";
import VueRouter from "vue-router"; import VueRouter from "vue-router";
import Setup from "./forms/Setup"; import Setup from "./forms/Setup";
import Api from "./components/API"; import Api from "./API";
const routes = [ const routes = [
{ {

View File

@ -1,6 +1,6 @@
import Vuex from 'vuex' import Vuex from 'vuex'
import Vue from 'vue' import Vue from 'vue'
import Api from "./components/API" import Api from "./API"
Vue.use(Vuex) Vue.use(Vuex)
@ -105,7 +105,7 @@ export default new Vuex.Store({
} }
}, },
actions: { actions: {
async loadRequired (context) { async loadRequired(context) {
const core = await Api.core() const core = await Api.core()
context.commit("setCore", core); context.commit("setCore", core);
const groups = await Api.groups() const groups = await Api.groups()
@ -125,7 +125,7 @@ export default new Vuex.Store({
// } // }
window.console.log('finished loading required data') window.console.log('finished loading required data')
}, },
async loadAdmin (context) { async loadAdmin(context) {
const core = await Api.core() const core = await Api.core()
context.commit("setCore", core); context.commit("setCore", core);
const groups = await Api.groups() const groups = await Api.groups()

16
frontend/test/API.spec.js Normal file
View File

@ -0,0 +1,16 @@
import API from "@/API"
import { shallowMount, mount } from '@vue/test-utils'
describe('API Tests', async () => {
await it('should get core info', async () => {
const wrapper = mount(API)
const core = await wrapper.core()
expect(core).toBe(9)
})
});

View File

@ -0,0 +1,24 @@
// import Vuex from 'vuex'
// import Api from '../src/components/API'
// import thisStore from '../src/store'
//
// import { createLocalVue } from '@vue/test-utils'
// const localVue = createLocalVue()
// localVue.use(Vuex)
//
// describe('MyName test', async () => {
// const services = [
// { id: 1, title: 'Apple', order_id: 3 },
// { id: 2, title: 'Orange', order_id: 2},
// { id: 3, title: 'Carrot', order_id: 1}
// ]
//
// const store = new Vuex.Store(thisStore)
//
// await store.dispatch('loadRequired')
//
// console.log(store.getters.services)
//
// expect(store.getters.services).toEqual(services.slice(0, 20))
// })

View File

@ -0,0 +1,32 @@
import { shallowMount, mount } from '@vue/test-utils'
import FormLogin from "../../src/forms/Login.vue"
const wrapper = shallowMount(FormLogin)
describe('Login Form', () => {
it('has a created hook', () => {
expect(typeof FormLogin.methods.checkForm).toBe('function')
})
it('should login', async () => {
expect(wrapper.vm.$data.loading).toBe(false)
expect(wrapper.vm.$data.username).toBe('')
expect(wrapper.vm.$data.password).toBe('')
wrapper.setData({ username: 'admin' })
wrapper.setData({ password: 'admin' })
wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
expect(wrapper.vm.$data.loading).toBe(true)
done()
})
});

View File

@ -0,0 +1,17 @@
// import { mount } from '@vue/test-utils';
// import Index from '../../src/pages/Index';
//
// const wrapper = mount(Index);
// describe('MyName test', () => {
// it('Displays my name when I write it', () => {
//
// expect(wrapper.vm.$data.logged_in).toBe('My name');
//
// const input = wrapper.find('input');
// input.element.value = 'Stefan';
// input.trigger('input');
//
// expect(wrapper.vm.$data.name).toBe('Stefan');
// })
// });

5
frontend/test/setup.js Normal file
View File

@ -0,0 +1,5 @@
require('jsdom-global')()
global.expect = require('expect')

File diff suppressed because it is too large Load Diff

View File

@ -89,7 +89,6 @@ func apiCoreHandler(w http.ResponseWriter, r *http.Request) {
if c.Timezone != app.Timezone { if c.Timezone != app.Timezone {
app.Timezone = c.Timezone app.Timezone = c.Timezone
} }
app.UpdateNotify = c.UpdateNotify
app.UseCdn = types.NewNullBool(c.UseCdn.Bool) app.UseCdn = types.NewNullBool(c.UseCdn.Bool)
core.CoreApp, err = core.UpdateCore(app) core.CoreApp, err = core.UpdateCore(app)
returnJson(core.CoreApp, w, r) returnJson(core.CoreApp, w, r)

View File

@ -25,6 +25,9 @@ var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap
"USE_CDN": func() bool { "USE_CDN": func() bool {
return core.CoreApp.UseCdn.Bool return core.CoreApp.UseCdn.Bool
}, },
"USING_ASSETS": func() bool {
return core.CoreApp.UsingAssets()
},
"BasePath": func() string { "BasePath": func() string {
return basePath return basePath
}, },

View File

@ -70,9 +70,6 @@ func (u *discord) OnSuccess(s *types.Service) {
if !s.Online || !s.SuccessNotified { if !s.Online || !s.SuccessNotified {
u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id)) u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))
var msg interface{} var msg interface{}
if s.UpdateNotify {
s.UpdateNotify = false
}
msg = s.DownText msg = s.DownText
u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg) u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)

View File

@ -193,9 +193,6 @@ func (u *email) OnFailure(s *types.Service, f *types.Failure) {
func (u *email) OnSuccess(s *types.Service) { func (u *email) OnSuccess(s *types.Service) {
if !s.Online || !s.SuccessNotified { if !s.Online || !s.SuccessNotified {
var msg string var msg string
if s.UpdateNotify {
s.UpdateNotify = false
}
msg = s.DownText msg = s.DownText
u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id)) u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))

View File

@ -72,9 +72,6 @@ func (u *lineNotifier) OnFailure(s *types.Service, f *types.Failure) {
func (u *lineNotifier) OnSuccess(s *types.Service) { func (u *lineNotifier) OnSuccess(s *types.Service) {
if !s.Online || !s.SuccessNotified { if !s.Online || !s.SuccessNotified {
var msg string var msg string
if s.UpdateNotify {
s.UpdateNotify = false
}
msg = s.DownText msg = s.DownText
u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id)) u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))

View File

@ -99,9 +99,6 @@ func (u *mobilePush) OnSuccess(s *types.Service) {
data := dataJson(s, nil) data := dataJson(s, nil)
if !s.Online || !s.SuccessNotified { if !s.Online || !s.SuccessNotified {
var msgStr string var msgStr string
if s.UpdateNotify {
s.UpdateNotify = false
}
msgStr = s.DownText msgStr = s.DownText
u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id)) u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))

View File

@ -92,9 +92,6 @@ func (u *telegram) OnSuccess(s *types.Service) {
if !s.Online || !s.SuccessNotified { if !s.Online || !s.SuccessNotified {
u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id)) u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))
var msg interface{} var msg interface{}
if s.UpdateNotify {
s.UpdateNotify = false
}
msg = s.DownText msg = s.DownText
u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg) u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)

View File

@ -102,9 +102,6 @@ func (u *twilio) OnSuccess(s *types.Service) {
if !s.Online || !s.SuccessNotified { if !s.Online || !s.SuccessNotified {
u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id)) u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id))
var msg string var msg string
if s.UpdateNotify {
s.UpdateNotify = false
}
msg = s.DownText msg = s.DownText
u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg) u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg)

View File

@ -41,7 +41,6 @@ type Core struct {
Setup bool `gorm:"-" json:"setup"` Setup bool `gorm:"-" json:"setup"`
MigrationId int64 `gorm:"column:migration_id" json:"migration_id,omitempty"` MigrationId int64 `gorm:"column:migration_id" json:"migration_id,omitempty"`
UseCdn NullBool `gorm:"column:use_cdn;default:false" json:"using_cdn,omitempty"` UseCdn NullBool `gorm:"column:use_cdn;default:false" json:"using_cdn,omitempty"`
UpdateNotify NullBool `gorm:"column:update_notify;default:true" json:"update_notify,omitempty"`
Timezone float32 `gorm:"column:timezone;default:-8.0" json:"timezone,omitempty"` Timezone float32 `gorm:"column:timezone;default:-8.0" json:"timezone,omitempty"`
LoggedIn bool `gorm:"-" json:"logged_in"` LoggedIn bool `gorm:"-" json:"logged_in"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`

View File

@ -33,7 +33,6 @@ type Service struct {
Port int `gorm:"not null;column:port" json:"port" scope:"user,admin"` Port int `gorm:"not null;column:port" json:"port" scope:"user,admin"`
Timeout int `gorm:"default:30;column:timeout" json:"timeout" scope:"user,admin"` Timeout int `gorm:"default:30;column:timeout" json:"timeout" scope:"user,admin"`
Order int `gorm:"default:0;column:order_id" json:"order_id"` Order int `gorm:"default:0;column:order_id" json:"order_id"`
AllowNotifications NullBool `gorm:"default:true;column:allow_notifications" json:"allow_notifications" scope:"user,admin"`
VerifySSL NullBool `gorm:"default:false;column:verify_ssl" json:"verify_ssl" scope:"user,admin"` VerifySSL NullBool `gorm:"default:false;column:verify_ssl" json:"verify_ssl" scope:"user,admin"`
Public NullBool `gorm:"default:true;column:public" json:"public"` Public NullBool `gorm:"default:true;column:public" json:"public"`
GroupId int `gorm:"default:0;column:group_id" json:"group_id"` GroupId int `gorm:"default:0;column:group_id" json:"group_id"`
@ -53,10 +52,11 @@ type Service struct {
Checkpoint time.Time `gorm:"-" json:"-"` Checkpoint time.Time `gorm:"-" json:"-"`
SleepDuration time.Duration `gorm:"-" json:"-"` SleepDuration time.Duration `gorm:"-" json:"-"`
LastResponse string `gorm:"-" json:"-"` LastResponse string `gorm:"-" json:"-"`
UserNotified bool `gorm:"-" json:"-"` // True if the User was already notified about a Downtime AllowNotifications NullBool `gorm:"default:true;column:allow_notifications" json:"allow_notifications" scope:"user,admin"`
UpdateNotify bool `gorm:"-" json:"-"` // This Variable is a simple copy of `core.CoreApp.UpdateNotify.Bool` UserNotified bool `gorm:"-" json:"-"` // True if the User was already notified about a Downtime
DownText string `gorm:"-" json:"-"` // Contains the current generated Downtime Text UpdateNotify NullBool `gorm:"default:true;column:notify_all_changes" json:"notify_all_changes" scope:"user,admin"` // This Variable is a simple copy of `core.CoreApp.UpdateNotify.Bool`
SuccessNotified bool `gorm:"-" json:"-"` // Is 'true' if the user has already be informed that the Services now again available DownText string `gorm:"-" json:"-"` // Contains the current generated Downtime Text
SuccessNotified bool `gorm:"-" json:"-"` // Is 'true' if the user has already be informed that the Services now again available
LastStatusCode int `gorm:"-" json:"status_code"` LastStatusCode int `gorm:"-" json:"status_code"`
LastOnline time.Time `gorm:"-" json:"last_success"` LastOnline time.Time `gorm:"-" json:"last_success"`
Failures []FailureInterface `gorm:"-" json:"failures,omitempty" scope:"user,admin"` Failures []FailureInterface `gorm:"-" json:"failures,omitempty" scope:"user,admin"`