mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-08-21 23:07:04 +08:00
A complete maintenance planning system has been created
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
<div v-if="! $root.isMobile" class="col-12 col-md-5 col-xl-4">
|
||||
<div>
|
||||
<router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("Add New Monitor") }}</router-link>
|
||||
<router-link to="/addMaintenance" class="btn btn-primary mb-3 float-end"><font-awesome-icon icon="wrench" /> {{ $t("Add New Maintenance") }}</router-link>
|
||||
</div>
|
||||
<MonitorList :scrollbar="true" />
|
||||
</div>
|
||||
|
@@ -15,6 +15,10 @@
|
||||
<h3>{{ $t("Down") }}</h3>
|
||||
<span class="num text-danger">{{ stats.down }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>{{ $t("Maintenance") }}</h3>
|
||||
<span class="num text-maintenance">{{ stats.maintenance }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>{{ $t("Unknown") }}</h3>
|
||||
<span class="num text-secondary">{{ stats.unknown }}</span>
|
||||
@@ -38,7 +42,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(beat, index) in displayedRecords" :key="index" :class="{ 'shadow-box': $root.windowWidth <= 550}">
|
||||
<td><router-link :to="`/dashboard/${beat.monitorID}`">{{ beat.name }}</router-link></td>
|
||||
<td><router-link :to="`/dashboard/monitor/${beat.monitorID}`">{{ beat.name }}</router-link></td>
|
||||
<td><Status :status="beat.status" /></td>
|
||||
<td :class="{ 'border-0':! beat.msg}"><Datetime :value="beat.time" /></td>
|
||||
<td class="border-0">{{ beat.msg }}</td>
|
||||
@@ -93,6 +97,7 @@ export default {
|
||||
let result = {
|
||||
up: 0,
|
||||
down: 0,
|
||||
maintenance: 0,
|
||||
unknown: 0,
|
||||
pause: 0,
|
||||
};
|
||||
@@ -100,8 +105,11 @@ export default {
|
||||
for (let monitorID in this.$root.monitorList) {
|
||||
let beat = this.$root.lastHeartbeatList[monitorID];
|
||||
let monitor = this.$root.monitorList[monitorID];
|
||||
|
||||
if (monitor && ! monitor.active) {
|
||||
|
||||
if (monitor && monitor.maintenance) {
|
||||
result.maintenance++;
|
||||
}
|
||||
else if (monitor && !monitor.active) {
|
||||
result.pause++;
|
||||
} else if (beat) {
|
||||
if (beat.status === 1) {
|
||||
@@ -173,6 +181,14 @@ export default {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.text-maintenance {
|
||||
color: $maintenance;
|
||||
}
|
||||
|
||||
.bg-maintenance {
|
||||
background-color: $maintenance;
|
||||
}
|
||||
|
||||
.shadow-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
@@ -499,4 +499,8 @@ table {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.bg-maintenance {
|
||||
background-color: $maintenance;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
247
src/pages/EditMaintenance.vue
Normal file
247
src/pages/EditMaintenance.vue
Normal file
@@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<transition name="slide-fade" appear>
|
||||
<div>
|
||||
<h1 class="mb-3">{{ pageName }}</h1>
|
||||
<form @submit.prevent="submit">
|
||||
<div class="shadow-box">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h2 class="mb-2">{{ $t("General") }}</h2>
|
||||
|
||||
<!-- Title -->
|
||||
<div class="my-3">
|
||||
<label for="name" class="form-label">{{ $t("Title") }}</label>
|
||||
<input id="name" v-model="maintenance.title" type="text" class="form-control"
|
||||
:placeholder="titlePlaceholder" required>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="my-3">
|
||||
<label for="description" class="form-label">{{ $t("Description") }}</label>
|
||||
<textarea id="description" v-model="maintenance.description" class="form-control"
|
||||
:placeholder="descriptionPlaceholder"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Affected Monitors -->
|
||||
<div class="my-3">
|
||||
<label for="affected_monitors" class="form-label">{{ $t("Affected Monitors") }}</label>
|
||||
|
||||
<VueMultiselect
|
||||
id="affected_monitors"
|
||||
v-model="affectedMonitors"
|
||||
:options="affectedMonitorsOptions"
|
||||
track-by="id"
|
||||
label="name"
|
||||
:multiple="true"
|
||||
:allow-empty="false"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="false"
|
||||
:preserve-search="true"
|
||||
:placeholder="$t('Pick Affected Monitors...')"
|
||||
:preselect-first="false"
|
||||
:max-height="600"
|
||||
:taggable="false"
|
||||
></VueMultiselect>
|
||||
|
||||
<div class="form-text">
|
||||
{{ $t("affectedMonitorsDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Start Date Time -->
|
||||
<div class="my-3">
|
||||
<label for="start_date" class="form-label">{{ $t("Start of maintenance") }}</label>
|
||||
<input :type="'datetime-local'" id="start_date" v-model="maintenance.start_date"
|
||||
class="form-control" :class="{'darkCalendar': dark }" required>
|
||||
</div>
|
||||
|
||||
<!-- End Date Time -->
|
||||
<div class="my-3">
|
||||
<label for="end_date" class="form-label">{{ $t("Expected end of maintenance") }}</label>
|
||||
<input :type="'datetime-local'" id="end_date" v-model="maintenance.end_date"
|
||||
class="form-control" :class="{'darkCalendar': dark }" required>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 mb-1">
|
||||
<button id="monitor-submit-btn" class="btn btn-primary" type="submit"
|
||||
:disabled="processing">{{ $t("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CopyableInput from "../components/CopyableInput.vue";
|
||||
|
||||
import {useToast} from "vue-toastification";
|
||||
import VueMultiselect from "vue-multiselect";
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CopyableInput,
|
||||
VueMultiselect,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
processing: false,
|
||||
maintenance: {},
|
||||
affectedMonitors: [],
|
||||
affectedMonitorsOptions: [],
|
||||
dark: (this.$root.theme === "dark"),
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
pageName() {
|
||||
return this.$t((this.isAdd) ? "Schedule maintenance" : "Edit");
|
||||
},
|
||||
|
||||
isAdd() {
|
||||
return this.$route.path === "/addMaintenance";
|
||||
},
|
||||
|
||||
isEdit() {
|
||||
return this.$route.path.startsWith("/editMaintenance");
|
||||
},
|
||||
|
||||
titlePlaceholder() {
|
||||
return this.$t("Network infrastructure maintenance");
|
||||
},
|
||||
|
||||
descriptionPlaceholder() {
|
||||
return this.$t("Example: Network infrastructure maintenance is underway which will affect some of our services.");
|
||||
}
|
||||
|
||||
},
|
||||
watch: {
|
||||
|
||||
"$route.fullPath"() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
|
||||
this.$root.getMonitorList((res) => {
|
||||
if (res.ok) {
|
||||
Object.values(this.$root.monitorList).map(monitor => {
|
||||
this.affectedMonitorsOptions.push({
|
||||
id: monitor.id,
|
||||
name: monitor.name
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
this.affectedMonitors = [];
|
||||
|
||||
if (this.isAdd) {
|
||||
this.maintenance = {
|
||||
title: "",
|
||||
description: "",
|
||||
start_date: "",
|
||||
end_date: "",
|
||||
};
|
||||
} else if (this.isEdit) {
|
||||
this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => {
|
||||
if (res.ok) {
|
||||
this.maintenance = res.maintenance;
|
||||
|
||||
this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => {
|
||||
if (res.ok) {
|
||||
Object.values(res.monitors).map(monitor => {
|
||||
this.affectedMonitors.push(monitor);
|
||||
});
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
async submit() {
|
||||
this.processing = true;
|
||||
|
||||
if (this.affectedMonitors.length === 0) {
|
||||
toast.error(this.$t("Select at least one affected monitor"));
|
||||
return this.processing = false;
|
||||
}
|
||||
|
||||
if (this.isAdd) {
|
||||
this.$root.addMaintenance(this.maintenance, async (res) => {
|
||||
|
||||
if (res.ok) {
|
||||
await this.addMonitorMaintenance(res.maintenanceID, () => {
|
||||
toast.success(res.msg);
|
||||
this.processing = false;
|
||||
this.$root.getMaintenanceList();
|
||||
this.$router.push("/dashboard/maintenance/" + res.maintenanceID);
|
||||
});
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
this.processing = false;
|
||||
}
|
||||
|
||||
});
|
||||
} else {
|
||||
this.$root.getSocket().emit("editMaintenance", this.maintenance, async (res) => {
|
||||
if (res.ok) {
|
||||
await this.addMonitorMaintenance(res.maintenanceID, () => {
|
||||
this.processing = false;
|
||||
this.$root.toastRes(res);
|
||||
this.init();
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.processing = false;
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async addMonitorMaintenance(maintenanceID, callback) {
|
||||
await this.$root.addMonitorMaintenance(maintenanceID, this.affectedMonitors, async (res) => {
|
||||
if (!res.ok) {
|
||||
toast.error(res.msg);
|
||||
} else {
|
||||
this.$root.getMonitorList();
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.shadow-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.darkCalendar::-webkit-calendar-picker-indicator {
|
||||
filter: invert(1);
|
||||
}
|
||||
</style>
|
@@ -509,7 +509,7 @@ export default {
|
||||
toast.success(res.msg);
|
||||
this.processing = false;
|
||||
this.$root.getMonitorList();
|
||||
this.$router.push("/dashboard/" + res.monitorID);
|
||||
this.$router.push("/dashboard/monitor/" + res.monitorID);
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
this.processing = false;
|
||||
|
141
src/pages/MaintenanceDetails.vue
Normal file
141
src/pages/MaintenanceDetails.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<transition name="slide-fade" appear>
|
||||
<div v-if="maintenance">
|
||||
<h1> {{ maintenance.title }}</h1>
|
||||
<p class="url">
|
||||
<span>Start: {{ $root.datetimeMaintenance(maintenance.start_date) }}</span>
|
||||
<br>
|
||||
<span>End: {{ $root.datetimeMaintenance(maintenance.end_date) }}</span>
|
||||
</p>
|
||||
|
||||
<div class="functions" style="margin-top: 10px">
|
||||
<router-link :to=" '/editMaintenance/' + maintenance.id " class="btn btn-secondary">
|
||||
<font-awesome-icon icon="edit" /> {{ $t("Edit") }}
|
||||
</router-link>
|
||||
<button class="btn btn-danger" @click="deleteDialog">
|
||||
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label for="description" class="form-label" style="margin-top: 20px">{{ $t("Description") }}</label>
|
||||
<textarea id="description" class="form-control" disabled>{{ maintenance.description }}</textarea>
|
||||
|
||||
<label for="affected_monitors" class="form-label" style="margin-top: 20px">{{ $t("Affected Monitors") }}</label>
|
||||
<br>
|
||||
<button v-for="monitor in this.affectedMonitors" class="btn btn-monitor" style="margin: 5px; cursor: auto; color: white; font-weight: bold">
|
||||
{{ monitor }}
|
||||
</button>
|
||||
|
||||
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteMaintenance">
|
||||
{{ $t("deleteMaintenanceMsg") }}
|
||||
</Confirm>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useToast } from "vue-toastification";
|
||||
const toast = useToast();
|
||||
import Confirm from "../components/Confirm.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Confirm,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
affectedMonitors: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
maintenance() {
|
||||
let id = this.$route.params.id;
|
||||
return this.$root.maintenanceList[id];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => {
|
||||
if (res.ok) {
|
||||
this.affectedMonitors = Object.values(res.monitors).map(monitor => monitor.name);
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
deleteDialog() {
|
||||
this.$refs.confirmDelete.show();
|
||||
},
|
||||
|
||||
deleteMaintenance() {
|
||||
this.$root.deleteMaintenance(this.maintenance.id, (res) => {
|
||||
if (res.ok) {
|
||||
toast.success(res.msg);
|
||||
this.$router.push("/dashboard");
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.functions {
|
||||
text-align: center;
|
||||
|
||||
button, a {
|
||||
margin-left: 10px !important;
|
||||
margin-right: 10px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
a.btn {
|
||||
padding-left: 25px;
|
||||
padding-right: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.url {
|
||||
color: $primary;
|
||||
margin-bottom: 20px;
|
||||
font-weight: bold;
|
||||
|
||||
a {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.functions {
|
||||
button, a {
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 100px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.btn-monitor {
|
||||
background-color: #5cdd8b;
|
||||
}
|
||||
|
||||
</style>
|
@@ -144,6 +144,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Maintenance -->
|
||||
<div v-if="maintenance.length !== 0" v-for="maintenanceItem in maintenance" class="shadow-box alert mb-4 p-4 maintenance" role="alert" :class="maintenanceClass">
|
||||
<h4 v-text="maintenanceItem.title" class="alert-heading" />
|
||||
|
||||
<div v-text="maintenanceItem.description" class="content" />
|
||||
|
||||
<!-- Incident Date -->
|
||||
<div class="date mt-3">
|
||||
{{ $t("End") }}: {{ $root.datetimeMaintenance(maintenanceItem.end_date) }} ({{ dateFromNowMaintenance(maintenanceItem.start_date) }})<br />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overall Status -->
|
||||
<div class="shadow-box list p-4 overall-status mb-4">
|
||||
<div v-if="Object.keys($root.publicMonitorList).length === 0 && loadedData">
|
||||
@@ -167,6 +179,11 @@
|
||||
{{ $t("Degraded Service") }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="isMaintenance">
|
||||
<font-awesome-icon icon="wrench" class="statusMaintenance" />
|
||||
{{ $t("Maintenance") }}
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<font-awesome-icon icon="question-circle" style="color: #efefef;" />
|
||||
</div>
|
||||
@@ -217,7 +234,14 @@
|
||||
import axios from "axios";
|
||||
import PublicGroupList from "../components/PublicGroupList.vue";
|
||||
import ImageCropUpload from "vue-image-crop-upload";
|
||||
import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_PARTIAL_DOWN, UP } from "../util.ts";
|
||||
import {
|
||||
STATUS_PAGE_ALL_DOWN,
|
||||
STATUS_PAGE_ALL_UP,
|
||||
STATUS_PAGE_MAINTENANCE,
|
||||
STATUS_PAGE_PARTIAL_DOWN,
|
||||
UP,
|
||||
MAINTENANCE
|
||||
} from "../util.ts";
|
||||
import { useToast } from "vue-toastification";
|
||||
import dayjs from "dayjs";
|
||||
const toast = useToast();
|
||||
@@ -259,6 +283,7 @@ export default {
|
||||
loadedTheme: false,
|
||||
loadedData: false,
|
||||
baseURL: "",
|
||||
maintenance: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -320,6 +345,10 @@ export default {
|
||||
return "bg-" + this.incident.style;
|
||||
},
|
||||
|
||||
maintenanceClass() {
|
||||
return "bg-maintenance";
|
||||
},
|
||||
|
||||
overallStatus() {
|
||||
|
||||
if (Object.keys(this.$root.publicLastHeartbeatList).length === 0) {
|
||||
@@ -332,7 +361,10 @@ export default {
|
||||
for (let id in this.$root.publicLastHeartbeatList) {
|
||||
let beat = this.$root.publicLastHeartbeatList[id];
|
||||
|
||||
if (beat.status === UP) {
|
||||
if (beat.status === MAINTENANCE) {
|
||||
return STATUS_PAGE_MAINTENANCE;
|
||||
}
|
||||
else if (beat.status === UP) {
|
||||
hasUp = true;
|
||||
} else {
|
||||
status = STATUS_PAGE_PARTIAL_DOWN;
|
||||
@@ -358,6 +390,10 @@ export default {
|
||||
return this.overallStatus === STATUS_PAGE_ALL_DOWN;
|
||||
},
|
||||
|
||||
isMaintenance() {
|
||||
return this.overallStatus === STATUS_PAGE_MAINTENANCE;
|
||||
},
|
||||
|
||||
},
|
||||
watch: {
|
||||
|
||||
@@ -423,6 +459,10 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
axios.get("/api/status-page/maintenance-list").then((res) => {
|
||||
this.maintenance = res.data;
|
||||
});
|
||||
|
||||
axios.get("/api/status-page/monitor-list").then((res) => {
|
||||
this.$root.publicGroupList = res.data;
|
||||
});
|
||||
@@ -580,6 +620,10 @@ export default {
|
||||
return dayjs.utc(date).fromNow();
|
||||
},
|
||||
|
||||
dateFromNowMaintenance(date) {
|
||||
return dayjs(date).fromNow();
|
||||
},
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -671,6 +715,22 @@ footer {
|
||||
}
|
||||
}
|
||||
|
||||
.maintenance {
|
||||
color: white;
|
||||
|
||||
.date {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-maintenance {
|
||||
background-color: $maintenance;
|
||||
}
|
||||
|
||||
.statusMaintenance {
|
||||
color: $maintenance;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
|
Reference in New Issue
Block a user