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))
}
async incident_edit(incident_id, data) {
return axios.patch('api/incidents/' + incident_id, data).then(response => (response.data))
}
async incident_delete(incident) {
return axios.delete('api/incidents/' + incident.id).then(response => (response.data))
}

View File

@ -53,8 +53,8 @@
:key="downtime.id"
>
<td>
<span>
{{ downtime.service.name }}
<span :class="{'text-danger': !downtime.service}">
{{ (downtime.service && downtime.service.name) || 'Deleted service' }}
</span>
</td>
<td class="d-none d-md-table-cell">
@ -65,16 +65,14 @@
</span>
</td>
<td class="d-none d-md-table-cell">
<span
class=""
>
<span>
{{ downtime.end ? niceDateWithYear(downtime.end) : 'Ongoing' }}
</span>
</td>
<td class="d-none d-md-table-cell">
<span
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 }}
</span>
@ -87,7 +85,7 @@
</span>
</td>
<td class="text-right">
<div class="btn-group">
<div v-if="downtime.service" class="btn-group">
<button
v-if="$store.state.admin"
:disabled="isLoading"

View File

@ -3,9 +3,15 @@
<div v-for="incident in incidents" :key="incident.id" class="card contain-card mb-4">
<div class="card-header">Incident: {{incident.title}}
<button @click="deleteIncident(incident)" class="btn btn-sm btn-danger float-right">
<font-awesome-icon icon="times" />
</button>
<div v-if="$store.state.admin" class="btn-group float-right">
<button @click.prevent="editIncident(incident)" class="btn btn-sm btn-outline-secondary" type="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>
<FormIncidentUpdates :incident="incident"/>
@ -15,9 +21,16 @@
<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">
<form @submit.prevent="createIncident">
<form @submit.prevent="saveMessage">
<div class="form-group row">
<label class="col-sm-4 col-form-label">Title</label>
<div class="col-sm-8">
@ -34,10 +47,11 @@
<div class="form-group row">
<div class="col-sm-12">
<button @click.prevent="createIncident"
:disabled="!incident.title || !incident.description"
<button @click.prevent="saveMessage"
:disabled="!incident.title || !incident.description || isLoading"
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>
</div>
</div>
@ -54,20 +68,22 @@ import Api from "../../API";
const FormIncidentUpdates = () => import(/* webpackChunkName: "dashboard" */ '@/forms/IncidentUpdates')
export default {
name: 'Incidents',
components: {FormIncidentUpdates},
data() {
return {
serviceID: 0,
incidents: [],
incident: {
title: "",
description: "",
service: 0,
}
}
},
export default {
name: 'Incidents',
components: {FormIncidentUpdates},
data() {
return {
serviceID: 0,
incidents: [],
isLoading: false,
incidentId: null,
incident: {
title: "",
description: "",
service: 0,
}
}
},
created() {
this.serviceID = Number(this.$route.params.id);
@ -80,13 +96,19 @@ const FormIncidentUpdates = () => import(/* webpackChunkName: "dashboard" */ '@/
methods: {
async delete(i) {
this.res = await Api.incident_delete(i)
if (this.res.status === "success") {
this.incidents = this.incidents.filter(obj => obj.id !== i.id);
//await this.loadIncidents()
}
},
async delete(i) {
this.isLoading = true;
this.incidentId = i.id;
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) {
const modal = {
visible: true,
@ -99,23 +121,62 @@ const FormIncidentUpdates = () => import(/* webpackChunkName: "dashboard" */ '@/
this.$store.commit("setModal", modal)
},
async saveMessage() {
if (this.incident.id) {
this.updateIncident();
} else {
this.createIncident();
}
},
async createIncident() {
this.res = await Api.incident_create(this.serviceID, this.incident)
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
this.isLoading = true;
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()
} // TODO: further error checking here... maybe alert user it failed with modal or so
// reset the form data
this.incident = {
title: "",
description: "",
service: this.serviceID,
this.resetIncident();
this.isLoading = false;
},
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() {
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)
},
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 false

View File

@ -1,9 +1,9 @@
<template>
<div class="dashboard_card card mb-4" :class="{'offline-card': !service.online}">
<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>
<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')}}
</span>
</h6>
@ -44,7 +44,7 @@
<font-awesome-icon icon="calendar-check"/>
</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">
<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>
</div>
</div>

View File

@ -2,8 +2,9 @@
<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="text-muted">- {{update.message}}
<button v-if="admin" @click="delete_update(update)" type="button" class="close">
<span aria-hidden="true">&times;</span>
<button v-if="admin" :disabled="isLoading && incidentId" @click="delete_update(update)" type="button" class="close">
<FontAwesomeIcon v-if="isLoading && incidentId === update.id" icon="circle-notch" spin size="xs" />
<FontAwesomeIcon v-else icon="trash" size="xs" />
</button>
</span>
<span class="d-block small">{{ago(update.created_at)}} ago</span>
@ -26,11 +27,24 @@
required: false
}
},
data() {
return {
isLoading: false,
incidentId: null
}
},
methods: {
async delete_update(update) {
this.res = await Api.incident_update_delete(update)
if (this.res.status === "success") {
this.onUpdate()
this.isLoading = true;
this.incidentId = update.id;
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>
<div class="card-body pt-3">
<div v-if="updates.length===0" class="alert alert-link text-danger">
No updates found, create a new Incident Update below.
</div>
@ -23,9 +22,9 @@
<div class="col-12 col-md-2">
<button @click.prevent="createIncidentUpdate"
:disabled="!incident_update.message"
:disabled="!incident_update.message || isLoading"
type="submit" class="btn btn-block btn-primary">
Add
Add <FontAwesomeIcon v-if="isLoading" icon="circle-notch" spin />
</button>
</div>
</form>
@ -49,6 +48,7 @@
data () {
return {
updates: [],
isLoading: false,
incident_update: {
incident: this.incident.id,
message: "",
@ -58,15 +58,18 @@
},
async mounted() {
await this.loadUpdates()
this.loadUpdates()
},
methods: {
async createIncidentUpdate() {
this.isLoading = true;
this.res = await Api.incident_update_create(this.incident_update)
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()
this.isLoading = false;
} // TODO: further error checking here... maybe alert user it failed with modal or so
// reset the form data
@ -79,7 +82,7 @@
},
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",
copy: "Copy",
close: "Close",
cancel: "Cancel",
secret: "Secret",
regen_api: "Regenerate API Keys",
regen_desc:
@ -119,6 +120,8 @@ const english = {
administrator: "Administrator",
checkins: "Checkins",
incidents: "Incidents",
incident_create: 'Create Incident',
incident_edit: 'Edit Incident',
service_info: "Service Info",
service_name: "Service Name",
service_description: "Service Description",