pull/429/head
hunterlong 2020-01-31 19:53:00 -08:00
parent de4143b4f8
commit d89175a340
38 changed files with 683 additions and 276 deletions

View File

@ -64,9 +64,11 @@ func (c *Core) ToCore() *types.Core {
func InitApp() {
SelectCore()
InsertNotifierDB()
InsertIntegratorDB()
CoreApp.SelectAllServices(true)
checkServices()
AttachNotifiers()
AddIntegrations()
CoreApp.Notifications = notifier.AllCommunications
CoreApp.Integrations = integrations.Integrations
go DatabaseMaintence()
@ -85,6 +87,18 @@ func InsertNotifierDB() error {
return nil
}
// InsertIntegratorDB inject the Statping database instance to the Integrations package
func InsertIntegratorDB() error {
if DbSession == nil {
err := CoreApp.Connect(false, utils.Directory)
if err != nil {
return errors.New("database connection has not been created")
}
}
integrations.SetDB(DbSession)
return nil
}
// UpdateCore will update the CoreApp variable inside of the 'core' table in database
func UpdateCore(c *Core) (*Core, error) {
db := coreDB().Update(&c)
@ -198,6 +212,15 @@ func AttachNotifiers() error {
)
}
// AddIntegrations will attach all the integrations into the system
func AddIntegrations() error {
return integrations.AddIntegrations(
integrations.CsvIntegrator,
integrations.TraefikIntegrator,
integrations.DockerIntegrator,
)
}
// ServiceOrder will reorder the services based on 'order_id' (Order)
type ServiceOrder []types.ServiceInterface

View File

@ -37,7 +37,7 @@ var (
)
func init() {
DbModels = []interface{}{&types.Service{}, &types.User{}, &types.Hit{}, &types.Failure{}, &types.Message{}, &types.Group{}, &types.Checkin{}, &types.CheckinHit{}, &notifier.Notification{}, &types.Incident{}, &types.IncidentUpdate{}}
DbModels = []interface{}{&types.Service{}, &types.User{}, &types.Hit{}, &types.Failure{}, &types.Message{}, &types.Group{}, &types.Checkin{}, &types.CheckinHit{}, &notifier.Notification{}, &types.Incident{}, &types.IncidentUpdate{}, &types.Integration{}}
gorm.NowFunc = func() time.Time {
return time.Now().UTC()

View File

@ -13,6 +13,18 @@ type IncidentUpdate struct {
*types.IncidentUpdate
}
// ReturnIncident returns *core.Incident based off a *types.Incident
func ReturnIncident(u *types.Incident) *Incident {
return &Incident{u}
}
// SelectIncident returns the Incident based on the Incident's ID.
func SelectIncident(id int64) (*Incident, error) {
var incident Incident
err := incidentsDB().Where("id = ?", id).First(&incident)
return &incident, err.Error()
}
// AllIncidents will return all incidents and updates recorded
func AllIncidents() []*Incident {
var incidents []*Incident

View File

@ -32,7 +32,7 @@ type csvIntegration struct {
*types.Integration
}
var csvIntegrator = &csvIntegration{&types.Integration{
var CsvIntegrator = &csvIntegration{&types.Integration{
ShortName: "csv",
Name: "CSV File",
Icon: "<i class=\"fas fa-file-csv\"></i>",

View File

@ -14,23 +14,23 @@ func TestCsvFileIntegration(t *testing.T) {
t.Run("Set Field Value", func(t *testing.T) {
formPost := map[string][]string{}
formPost["input"] = []string{string(data)}
_, err = SetFields(csvIntegrator, formPost)
_, err = SetFields(CsvIntegrator, formPost)
require.Nil(t, err)
})
t.Run("Get Field Value", func(t *testing.T) {
value := Value(csvIntegrator, "input").(string)
value := Value(CsvIntegrator, "input").(string)
assert.Equal(t, string(data), value)
})
t.Run("List Services from CSV File", func(t *testing.T) {
services, err := csvIntegrator.List()
services, err := CsvIntegrator.List()
require.Nil(t, err)
assert.Equal(t, 10, len(services))
})
t.Run("Confirm Services from CSV File", func(t *testing.T) {
services, err := csvIntegrator.List()
services, err := CsvIntegrator.List()
require.Nil(t, err)
assert.Equal(t, "Bulk Upload", services[0].Name)
assert.Equal(t, "http://google.com", services[0].Domain)

View File

@ -27,7 +27,7 @@ type dockerIntegration struct {
*types.Integration
}
var dockerIntegrator = &dockerIntegration{&types.Integration{
var DockerIntegrator = &dockerIntegration{&types.Integration{
ShortName: "docker",
Name: "Docker",
Icon: "<i class=\"fab fa-docker\"></i>",

View File

@ -12,25 +12,25 @@ func TestDockerIntegration(t *testing.T) {
formPost := map[string][]string{}
formPost["path"] = []string{"unix:///var/run/docker.sock"}
formPost["version"] = []string{"1.25"}
_, err := SetFields(csvIntegrator, formPost)
_, err := SetFields(CsvIntegrator, formPost)
require.Nil(t, err)
})
t.Run("Get Field Value", func(t *testing.T) {
path := Value(dockerIntegrator, "path").(string)
version := Value(dockerIntegrator, "version").(string)
path := Value(DockerIntegrator, "path").(string)
version := Value(DockerIntegrator, "version").(string)
assert.Equal(t, "unix:///var/run/docker.sock", path)
assert.Equal(t, "1.25", version)
})
t.Run("List Services from Docker", func(t *testing.T) {
services, err := dockerIntegrator.List()
services, err := DockerIntegrator.List()
require.Nil(t, err)
assert.Equal(t, 0, len(services))
})
t.Run("Confirm Services from Docker", func(t *testing.T) {
services, err := dockerIntegrator.List()
services, err := DockerIntegrator.List()
require.Nil(t, err)
for _, s := range services {
t.Log(s)

View File

@ -16,7 +16,9 @@
package integrations
import (
"encoding/json"
"errors"
"fmt"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
)
@ -24,14 +26,25 @@ import (
var (
Integrations []types.Integrator
log = utils.Log.WithField("type", "integration")
db types.Database
)
func init() {
Integrations = append(Integrations,
csvIntegrator,
dockerIntegrator,
traefikIntegrator,
)
//func init() {
// Integrations = append(Integrations,
// CsvIntegrator,
// DockerIntegrator,
// TraefikIntegrator,
// )
//}
// integrationsDb returns the 'integrations' database column
func integrationsDb() types.Database {
return db.Model(&types.Integration{})
}
// SetDB is called by core to inject the database for a integrator to use
func SetDB(d types.Database) {
db = d
}
func Value(intg types.Integrator, fieldName string) interface{} {
@ -43,6 +56,35 @@ func Value(intg types.Integrator, fieldName string) interface{} {
return nil
}
func Update(integrator *types.Integration) error {
fields := FieldsToJson(integrator)
fmt.Println(fields)
set := db.Model(&types.Integration{}).Where("name = ?", integrator.Name)
set.Set("enabled", integrator.Enabled)
set.Set("fields", fields)
return set.Error()
}
func FieldsToJson(integrator *types.Integration) string {
jsonData := make(map[string]interface{})
for _, v := range integrator.Fields {
jsonData[v.Name] = v.Value
}
data, _ := json.Marshal(jsonData)
return string(data)
}
func JsonToFields(intg types.Integrator, input string) []*types.IntegrationField {
integrator := intg.Get()
var jsonData map[string]interface{}
json.Unmarshal([]byte(input), &jsonData)
for _, v := range integrator.Fields {
v.Value = jsonData[v.Name]
}
return integrator.Fields
}
func SetFields(intg types.Integrator, data map[string][]string) (*types.Integration, error) {
i := intg.Get()
for _, v := range i.Fields {
@ -62,3 +104,63 @@ func Find(name string) (types.Integrator, error) {
}
return nil, errors.New(name + " not found")
}
// db will return the notifier database column/record
func integratorDb(n *types.Integration) types.Database {
return db.Model(&types.Integration{}).Where("name = ?", n.Name).Find(n)
}
// isInDatabase returns true if the integration has already been installed
func isInDatabase(i types.Integrator) bool {
inDb := integratorDb(i.Get()).RecordNotFound()
return !inDb
}
// SelectIntegration returns the Notification struct from the database
func SelectIntegration(i types.Integrator) (*types.Integration, error) {
integration := i.Get()
err := db.Model(&types.Integration{}).Where("name = ?", integration.Name).Scan(&integration)
return integration, err.Error()
}
// AddIntegrations accept a Integrator interface to be added into the array
func AddIntegrations(integrations ...types.Integrator) error {
for _, i := range integrations {
if utils.IsType(i, new(types.Integrator)) {
Integrations = append(Integrations, i)
err := install(i)
if err != nil {
return err
}
} else {
return errors.New("notifier does not have the required methods")
}
}
return nil
}
// install will check the database for the notification, if its not inserted it will insert a new record for it
func install(i types.Integrator) error {
inDb := isInDatabase(i)
log.WithField("installed", inDb).
WithFields(utils.ToFields(i)).
Debugln(fmt.Sprintf("Checking if integrator '%v' is installed: %v", i.Get().Name, inDb))
if !inDb {
_, err := insertDatabase(i)
if err != nil {
log.Errorln(err)
return err
}
}
return nil
}
// insertDatabase will create a new record into the database for the integrator
func insertDatabase(i types.Integrator) (string, error) {
integrator := i.Get()
query := db.Create(integrator)
if query.Error() != nil {
return "", query.Error()
}
return integrator.Name, query.Error()
}

View File

@ -27,7 +27,7 @@ type traefikIntegration struct {
*types.Integration
}
var traefikIntegrator = &traefikIntegration{&types.Integration{
var TraefikIntegrator = &traefikIntegration{&types.Integration{
ShortName: "traefik",
Name: "Traefik",
Icon: "<i class=\"fas fa-network-wired\"></i>",

View File

@ -10,14 +10,14 @@ func TestTraefikIntegration(t *testing.T) {
t.Run("List Services from Traefik", func(t *testing.T) {
t.SkipNow()
services, err := traefikIntegrator.List()
services, err := TraefikIntegrator.List()
require.Nil(t, err)
assert.NotEqual(t, 0, len(services))
})
t.Run("Confirm Services from Traefik", func(t *testing.T) {
t.SkipNow()
services, err := traefikIntegrator.List()
services, err := TraefikIntegrator.List()
require.Nil(t, err)
for _, s := range services {
t.Log(s)

View File

@ -24,7 +24,7 @@ import (
// OnSave will trigger a notifier when it has been saved - Notifier interface
func OnSave(method string) {
for _, comm := range AllCommunications {
if isType(comm, new(Notifier)) {
if utils.IsType(comm, new(Notifier)) {
notifier := comm.(Notifier)
if notifier.Select().Method == method {
notifier.OnSave()
@ -52,11 +52,11 @@ func OnFailure(s *types.Service, f *types.Failure) {
sendMessages:
for _, comm := range AllCommunications {
if isType(comm, new(BasicEvents)) && isEnabled(comm) && (s.Online || inLimits(comm)) {
if utils.IsType(comm, new(BasicEvents)) && isEnabled(comm) && (s.Online || inLimits(comm)) {
notifier := comm.(Notifier).Select()
log.
WithField("trigger", "OnFailure").
WithFields(utils.ToFields(notifier, s)).Infoln(fmt.Sprintf("Sending [OnFailure] '%v' notification for service %v", notifier.Method, s.Name))
WithFields(utils.ToFields(notifier, s)).Debugln(fmt.Sprintf("Sending [OnFailure] '%v' notification for service %v", notifier.Method, s.Name))
comm.(BasicEvents).OnFailure(s, f)
}
}
@ -74,11 +74,11 @@ func OnSuccess(s *types.Service) {
}
for _, comm := range AllCommunications {
if isType(comm, new(BasicEvents)) && isEnabled(comm) && (!s.Online || inLimits(comm)) {
if utils.IsType(comm, new(BasicEvents)) && isEnabled(comm) && (!s.Online || inLimits(comm)) {
notifier := comm.(Notifier).Select()
log.
WithField("trigger", "OnSuccess").
WithFields(utils.ToFields(notifier, s)).Infoln(fmt.Sprintf("Sending [OnSuccess] '%v' notification for service %v", notifier.Method, s.Name))
WithFields(utils.ToFields(notifier, s)).Debugln(fmt.Sprintf("Sending [OnSuccess] '%v' notification for service %v", notifier.Method, s.Name))
comm.(BasicEvents).OnSuccess(s)
}
}
@ -87,10 +87,10 @@ func OnSuccess(s *types.Service) {
// OnNewService is triggered when a new service is created - ServiceEvents interface
func OnNewService(s *types.Service) {
for _, comm := range AllCommunications {
if isType(comm, new(ServiceEvents)) && isEnabled(comm) && inLimits(comm) {
if utils.IsType(comm, new(ServiceEvents)) && isEnabled(comm) && inLimits(comm) {
log.
WithField("trigger", "OnNewService").
Infoln(fmt.Sprintf("Sending new service notification for service %v", s.Name))
Debugln(fmt.Sprintf("Sending new service notification for service %v", s.Name))
comm.(ServiceEvents).OnNewService(s)
}
}
@ -102,8 +102,8 @@ func OnUpdatedService(s *types.Service) {
return
}
for _, comm := range AllCommunications {
if isType(comm, new(ServiceEvents)) && isEnabled(comm) && inLimits(comm) {
log.Infoln(fmt.Sprintf("Sending updated service notification for service %v", s.Name))
if utils.IsType(comm, new(ServiceEvents)) && isEnabled(comm) && inLimits(comm) {
log.Debugln(fmt.Sprintf("Sending updated service notification for service %v", s.Name))
comm.(ServiceEvents).OnUpdatedService(s)
}
}
@ -115,8 +115,8 @@ func OnDeletedService(s *types.Service) {
return
}
for _, comm := range AllCommunications {
if isType(comm, new(ServiceEvents)) && isEnabled(comm) && inLimits(comm) {
log.Infoln(fmt.Sprintf("Sending deleted service notification for service %v", s.Name))
if utils.IsType(comm, new(ServiceEvents)) && isEnabled(comm) && inLimits(comm) {
log.Debugln(fmt.Sprintf("Sending deleted service notification for service %v", s.Name))
comm.(ServiceEvents).OnDeletedService(s)
}
}
@ -125,8 +125,8 @@ func OnDeletedService(s *types.Service) {
// OnNewUser is triggered when a new user is created - UserEvents interface
func OnNewUser(u *types.User) {
for _, comm := range AllCommunications {
if isType(comm, new(UserEvents)) && isEnabled(comm) && inLimits(comm) {
log.Infoln(fmt.Sprintf("Sending new user notification for user %v", u.Username))
if utils.IsType(comm, new(UserEvents)) && isEnabled(comm) && inLimits(comm) {
log.Debugln(fmt.Sprintf("Sending new user notification for user %v", u.Username))
comm.(UserEvents).OnNewUser(u)
}
}
@ -135,8 +135,8 @@ func OnNewUser(u *types.User) {
// OnUpdatedUser is triggered when a new user is updated - UserEvents interface
func OnUpdatedUser(u *types.User) {
for _, comm := range AllCommunications {
if isType(comm, new(UserEvents)) && isEnabled(comm) && inLimits(comm) {
log.Infoln(fmt.Sprintf("Sending updated user notification for user %v", u.Username))
if utils.IsType(comm, new(UserEvents)) && isEnabled(comm) && inLimits(comm) {
log.Debugln(fmt.Sprintf("Sending updated user notification for user %v", u.Username))
comm.(UserEvents).OnUpdatedUser(u)
}
}
@ -145,8 +145,8 @@ func OnUpdatedUser(u *types.User) {
// OnDeletedUser is triggered when a new user is deleted - UserEvents interface
func OnDeletedUser(u *types.User) {
for _, comm := range AllCommunications {
if isType(comm, new(UserEvents)) && isEnabled(comm) && inLimits(comm) {
log.Infoln(fmt.Sprintf("Sending deleted user notification for user %v", u.Username))
if utils.IsType(comm, new(UserEvents)) && isEnabled(comm) && inLimits(comm) {
log.Debugln(fmt.Sprintf("Sending deleted user notification for user %v", u.Username))
comm.(UserEvents).OnDeletedUser(u)
}
}
@ -155,8 +155,8 @@ func OnDeletedUser(u *types.User) {
// OnUpdatedCore is triggered when the CoreApp settings are saved - CoreEvents interface
func OnUpdatedCore(c *types.Core) {
for _, comm := range AllCommunications {
if isType(comm, new(CoreEvents)) && isEnabled(comm) && inLimits(comm) {
log.Infoln(fmt.Sprintf("Sending updated core notification"))
if utils.IsType(comm, new(CoreEvents)) && isEnabled(comm) && inLimits(comm) {
log.Debugln(fmt.Sprintf("Sending updated core notification"))
comm.(CoreEvents).OnUpdatedCore(c)
}
}
@ -165,7 +165,7 @@ func OnUpdatedCore(c *types.Core) {
// OnStart is triggered when the Statping service has started
func OnStart(c *types.Core) {
for _, comm := range AllCommunications {
if isType(comm, new(CoreEvents)) && isEnabled(comm) && inLimits(comm) {
if utils.IsType(comm, new(CoreEvents)) && isEnabled(comm) && inLimits(comm) {
comm.(CoreEvents).OnUpdatedCore(c)
}
}
@ -174,7 +174,7 @@ func OnStart(c *types.Core) {
// OnNewNotifier is triggered when a new notifier is loaded
func OnNewNotifier(n *Notification) {
for _, comm := range AllCommunications {
if isType(comm, new(NotifierEvents)) && isEnabled(comm) && inLimits(comm) {
if utils.IsType(comm, new(NotifierEvents)) && isEnabled(comm) && inLimits(comm) {
comm.(NotifierEvents).OnNewNotifier(n)
}
}
@ -183,7 +183,7 @@ func OnNewNotifier(n *Notification) {
// OnUpdatedNotifier is triggered when a notifier has been updated
func OnUpdatedNotifier(n *Notification) {
for _, comm := range AllCommunications {
if isType(comm, new(NotifierEvents)) && isEnabled(comm) && inLimits(comm) {
if utils.IsType(comm, new(NotifierEvents)) && isEnabled(comm) && inLimits(comm) {
log.Infoln(fmt.Sprintf("Sending updated notifier for %v", n.Id))
comm.(NotifierEvents).OnUpdatedNotifier(n)
}

View File

@ -102,7 +102,7 @@ func (n *Notification) AfterFind() (err error) {
func (n *Notification) AddQueue(uid string, msg interface{}) {
data := &QueueData{uid, msg}
n.Queue = append(n.Queue, data)
log.WithFields(utils.ToFields(data, n)).Infoln(fmt.Sprintf("Notifier '%v' added new item (%v) to the queue. (%v queued)", n.Method, uid, len(n.Queue)))
log.WithFields(utils.ToFields(data, n)).Debug(fmt.Sprintf("Notifier '%v' added new item (%v) to the queue. (%v queued)", n.Method, uid, len(n.Queue)))
}
// CanTest returns true if the notifier implements the OnTest interface
@ -129,7 +129,7 @@ func asNotification(n Notifier) *Notification {
// AddNotifier accept a Notifier interface to be added into the array
func AddNotifiers(notifiers ...Notifier) error {
for _, n := range notifiers {
if isType(n, new(Notifier)) {
if utils.IsType(n, new(Notifier)) {
err := checkNotifierForm(n)
if err != nil {
return err
@ -252,7 +252,7 @@ func Init(n Notifier) (*Notification, error) {
if notify.Delay.Seconds() == 0 {
notify.Delay = time.Duration(1 * time.Second)
}
notify.testable = isType(n, new(Tester))
notify.testable = utils.IsType(n, new(Tester))
notify.Form = n.Select().Form
}
return notify, err
@ -261,7 +261,7 @@ func Init(n Notifier) (*Notification, error) {
// startAllNotifiers will start the go routine for each loaded notifier
func startAllNotifiers() {
for _, comm := range AllCommunications {
if isType(comm, new(Notifier)) {
if utils.IsType(comm, new(Notifier)) {
notify := comm.(Notifier)
if notify.Select().Enabled.Bool {
notify.Select().close()
@ -290,9 +290,9 @@ CheckNotifier:
msg := notification.Queue[0]
err := n.Send(msg.Data)
if err != nil {
log.WithFields(utils.ToFields(notification, msg)).Warnln(fmt.Sprintf("Notifier '%v' had an error: %v", notification.Method, err))
log.WithFields(utils.ToFields(notification, msg)).Error(fmt.Sprintf("Notifier '%v' had an error: %v", notification.Method, err))
} else {
log.WithFields(utils.ToFields(notification, msg)).Infoln(fmt.Sprintf("Notifier '%v' sent outgoing message (%v) %v left in queue.", notification.Method, msg.Id, len(notification.Queue)))
log.WithFields(utils.ToFields(notification, msg)).Debug(fmt.Sprintf("Notifier '%v' sent outgoing message (%v) %v left in queue.", notification.Method, msg.Id, len(notification.Queue)))
}
notification.makeLog(msg.Data)
if len(notification.Queue) > 1 {
@ -386,13 +386,6 @@ func (n *Notification) GetValue(dbField string) string {
return ""
}
// isType will return true if a variable can implement an interface
func isType(n interface{}, obj interface{}) bool {
one := reflect.TypeOf(n)
two := reflect.ValueOf(obj).Elem()
return one.Implements(two.Type())
}
// isEnabled returns true if the notifier is enabled
func isEnabled(n interface{}) bool {
notifier := n.(Notifier).Select()

View File

@ -548,6 +548,10 @@ func TmpRecords(dbFile string) error {
if err := InsertNotifierDB(); err != nil {
return err
}
log.Infoln("inserting integrations into database")
if err := InsertIntegratorDB(); err != nil {
return err
}
log.Infoln("loading all services")
if _, err := CoreApp.SelectAllServices(false); err != nil {
return err
@ -555,6 +559,9 @@ func TmpRecords(dbFile string) error {
if err := AttachNotifiers(); err != nil {
return err
}
if err := AddIntegrations(); err != nil {
return err
}
CoreApp.Notifications = notifier.AllCommunications
return nil
}
@ -584,6 +591,10 @@ func TmpRecords(dbFile string) error {
if err := InsertNotifierDB(); err != nil {
return err
}
log.Infoln("inserting integrations into database")
if err := InsertIntegratorDB(); err != nil {
return err
}
log.Infoln("loading all services")
if _, err := CoreApp.SelectAllServices(false); err != nil {
return err

View File

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

View File

@ -49,13 +49,17 @@ class Api {
}
async services_reorder (data) {
return axios.post('/api/services/reorder', data).then(response => (response.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))
}
@ -116,10 +120,38 @@ class Api {
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 login (username, password) {
const f = {username: username, password: password}
return axios.post('/api/login', qs.stringify(f))
@ -138,7 +170,11 @@ class Api {
}
token () {
return JSON.parse(localStorage.getItem(tokenKey));
const tk = localStorage.getItem(tokenKey)
if (!tk) {
return {};
}
return JSON.parse(tk);
}
authToken () {

View File

@ -17,8 +17,8 @@
<th scope="col"></th>
</tr>
</thead>
<draggable tag="tbody" v-model="servicesList" :list="$store.getters.servicesInOrder" :key="this.$store.getters.servicesInOrder.length" class="sortable" handle=".drag_icon">
<tr v-for="(service, index) in $store.getters.services" :key="index">
<draggable tag="tbody" v-model="servicesList" :key="$store.getters.servicesInOrder.length" class="sortable" handle=".drag_icon">
<tr v-for="(service, index) in $store.getters.servicesInOrder" :key="index">
<td>
<span class="drag_icon d-none d-md-inline">
<font-awesome-icon icon="bars" />
@ -66,7 +66,7 @@
</thead>
<draggable tag="tbody" v-model="groupsList" class="sortable_groups" handle=".drag_icon">
<tr v-for="(group, index) in $store.getters.groupsCleaned" v-bind:key="index">
<tr v-for="(group, index) in $store.getters.groupsInOrder" v-bind:key="index">
<td><span class="drag_icon d-none d-md-inline"><font-awesome-icon icon="bars" /></span> {{group.name}}</td>
<td>{{$store.getters.servicesInGroup(group.id).length}}</td>
<td>
@ -121,23 +121,23 @@
value.forEach((s, k) => {
data.push({service: s.id, order: k+1})
});
alert(JSON.stringify(data))
const ord = await Api.services_reorder(data)
alert(JSON.parse(ord))
await Api.services_reorder(data)
const services = await Api.services()
this.$store.commit('setServices', services)
}
},
groupsList: {
get() {
return this.$store.state.groups
return this.$store.state.groupsInOrder
},
async set(value) {
let data = [];
value.forEach((s, k) => {
data.push({group: s.id, order: k+1})
});
alert(JSON.stringify(data))
const ord = await Api.services_reorder(data)
alert(JSON.parse(ord))
await Api.groups_reorder(data)
const groups = await Api.groups()
this.$store.commit('setGroups', groups)
}
}
},

View File

@ -5,6 +5,7 @@
<thead>
<tr>
<th scope="col">Username</th>
<th scope="col">Type</th>
<th scope="col"></th>
</tr>
</thead>
@ -12,6 +13,8 @@
<tr v-for="(user, index) in $store.getters.users" v-bind:key="index" >
<td>{{user.username}}</td>
<td v-if="user.admin"><span class="badge badge-danger">ADMIN</span></td>
<td v-if="!user.admin"><span class="badge badge-primary">USER</span></td>
<td class="text-right">
<div class="btn-group">
<a @click.prevent="editUser(user, edit)" href="" class="btn btn-outline-secondary"><font-awesome-icon icon="user" /> Edit</a>

View File

@ -25,8 +25,8 @@
</div>
<div class="form-group row">
<div class="col-sm-12">
<button @click="saveGroup" type="submit" class="btn btn-block" :class="{'btn-primary': !group.name, 'btn-secondary': group.name}">
{{group.id ? "Update Group" : "Create Group"}}
<button @click="saveGroup" type="submit" :disabled="loading || group.name === ''" class="btn btn-block" :class="{'btn-primary': !group.id, 'btn-secondary': group.id}">
{{loading ? "Loading..." : group.id ? "Update Group" : "Create Group"}}
</button>
</div>
</div>
@ -52,6 +52,7 @@
},
data () {
return {
loading: false,
group: {
name: "",
public: true
@ -70,13 +71,14 @@
},
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}

View File

@ -0,0 +1,107 @@
<template>
<form @submit="updateIntegration">
<h4 class="text-capitalize">{{integration.full_name}}</h4>
<p class="small text-muted" v-html="integration.description"></p>
<div v-for="(field, index) in integration.fields" v-bind:key="index" class="form-group">
<label class="text-capitalize">{{field.name}}</label>
<textarea v-if="field.type === 'textarea'" v-model="field.value" rows="3" class="form-control"></textarea>
<input v-else :type="field.type" v-model="field.value" class="form-control">
<small class="form-text text-muted" v-html="field.description"></small>
</div>
<div class="col-12">
<div class="col-3">
<span @click="integration.enabled = !!integration.enabled" class="switch">
<input type="checkbox" name="enabled-option" class="switch" v-model="integration.enabled" v-bind:id="`switch-${integration.name}`" v-bind:checked="integration.enabled">
<label v-bind:for="`switch-${integration.name}`"></label>
</span>
</div>
<div v-if="services.length !== 0" class="col-12">
<table class="table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Domain</th>
<th scope="col">Port</th>
<th scope="col">Interval</th>
<th scope="col">Timeout</th>
<th scope="col">Type</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr v-for="(service, index) in services" v-bind:key="index">
<td><input v-model="service.name" type="text" style="width: 80pt"></td>
<td>{{service.domain}}</td>
<td>{{service.port}}</td>
<td><input v-model="service.check_interval" type="number" style="width: 35pt"></td>
<td><input v-model="service.timeout" type="number" style="width: 35pt"></td>
<td>{{service.type}}</td>
<td><button @click.prevent="addService(service)" v-bind:disabled="service.added" class="btn btn-sm btn-outline-primary">Add</button></td>
</tr>
</tbody>
</table>
</div>
{{out}}
<div class="col-12">
<button @click.prevent="updateIntegration" type="submit" class="btn btn-block btn-info">Fetch Services</button>
</div>
</div>
<div class="alert alert-danger d-none" role="alert"></div>
</form>
</template>
<script>
import Api from "../components/API";
export default {
name: 'FormIntegration',
props: {
integration: {
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)
window.console.log(out)
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
}
window.console.log(out)
const integrations = await Api.integrations()
this.$store.commit('setIntegrations', integrations)
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -74,7 +74,9 @@
<div class="form-group row">
<div class="col-sm-12">
<button @click="saveMessage" type="submit" class="btn btn-block" :class="{'btn-primary': !message.id, 'btn-secondary': message.id}">
<button @click="saveMessage"
:disabled="!message.title || !message.description"
type="submit" class="btn btn-block" :class="{'btn-primary': !message.id, 'btn-secondary': message.id}">
{{message.id ? "Edit Message" : "Create Message"}}
</button>
</div>

View File

@ -1,7 +1,14 @@
<template>
<form @submit="saveNotifier">
<div v-if="error" class="alert alert-danger col-12" role="alert">{{error}}</div>
<div v-if="ok" class="alert alert-success col-12" role="alert">
<i class="fa fa-smile-beam"></i> The {{notifier.method}} notifier is working correctly!
</div>
<h4 class="text-capitalize">{{notifier.title}}</h4>
<p class="small text-muted" v-html="notifier.description"></p>
<p class="small text-muted" v-html="notifier.description"/>
<div v-for="(form, index) in notifier.form" v-bind:key="index" class="form-group">
<label class="text-capitalize">{{form.title}}</label>
@ -32,30 +39,20 @@
<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}">
<i class="fa fa-check-circle"></i> {{saved ? "Saved" : "Save"}}
<i class="fa fa-check-circle"></i> {{loading ? "Loading..." : saved ? "Saved" : "Save"}}
</button>
</div>
<div class="col-12 col-sm-12">
<button @click="testNotifier" class="btn btn-secondary btn-block text-capitalize col-12 float-right"><i class="fa fa-vial"></i> Test Notifier</button>
<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 class="col-12 col-sm-12 mt-2">
<div class="alert alert-danger d-none" id="command-error" role="alert">
<i class="fa fa-exclamation-triangle"></i> {{notifier.method}} has an error!
</div>
<div class="alert alert-success d-none" id="command-success" role="alert">
<i class="fa fa-smile-beam"></i> The {{notifier.method}} notifier is working correctly!
</div>
</div>
</div>
<span class="d-block small text-center mt-3 mb-5">
<span class="text-capitalize">{{notifier.title}}</span> Notifier created by <a :href="notifier.author_url" target="_blank">{{notifier.author}}</a>
</span>
<div v-if="error" class="alert alert-danger d-none" id="alerter" role="alert"></div>
</form>
</template>
@ -72,8 +69,10 @@ export default {
},
data () {
return {
loading: false,
error: null,
saved: false,
ok: false,
}
},
mounted() {
@ -82,6 +81,7 @@ export default {
methods: {
async saveNotifier(e) {
e.preventDefault();
this.loading = true
let form = {}
this.notifier.form.forEach((f) => {
form[f.field] = this.notifier[f.field]
@ -93,12 +93,15 @@ export default {
const notifiers = await Api.notifiers()
this.$store.commit('setNotifiers', notifiers)
this.saved = true
this.loading = false
setTimeout(() => {
this.saved = false
}, 2000)
},
async testNotifier(e) {
e.preventDefault();
this.ok = false
this.loading = true
let form = {}
this.notifier.form.forEach((f) => {
form[f.field] = this.notifier[f.field]
@ -106,13 +109,13 @@ export default {
form.enabled = this.notifier.enabled
form.limits = parseInt(this.notifier.limits)
form.method = this.notifier.method
alert(JSON.stringify(form))
const tested = await Api.notifier_test(form)
if (tested === "ok") {
alert('This notifier seems to be working!')
if (tested === 'ok') {
this.ok = true
} else {
this.error = tested
}
this.loading = false
},
}
}

View File

@ -168,6 +168,7 @@
name: 'FormService',
data () {
return {
loading: false,
service: {
name: "",
type: "http",

View File

@ -11,7 +11,7 @@
<div class="form-group row">
<label class="col-sm-4 col-form-label">Username</label>
<div class="col-6 col-md-4">
<input v-model="user.username" type="text" class="form-control" placeholder="Username" required autocorrect="off" autocapitalize="none">
<input v-model="user.username" type="text" class="form-control" placeholder="Username" required autocorrect="off" autocapitalize="none" v-bind:readonly="user.id">
</div>
<div class="col-6 col-md-4">
<span @click="user.admin = !!user.admin" class="switch">
@ -40,8 +40,10 @@
</div>
<div class="form-group row">
<div class="col-sm-12">
<button @click="saveUser" class="btn btn-block" :class="{'btn-primary': !user.id, 'btn-secondary': user.id}">
{{user.id ? "Update User" : "Create User"}}
<button @click="saveUser"
:disabled="loading || !user.username || !user.email || !user.password || !user.confirm_password || (user.password !== user.confirm_password)"
class="btn btn-block" :class="{'btn-primary': !user.id, 'btn-secondary': user.id}">
{{loading ? "Loading..." : user.id ? "Update User" : "Create User"}}
</button>
</div>
</div>
@ -67,6 +69,7 @@
},
data () {
return {
loading: false,
user: {
username: "",
admin: false,
@ -91,11 +94,13 @@
},
async saveUser(e) {
e.preventDefault();
this.loading = true
if (this.user.id) {
await this.updateUser()
} else {
await this.createUser()
}
this.loading = false
},
async createUser() {
let user = this.user

View File

@ -0,0 +1,32 @@
<template>
<div class="col-12 bg-white p-4">
Help
</div>
</template>
<script>
export default {
name: 'Help',
components: {
},
data () {
return {
}
},
created() {
},
mounted() {
},
methods: {
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -60,7 +60,7 @@
this.error = true
} else if (auth.token) {
this.auth = Api.saveToken(this.username, auth.token)
await this.$store.dispatch('loadRequired')
await this.$store.dispatch('loadAdmin')
this.$router.push('/dashboard')
}
this.loading = false

View File

@ -0,0 +1,69 @@
<template>
<div class="col-12 bg-white p-4">
<p v-if="logs.length === 0" class="text-monospace sm">
Loading Logs...
</p>
<p v-for="(log, index) in logs.reverse()" class="text-monospace sm">{{log}}</p>
</div>
</template>
<script>
import Api from "../components/API";
export default {
name: 'Logs',
components: {
},
data () {
return {
logs: [],
last: "",
t: null
}
},
created() {
if (!this.t) {
this.t = setInterval(() => {
this.lastLog()
}, 650)
}
},
async mounted() {
await this.getLogs()
},
beforeDestroy() {
clearInterval(this.t)
},
methods: {
cleanLog(l) {
const splitLog = l.split(": ")
const last = splitLog.slice(1);
return last.join(": ")
},
async getLogs() {
const logs = await Api.logs()
this.logs = logs.reverse()
this.last = this.cleanLog(this.logs[this.logs.length-1])
},
async lastLog() {
const log = await Api.logs_last()
const cleanLast = this.cleanLog(log)
if (this.last !== cleanLast) {
this.last = cleanLast
this.logs.reverse().push(log)
}
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.sm {
font-size: 8pt;
}
</style>

View File

@ -5,23 +5,21 @@
<div class="nav flex-column nav-pills" id="v-pills-tab" role="tablist" aria-orientation="vertical">
<h6 class="text-muted">Main Settings</h6>
<a v-on:click="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 v-on:click="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 v-on:click="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"><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>
<h6 class="mt-4 text-muted">Notifiers</h6>
<a v-for="(notifier, index) in $store.getters.notifiers" v-bind:key="index" v-on:click="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">
<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}}
</a>
<h6 class="mt-4 text-muted">Integrations (beta)</h6>
<a class="nav-link text-capitalize" id="v-pills-integration-csv-tab" data-toggle="pill" href="#v-pills-integration-csv" role="tab" aria-controls="v-pills-integration-csv" aria-selected="false"><i class="fas fa-file-csv"></i> CSV File</a>
<a class="nav-link text-capitalize" id="v-pills-integration-docker-tab" data-toggle="pill" href="#v-pills-integration-docker" role="tab" aria-controls="v-pills-integration-docker" aria-selected="false"><i class="fab fa-docker"></i> Docker</a>
<a class="nav-link text-capitalize" id="v-pills-integration-traefik-tab" data-toggle="pill" href="#v-pills-integration-traefik" role="tab" aria-controls="v-pills-integration-traefik" aria-selected="false"><i class="fas fa-network-wired"></i> Traefik</a>
<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}}
</a>
</div>
</div>
@ -101,149 +99,25 @@
</thead>
<tbody>
<tr>
<td>/api/services/7/data?start=1577937580&amp;end=9999999999&amp;group=hour</td>
<td>13951</td>
<td>2020-01-15 20:00:10 -0800 -0800</td>
<tr v-for="(cache, index) in cache">
<td>{{cache.url}}</td>
<td>{{cache.size}}</td>
<td>{{cache.expiration}}</td>
</tr>
</tbody>
</table>
<a href="api/clear_cache" class="btn btn-danger btn-block">Clear Cache</a>
<a @click.prevent="clearCache" href="#" class="btn btn-danger btn-block">Clear Cache</a>
</div>
<div v-for="(notifier, index) in $store.getters.notifiers" v-bind:key="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`">
<Notifier :notifier="notifier"/>
</div>
<div class="tab-pane fade" id="v-pills-integration-csv" role="tabpanel" aria-labelledby="v-pills-integration-csv-tab">
<form class="integration_csv" action="settings/integrator/csv" method="POST">
<input type="hidden" name="integrator" class="form-control" value="csv">
<h4 class="text-capitalize">csv</h4>
<p class="small text-muted">Import multiple services from a CSV file. Please have your CSV file formatted with the correct amount of columns based on the <a href="https://raw.githubusercontent.com/hunterlong/statping/master/source/tmpl/bulk_import.csv">example file on Github</a>.</p>
<div class="form-group">
<label class="text-capitalize" for="input">input</label>
<textarea rows="3" class="form-control" name="input" id="input"></textarea>
</div>
<button type="submit" class="btn btn-block btn-info fetch_integrator">Fetch Services</button>
<div class="alert alert-danger d-none" role="alert"></div>
</form>
<div v-for="(integration, index) in $store.getters.integrations" v-bind:key="`${integration.name}_${index}`" class="tab-pane fade" v-bind:class="{active: liClass(`v-pills-integration-${integration.name}`), show: liClass(`v-pills-integration-${integration.name}`)}" v-bind:id="`v-pills-integration-${integration.name}`" role="tabpanel">
<FormIntegration :integration="integration"/>
</div>
<div class="tab-pane fade" id="v-pills-integration-docker" role="tabpanel" aria-labelledby="v-pills-integration-docker-tab">
<form class="integration_docker" action="settings/integrator/docker" method="POST">
<input type="hidden" name="integrator" class="form-control" value="docker">
<h4 class="text-capitalize">docker</h4>
<p class="small text-muted">Import multiple services from Docker by attaching the unix socket to Statping.
You can also do this in Docker by setting <u>-v /var/run/docker.sock:/var/run/docker.sock</u> in the Statping Docker container.
All of the containers with open TCP/UDP ports will be listed for you to choose which services you want to add. If you running Statping inside of a container,
this container must be attached to all networks you want to communicate with.</p>
<div class="form-group">
<label class="text-capitalize" for="path">path</label>
<input type="text" name="path" class="form-control" value="unix:///var/run/docker.sock" id="path">
<small class="form-text text-muted">The absolute path to the Docker unix socket</small>
</div>
<div class="form-group">
<label class="text-capitalize" for="version">version</label>
<input type="text" name="version" class="form-control" value="1.25" id="version">
<small class="form-text text-muted">Version number of Docker server</small>
</div>
<button type="submit" class="btn btn-block btn-info fetch_integrator">Fetch Services</button>
<div class="alert alert-danger d-none" id="integration_alerter" role="alert"></div>
</form>
</div>
<div class="tab-pane fade" id="v-pills-integration-traefik" role="tabpanel" aria-labelledby="v-pills-integration-traefik-tab">
<form class="integration_traefik" action="settings/integrator/traefik" method="POST">
<input type="hidden" name="integrator" class="form-control" value="traefik">
<h4 class="text-capitalize">traefik</h4>
<div class="form-group">
<label class="text-capitalize" for="endpoint">endpoint</label>
<input type="text" name="endpoint" class="form-control" value="http://localhost:8080" id="endpoint">
<small class="form-text text-muted">The URL for the traefik API Endpoint</small>
</div>
<div class="form-group">
<label class="text-capitalize" for="username">username</label>
<input type="text" name="username" class="form-control" value="" id="username">
<small class="form-text text-muted">Username for HTTP Basic Authentication</small>
</div>
<div class="form-group">
<label class="text-capitalize" for="password">password</label>
<input type="password" name="password" class="form-control" value="" id="password">
<small class="form-text text-muted">Password for HTTP Basic Authentication</small>
</div>
<button type="submit" class="btn btn-block btn-info fetch_integrator">Fetch Services</button>
<div class="alert alert-danger d-none" role="alert"></div>
</form>
</div>
<div class="tab-pane fade" id="v-pills-browse" role="tabpanel" aria-labelledby="v-pills-browse-tab">
</div>
<div class="tab-pane fade" id="v-pills-backups" role="tabpanel" aria-labelledby="v-pills-backups-tab">
<a href="backups/create" class="btn btn-primary btn-block">Backup Database</a>
</div>
</div>
</div>
@ -254,11 +128,13 @@
<script>
import Api from '../components/API';
import CoreSettings from '../forms/CoreSettings';
import FormIntegration from '../forms/Integration';
import Notifier from "../forms/Notifier";
export default {
name: 'Settings',
components: {
FormIntegration,
Notifier,
CoreSettings
},
@ -266,16 +142,15 @@
return {
tab: "v-pills-home-tab",
qrcode: "",
core: this.$store.getters.core
core: this.$store.getters.core,
cache: [],
}
},
async created() {
const core = await Api.core()
this.$store.commit('setCore', core)
const notifiers = await Api.notifiers()
this.$store.commit('setNotifiers', notifiers)
const qrurl = `statping://setup?domain=${core.domain}&api=${core.api_secret}`
this.qrcode = "https://chart.googleapis.com/chart?chs=500x500&cht=qr&chl=" + encodeURI(qrurl)
this.cache = await Api.cache()
},
beforeMount() {
@ -286,7 +161,11 @@
},
liClass (id) {
return this.tab === id
}
},
async clearCache () {
await Api.clearCache()
this.cache = await Api.cache()
}
}
}
</script>

View File

@ -1,3 +1,4 @@
import Help from './pages/Help';
import Index from "./pages/Index";
import Dashboard from "./pages/Dashboard";
import DashboardIndex from "./components/Dashboard/DashboardIndex";
@ -5,12 +6,15 @@ import DashboardUsers from "./components/Dashboard/DashboardUsers";
import DashboardServices from "./components/Dashboard/DashboardServices";
import EditService from "./components/Dashboard/EditService";
import DashboardMessages from "./components/Dashboard/DashboardMessages";
import Logs from './pages/Logs';
import Settings from "./pages/Settings";
import Login from "./pages/Login";
import Service from "./pages/Service";
import VueRouter from "vue-router";
import Setup from "./forms/Setup";
import Api from "./components/API";
const routes = [
{
path: '/setup',
@ -52,10 +56,10 @@ const routes = [
component: Settings
},{
path: 'logs',
component: DashboardUsers
component: Logs
},{
path: 'help',
component: DashboardUsers
component: Help
}]
},
{
@ -79,15 +83,13 @@ const router = new VueRouter({
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
const tk = localStorage.getItem("statping_user")
if (tk !== null) {
next()
return
}
if (to.path !== '/login') {
const tk = Api.token()
if (to.path !== '/login' && !tk.token) {
next('/login')
return
}
next()
return
} else {
next()
}

View File

@ -25,7 +25,8 @@ export default new Vuex.Store({
groups: [],
messages: [],
users: [],
notifiers: []
notifiers: [],
integrations: []
},
getters: {
hasAllData: state => state.hasAllData,
@ -37,9 +38,10 @@ export default new Vuex.Store({
messages: state => state.messages,
users: state => state.users,
notifiers: state => state.notifiers,
integrations: state => state.integrations,
servicesInOrder: state => state.services.sort((a, b) => a.order_id - b.order_id),
groupsCleaned: state => state.groups.filter(g => g.name !== ''),
groupsInOrder: state => state.groups.filter(g => g.name !== '').sort((a, b) => a.order_id - b.order_id),
serviceById: (state) => (id) => {
return state.services.find(s => s.id === id)
@ -93,6 +95,9 @@ export default new Vuex.Store({
},
setNotifiers(state, notifiers) {
state.notifiers = notifiers
},
setIntegrations(state, integrations) {
state.integrations = integrations
}
},
actions: {
@ -113,6 +118,8 @@ export default new Vuex.Store({
context.commit("setNotifiers", notifiers);
const users = await Api.users()
context.commit("setUsers", users);
const integrations = await Api.integrations()
context.commit("setIntegrations", integrations);
}
}
});

View File

@ -86,9 +86,31 @@ func apiCoreHandler(w http.ResponseWriter, r *http.Request) {
returnJson(core.CoreApp, w, r)
}
type cacheJson struct {
URL string `json:"url"`
Expiration int64 `json:"expiration"`
Size int `json:"size"`
}
func apiCacheHandler(w http.ResponseWriter, r *http.Request) {
cache := CacheStorage
var cacheList []cacheJson
for k, v := range cache.List() {
cacheList = append(cacheList, cacheJson{
URL: k,
Expiration: v.Expiration,
Size: len(v.Content),
})
}
returnJson(cacheList, w, r)
}
func apiClearCacheHandler(w http.ResponseWriter, r *http.Request) {
CacheStorage = NewStorage()
http.Redirect(w, r, basePath, http.StatusSeeOther)
output := apiResponse{
Status: "success",
}
returnJson(output, w, r)
}
func sendErrorJson(err error, w http.ResponseWriter, r *http.Request) {

View File

@ -76,7 +76,7 @@ func logsHandler(w http.ResponseWriter, r *http.Request) {
logs = append(logs, utils.LastLines[i].FormatForHtml()+"\r\n")
}
utils.LockLines.Unlock()
ExecuteResponse(w, r, "logs.gohtml", logs, nil)
returnJson(logs, w, r)
}
func logsLineHandler(w http.ResponseWriter, r *http.Request) {

View File

@ -195,7 +195,6 @@ func IsAdmin(r *http.Request) bool {
if err != nil {
return false
}
fmt.Println("user: ", claim.Username, claim.Admin)
return claim.Admin
}

View File

@ -1,7 +1,11 @@
package handlers
import (
"encoding/json"
"github.com/gorilla/mux"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"net/http"
)
@ -9,3 +13,58 @@ func apiAllIncidentsHandler(w http.ResponseWriter, r *http.Request) {
incidents := core.AllIncidents()
returnJson(incidents, w, r)
}
func apiCreateIncidentHandler(w http.ResponseWriter, r *http.Request) {
var incident *types.Incident
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&incident)
if err != nil {
sendErrorJson(err, w, r)
return
}
newIncident := core.ReturnIncident(incident)
_, err = newIncident.Create()
if err != nil {
sendErrorJson(err, w, r)
return
}
sendJsonAction(newIncident, "create", w, r)
}
func apiIncidentUpdateHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
incident, err := core.SelectIncident(utils.ToInt(vars["id"]))
if err != nil {
sendErrorJson(err, w, r)
return
}
decoder := json.NewDecoder(r.Body)
err = decoder.Decode(&incident)
if err != nil {
sendErrorJson(err, w, r)
return
}
_, err = incident.Update()
if err != nil {
sendErrorJson(err, w, r)
return
}
sendJsonAction(incident, "update", w, r)
}
func apiDeleteIncidentHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
incident, err := core.SelectIncident(utils.ToInt(vars["id"]))
if err != nil {
sendErrorJson(err, w, r)
return
}
err = incident.Delete()
if err != nil {
sendErrorJson(err, w, r)
return
}
sendJsonAction(incident, "delete", w, r)
}

View File

@ -1,8 +1,10 @@
package handlers
import (
"encoding/json"
"github.com/gorilla/mux"
"github.com/hunterlong/statping/core/integrations"
"github.com/hunterlong/statping/types"
"net/http"
)
@ -11,20 +13,41 @@ func apiAllIntegrationsHandler(w http.ResponseWriter, r *http.Request) {
returnJson(integrations, w, r)
}
func apiIntegrationHandler(w http.ResponseWriter, r *http.Request) {
func apiIntegrationViewHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
intg := vars["name"]
r.ParseForm()
for k, v := range r.PostForm {
log.Info(k, v)
}
integration, err := integrations.Find(intg)
intgr, err := integrations.Find(vars["name"])
if err != nil {
sendErrorJson(err, w, r)
return
}
list, err := integration.List()
returnJson(intgr.Get(), w, r)
}
func apiIntegrationHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
intgr, err := integrations.Find(vars["name"])
if err != nil {
sendErrorJson(err, w, r)
return
}
var intJson *types.Integration
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&intJson); err != nil {
sendErrorJson(err, w, r)
return
}
integration := intgr.Get()
integration.Enabled = intJson.Enabled
integration.Fields = intJson.Fields
if err := integrations.Update(integration); err != nil {
sendErrorJson(err, w, r)
return
}
list, err := intgr.List()
if err != nil {
sendErrorJson(err, w, r)
return

View File

@ -65,7 +65,8 @@ func sendLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t1 := utils.Now()
t2 := utils.Now().Sub(t1)
if r.RequestURI == "/logs/line" {
if r.RequestURI == "/api/logs" || r.RequestURI == "/api/logs/last" {
next.ServeHTTP(w, r)
return
}
log.WithFields(utils.ToFields(w, r)).

View File

@ -76,11 +76,15 @@ func Router() *mux.Router {
r.Handle("/api/setup", http.HandlerFunc(processSetupHandler)).Methods("POST")
r.Handle("/api/logout", http.HandlerFunc(logoutHandler))
r.Handle("/api/renew", authenticated(apiRenewHandler, false))
r.Handle("/api/cache", authenticated(apiCacheHandler, false)).Methods("GET")
r.Handle("/api/clear_cache", authenticated(apiClearCacheHandler, false))
r.Handle("/api/core", authenticated(apiCoreHandler, false)).Methods("POST")
r.Handle("/api/logs", authenticated(logsHandler, false)).Methods("GET")
r.Handle("/api/logs/last", authenticated(logsLineHandler, false)).Methods("GET")
// API INTEGRATIONS Routes
r.Handle("/api/integrations", authenticated(apiAllIntegrationsHandler, false)).Methods("GET")
r.Handle("/api/integrations/{name}", authenticated(apiIntegrationHandler, false)).Methods("GET")
r.Handle("/api/integrations/{name}", authenticated(apiIntegrationViewHandler, false)).Methods("GET")
r.Handle("/api/integrations/{name}", authenticated(apiIntegrationHandler, false)).Methods("POST")
// API GROUPS Routes
@ -109,6 +113,9 @@ func Router() *mux.Router {
// API INCIDENTS Routes
r.Handle("/api/incidents", readOnly(apiAllIncidentsHandler, false)).Methods("GET")
r.Handle("/api/incidents", authenticated(apiCreateIncidentHandler, false)).Methods("POST")
r.Handle("/api/incidents/:id", authenticated(apiIncidentUpdateHandler, false)).Methods("POST")
r.Handle("/api/incidents/:id", authenticated(apiDeleteIncidentHandler, false)).Methods("DELETE")
// API USER Routes
r.Handle("/api/users", authenticated(apiAllUsersHandler, false)).Methods("GET")

View File

@ -1,20 +1,20 @@
package types
type Integration struct {
ShortName string `json:"name"`
Name string `json:"full_name"`
Icon string `json:"-"`
Description string `json:"description"`
Enabled bool `json:"enabled"`
Fields []*IntegrationField `json:"fields"`
ShortName string `gorm:"column:name" json:"name"`
Name string `gorm:"-" json:"full_name,omitempty"`
Icon string `gorm:"-" json:"-"`
Description string `gorm:"-" json:"description,omitempty"`
Enabled bool `gorm:"column:enabled;default:false" json:"enabled"`
Fields []*IntegrationField `gorm:"column:fields" json:"fields"`
}
type IntegrationField struct {
Name string `json:"name"`
Value interface{} `json:"value"`
Type string `json:"type"`
Description string `json:"description,omitempty"`
MimeType string `json:"mime_type,omitempty"`
Name string `gorm:"-" json:"name"`
Value interface{} `gorm:"-" json:"value"`
Type string `gorm:"-" json:"type"`
Description string `gorm:"-" json:"description,omitempty"`
MimeType string `gorm:"-" json:"mime_type,omitempty"`
}
type Integrator interface {

View File

@ -246,6 +246,13 @@ func CopyFile(src, dst string) error {
return out.Close()
}
// IsType will return true if a variable can implement an interface
func IsType(n interface{}, obj interface{}) bool {
one := reflect.TypeOf(n)
two := reflect.ValueOf(obj).Elem()
return one.Implements(two.Type())
}
// Command will run a terminal command with 'sh -c COMMAND' and return stdout and errOut as strings
// in, out, err := Command("sass assets/scss assets/css/base.css")
func Command(cmd string) (string, string, error) {