mirror of https://github.com/statping/statping
PCORE-2161: Adding edit option to created incidents in admin page for external status page (#33)
* feat: added edit option for created incident and fixed bugs in dashboard service summary * feat: added loading ui when adding incidents * fix: downtime page log errors * fix: downtime servie name error * fix: minor style changepull/1113/head
parent
b86b998808
commit
64be130929
|
@ -148,6 +148,10 @@ class Api {
|
||||||
return axios.post('api/services/' + service_id + '/incidents', data).then(response => (response.data))
|
return axios.post('api/services/' + service_id + '/incidents', data).then(response => (response.data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async incident_edit(incident_id, data) {
|
||||||
|
return axios.patch('api/incidents/' + incident_id, data).then(response => (response.data))
|
||||||
|
}
|
||||||
|
|
||||||
async incident_delete(incident) {
|
async incident_delete(incident) {
|
||||||
return axios.delete('api/incidents/' + incident.id).then(response => (response.data))
|
return axios.delete('api/incidents/' + incident.id).then(response => (response.data))
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,8 +53,8 @@
|
||||||
:key="downtime.id"
|
:key="downtime.id"
|
||||||
>
|
>
|
||||||
<td>
|
<td>
|
||||||
<span>
|
<span :class="{'text-danger': !downtime.service}">
|
||||||
{{ downtime.service.name }}
|
{{ (downtime.service && downtime.service.name) || 'Deleted service' }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="d-none d-md-table-cell">
|
<td class="d-none d-md-table-cell">
|
||||||
|
@ -65,16 +65,14 @@
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="d-none d-md-table-cell">
|
<td class="d-none d-md-table-cell">
|
||||||
<span
|
<span>
|
||||||
class=""
|
|
||||||
>
|
|
||||||
{{ downtime.end ? niceDateWithYear(downtime.end) : 'Ongoing' }}
|
{{ downtime.end ? niceDateWithYear(downtime.end) : 'Ongoing' }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="d-none d-md-table-cell">
|
<td class="d-none d-md-table-cell">
|
||||||
<span
|
<span
|
||||||
class="badge text-uppercase"
|
class="badge text-uppercase"
|
||||||
:class="[downtime.sub_status === 'down' ? 'badge-danger' : 'badge-warning' ]"
|
:class="[downtime.sub_status === 'down' ? 'badge-danger' : 'badge-warning']"
|
||||||
>
|
>
|
||||||
{{ downtime.sub_status }}
|
{{ downtime.sub_status }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -87,7 +85,7 @@
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<div class="btn-group">
|
<div v-if="downtime.service" class="btn-group">
|
||||||
<button
|
<button
|
||||||
v-if="$store.state.admin"
|
v-if="$store.state.admin"
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
|
|
|
@ -3,9 +3,15 @@
|
||||||
|
|
||||||
<div v-for="incident in incidents" :key="incident.id" class="card contain-card mb-4">
|
<div v-for="incident in incidents" :key="incident.id" class="card contain-card mb-4">
|
||||||
<div class="card-header">Incident: {{incident.title}}
|
<div class="card-header">Incident: {{incident.title}}
|
||||||
<button @click="deleteIncident(incident)" class="btn btn-sm btn-danger float-right">
|
<div v-if="$store.state.admin" class="btn-group float-right">
|
||||||
<font-awesome-icon icon="times" />
|
<button @click.prevent="editIncident(incident)" class="btn btn-sm btn-outline-secondary" type="button">
|
||||||
|
<FontAwesomeIcon icon="edit" />
|
||||||
</button>
|
</button>
|
||||||
|
<button @click.prevent="deleteIncident(incident)" class="btn btn-sm btn-danger" type="button" :disabled="incidentId === incident.id && isLoading">
|
||||||
|
<FontAwesomeIcon v-if="incidentId === incident.id && isLoading" icon="circle-notch" spin />
|
||||||
|
<FontAwesomeIcon v-else icon="trash" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormIncidentUpdates :incident="incident"/>
|
<FormIncidentUpdates :incident="incident"/>
|
||||||
|
@ -15,9 +21,16 @@
|
||||||
|
|
||||||
|
|
||||||
<div class="card contain-card">
|
<div class="card contain-card">
|
||||||
<div class="card-header">Create Incident</div>
|
<div class="card-header">{{incident.id ? `${$t('update')} ${incident.title}` : $t('incident_create')}}
|
||||||
|
<transition name="slide-fade">
|
||||||
|
<button @click="resetIncident" v-if="incident.id" class="btn btn-sm float-right btn-danger btn-sm">
|
||||||
|
{{ $t('cancel') }}
|
||||||
|
</button>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form @submit.prevent="createIncident">
|
<form @submit.prevent="saveMessage">
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-4 col-form-label">Title</label>
|
<label class="col-sm-4 col-form-label">Title</label>
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
|
@ -34,10 +47,11 @@
|
||||||
|
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button @click.prevent="createIncident"
|
<button @click.prevent="saveMessage"
|
||||||
:disabled="!incident.title || !incident.description"
|
:disabled="!incident.title || !incident.description || isLoading"
|
||||||
type="submit" class="btn btn-block btn-primary">
|
type="submit" class="btn btn-block btn-primary">
|
||||||
Create Incident
|
{{incident.id ? $t('incident_edit') : $t('incident_create')}}
|
||||||
|
<FontAwesomeIcon v-if="isLoading" icon="circle-notch" spin />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,13 +68,15 @@ import Api from "../../API";
|
||||||
|
|
||||||
const FormIncidentUpdates = () => import(/* webpackChunkName: "dashboard" */ '@/forms/IncidentUpdates')
|
const FormIncidentUpdates = () => import(/* webpackChunkName: "dashboard" */ '@/forms/IncidentUpdates')
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Incidents',
|
name: 'Incidents',
|
||||||
components: {FormIncidentUpdates},
|
components: {FormIncidentUpdates},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
serviceID: 0,
|
serviceID: 0,
|
||||||
incidents: [],
|
incidents: [],
|
||||||
|
isLoading: false,
|
||||||
|
incidentId: null,
|
||||||
incident: {
|
incident: {
|
||||||
title: "",
|
title: "",
|
||||||
description: "",
|
description: "",
|
||||||
|
@ -81,12 +97,18 @@ const FormIncidentUpdates = () => import(/* webpackChunkName: "dashboard" */ '@/
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
async delete(i) {
|
async delete(i) {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.incidentId = i.id;
|
||||||
|
|
||||||
this.res = await Api.incident_delete(i)
|
this.res = await Api.incident_delete(i)
|
||||||
if (this.res.status === "success") {
|
if (this.res.status === "success") {
|
||||||
this.incidents = this.incidents.filter(obj => obj.id !== i.id);
|
this.incidents = this.incidents.filter(obj => obj.id !== i.id);
|
||||||
//await this.loadIncidents()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.isLoading = false;
|
||||||
|
this.incidentId = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteIncident(incident) {
|
async deleteIncident(incident) {
|
||||||
const modal = {
|
const modal = {
|
||||||
visible: true,
|
visible: true,
|
||||||
|
@ -99,23 +121,62 @@ const FormIncidentUpdates = () => import(/* webpackChunkName: "dashboard" */ '@/
|
||||||
this.$store.commit("setModal", modal)
|
this.$store.commit("setModal", modal)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async saveMessage() {
|
||||||
|
if (this.incident.id) {
|
||||||
|
this.updateIncident();
|
||||||
|
} else {
|
||||||
|
this.createIncident();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async createIncident() {
|
async createIncident() {
|
||||||
this.res = await Api.incident_create(this.serviceID, this.incident)
|
this.isLoading = true;
|
||||||
if (this.res.status === "success") {
|
|
||||||
this.incidents.push(this.res.output) // this is better in terms of not having to querry the db to get a fresh copy of all updates
|
const res = await Api.incident_create(this.serviceID, this.incident)
|
||||||
|
if (res.status === "success") {
|
||||||
|
this.incidents.push(res.output) // this is better in terms of not having to querry the db to get a fresh copy of all updates
|
||||||
//await this.loadIncidents()
|
//await this.loadIncidents()
|
||||||
} // TODO: further error checking here... maybe alert user it failed with modal or so
|
} // TODO: further error checking here... maybe alert user it failed with modal or so
|
||||||
|
|
||||||
// reset the form data
|
this.resetIncident();
|
||||||
this.incident = {
|
this.isLoading = false;
|
||||||
title: "",
|
},
|
||||||
description: "",
|
|
||||||
service: this.serviceID,
|
async updateIncident() {
|
||||||
|
const {id, title, description} = this.incident;
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
const res = await Api.incident_edit(id, {title, description});
|
||||||
|
if (res.status === "success") {
|
||||||
|
this.incidents = this.incidents.map(incident => {
|
||||||
|
if(incident.id === id) {
|
||||||
|
return res.output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return incident;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.resetIncident();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoading = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
editIncident(incident) {
|
||||||
|
this.incident = incident;
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadIncidents() {
|
async loadIncidents() {
|
||||||
this.incidents = await Api.incidents_service(this.serviceID)
|
this.incidents = await Api.incidents_service(this.serviceID)
|
||||||
|
},
|
||||||
|
|
||||||
|
resetIncident() {
|
||||||
|
// reset the form data
|
||||||
|
this.incident = {
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
service: 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,7 +69,7 @@ name: "ServiceEvents",
|
||||||
return this.$store.getters.serviceMessages(this.service.id)
|
return this.$store.getters.serviceMessages(this.service.id)
|
||||||
},
|
},
|
||||||
success_event() {
|
success_event() {
|
||||||
if (this.service.online && this.service.messages.length === 0 && this.service.incidents.length === 0) {
|
if (this.service.online && !this.service.messages && !this.service.incidents) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="dashboard_card card mb-4" :class="{'offline-card': !service.online}">
|
<div class="dashboard_card card mb-4" :class="{'offline-card': !service.online}">
|
||||||
<div class="card-header pb-1">
|
<div class="card-header pb-1">
|
||||||
<h6 v-observe-visibility="setVisible">
|
<h6 v-observe-visibility="setVisible" class="d-flex align-items-baseline justify-content-between">
|
||||||
<router-link :to="serviceLink(service)" class="no-decoration">{{service.name}}</router-link>
|
<router-link :to="serviceLink(service)" class="no-decoration">{{service.name}}</router-link>
|
||||||
<span class="badge float-right text-uppercase" :class="{'badge-success': service.online, 'badge-danger': !service.online}">
|
<span class="badge text-uppercase" :class="{'badge-success': service.online, 'badge-danger': !service.online}">
|
||||||
{{service.online ? $t('online') : $t('offline')}}
|
{{service.online ? $t('online') : $t('offline')}}
|
||||||
</span>
|
</span>
|
||||||
</h6>
|
</h6>
|
||||||
|
@ -44,7 +44,7 @@
|
||||||
<font-awesome-icon icon="calendar-check"/>
|
<font-awesome-icon icon="calendar-check"/>
|
||||||
</button>
|
</button>
|
||||||
<button @click="$router.push({path: `/dashboard/service/${service.id}/failures`, params: {id: service.id}})" @mouseleave="unsetHover" @mouseover="setHover($t('failures'))" class="btn btn-sm btn-white failures">
|
<button @click="$router.push({path: `/dashboard/service/${service.id}/failures`, params: {id: service.id}})" @mouseleave="unsetHover" @mouseover="setHover($t('failures'))" class="btn btn-sm btn-white failures">
|
||||||
<font-awesome-icon icon="exclamation-triangle"/> <span v-if="service.stats.failures !== 0" class="badge badge-danger ml-1">{{service.stats.failures}}</span>
|
<font-awesome-icon icon="exclamation-triangle"/><span v-if="service.stats && service.stats.failures !== 0" class="badge badge-danger ml-1">{{service.stats.failures}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,8 +2,9 @@
|
||||||
<div class="col-12 mb-3 pb-2 border-bottom" role="alert">
|
<div class="col-12 mb-3 pb-2 border-bottom" role="alert">
|
||||||
<span class="font-weight-bold text-capitalize" :class="{'text-success': update.type.toLowerCase()==='resolved', 'text-danger': update.type.toLowerCase()==='issue summary', 'text-warning': update.type.toLowerCase()==='update'}">{{update.type}}</span>
|
<span class="font-weight-bold text-capitalize" :class="{'text-success': update.type.toLowerCase()==='resolved', 'text-danger': update.type.toLowerCase()==='issue summary', 'text-warning': update.type.toLowerCase()==='update'}">{{update.type}}</span>
|
||||||
<span class="text-muted">- {{update.message}}
|
<span class="text-muted">- {{update.message}}
|
||||||
<button v-if="admin" @click="delete_update(update)" type="button" class="close">
|
<button v-if="admin" :disabled="isLoading && incidentId" @click="delete_update(update)" type="button" class="close">
|
||||||
<span aria-hidden="true">×</span>
|
<FontAwesomeIcon v-if="isLoading && incidentId === update.id" icon="circle-notch" spin size="xs" />
|
||||||
|
<FontAwesomeIcon v-else icon="trash" size="xs" />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<span class="d-block small">{{ago(update.created_at)}} ago</span>
|
<span class="d-block small">{{ago(update.created_at)}} ago</span>
|
||||||
|
@ -26,11 +27,24 @@
|
||||||
required: false
|
required: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
incidentId: null
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async delete_update(update) {
|
async delete_update(update) {
|
||||||
this.res = await Api.incident_update_delete(update)
|
this.isLoading = true;
|
||||||
if (this.res.status === "success") {
|
this.incidentId = update.id;
|
||||||
this.onUpdate()
|
|
||||||
|
const res = await Api.incident_update_delete(update);
|
||||||
|
|
||||||
|
if (res.status === "success") {
|
||||||
|
this.onUpdate();
|
||||||
|
|
||||||
|
this.isLoading = false;
|
||||||
|
this.incidentId = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="card-body pt-3">
|
<div class="card-body pt-3">
|
||||||
|
|
||||||
<div v-if="updates.length===0" class="alert alert-link text-danger">
|
<div v-if="updates.length===0" class="alert alert-link text-danger">
|
||||||
No updates found, create a new Incident Update below.
|
No updates found, create a new Incident Update below.
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,9 +22,9 @@
|
||||||
|
|
||||||
<div class="col-12 col-md-2">
|
<div class="col-12 col-md-2">
|
||||||
<button @click.prevent="createIncidentUpdate"
|
<button @click.prevent="createIncidentUpdate"
|
||||||
:disabled="!incident_update.message"
|
:disabled="!incident_update.message || isLoading"
|
||||||
type="submit" class="btn btn-block btn-primary">
|
type="submit" class="btn btn-block btn-primary">
|
||||||
Add
|
Add <FontAwesomeIcon v-if="isLoading" icon="circle-notch" spin />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -49,6 +48,7 @@
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
updates: [],
|
updates: [],
|
||||||
|
isLoading: false,
|
||||||
incident_update: {
|
incident_update: {
|
||||||
incident: this.incident.id,
|
incident: this.incident.id,
|
||||||
message: "",
|
message: "",
|
||||||
|
@ -58,15 +58,18 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.loadUpdates()
|
this.loadUpdates()
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async createIncidentUpdate() {
|
async createIncidentUpdate() {
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
this.res = await Api.incident_update_create(this.incident_update)
|
this.res = await Api.incident_update_create(this.incident_update)
|
||||||
if (this.res.status === "success") {
|
if (this.res.status === "success") {
|
||||||
this.updates.push(this.res.output) // this is better in terms of not having to querry the db to get a fresh copy of all updates
|
this.updates.push(this.res.output); // this is better in terms of not having to querry the db to get a fresh copy of all updates
|
||||||
//await this.loadUpdates()
|
//await this.loadUpdates()
|
||||||
|
this.isLoading = false;
|
||||||
} // TODO: further error checking here... maybe alert user it failed with modal or so
|
} // TODO: further error checking here... maybe alert user it failed with modal or so
|
||||||
|
|
||||||
// reset the form data
|
// reset the form data
|
||||||
|
@ -79,7 +82,7 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadUpdates() {
|
async loadUpdates() {
|
||||||
this.updates = await Api.incident_updates(this.incident)
|
this.updates = await Api.incident_updates(this.incident);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ const english = {
|
||||||
name: "Name",
|
name: "Name",
|
||||||
copy: "Copy",
|
copy: "Copy",
|
||||||
close: "Close",
|
close: "Close",
|
||||||
|
cancel: "Cancel",
|
||||||
secret: "Secret",
|
secret: "Secret",
|
||||||
regen_api: "Regenerate API Keys",
|
regen_api: "Regenerate API Keys",
|
||||||
regen_desc:
|
regen_desc:
|
||||||
|
@ -119,6 +120,8 @@ const english = {
|
||||||
administrator: "Administrator",
|
administrator: "Administrator",
|
||||||
checkins: "Checkins",
|
checkins: "Checkins",
|
||||||
incidents: "Incidents",
|
incidents: "Incidents",
|
||||||
|
incident_create: 'Create Incident',
|
||||||
|
incident_edit: 'Edit Incident',
|
||||||
service_info: "Service Info",
|
service_info: "Service Info",
|
||||||
service_name: "Service Name",
|
service_name: "Service Name",
|
||||||
service_description: "Service Description",
|
service_description: "Service Description",
|
||||||
|
|
Loading…
Reference in New Issue