pull/429/head
hunterlong 2020-02-19 00:13:09 -08:00
parent d2331fe14b
commit b8dbad85fe
24 changed files with 8580 additions and 196 deletions

View File

@ -29,7 +29,6 @@ const webpackConfig = merge(commonConfig, {
new FriendlyErrorsPlugin(),
new HtmlPlugin({
template: 'public/index.html',
chunksSortMode: 'dependency'
})
],
devServer: {

View File

@ -11,7 +11,7 @@
"dependencies": {
"@fortawesome/fontawesome-free-solid": "^5.1.0-3",
"@fortawesome/fontawesome-svg-core": "^1.2.26",
"@fortawesome/free-brands-svg-icons": "^5.12.0",
"@fortawesome/free-brands-svg-icons": "^5.12.1",
"@fortawesome/free-solid-svg-icons": "^5.12.0",
"@fortawesome/vue-fontawesome": "^0.1.9",
"apexcharts": "^3.15.0",

View File

@ -3,10 +3,10 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{Name}}</title>
<title>{{CoreApp.Name}}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, maximum-scale=1.0, user-scalable=0">
<meta name="description" content="{{Description}}">
<meta name="description" content="{{CoreApp.Description}}">
<base href="{{BasePath}}">
{{if USE_CDN}}
<link rel="shortcut icon" type="image/x-icon" href="https://assets.statping.com/favicon.ico">

View File

@ -1,13 +1,13 @@
<template>
<div id="app">
<router-view :loaded="loaded"/>
<Footer :version="version" v-if="$route.path !== '/setup'"/>
<Footer :logged_in="logged_in" :version="version" v-if="$route.path !== '/setup'"/>
</div>
</template>
<script>
import Api from './components/API';
import Footer from "./components/Footer";
import Footer from "./components/Index/Footer";
export default {
name: 'app',
@ -22,7 +22,11 @@
}
},
async created() {
await this.$store.dispatch('loadRequired')
await this.$store.dispatch('loadRequired')
if (this.$store.getters.core.logged_in) {
await this.$store.dispatch('loadAdmin')
}
this.loaded = true
if (!this.$store.getters.core.setup) {
this.$router.push('/setup')
@ -32,8 +36,9 @@
async mounted() {
if (this.$route.path !== '/setup') {
const tk = localStorage.getItem("statping_user")
if (tk) {
// await this.$store.dispatch('loadAdmin')
if (this.$store.getters.core.logged_in) {
this.logged_in = true
await this.$store.dispatch('loadAdmin')
}
}
},

View File

@ -147,9 +147,14 @@ HTML,BODY {
text-decoration: none;
}
.card-title A {
color: $service-title;
text-decoration: none;
}
.chart-container {
position: relative;
height: 200px;
height: 24.1vh;
width: 100%;
overflow: hidden;
}
@ -284,6 +289,33 @@ input.inputTags-field:focus {
margin-right: 10px;
}
@keyframes fadeInOut {
0% { opacity:1; }
50% { opacity:0.3; }
100% { opacity:1; }
}
@-o-keyframes fadeInOut {
0% { opacity:1; }
50% { opacity:0.3; }
100% { opacity:1; }
}
@-moz-keyframes fadeInOut {
0% { opacity:1; }
50% { opacity:0.3; }
100% { opacity:1; }
}
@-webkit-keyframes fadeInOut {
0% { opacity:1; }
50% { opacity:0.3; }
100% { opacity:1; }
}
.animate-fader {
-webkit-animation: fadeInOut 1s infinite;
-moz-animation: fadeInOut 1s infinite;
-o-animation: fadeInOut 1s infinite;
animation: fadeInOut 21 infinite;
}
.CodeMirror {
/* Bootstrap Settings */
box-sizing: border-box;

View File

@ -52,7 +52,7 @@
}
.lg_number {
font-size: 7.8vw;
font-size: 7vw;
}
.stats_area {
@ -66,6 +66,10 @@
font-size: 0.6rem;
}
.navbar-item {
border-bottom: 1px solid #eaeaea;
}
.list-group-item {
border-top: 1px solid #e4e4e4;
border: 0px;

View File

@ -25,8 +25,10 @@
</span> {{service.name}}
</td>
<td class="d-none d-md-table-cell">
<span class="badge" :class="{'badge-success': service.online, 'badge-danger': !service.online}">{{service.online ? "ONLINE" : "OFFLINE"}}</span>
<ToggleSwitch :service="service"/>
<span class="badge" :class="{'animate-fader': !service.online, 'badge-success': service.online, 'badge-danger': !service.online}">
{{service.online ? "ONLINE" : "OFFLINE"}}
</span>
<ToggleSwitch v-if="service.online" :service="service"/>
</td>
<td class="d-none d-md-table-cell">
<span class="badge" :class="{'badge-primary': service.public, 'badge-secondary': !service.public}">
@ -117,7 +119,7 @@
computed: {
servicesList: {
get() {
return this.$store.getters.servicesInOrder
return this.$store.state.servicesInOrder
},
async set(value) {
let data = [];

View File

@ -7,10 +7,10 @@
</span>
</h5>
<div v-if="loaded && service.online" class="row">
<div class="col-6">
<div class="col-md-6 col-sm-12">
<ServiceSparkLine :title="set1_name" subtitle="Last Day Latency" :series="set1"/>
</div>
<div class="col-6">
<div class="col-md-6 col-sm-12">
<ServiceSparkLine :title="set2_name" subtitle="Last 7 Days Latency" :series="set2"/>
</div>
</div>

View File

@ -1,33 +1,34 @@
<template>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<router-link to="/" class="navbar-brand">Statping</router-link>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
<button @click="navopen = !navopen" class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
<font-awesome-icon v-if="!navopen" icon="bars"/>
<font-awesome-icon v-if="navopen" icon="times"/>
</button>
<div class="collapse navbar-collapse" id="navbarText">
<div class="navbar-collapse" :class="{collapse: !navopen}" id="navbarText">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<li @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard" class="nav-link">Dashboard</router-link>
</li>
<li class="nav-item">
<li @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/services" class="nav-link">Services</router-link>
</li>
<li class="nav-item">
<li @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/users" class="nav-link">Users</router-link>
</li>
<li class="nav-item">
<li @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/messages" class="nav-link">Messages</router-link>
</li>
<li class="nav-item">
<li @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/settings" class="nav-link">Settings</router-link>
</li>
<li class="nav-item">
<li @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/logs" class="nav-link">Logs</router-link>
</li>
<li class="nav-item">
<li @click="navopen = !navopen" class="nav-item navbar-item">
<router-link to="/dashboard/help" class="nav-link">Help</router-link>
</li>
</ul>
@ -44,9 +45,11 @@
export default {
name: 'TopNav',
props: {
},
data () {
return {
navopen: false,
}
},
methods: {
async logout () {
await Api.logout()

View File

@ -1,16 +1,17 @@
<template>
<footer>
<div v-if="!$store.getters.core.footer" class="footer text-center mb-4 p-2">
<a href="https://github.com/hunterlong/statping" target="_blank">Statping {{version}} made with <i class="text-danger fas fa-heart"></i></a> |
<a href="/dashboard">Dashboard</a>
</div>
<div v-else class="footer text-center mb-4 p-2" v-html="$store.getters.core.footer">
<a href="https://github.com/hunterlong/statping" target="_blank">
Statping {{$store.getters.core.version}} made with <font-awesome-icon style="color: #d40d0d" icon="heart"/>
</a> |
<router-link :to="$store.getters.core.logged_in ? '/dashboard' : '/login'">Dashboard</router-link>
</div>
<div v-else class="footer text-center mb-4 p-2" v-html="$store.getters.core.footer"></div>
</footer>
</template>
<script>
import Dashboard from "../pages/Dashboard";
import Dashboard from "../../pages/Dashboard";
export default {
name: 'Footer',
@ -18,8 +19,14 @@
Dashboard
},
props: {
version: String
}
version: String,
logged_in: Boolean
},
watch: {
logged_in() {
}
}
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<apexchart v-if="ready" width="100%" height="215" type="area" :options="chartOptions" :series="series"></apexchart>
<apexchart v-if="ready" width="100%" height="225" type="area" :options="chartOptions" :series="series"></apexchart>
</template>
<script>

View File

@ -17,7 +17,7 @@
<small class="form-text text-muted" v-html="form.small_text"></small>
</div>
<div class="row">
<div class="row mt-4">
<div class="col-9 col-sm-6">
<div class="input-group mb-2">
<div class="input-group-prepend">
@ -43,14 +43,14 @@
</button>
</div>
<div class="col-12 col-sm-12">
<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>
{{loading ? "Loading..." : "Test Notifier"}}</button>
</div>
</div>
<span class="d-block small text-center mt-3 mb-5">
<span class="d-block small text-center mt-5 mb-5">
<span class="text-capitalize">{{notifier.title}}</span> Notifier created by <a :href="notifier.author_url" target="_blank">{{notifier.author}}</a>
</span>
</form>

9
frontend/src/icons.js Normal file
View File

@ -0,0 +1,9 @@
import {library} from '@fortawesome/fontawesome-svg-core'
import {fas} from '@fortawesome/fontawesome-free-solid';
import {fab} from '@fortawesome/free-brands-svg-icons';
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
import Vue from "vue";
library.add(fas, fab)
Vue.component('font-awesome-icon', FontAwesomeIcon)

View File

@ -5,16 +5,11 @@ import VueApexCharts from 'vue-apexcharts'
import App from '@/App.vue'
import store from './store'
import {library} from '@fortawesome/fontawesome-svg-core'
import {fas} from '@fortawesome/fontawesome-free-solid';
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
import router from './routes'
import "./mixin"
library.add(fas)
import "./icons"
Vue.component('apexchart', VueApexCharts)
Vue.component('font-awesome-icon', FontAwesomeIcon)
Vue.use(VueRouter);
Vue.use(require('vue-moment'));

View File

@ -56,6 +56,36 @@ export default Vue.mixin({
loggedIn() {
const core = this.$store.getters.core
return core.logged_in === true
},
iconName(name) {
switch (name) {
case "fas fa-terminal":
return "terminal"
case "fab fa-discord":
return ["fab", "discord"]
case "far fa-envelope":
return "envelope"
case "far fa-bell":
return "bell"
case "fas fa-mobile-alt":
return "mobile"
case "fab fa-slack":
return ["fab", "slack-hash"]
case "fab fa-telegram-plane":
return ["fab", "telegram-plane"]
case "far fa-comment-alt":
return "comment"
case "fas fa-code-branch":
return "code-branch"
case "csv":
return "file"
case "docker":
return ["fab", "docker"]
case "traefik":
return "server"
default:
return "bars"
}
},
}
});

View File

@ -14,6 +14,15 @@
components: {
TopNav,
},
async mounted() {
if (this.$route.path !== "/login") {
try {
const u = await Api.users()
} catch (e) {
this.$router.push('/logout')
}
}
},
data () {
return {
authenticated: false

View File

@ -2,7 +2,7 @@
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
<div class="col-10 offset-1 col-md-8 offset-md-2 mt-md-2">
<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>
<FormLogin/>

View File

@ -5,20 +5,26 @@
<div class="nav flex-column nav-pills" id="v-pills-tab" role="tablist" aria-orientation="vertical">
<h6 class="text-muted">Main Settings</h6>
<a @click.prevent="changeTab" class="nav-link" v-bind:class="{active: liClass('v-pills-home-tab')}" id="v-pills-home-tab" data-toggle="pill" href="#v-pills-home" role="tab" aria-controls="v-pills-home" aria-selected="true"><i class="fa fa-cogs"></i> Settings</a>
<a @click.prevent="changeTab" class="nav-link" v-bind:class="{active: liClass('v-pills-style-tab')}" id="v-pills-style-tab" data-toggle="pill" href="#v-pills-style" role="tab" aria-controls="v-pills-style" aria-selected="false"><i class="fa fa-image"></i> Theme Editor</a>
<a @click.prevent="changeTab" class="nav-link" v-bind:class="{active: liClass('v-pills-cache-tab')}" id="v-pills-cache-tab" data-toggle="pill" href="#v-pills-cache" role="tab" aria-controls="v-pills-cache" aria-selected="false"><i class="fa fa-paperclip"></i> Cache</a>
<a @click.prevent="changeTab" class="nav-link" v-bind:class="{active: liClass('v-pills-home-tab')}" id="v-pills-home-tab" data-toggle="pill" href="#v-pills-home" role="tab" aria-controls="v-pills-home" aria-selected="true">
<font-awesome-icon icon="cog" class="mr-2"/> Settings
</a>
<a @click.prevent="changeTab" class="nav-link" v-bind:class="{active: liClass('v-pills-style-tab')}" id="v-pills-style-tab" data-toggle="pill" href="#v-pills-style" role="tab" aria-controls="v-pills-style" aria-selected="false">
<font-awesome-icon icon="image" class="mr-2"/> Theme Editor
</a>
<a @click.prevent="changeTab" class="nav-link" v-bind:class="{active: liClass('v-pills-cache-tab')}" id="v-pills-cache-tab" data-toggle="pill" href="#v-pills-cache" role="tab" aria-controls="v-pills-cache" aria-selected="false">
<font-awesome-icon icon="paperclip" class="mr-2"/> Cache
</a>
<h6 class="mt-4 text-muted">Notifiers</h6>
<a v-for="(notifier, index) in $store.getters.notifiers" v-bind:key="`${notifier.method}_${index}`" @click.prevent="changeTab" class="nav-link text-capitalize" v-bind:class="{active: liClass(`v-pills-${notifier.method.toLowerCase()}-tab`)}" v-bind:id="`v-pills-${notifier.method.toLowerCase()}-tab`" data-toggle="pill" v-bind:href="`#v-pills-${notifier.method.toLowerCase()}`" role="tab" v-bind:aria-controls="`v-pills-${notifier.method.toLowerCase()}`" aria-selected="false">
<i class="fas fa-terminal"></i> {{notifier.method}}
<font-awesome-icon :icon="iconName(notifier.icon)" class="mr-2"/> {{notifier.method}}
</a>
<h6 class="mt-4 text-muted">Integrations (beta)</h6>
<a v-for="(integration, index) in $store.getters.integrations" v-bind:key="`${integration.name}_${index}`" @click.prevent="changeTab" class="nav-link text-capitalize" v-bind:class="{active: liClass(`v-pills-integration-${integration.name}`)}" v-bind:id="`v-pills-integration-${integration.name}`" data-toggle="pill" v-bind:href="`#v-pills-integration-${integration.name}`" role="tab" :aria-controls="`v-pills-integration-${integration.name}`" aria-selected="false">
<i class="fas fa-file-csv"></i> {{integration.full_name}}
<font-awesome-icon :icon="iconName(integration.name)" class="mr-2"/> {{integration.full_name}}
</a>
</div>

View File

@ -126,7 +126,15 @@ export default new Vuex.Store({
window.console.log('finished loading required data')
},
async loadAdmin (context) {
await context.dispatch('loadRequired')
const core = await Api.core()
context.commit("setCore", core);
const groups = await Api.groups()
context.commit("setGroups", groups);
const services = await Api.services()
context.commit("setServices", services);
const messages = await Api.messages()
context.commit("setMessages", messages)
context.commit("setHasPublicData", true)
const notifiers = await Api.notifiers()
context.commit("setNotifiers", notifiers);
const users = await Api.users()

8410
frontend/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,9 @@
package handlers
import (
"encoding/json"
"fmt"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"html/template"
"net/http"
"reflect"
"time"
)
var (
@ -18,137 +12,19 @@ var (
var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap {
return template.FuncMap{
"js": func(html interface{}) template.JS {
return template.JS(utils.ToString(html))
},
"safe": func(html string) template.HTML {
return template.HTML(html)
},
"safeURL": func(u string) template.URL {
return template.URL(u)
},
"Auth": func() bool {
return IsFullAuthenticated(r)
},
"IsUser": func() bool {
return IsUser(r)
},
"VERSION": func() string {
return core.VERSION
},
"CoreApp": func() *core.Core {
return core.CoreApp
},
"Services": func() []types.ServiceInterface {
return core.CoreApp.Services
},
"VisibleServices": func() []*core.Service {
auth := IsUser(r)
return core.SelectServices(auth)
},
"VisibleGroupServices": func(group *core.Group) []*core.Service {
auth := IsUser(r)
return group.VisibleServices(auth)
},
"Groups": func(includeAll bool) []*core.Group {
auth := IsUser(r)
return core.SelectGroups(includeAll, auth)
},
"Group": func(id int) *core.Group {
return core.SelectGroup(int64(id))
},
"len": func(g interface{}) int {
val := reflect.ValueOf(g)
return val.Len()
},
"IsNil": func(g interface{}) bool {
return g == nil
"CoreApp": func() core.Core {
c := *core.CoreApp
if c.Name == "" {
c.Name = "Statping"
}
return c
},
"USE_CDN": func() bool {
return core.CoreApp.UseCdn.Bool
},
"UPDATENOTIFY": func() bool {
return core.CoreApp.UpdateNotify.Bool
},
"QrAuth": func() string {
return fmt.Sprintf("statping://setup?domain=%v&api=%v", core.CoreApp.Domain, core.CoreApp.ApiSecret)
},
"Type": func(g interface{}) []string {
fooType := reflect.TypeOf(g)
var methods []string
methods = append(methods, fooType.String())
for i := 0; i < fooType.NumMethod(); i++ {
method := fooType.Method(i)
fmt.Println(method.Name)
methods = append(methods, method.Name)
}
return methods
},
"ToJSON": func(g interface{}) template.HTML {
data, _ := json.Marshal(g)
return template.HTML(string(data))
},
"underscore": func(html string) string {
return utils.UnderScoreString(html)
},
"URL": func() string {
return basePath + r.URL.String()
},
"CHART_DATA": func() string {
return ""
},
"Error": func() string {
return ""
},
"Cache": func() Cacher {
return CacheStorage
},
"ToString": func(v interface{}) string {
return utils.ToString(v)
},
"Ago": func(t time.Time) string {
return utils.Timestamp(t).Ago()
},
"Duration": func(t time.Duration) string {
duration, _ := time.ParseDuration(fmt.Sprintf("%vs", t.Seconds()))
return utils.FormatDuration(duration)
},
"ToUnix": func(t time.Time) int64 {
return t.UTC().Unix()
},
"ParseTime": func(t time.Time, format string) string {
return t.Format(format)
},
"FromUnix": func(t int64) string {
return utils.Timezoner(time.Unix(t, 0), core.CoreApp.Timezone).Format("Monday, January 02")
},
"UnixTime": func(t int64, nano bool) string {
if nano {
t = t / 1e9
}
return utils.Timezoner(time.Unix(t, 0), core.CoreApp.Timezone).String()
},
"ServiceLink": func(s *core.Service) string {
if s.Permalink.Valid {
return s.Permalink.String
}
return utils.ToString(s.Id)
},
"NewService": func() *types.Service {
return new(types.Service)
},
"NewUser": func() *types.User {
return new(types.User)
},
"NewCheckin": func() *types.Checkin {
return new(types.Checkin)
},
"NewMessage": func() *types.Message {
return new(types.Message)
},
"NewGroup": func() *types.Group {
return new(types.Group)
},
"BasePath": func() string {
return basePath
},

View File

@ -44,7 +44,7 @@ var (
httpServer *http.Server
usingSSL bool
mainTmpl = `{{define "main" }} {{ template "base" . }} {{ end }}`
templates = []string{"base.gohtml", "head.gohtml", "nav.gohtml", "footer.gohtml", "scripts.gohtml", "form_service.gohtml", "form_notifier.gohtml", "form_integration.gohtml", "form_group.gohtml", "form_user.gohtml", "form_checkin.gohtml", "form_message.gohtml"}
templates = []string{"base.gohtml"}
)
// RunHTTPServer will start a HTTP server on a specific IP and port

View File

@ -25,7 +25,7 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/setup", http.StatusSeeOther)
return
}
ExecuteResponse(w, r, "index.html", core.CoreApp, nil)
ExecuteResponse(w, r, "base.gohtml", core.CoreApp, nil)
}
func healthCheckHandler(w http.ResponseWriter, r *http.Request) {

View File

@ -1,11 +0,0 @@
{{ define "base" }}
<!doctype html>
<html lang="en">
{{block "head" .}} {{end}}
<body>
{{template "content" .}}
</body>
<footer>{{template "footer" .}}</footer>
{{template "scripts" .}}
</html>
{{end}}