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 change
pull/1113/head
Smit Patel 2022-08-11 12:04:16 +05:30 committed by GitHub
parent b86b998808
commit 64be130929
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 142 additions and 59 deletions

View File

@ -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))
} }

View File

@ -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"

View File

@ -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">
</button> <FontAwesomeIcon icon="edit" />
</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,20 +68,22 @@ 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: [],
incident: { isLoading: false,
title: "", incidentId: null,
description: "", incident: {
service: 0, title: "",
} description: "",
} service: 0,
}, }
}
},
created() { created() {
this.serviceID = Number(this.$route.params.id); this.serviceID = Number(this.$route.params.id);
@ -80,13 +96,19 @@ const FormIncidentUpdates = () => import(/* webpackChunkName: "dashboard" */ '@/
methods: { methods: {
async delete(i) { async delete(i) {
this.res = await Api.incident_delete(i) this.isLoading = true;
if (this.res.status === "success") { this.incidentId = i.id;
this.incidents = this.incidents.filter(obj => obj.id !== i.id);
//await this.loadIncidents() this.res = await Api.incident_delete(i)
} if (this.res.status === "success") {
}, this.incidents = this.incidents.filter(obj => obj.id !== i.id);
}
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,
}
} }
} }

View File

@ -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

View File

@ -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>

View File

@ -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">&times;</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;
} }
}, },
} }

View File

@ -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);
} }
} }
} }

View File

@ -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",