design changes, incidents

pull/483/head^2
Hunter Long 2020-04-06 17:06:46 -07:00
parent 2ab4fd97f3
commit 3e791c2c74
14 changed files with 188 additions and 59 deletions

View File

@ -1,3 +1,7 @@
# 0.90.23
- Added Incident Reporting
- Added Cypress tests
# 0.90.22
- Added range input types for integer form fields
- Modified Sentry error logging details

View File

@ -0,0 +1,36 @@
/// <reference types="cypress" />
import "../support/commands"
context('Incidents Tests', () => {
beforeEach(() => {
cy.restoreLocalStorageCache();
});
afterEach(() => {
cy.saveLocalStorageCache();
});
it('should Login', () => {
cy.visit('/login')
cy.get('#username').clear().type('admin')
cy.get('#password').clear().type('admin')
cy.get('button[type="submit"]').click()
cy.get('.navbar-brand').should('contain', 'Statping')
cy.getCookies()
cy.getCookies().should('have.length', 1)
})
it('should create new incident', () => {
cy.visit('/dashboard')
cy.wait(3000)
cy.get('.service_block').eq(0).find(".incident").click()
cy.get('#title').clear().type('Downtime')
cy.get('#description').clear().type('Recently we found an issue with authentication')
cy.get('button[type="submit"]').click()
})
})

View File

