mirror of https://github.com/statping/statping
merged with release
commit
fa743f2919
|
@ -41,4 +41,5 @@ tmp
|
|||
/frontend/cypress/videos/
|
||||
services.yml
|
||||
statping.wiki
|
||||
assets/
|
||||
assets/
|
||||
.vscode/settings.json
|
||||
|
|
|
@ -19,3 +19,6 @@ yarn-error.log*
|
|||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Package lock file
|
||||
package-lock.json
|
||||
|
|
|
@ -282,6 +282,27 @@ class Api {
|
|||
await axios.all([all])
|
||||
}
|
||||
|
||||
async downtimes ({ serviceId, start, end, skip, count, subStatus }) {
|
||||
return axios.get('api/downtimes', {
|
||||
params: { service_id: serviceId, start, end, skip, count, sub_status: subStatus }
|
||||
}).then((response) => response.data);
|
||||
}
|
||||
|
||||
async downtime (id) {
|
||||
return axios.get(`api/downtimes/${id}`).then((response) => response.data);
|
||||
}
|
||||
|
||||
async downtime_create (data) {
|
||||
return axios.post('/api/downtimes', data).then((response) => response.data);
|
||||
}
|
||||
|
||||
async downtime_update ({ id, data }) {
|
||||
return axios.patch(`/api/downtimes/${id}`, data).then((response) => response.data);
|
||||
}
|
||||
|
||||
async downtime_delete (id) {
|
||||
return axios.delete(`/api/downtimes/${id}`).then((response) => response.data);
|
||||
}
|
||||
}
|
||||
const api = new Api()
|
||||
export default api
|
||||
|
|
|
@ -85,6 +85,10 @@ A {
|
|||
|
||||
.nav-link {
|
||||
color: $navbar-color;
|
||||
|
||||
&.router-link-exact-active {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.form-control {
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
<template>
|
||||
<div class="col-12">
|
||||
<DowntimesFilterForm
|
||||
:handle-clear-filters="handleClearFilters"
|
||||
:params="params"
|
||||
:handle-filter-search="handleFilterSearch"
|
||||
:filter-errors="filterErrors"
|
||||
:handle-filter-change="handleFilterChange"
|
||||
/>
|
||||
|
||||
<div class="card contain-card mb-4">
|
||||
<div class="card-header">
|
||||
{{ $t('downtimes') }}
|
||||
<router-link
|
||||
v-if="$store.state.admin"
|
||||
to="/dashboard/create_downtime"
|
||||
class="btn btn-sm btn-success float-right"
|
||||
>
|
||||
<FontAwesomeIcon icon="plus" /> {{ $t('create') }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="card-body pt-0">
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="mt-5"
|
||||
>
|
||||
<div class="col-12 text-center">
|
||||
<FontAwesomeIcon
|
||||
icon="circle-notch"
|
||||
size="3x"
|
||||
spin
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 text-center mt-3 mb-3">
|
||||
<span class="text-muted">
|
||||
Loading Downtimes
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<DowntimesList :get-downtimes="getDowntimes" />
|
||||
<Pagination
|
||||
v-if="downtimes.length !== 0"
|
||||
:get-next-downtimes="getNextDowntimes"
|
||||
:get-prev-downtimes="getPrevDowntimes"
|
||||
:skip="params.skip"
|
||||
:count="params.count"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DowntimesList from './DowntimesList.vue';
|
||||
import { mapState } from 'vuex';
|
||||
import DowntimesFilterForm from '../../forms/DowntimeFilters.vue';
|
||||
import Pagination from '../Elements/Pagination.vue';
|
||||
import { removeEmptyParams } from '../../forms/Downtime.vue';
|
||||
|
||||
export const initialParams = {
|
||||
serviceId: '',
|
||||
start: '',
|
||||
end: '',
|
||||
skip: 0,
|
||||
count: 10,
|
||||
subStatus: ''
|
||||
};
|
||||
|
||||
export const convertToSec = (val) => {
|
||||
return (new Date(val).getTime())/1000;
|
||||
};
|
||||
|
||||
export const checkErrors = (params) => {
|
||||
const { start, end } = params;
|
||||
const errors = {};
|
||||
|
||||
// Converting into millisec
|
||||
const startSec = convertToSec(start);
|
||||
const endSec = convertToSec(end) + (60 * 60 * 23 + 59 * 60 + 59);
|
||||
|
||||
if (!start && end) {
|
||||
errors.start = 'Need to enter Start Date';
|
||||
} else if (start && !end) {
|
||||
errors.end = 'Need to enter End Date';
|
||||
} else if ( startSec > endSec ) {
|
||||
errors.end = 'End Date should be greater than Start Date';
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'DashboardDowntimes',
|
||||
components: {
|
||||
DowntimesList,
|
||||
Pagination,
|
||||
DowntimesFilterForm
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
isLoading: false,
|
||||
params: { ...initialParams },
|
||||
filterErrors: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState([ 'downtimes' ]),
|
||||
},
|
||||
created: function () {
|
||||
// Set start date
|
||||
const startDate = new Date();
|
||||
startDate.setDate(startDate.getDate() - 30);
|
||||
startDate.setHours(0,0,0,0);
|
||||
this.params.start = startDate.toJSON();
|
||||
|
||||
// Set end date
|
||||
const endDate = new Date();
|
||||
endDate.setHours(0,0,0,0);
|
||||
this.params.end = endDate.toJSON();
|
||||
|
||||
this.getDowntimes(this.params);
|
||||
},
|
||||
methods: {
|
||||
getDowntimes: async function (params = this.params) {
|
||||
const { start, end } = params;
|
||||
|
||||
const errors = checkErrors(this.params);
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
this.filterErrors = Object.assign({}, errors);
|
||||
return;
|
||||
}
|
||||
|
||||
let startSec = ''; let endSec = '';
|
||||
|
||||
if (start) {
|
||||
startSec = convertToSec(start);
|
||||
}
|
||||
|
||||
if (end) {
|
||||
endSec = convertToSec(end) + (60 * 60 * 23 + 59 * 60 + 59); // adding end of time for that particular date.
|
||||
}
|
||||
|
||||
const payload = removeEmptyParams({ ...params, start: startSec, end: endSec });
|
||||
|
||||
this.isLoading = true;
|
||||
await this.$store.dispatch({ type: 'getDowntimes', payload });
|
||||
this.isLoading = false;
|
||||
},
|
||||
getNextDowntimes: function () {
|
||||
this.params = { ...this.params, skip: this.params.skip + this.params.count };
|
||||
this.getDowntimes();
|
||||
},
|
||||
getPrevDowntimes: function () {
|
||||
this.params = { ...this.params, skip: this.params.skip - this.params.count };
|
||||
this.getDowntimes(this.params);
|
||||
},
|
||||
handleClearFilters: function () {
|
||||
this.params = { ...initialParams };
|
||||
},
|
||||
handleFilterSearch: function () {
|
||||
this.params = { ...this.params, skip: 0 };
|
||||
|
||||
this.getDowntimes();
|
||||
},
|
||||
handleFilterChange: function (e) {
|
||||
const { name } = e.target;
|
||||
|
||||
// reset error according to edit input
|
||||
delete this.filterErrors[name];
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,172 @@
|
|||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="downtimes.length === 0"
|
||||
class="alert alert-dark d-block mt-3 mb-0"
|
||||
>
|
||||
You currently don't have any downtimes for this services!
|
||||
</div>
|
||||
|
||||
<table
|
||||
v-else
|
||||
class="table"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
{{ $t('name') }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="d-none d-md-table-cell"
|
||||
>
|
||||
{{ $t('start_time') }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="d-none d-md-table-cell"
|
||||
>
|
||||
{{ $t('end_time') }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="d-none d-md-table-cell"
|
||||
>
|
||||
{{ $t('status') }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="d-none d-md-table-cell"
|
||||
>
|
||||
{{ $t('failures') }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="d-none d-md-table-cell"
|
||||
>
|
||||
{{ $t('actions') }}
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
<tr
|
||||
v-for="downtime in downtimes"
|
||||
:key="downtime.id"
|
||||
>
|
||||
<td>
|
||||
<span>
|
||||
{{ downtime.service.name }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell">
|
||||
<span
|
||||
class=""
|
||||
>
|
||||
{{ niceDateWithYear(downtime.start) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell">
|
||||
<span
|
||||
class=""
|
||||
>
|
||||
{{ 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' ]"
|
||||
>
|
||||
{{ downtime.sub_status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell">
|
||||
<span
|
||||
class=""
|
||||
>
|
||||
{{ downtime.failures }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<div class="btn-group">
|
||||
<button
|
||||
v-if="$store.state.admin"
|
||||
:disabled="isLoading"
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
@click.prevent="goto(`/dashboard/edit_downtime/${downtime.id}`)"
|
||||
>
|
||||
<FontAwesomeIcon icon="edit" />
|
||||
</button>
|
||||
<button
|
||||
v-if="$store.state.admin"
|
||||
:disabled="downtimeDeleteId === downtime.id && isLoading"
|
||||
class="btn btn-sm btn-danger"
|
||||
@click.prevent="handleDowntimeDelete(downtime)"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
v-if="!isLoading"
|
||||
icon="trash"
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
v-if="downtimeDeleteId === downtime.id && isLoading"
|
||||
icon="circle-notch"
|
||||
spin
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import Api from '../../API';
|
||||
|
||||
export default {
|
||||
name: 'DashboardDowntimeList',
|
||||
props: {
|
||||
getDowntimes: {
|
||||
type: Function,
|
||||
default: function () {}
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
isLoading: false,
|
||||
downtimeDeleteId: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState([ 'downtimes' ]),
|
||||
},
|
||||
methods: {
|
||||
goto: function (to) {
|
||||
this.$router.push(to);
|
||||
},
|
||||
setIsLoading ({ id, isLoading }) {
|
||||
this.downtimeDeleteId = id;
|
||||
this.isLoading = isLoading;
|
||||
},
|
||||
delete: async function (id) {
|
||||
this.setIsLoading({ id, isLoading: true });
|
||||
await Api.downtime_delete(id);
|
||||
this.setIsLoading({ id: null, isLoading: false });
|
||||
|
||||
this.getDowntimes();
|
||||
},
|
||||
handleDowntimeDelete: async function (downtime) {
|
||||
const modal = {
|
||||
visible: true,
|
||||
title: 'Delete Downtime',
|
||||
body: `Are you sure you want to delete the downtime for service ${downtime.service.name}?`,
|
||||
btnColor: 'btn-danger',
|
||||
btnText: 'Delete Downtime',
|
||||
func: () => this.delete(downtime.id),
|
||||
};
|
||||
this.$store.commit('setModal', modal);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,60 @@
|
|||
<template>
|
||||
<div class="col-12">
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="row mt-5"
|
||||
>
|
||||
<div class="col-12 text-center">
|
||||
<FontAwesomeIcon
|
||||
icon="circle-notch"
|
||||
size="3x"
|
||||
spin
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 text-center mt-3 mb-3">
|
||||
<span class="text-muted">
|
||||
Loading Downtime
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormDowntime
|
||||
v-else
|
||||
:edit-downtime="editDowntime"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '../../API';
|
||||
|
||||
const FormDowntime = () =>
|
||||
import(/* webpackChunkName: "dashboard" */ '../../forms/Downtime.vue');
|
||||
|
||||
export default {
|
||||
name: 'EditDowntime',
|
||||
components: {
|
||||
FormDowntime
|
||||
},
|
||||
data: function () {
|
||||
return { isLoading: false, editDowntime: null };
|
||||
},
|
||||
mounted () {
|
||||
this.getDowntime();
|
||||
},
|
||||
methods: {
|
||||
getDowntime: async function () {
|
||||
const id = this.$route.params.id;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
const { output } = await Api.downtime(id);
|
||||
this.isLoading = false;
|
||||
|
||||
this.editDowntime = output;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -14,6 +14,9 @@
|
|||
<li @click="navopen = !navopen" class="nav-item navbar-item">
|
||||
<router-link to="/dashboard/services" class="nav-link">{{ $t('services') }}</router-link>
|
||||
</li>
|
||||
<li @click="navopen = !navopen" class="nav-item navbar-item">
|
||||
<router-link to="/dashboard/downtimes" class="nav-link">{{'Downtimes'}}</router-link>
|
||||
</li>
|
||||
<li v-if="admin" @click="navopen = !navopen" class="nav-item navbar-item">
|
||||
<router-link to="/dashboard/users" class="nav-link">{{ $t('users') }}</router-link>
|
||||
</li>
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<ul class="pagination d-flex justify-content-center">
|
||||
<li class="page-item">
|
||||
<button
|
||||
class="btn btn-outline-secondary page-link"
|
||||
aria-label="Previous"
|
||||
:disabled="skip <= 0"
|
||||
@click="getPrevDowntimes"
|
||||
>
|
||||
<span aria-hidden="true">
|
||||
«
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li class="page-item">
|
||||
<button
|
||||
class="btn btn-outline-secondary page-link"
|
||||
aria-label="Next"
|
||||
:disabled="downtimes.length < count"
|
||||
@click="getNextDowntimes"
|
||||
>
|
||||
<span aria-hidden="true">
|
||||
»
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'DasboardDowntimePagination',
|
||||
props: {
|
||||
count:{
|
||||
type: Number,
|
||||
default: 10,
|
||||
},
|
||||
getNextDowntimes: {
|
||||
type: Function,
|
||||
default: function () {}
|
||||
},
|
||||
getPrevDowntimes: {
|
||||
type: Function,
|
||||
default: function () {}
|
||||
},
|
||||
skip: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState([ 'downtimes' ])
|
||||
},
|
||||
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,325 @@
|
|||
<template>
|
||||
<div class="card contain-card mb-4">
|
||||
<div class="card-header d-flex align-items-center">
|
||||
<button
|
||||
class="btn p-0 mr-2"
|
||||
@click="$router.push('/dashboard/downtimes');"
|
||||
>
|
||||
<FontAwesomeIcon icon="arrow-circle-left" />
|
||||
</button>
|
||||
<div>{{ $t("downtime_info") }}</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">
|
||||
{{
|
||||
$t("service_name")
|
||||
}}
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<select
|
||||
v-model="downtime.serviceId"
|
||||
name="service"
|
||||
class="form-control"
|
||||
required
|
||||
:disabled="$route.params.id"
|
||||
>
|
||||
<option
|
||||
v-for="(service) in services"
|
||||
:key="service.id"
|
||||
:value="service.id"
|
||||
>
|
||||
{{ service.name }}
|
||||
</option>
|
||||
</select>
|
||||
<small
|
||||
class="form-text text-muted"
|
||||
>
|
||||
Select Servive you want to have downtime for
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">
|
||||
{{
|
||||
$t("downtime_status")
|
||||
}}
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<select
|
||||
v-model="downtime.subStatus"
|
||||
name="service"
|
||||
class="form-control"
|
||||
required
|
||||
>
|
||||
<option
|
||||
value="degraded"
|
||||
>
|
||||
Degraded
|
||||
</option>
|
||||
<option value="down">
|
||||
Down
|
||||
</option>
|
||||
</select>
|
||||
<small
|
||||
class="form-text text-muted"
|
||||
>
|
||||
Choose status you want to give to the Servive
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">
|
||||
{{
|
||||
$t("downtime_date_range")
|
||||
}}
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<FlatPickr
|
||||
id="start"
|
||||
v-model="downtime.start"
|
||||
type="text"
|
||||
name="start"
|
||||
class="form-control form-control-plaintext"
|
||||
:config="config"
|
||||
placeholder="Select Start Date"
|
||||
@on-change="() => handleFormChange({target: {name: 'start'}})"
|
||||
/>
|
||||
<small
|
||||
v-if="errors.start"
|
||||
class="form-text text-danger"
|
||||
>
|
||||
{{ errors.start }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<FlatPickr
|
||||
id="end"
|
||||
v-model="downtime.end"
|
||||
type="text"
|
||||
name="end"
|
||||
class="form-control form-control-plaintext"
|
||||
:config="config"
|
||||
placeholder="Select End Date"
|
||||
@on-change="() => handleFormChange({target: {name: 'end'}})"
|
||||
/>
|
||||
<small
|
||||
v-if="errors.end"
|
||||
class="form-text text-danger"
|
||||
>
|
||||
{{ errors.end }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<small
|
||||
class="form-text text-muted"
|
||||
>
|
||||
Enter the Start and End date for which your service will be down/degraded
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label">
|
||||
{{
|
||||
$t("failures")
|
||||
}}
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<input
|
||||
v-model.number="downtime.failures"
|
||||
type="number"
|
||||
name="check_interval"
|
||||
class="form-control"
|
||||
min="0"
|
||||
required
|
||||
>
|
||||
<small
|
||||
v-if="errors.failures"
|
||||
class="form-text text-danger"
|
||||
>
|
||||
{{ errors.failures }}
|
||||
</small>
|
||||
<small
|
||||
class="form-text text-muted"
|
||||
>
|
||||
Select the number of failures you want for your service
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-12">
|
||||
<button
|
||||
:disabled="isLoading || !isCreateDowntimeBtnEnabled()"
|
||||
type="button"
|
||||
class="btn btn-success btn-block"
|
||||
@click.prevent="saveDowntime"
|
||||
>
|
||||
{{ $route.params.id ? $t("downtime_update") : $t("downtime_create") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import Api from '../API';
|
||||
import FlatPickr from 'vue-flatpickr-component';
|
||||
import 'flatpickr/dist/flatpickr.css';
|
||||
import { convertToSec } from '../components/Dashboard/DashboardDowntimes.vue';
|
||||
|
||||
const checkFormErrors = (value, id) => {
|
||||
const { failures, start, end } = value;
|
||||
const errors = {};
|
||||
let endSec = ''; let startSec = '';
|
||||
|
||||
// Converting into millisec
|
||||
if (start) {
|
||||
startSec = convertToSec(start);
|
||||
}
|
||||
|
||||
if (end) {
|
||||
endSec = convertToSec(end);
|
||||
}
|
||||
|
||||
// Check for valid positive numbers
|
||||
if (!(/^\d+$/.test(failures))) {
|
||||
errors.failures = 'Enter Valid Positve Number without decimal point';
|
||||
} else if (!start && end) {
|
||||
errors.start = 'Need to enter Start Date';
|
||||
} else if ( id && startSec && !endSec ) {
|
||||
errors.end = 'Need to enter End Date';
|
||||
} else if ( endSec && startSec > endSec ) {
|
||||
errors.end = 'End Date should be greater than Start Date';
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
export const removeEmptyParams = (obj) => {
|
||||
const updatedObj = {};
|
||||
|
||||
for (const [ key , value ] of Object.entries(obj)) {
|
||||
if (value) {
|
||||
updatedObj[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return updatedObj;
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'FormDowntime',
|
||||
components: {
|
||||
FlatPickr,
|
||||
},
|
||||
props: {
|
||||
editDowntime: {
|
||||
type: Object,
|
||||
default: null,
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
isLoading: false,
|
||||
errors: {},
|
||||
downtime: {
|
||||
serviceId: '',
|
||||
subStatus: 'degraded',
|
||||
failures: 20,
|
||||
start: new Date().toJSON(),
|
||||
end: null,
|
||||
},
|
||||
config: {
|
||||
altFormat: 'J M, Y, h:iK',
|
||||
altInput: true,
|
||||
enableTime: true,
|
||||
dateFormat: 'Z',
|
||||
maxDate: new Date().toJSON(),
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
services : function (state) {
|
||||
const { id } = this.$route.params;
|
||||
|
||||
if (!id && state.services.length > 0) {
|
||||
this.downtime.serviceId = state.services[0].id;
|
||||
}
|
||||
|
||||
return state.services;
|
||||
}
|
||||
}),
|
||||
},
|
||||
mounted: function () {
|
||||
if (this.editDowntime) {
|
||||
const { service_id, sub_status, failures, start, end } = this.editDowntime;
|
||||
|
||||
this.downtime = {
|
||||
start,
|
||||
end,
|
||||
failures,
|
||||
serviceId: service_id,
|
||||
subStatus: sub_status
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isCreateDowntimeBtnEnabled: function () {
|
||||
const { id } = this.$route.params;
|
||||
const { serviceId, subStatus, failures, start, end } = this.downtime;
|
||||
|
||||
return serviceId && subStatus && failures && start && (id ? end : true);
|
||||
},
|
||||
saveDowntime: async function () {
|
||||
const { id } = this.$route.params;
|
||||
const errors = checkFormErrors(this.downtime, id);
|
||||
|
||||
// Check of invalid input.
|
||||
if (Object.keys(errors).length > 0) {
|
||||
this.errors = Object.assign({}, errors);
|
||||
return;
|
||||
}
|
||||
|
||||
const { serviceId, subStatus, ...rest } = removeEmptyParams(this.downtime);
|
||||
|
||||
const downtime = {
|
||||
...rest,
|
||||
...(!id && { 'service_id': serviceId }),
|
||||
'sub_status': subStatus,
|
||||
};
|
||||
|
||||
this.isLoading=true;
|
||||
|
||||
try {
|
||||
if (id) {
|
||||
await Api.downtime_update({ id, data: downtime });
|
||||
} else {
|
||||
await Api.downtime_create(downtime);
|
||||
}
|
||||
this.isLoading=false;
|
||||
} catch (error) {
|
||||
this.isLoading=false;
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
this.$router.push('/dashboard/downtimes');
|
||||
},
|
||||
handleFormChange: function (e) {
|
||||
const { name } = e.target;
|
||||
|
||||
delete this.errors[name];
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,173 @@
|
|||
<template>
|
||||
<div class="card contain-card mb-4">
|
||||
<div class="card-header">
|
||||
{{ $t('filters') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-2">
|
||||
<label class="col-form-label">
|
||||
{{ $t('service') }}
|
||||
</label>
|
||||
<select
|
||||
v-model="params.serviceId"
|
||||
name="service"
|
||||
class="form-control"
|
||||
>
|
||||
<option value="">
|
||||
All
|
||||
</option>
|
||||
<option
|
||||
v-for="service in services"
|
||||
:key="service.id"
|
||||
:value="service.id"
|
||||
>
|
||||
{{ service.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-5">
|
||||
<label class="col-form-label">
|
||||
{{ $t('downtime_date_range') }}
|
||||
</label>
|
||||
<div class="form-row">
|
||||
<div class="col-sm-6">
|
||||
<FlatPickr
|
||||
id="start"
|
||||
v-model="params.start"
|
||||
type="text"
|
||||
name="start"
|
||||
class="form-control form-control-plaintext"
|
||||
value=""
|
||||
:config="config"
|
||||
placeholder="Select Start Date"
|
||||
@on-change="() => handleFilterChange({target: {name: 'start'}})"
|
||||
/>
|
||||
<small
|
||||
v-if="filterErrors.start"
|
||||
class="form-text text-danger"
|
||||
>
|
||||
{{ filterErrors.start }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<FlatPickr
|
||||
id="end"
|
||||
v-model="params.end"
|
||||
type="text"
|
||||
name="end"
|
||||
class="form-control form-control-plaintext"
|
||||
value=""
|
||||
:config="config"
|
||||
placeholder="Select End Date"
|
||||
@on-change="() => handleFilterChange({target: {name: 'end'}})"
|
||||
/>
|
||||
<small
|
||||
v-if="filterErrors.end"
|
||||
class="form-text text-danger"
|
||||
>
|
||||
{{ filterErrors.end }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<label class="col-form-label">
|
||||
{{ $t('status') }}
|
||||
</label>
|
||||
<select
|
||||
v-model="params.subStatus"
|
||||
name="status"
|
||||
class="form-control"
|
||||
>
|
||||
<option value="">
|
||||
All
|
||||
</option>
|
||||
<option value="degraded">
|
||||
Degraded
|
||||
</option>
|
||||
<option value="down">
|
||||
Down
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-3">
|
||||
<label class="col-form-label invisible">
|
||||
{{ $t('actions') }}
|
||||
</label>
|
||||
<div
|
||||
class="d-flex justify-content-end"
|
||||
role="group"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary mr-1"
|
||||
@click.prevent="handleFilterSearch"
|
||||
>
|
||||
{{ $t('search') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
@click.prevent="handleClearFilters"
|
||||
>
|
||||
{{ $t('clear') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import FlatPickr from 'vue-flatpickr-component';
|
||||
import 'flatpickr/dist/flatpickr.css';
|
||||
import { initialParams } from '../components/Dashboard/DashboardDowntimes.vue';
|
||||
|
||||
export default {
|
||||
name: 'DashboardDowntimeFilters',
|
||||
components: {
|
||||
FlatPickr
|
||||
},
|
||||
props: {
|
||||
params: {
|
||||
type: Object,
|
||||
default: initialParams
|
||||
},
|
||||
handleClearFilters: {
|
||||
type: Function,
|
||||
default: function () {}
|
||||
},
|
||||
handleFilterSearch: {
|
||||
type: Function,
|
||||
default: function () {}
|
||||
},
|
||||
filterErrors: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
handleFilterChange: {
|
||||
type: Function,
|
||||
default: function () {}
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
config: {
|
||||
altFormat: 'J M, Y',
|
||||
altInput: true,
|
||||
dateFormat: 'Z',
|
||||
maxDate: new Date().toJSON(),
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState([ 'services' ])
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -6,4 +6,4 @@ import Vue from "vue";
|
|||
|
||||
library.add(fas, fab)
|
||||
|
||||
Vue.component('font-awesome-icon', FontAwesomeIcon)
|
||||
Vue.component('FontAwesomeIcon', FontAwesomeIcon);
|
||||
|
|
|
@ -140,6 +140,20 @@ const english = {
|
|||
notify_all: "Notify All Changes",
|
||||
service_update: "Update Service",
|
||||
service_create: "Create Service",
|
||||
start_time: 'Start Time',
|
||||
end_time: "End Time",
|
||||
actions: "Actions",
|
||||
services: "Services",
|
||||
downtimes: "Downtimes",
|
||||
downtime_info: "Downtime Info",
|
||||
downtime_status: "Downtime Status",
|
||||
downtime_date_range: "Downtime Date Range",
|
||||
downtime_create: "Create Downtime",
|
||||
downtime_update: "Update Downtime",
|
||||
filters: "Filters",
|
||||
service: "Service",
|
||||
clear: "Clear",
|
||||
search: "Search",
|
||||
};
|
||||
|
||||
export default english;
|
||||
|
|
|
@ -256,6 +256,9 @@ export default Vue.mixin({
|
|||
},
|
||||
addSeconds(date, amount) {
|
||||
return addSeconds(date, amount)
|
||||
}
|
||||
},
|
||||
niceDateWithYear (val) {
|
||||
return format(parseISO(val), 'do MMM, yyyy h:mma');
|
||||
},
|
||||
}
|
||||
});
|
||||
|
|
|
@ -16,6 +16,9 @@ const Checkins = () => import(/* webpackChunkName: "dashboard" */ '@/components/
|
|||
const Failures = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/Failures')
|
||||
const NotFound = () => import(/* webpackChunkName: "index" */ '@/pages/NotFound')
|
||||
const Importer = () => import(/* webpackChunkName: "index" */ '@/components/Dashboard/Importer')
|
||||
const DashboardDowntimes = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/DashboardDowntimes')
|
||||
const EditDowntime = () => import(/* webpackChunkName: "dashboard" */ '@/components/Dashboard/EditDowntime')
|
||||
|
||||
|
||||
import VueRouter from "vue-router";
|
||||
import Api from "./API";
|
||||
|
@ -137,6 +140,27 @@ const routes = [
|
|||
requiresAuth: true,
|
||||
title: 'Statping - Service Failures',
|
||||
}
|
||||
},{
|
||||
path: 'downtimes',
|
||||
component: DashboardDowntimes,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: 'Statping - Downtimes',
|
||||
}
|
||||
},{
|
||||
path: 'create_downtime',
|
||||
component: EditDowntime,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: 'Statping - Create Downtime',
|
||||
}
|
||||
},{
|
||||
path: 'edit_downtime/:id',
|
||||
component: EditDowntime,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: 'Statping - Update Downtime',
|
||||
}
|
||||
},{
|
||||
path: 'messages',
|
||||
component: DashboardMessages,
|
||||
|
@ -228,3 +252,4 @@ router.beforeEach((to, from, next) => {
|
|||
});
|
||||
|
||||
export default router
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ export default new Vuex.Store({
|
|||
oauth: {},
|
||||
token: null,
|
||||
services: [],
|
||||
downtimes: [],
|
||||
service: null,
|
||||
groups: [],
|
||||
messages: [],
|
||||
|
@ -152,12 +153,19 @@ export default new Vuex.Store({
|
|||
setModal(state, modal) {
|
||||
state.modal = modal
|
||||
},
|
||||
setDowntimes (state, downtimes) {
|
||||
state.downtimes = downtimes;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async getAllServices(context) {
|
||||
const services = await Api.services()
|
||||
context.commit("setServices", services);
|
||||
},
|
||||
async getDowntimes (context, { payload }) {
|
||||
const { output } = await Api.downtimes(payload);
|
||||
context.commit('setDowntimes', output ?? []);
|
||||
},
|
||||
async loadCore(context) {
|
||||
const core = await Api.core()
|
||||
const token = await Api.token()
|
||||
|
|
|
@ -167,7 +167,9 @@ func sendJsonAction(obj interface{}, method string, w http.ResponseWriter, r *ht
|
|||
case *downtimes.Downtime:
|
||||
objName = "downtime"
|
||||
objId = v.Id
|
||||
|
||||
case *DowntimeService:
|
||||
objName = "downtime_with_service"
|
||||
objId = v.Id
|
||||
default:
|
||||
objName = fmt.Sprintf("%T", v)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/statping/statping/types/services"
|
||||
"github.com/statping/statping/utils"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -21,10 +22,75 @@ func findDowntime(r *http.Request) (*downtimes.Downtime, error) {
|
|||
return downtime, nil
|
||||
}
|
||||
|
||||
func convertToMap(query url.Values) map[string]string {
|
||||
vars := make(map[string]string)
|
||||
if query.Get("start") != "" {
|
||||
vars["start"] = query.Get("start")
|
||||
}
|
||||
if query.Get("end") != "" {
|
||||
vars["end"] = query.Get("end")
|
||||
}
|
||||
if query.Get("sub_status") != "" {
|
||||
vars["sub_status"] = query.Get("sub_status")
|
||||
}
|
||||
if query.Get("service_id") != "" {
|
||||
vars["service_id"] = query.Get("service_id")
|
||||
}
|
||||
if query.Get("type") != "" {
|
||||
vars["type"] = query.Get("type")
|
||||
}
|
||||
if query.Get("skip") != "" {
|
||||
vars["skip"] = query.Get("skip")
|
||||
}
|
||||
if query.Get("count") != "" {
|
||||
vars["count"] = query.Get("count")
|
||||
}
|
||||
return vars
|
||||
}
|
||||
|
||||
type DowntimeService struct {
|
||||
Id int64 `gorm:"primary_key;column:id" json:"id"`
|
||||
Service *services.Service `gorm:"foreignKey:service" json:"service"`
|
||||
ServiceId int64 `gorm:"index;column:service" json:"service_id"`
|
||||
SubStatus string `gorm:"column:sub_status" json:"sub_status"`
|
||||
Failures int `gorm:"column:failures" json:"failures"`
|
||||
Start *time.Time `gorm:"index;column:start" json:"start"`
|
||||
End *time.Time `gorm:"column:end" json:"end"`
|
||||
Type string `gorm:"default:'auto';column:type" json:"type"`
|
||||
}
|
||||
|
||||
func apiAllDowntimes(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
vars := convertToMap(query)
|
||||
downtime, err := downtimes.FindAll(vars)
|
||||
var downtimeWithService []DowntimeService
|
||||
servicesMap := services.All()
|
||||
if downtime == nil {
|
||||
sendJsonAction(downtimeWithService, "fetch", w, r)
|
||||
return
|
||||
}
|
||||
for _, dtime := range *downtime {
|
||||
var downtimeWithServiceVar DowntimeService
|
||||
downtimeWithServiceVar.Id = dtime.Id
|
||||
downtimeWithServiceVar.ServiceId = dtime.ServiceId
|
||||
downtimeWithServiceVar.SubStatus = dtime.SubStatus
|
||||
downtimeWithServiceVar.Failures = dtime.Failures
|
||||
downtimeWithServiceVar.Start = dtime.Start
|
||||
downtimeWithServiceVar.End = dtime.End
|
||||
downtimeWithServiceVar.Type = dtime.Type
|
||||
downtimeWithServiceVar.Service = servicesMap[dtime.ServiceId]
|
||||
downtimeWithService = append(downtimeWithService, downtimeWithServiceVar)
|
||||
}
|
||||
if err != nil {
|
||||
sendErrorJson(err, w, r)
|
||||
return
|
||||
}
|
||||
sendJsonAction(downtimeWithService, "fetch", w, r)
|
||||
}
|
||||
|
||||
func apiAllDowntimesForServiceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
serviceId := utils.ToInt(vars["service_id"])
|
||||
|
||||
ninetyDaysAgo := time.Now().Add(time.Duration(-90*24) * time.Hour)
|
||||
|
||||
downtime, err := downtimes.FindByService(serviceId, ninetyDaysAgo, time.Now())
|
||||
|
|
|
@ -194,6 +194,7 @@ func Router() *mux.Router {
|
|||
//r.Handle("/checkin/{api}", http.HandlerFunc(checkinHitHandler))
|
||||
|
||||
// API DOWNTIME Routes
|
||||
api.Handle("/api/downtimes", authenticated(apiAllDowntimes, false)).Methods("GET")
|
||||
api.Handle("/api/service/{service_id}/downtimes", authenticated(apiAllDowntimesForServiceHandler, false)).Methods("GET")
|
||||
api.Handle("/api/downtimes", authenticated(apiCreateDowntimeHandler, false)).Methods("POST")
|
||||
api.Handle("/api/downtimes/{id}", authenticated(apiDowntimeHandler, false)).Methods("GET")
|
||||
|
|
|
@ -3,6 +3,7 @@ package downtimes
|
|||
import (
|
||||
"fmt"
|
||||
"github.com/statping/statping/database"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -66,6 +67,68 @@ func FindDowntime(timeVar time.Time) []Downtime {
|
|||
|
||||
}
|
||||
|
||||
func ConvertToUnixTime(str string) (time.Time, error) {
|
||||
i, err := strconv.ParseInt(str, 10, 64)
|
||||
var t time.Time
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
tm := time.Unix(i, 0)
|
||||
return tm, nil
|
||||
}
|
||||
|
||||
func FindAll(vars map[string]string) (*[]Downtime, error) {
|
||||
var downtime []Downtime
|
||||
var start time.Time
|
||||
var end time.Time
|
||||
st, err1 := vars["start"]
|
||||
en, err2 := vars["end"]
|
||||
startInt, err := strconv.ParseInt(st, 10, 64)
|
||||
endInt, err := strconv.ParseInt(en, 10, 64)
|
||||
if err1 && err2 && (endInt > startInt) {
|
||||
start, err = ConvertToUnixTime(vars["start"])
|
||||
if err != nil {
|
||||
return &downtime, err
|
||||
}
|
||||
end, err = ConvertToUnixTime(vars["end"])
|
||||
if err != nil {
|
||||
return &downtime, err
|
||||
}
|
||||
} else {
|
||||
ninetyDaysAgo := time.Now().Add(time.Duration(-90*24) * time.Hour)
|
||||
start = ninetyDaysAgo
|
||||
end = time.Now()
|
||||
}
|
||||
q := db.Where("start BETWEEN ? AND ?", start, end)
|
||||
if subStatusVar, subStatusErr := vars["sub_status"]; subStatusErr {
|
||||
q = q.Where("sub_status = ?", subStatusVar)
|
||||
}
|
||||
if serviceIdVar, serviceIdErr := vars["service_id"]; serviceIdErr {
|
||||
q = q.Where("service = ?", serviceIdVar)
|
||||
}
|
||||
if typeVar, typeErr := vars["type"]; typeErr {
|
||||
q = q.Where("type = ?", typeVar)
|
||||
}
|
||||
var count int64
|
||||
if countVar, countErr := vars["count"]; countErr {
|
||||
count, err = strconv.ParseInt(countVar, 10, 64)
|
||||
if count > 100 {
|
||||
count = 100
|
||||
}
|
||||
} else {
|
||||
count = 20
|
||||
}
|
||||
var skip int64
|
||||
if skipVar, err6 := vars["skip"]; err6 {
|
||||
skip, err = strconv.ParseInt(skipVar, 10, 64)
|
||||
} else {
|
||||
skip = 0
|
||||
}
|
||||
q = q.Order("start DESC")
|
||||
q = q.Limit((int)(count)).Offset((int)(skip)).Find(&downtime)
|
||||
return &downtime, q.Error()
|
||||
}
|
||||
|
||||
func (c *Downtime) Create() error {
|
||||
q := db.Create(c)
|
||||
return q.Error()
|
||||
|
|
Loading…
Reference in New Issue