@ -26,35 +26,35 @@ context('Services Tests', () => {
it('should goto services', () => {
cy.visit('/dashboard/services')
cy.get(':nth-child(1) > .card-body > .table > tbody > tr').should('have.length', 5)
cy.get('#services_list > tr').should('have.length', 5)
cy.get('.sortable_groups > tr').should('have.length', 3)
})
it('should create new HTTP service', () => {
cy.visit('/dashboard/create_service')
cy.get(':nth-child(1) > .card-body > :nth-child(1) > .col-sm-8 > .form-control').clear().type('HTTP Service')
cy.get('#name').clear().type('HTTP Service')
cy.get('#service_type').select('http')
cy.get('#service_url').clear().type('http://localhost:8888')
cy.get('#service_response_code').clear().type('200')
cy.get('#service_interval').clear().type('30')
cy.get(':nth-child(3) > .card-body > :nth-child(2) > .col-sm-8 > .form-control').clear().type('5')
cy.get('#service_interval').invoke('val', 30).trigger('change')
cy.get('#timeout').invoke('val', 5).trigger('change')
cy.get('#permalink').clear().type('http_service')
cy.get('#notify_after').clear().type('3')
cy.get('#notify_after').invoke('val', 3).trigger('change')
cy.get('button[type="submit"]').click()
})
it('should create new TCP service', () => {
cy.visit('/dashboard/create_service')
cy.get(':nth-child(1) > .card-body > :nth-child(1) > .col-sm-8 > .form-control').clear().type('TCP Service')
cy.get('#name').clear().type('TCP Service')
cy.get('#service_type').select('tcp')
cy.get('#service_url').clear().type('localhost')
cy.get('#service_port').clear().type('8888')
cy.get('#service_interval').clear().type('30')
cy.get(':nth-child(3) > .card-body > :nth-child(2) > .col-sm-8 > .form-control').clear().type('5')
cy.get('#notify_after').clear().type('3')
cy.get('#service_interval').invoke('val', 30).trigger('change')
cy.get('#timeout').invoke('val', 5).trigger('change')
cy.get('#notify_after').invoke('val', 3).trigger('change')
cy.get('#permalink').clear().type('tcp_service')
@ -63,14 +63,14 @@ context('Services Tests', () => {
it('should create new UDP service', () => {
cy.visit('/dashboard/create_service')
cy.get(':nth-child(1) > .card-body > :nth-child(1) > .col-sm-8 > .form-control').clear().type('UDP Service')
cy.get('#name').clear().type('UDP Service')
cy.get('#service_type').select('udp')
cy.get('#service_url').clear().type('8.8.8.8')
cy.get('#service_port').clear().type('53')
cy.get('#service_interval').clear().type('30')
cy.get(':nth-child(3) > .card-body > :nth-child(2) > .col-sm-8 > .form-control').clear().type('5')
cy.get('#notify_after').clear().type('3')
cy.get('#service_interval').invoke('val', 30).trigger('change')
cy.get('#timeout').invoke('val', 5).trigger('change')
cy.get('#notify_after').invoke('val', 3).trigger('change')
cy.get('#permalink').clear().type('udp_service')
@ -79,12 +79,12 @@ context('Services Tests', () => {
it('should create new ICMP service', () => {
cy.visit('/dashboard/create_service')
cy.get(':nth-child(1) > .card-body > :nth-child(1) > .col-sm-8 > .form-control').clear().type('ICMP Service')
cy.get('#name').clear().type('ICMP Service')
cy.get('#service_type').select('icmp')
cy.get('#service_url').clear().type('8.8.8.8')
cy.get('#service_interval').clear().type('30')
cy.get('#notify_after').clear().type('3')
cy.get('#service_interval').invoke('val', 30).trigger('change')
cy.get('#notify_after').invoke('val', 3).trigger('change')
cy.get('#permalink').clear().type('icmp_service')
@ -93,7 +93,16 @@ context('Services Tests', () => {
it('should confirm new services', () => {
cy.visit('/dashboard/services')
cy.get(':nth-child(1) > .card-body > .table > tbody > tr').should('have.length', 9)
cy.get('#services_list > tr').should('have.length', 9)
})
it('should delete new services', () => {
cy.visit('/dashboard/services')
cy.get('#services_list > tr').eq(0).find('.btn-danger').click()
cy.get('#services_list > tr').eq(0).find('.btn-danger').click()
cy.get('#services_list > tr').eq(0).find('.btn-danger').click()
cy.get('#services_list > tr').eq(0).find('.btn-danger').click()
cy.get('#services_list > tr').should('have.length', 4)
})
})

View File

@ -4,6 +4,7 @@
<meta charset="utf-8">
<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">
<base href="/">
<title>Statping</title>
</head>
<body>

View File

@ -16,7 +16,7 @@
</div>
</div>
<div v-for="(service, index) in $store.getters.services" v-bind:key="index">
<div v-for="(service, index) in $store.getters.services" class="service_block" v-bind:key="index">
<ServiceInfo :service=service />
</div>
</div>

View File

@ -9,7 +9,7 @@
<th scope="col"></th>
</tr>
</thead>
<draggable tag="tbody" v-model="servicesList" handle=".drag_icon">
<draggable id="services_list" tag="tbody" v-model="servicesList" handle=".drag_icon">
<tr v-for="(service, index) in $store.getters.servicesInOrder" :key="service.id">
<td>
<span v-if="$store.state.admin" class="drag_icon d-none d-md-inline">

View File

@ -9,6 +9,7 @@
<GroupServiceFailures :service="service"/>
<IncidentsBlock :service="service"/>
</a>
</div>
@ -18,10 +19,12 @@
<script>
import Api from '../../API';
import GroupServiceFailures from './GroupServiceFailures';
import IncidentsBlock from './IncidentsBlock';
export default {
name: 'Group',
components: {
IncidentsBlock,
GroupServiceFailures
},

View File

@ -0,0 +1,45 @@
<template>
<div class="row mt-4">
<div v-for="(incident, i) in incidents" class="col-12">
<h6><span class="badge badge-secondary">New</span>
{{incident.title}}
<span class="font-2">{{incident.created_at}}</span>
</h6>
{{incident.description}}
{{incident}}
</div>
</div>
</template>
<script>
import Api from '../../API';
export default {
name: 'IncidentsBlock',
props: {
service: {
type: Object
}
},
data() {
return {
incidents: null,
}
},
mounted () {
this.getIncidents()
},
methods: {
async getIncidents() {
this.incidents = await Api.incidents_service(this.service)
},
async getIncidentsUpdates() {
this.incidents = await Api.incidents_service(this.service)
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -1,7 +1,8 @@
<template v-if="service">
<div class="col-12 card mb-4" style="min-height: 280px;" :class="{'offline-card': !service.online}">
<div class="card-body p-3 p-md-1 pt-md-3 pb-md-1">
<h4 class="card-title mb-4"><router-link :to="serviceLink(service)">{{service.name}}</router-link>
<h4 class="card-title mb-4">
<router-link :to="serviceLink(service)">{{service.name}}</router-link>
<span class="badge float-right" :class="{'badge-success': service.online, 'badge-danger': !service.online}">
{{service.online ? "ONLINE" : "OFFLINE"}}
</span>
@ -44,13 +45,13 @@
</div>
<div class="col-4">
<button @click.prevent="Tab('incident')" class="btn btn-block btn-outline-secondary" :class="{'text-white btn-secondary': openTab==='incident'}" >Create Incident</button>
<button @click.prevent="Tab('incident')" class="btn btn-block btn-outline-secondary incident" :class="{'text-white btn-secondary': openTab==='incident'}" >Incidents</button>
</div>
<div class="col-4">
<button @click.prevent="Tab('message')" class="btn btn-block btn-outline-secondary" :class="{'text-white btn-secondary': openTab==='message'}">Create Announcement</button>
<button @click.prevent="Tab('message')" class="btn btn-block btn-outline-secondary message" :class="{'text-white btn-secondary': openTab==='message'}">Announcements</button>
</div>
<div class="col-4">
<button @click.prevent="Tab('failures')" class="btn btn-block btn-outline-secondary" :disabled="service.stats.failures === 0" :class="{'text-white btn-secondary': openTab==='failures'}">
<button @click.prevent="Tab('failures')" class="btn btn-block btn-outline-secondary failures" :disabled="service.stats.failures === 0" :class="{'text-white btn-secondary': openTab==='failures'}">
Failures <span class="badge badge-danger float-right mt-1">{{service.stats.failures}}</span></button>
</div>

View File

@ -3,7 +3,7 @@
<div v-for="(incident, i) in incidents" class="card contain-card text-black-50 bg-white mb-4">
<div class="card-header">Incident: {{incident.title}}
<button v-if="IsAdmin()" @click="deleteIncident(incident)" class="btn btn-sm btn-danger float-right">
<button @click="deleteIncident(incident)" class="btn btn-sm btn-danger float-right">
<font-awesome-icon icon="times" /> Delete
</button></div>
<div class="card-body bg-light pt-1">
@ -15,7 +15,7 @@
</div>
<div v-if="IsAdmin()" class="card contain-card text-black-50 bg-white mb-5">
<div class="card contain-card text-black-50 bg-white mb-5">
<div class="card-header">Create Incident for {{service.name}}</div>
<div class="card-body">
<form @submit.prevent="createIncident">
@ -79,16 +79,16 @@
await this.loadIncidents()
},
methods: {
async loadIncidents() {
this.incidents = await Api.incidents_service(this.service)
},
async loadIncidents() {
this.incidents = await Api.incidents_service(this.service)
},
async createIncident() {
await Api.incident_create(this.service, this.incident)
await Api.incident_create(this.service, this.incident)
await this.loadIncidents()
this.incident = {
title: "",
description: "",
service: this.service.id,
description: "",
service: this.service.id,
}
},
async deleteIncident(incident) {

View File

@ -6,7 +6,7 @@
<div class="form-group row">
<label class="col-sm-4 col-form-label">Service Name</label>
<div class="col-sm-8">
<input v-model="service.name" @input="updatePermalink" type="text" name="name" class="form-control" placeholder="Server Name" required spellcheck="false" autocorrect="off">
<input v-model="service.name" @input="updatePermalink" id="name" type="text" name="name" class="form-control" placeholder="Server Name" required spellcheck="false" autocorrect="off">
<small class="form-text text-muted">Give your service a name you can recognize</small>
</div>
</div>
@ -101,7 +101,7 @@
<label class="col-sm-4 col-form-label">Request Timeout</label>
<div class="col-sm-8">
<span class="slider-info">{{secondsHumanize(service.timeout)}}</span>
<input v-model="service.timeout" type="range" name="timeout" class="slider" min="1" max="180">
<input v-model="service.timeout" type="range" id="timeout" name="timeout" class="slider" min="1" max="180">
<small class="form-text text-muted">If the endpoint does not respond within this time it will be considered to be offline</small>
</div>
</div>

View File

@ -16,7 +16,7 @@ func Samples() error {
group2 := &Group{
Name: "Linked Services",
Public: null.NewNullBool(false),
Public: null.NewNullBool(true),
Order: 1,
}
if err := group2.Create(); err != nil {

View File

@ -1,5 +1,10 @@
package incidents
import (
"github.com/statping/statping/utils"
"time"
)
func Samples() error {
incident1 := &Incident{
Title: "Github Downtime",
@ -10,30 +15,12 @@ func Samples() error {
return err
}
i1 := &IncidentUpdate{
IncidentId: incident1.Id,
Message: "Github's page for Statping seems to be sending a 501 error.",
Type: "Investigating",
incident2 := &Incident{
Title: "Recent Downtime",
Description: "We've noticed an issue with authentications and we're looking into it now.",
ServiceId: 4,
}
if err := i1.Create(); err != nil {
return err
}
i2 := &IncidentUpdate{
IncidentId: incident1.Id,
Message: "Problem is continuing and we are looking at the issues.",
Type: "Update",
}
if err := i2.Create(); err != nil {
return err
}
i3 := &IncidentUpdate{
IncidentId: incident1.Id,
Message: "Github is now back online and everything is working.",
Type: "Resolved",
}
if err := i3.Create(); err != nil {
if err := incident2.Create(); err != nil {
return err
}
@ -41,13 +28,56 @@ func Samples() error {
}
func SamplesUpdates() error {
u1 := &IncidentUpdate{
t := utils.Now()
i1 := &IncidentUpdate{
IncidentId: 1,
Message: "Github's page for Statping seems to be sending a 501 error.",
Type: "Investigating",
CreatedAt: t.Add(-60 * time.Minute),
}
if err := i1.Create(); err != nil {
return err
}
i2 := &IncidentUpdate{
IncidentId: 1,
Message: "Problem is continuing and we are looking at the issues.",
Type: "Update",
CreatedAt: t.Add(-30 * time.Minute),
}
if err := i2.Create(); err != nil {
return err
}
i3 := &IncidentUpdate{
IncidentId: 1,
Message: "Github is now back online and everything is working.",
Type: "Resolved",
CreatedAt: t.Add(-5 * time.Minute),
}
if err := i3.Create(); err != nil {
return err
}
u1 := &IncidentUpdate{
IncidentId: 2,
Message: "Github is now back online and everything is working.",
Type: "Resolved",
CreatedAt: t.Add(-120 * time.Minute),
}
if err := u1.Create(); err != nil {
return err
}
u2 := &IncidentUpdate{
IncidentId: 2,
Message: "Github is now back online and everything is working.",
Type: "Resolved",
CreatedAt: t.Add(-60 * time.Minute),
}
if err := u2.Create(); err != nil {
return err
}
return nil
}

View File

@ -1 +1 @@
0.90.22
0.90.23