mirror of
https://github.com/louislam/uptime-kuma.git
synced 2026-01-31 11:03:11 +08:00
feat(maintenance): add quick duration buttons and pre-fill datetime fields (#6718)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Frank Elsinga <frank@elsinga.de>
This commit is contained in:
@@ -36,6 +36,9 @@ export default defineConfig({
|
|||||||
srcDir: "src",
|
srcDir: "src",
|
||||||
filename: "serviceWorker.ts",
|
filename: "serviceWorker.ts",
|
||||||
strategies: "injectManifest",
|
strategies: "injectManifest",
|
||||||
|
injectManifest: {
|
||||||
|
maximumFileSizeToCacheInBytes: 3 * 1024 * 1024, // 3 MiB
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
css: {
|
css: {
|
||||||
|
|||||||
@@ -85,12 +85,12 @@ export default {
|
|||||||
|
|
||||||
title() {
|
title() {
|
||||||
if (this.type === "1y") {
|
if (this.type === "1y") {
|
||||||
return `1 ${this.$tc("year", 1)}`;
|
return this.$tc("years", 1, { n: 1 });
|
||||||
}
|
}
|
||||||
if (this.type === "720") {
|
if (this.type === "720") {
|
||||||
return `30 ${this.$tc("day", 30)}`;
|
return this.$tc("days", 30, { n: 30 });
|
||||||
}
|
}
|
||||||
return `24 ${this.$tc("hour", 24)}`;
|
return this.$tc("hours", 24, { n: 24 });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
<ActionInput
|
<ActionInput
|
||||||
v-model="tlsExpiryNotifInput"
|
v-model="tlsExpiryNotifInput"
|
||||||
:type="'number'"
|
:type="'number'"
|
||||||
:placeholder="$t('day')"
|
:placeholder="$tc('days', 1, { n: 1 })"
|
||||||
:icon="'plus'"
|
:icon="'plus'"
|
||||||
:action="() => addTlsExpiryNotifDay(tlsExpiryNotifInput)"
|
:action="() => addTlsExpiryNotifDay(tlsExpiryNotifInput)"
|
||||||
:action-aria-label="$t('Add a new expiry notification day')"
|
:action-aria-label="$t('Add a new expiry notification day')"
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
<ActionInput
|
<ActionInput
|
||||||
v-model="domainExpiryNotifInput"
|
v-model="domainExpiryNotifInput"
|
||||||
:type="'number'"
|
:type="'number'"
|
||||||
:placeholder="$t('day')"
|
:placeholder="$tc('days', 1, { n: 1 })"
|
||||||
:icon="'plus'"
|
:icon="'plus'"
|
||||||
:action="() => addDomainExpiryNotifDay(domainExpiryNotifInput)"
|
:action="() => addDomainExpiryNotifDay(domainExpiryNotifInput)"
|
||||||
:action-aria-label="$t('Add a new expiry notification day')"
|
:action-aria-label="$t('Add a new expiry notification day')"
|
||||||
|
|||||||
@@ -55,9 +55,11 @@
|
|||||||
"Monitor": "Monitor | Monitors",
|
"Monitor": "Monitor | Monitors",
|
||||||
"now": "now",
|
"now": "now",
|
||||||
"time ago": "{0} ago",
|
"time ago": "{0} ago",
|
||||||
"day": "day | days",
|
"days": "{n} day | {n} days",
|
||||||
"hour": "hour | hours",
|
"hours": "{n} hour | {n} hours",
|
||||||
"year": "year | years",
|
"minutes": "{n} minute | {n} minutes",
|
||||||
|
"minuteShort": "{n} min | {n} min",
|
||||||
|
"years": "{n} year | {n} years",
|
||||||
"Response": "Response",
|
"Response": "Response",
|
||||||
"Ping": "Ping",
|
"Ping": "Ping",
|
||||||
"Monitor Type": "Monitor Type",
|
"Monitor Type": "Monitor Type",
|
||||||
@@ -669,6 +671,8 @@
|
|||||||
"recurringIntervalMessage": "Run once every day | Run once every {0} days",
|
"recurringIntervalMessage": "Run once every day | Run once every {0} days",
|
||||||
"affectedMonitorsDescription": "Select monitors that are affected by current maintenance",
|
"affectedMonitorsDescription": "Select monitors that are affected by current maintenance",
|
||||||
"affectedStatusPages": "Show this maintenance message on selected status pages",
|
"affectedStatusPages": "Show this maintenance message on selected status pages",
|
||||||
|
"Sets end time based on start time": "Sets end time based on start time",
|
||||||
|
"Please set start time first": "Please set start time first",
|
||||||
"noMonitorsSelectedWarning": "You are creating a maintenance without any affected monitors. Are you sure you want to continue?",
|
"noMonitorsSelectedWarning": "You are creating a maintenance without any affected monitors. Are you sure you want to continue?",
|
||||||
"noMonitorsOrStatusPagesSelectedError": "Cannot create maintenance without affected monitors or status pages",
|
"noMonitorsOrStatusPagesSelectedError": "Cannot create maintenance without affected monitors or status pages",
|
||||||
"passwordNotMatchMsg": "The repeat password does not match.",
|
"passwordNotMatchMsg": "The repeat password does not match.",
|
||||||
|
|||||||
@@ -123,9 +123,6 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Single Maintenance Window -->
|
|
||||||
<template v-if="maintenance.strategy === 'single'"></template>
|
|
||||||
|
|
||||||
<template v-if="maintenance.strategy === 'cron'">
|
<template v-if="maintenance.strategy === 'cron'">
|
||||||
<!-- Cron -->
|
<!-- Cron -->
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
@@ -331,6 +328,102 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-if="maintenance.strategy === 'single'">
|
||||||
|
<div class="my-3">
|
||||||
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="
|
||||||
|
currentDurationMinutes === 15 ? 'btn-primary' : 'btn-outline-primary'
|
||||||
|
"
|
||||||
|
:disabled="currentDurationMinutes === 15"
|
||||||
|
@click="setQuickDuration(15)"
|
||||||
|
>
|
||||||
|
{{ $tc("minuteShort", 15, { n: 15 }) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="
|
||||||
|
currentDurationMinutes === 30 ? 'btn-primary' : 'btn-outline-primary'
|
||||||
|
"
|
||||||
|
:disabled="currentDurationMinutes === 30"
|
||||||
|
@click="setQuickDuration(30)"
|
||||||
|
>
|
||||||
|
{{ $tc("minuteShort", 30, { n: 30 }) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="
|
||||||
|
currentDurationMinutes === 60 ? 'btn-primary' : 'btn-outline-primary'
|
||||||
|
"
|
||||||
|
:disabled="currentDurationMinutes === 60"
|
||||||
|
@click="setQuickDuration(60)"
|
||||||
|
>
|
||||||
|
{{ $tc("hours", 1, { n: 1 }) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="
|
||||||
|
currentDurationMinutes === 120 ? 'btn-primary' : 'btn-outline-primary'
|
||||||
|
"
|
||||||
|
:disabled="currentDurationMinutes === 120"
|
||||||
|
@click="setQuickDuration(120)"
|
||||||
|
>
|
||||||
|
{{ $tc("hours", 2, { n: 2 }) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="
|
||||||
|
currentDurationMinutes === 240 ? 'btn-primary' : 'btn-outline-primary'
|
||||||
|
"
|
||||||
|
:disabled="currentDurationMinutes === 240"
|
||||||
|
@click="setQuickDuration(240)"
|
||||||
|
>
|
||||||
|
{{ $tc("hours", 4, { n: 4 }) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="
|
||||||
|
currentDurationMinutes === 480 ? 'btn-primary' : 'btn-outline-primary'
|
||||||
|
"
|
||||||
|
:disabled="currentDurationMinutes === 480"
|
||||||
|
@click="setQuickDuration(480)"
|
||||||
|
>
|
||||||
|
{{ $tc("hours", 8, { n: 8 }) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="
|
||||||
|
currentDurationMinutes === 720 ? 'btn-primary' : 'btn-outline-primary'
|
||||||
|
"
|
||||||
|
:disabled="currentDurationMinutes === 720"
|
||||||
|
@click="setQuickDuration(720)"
|
||||||
|
>
|
||||||
|
{{ $tc("hours", 12, { n: 12 }) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="
|
||||||
|
currentDurationMinutes === 1440 ? 'btn-primary' : 'btn-outline-primary'
|
||||||
|
"
|
||||||
|
:disabled="currentDurationMinutes === 1440"
|
||||||
|
@click="setQuickDuration(1440)"
|
||||||
|
>
|
||||||
|
{{ $tc("hours", 24, { n: 24 }) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">{{ $t("Sets end time based on start time") }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -511,6 +604,22 @@ export default {
|
|||||||
hasStatusPages() {
|
hasStatusPages() {
|
||||||
return this.showOnAllPages || this.selectedStatusPages.length > 0;
|
return this.showOnAllPages || this.selectedStatusPages.length > 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the current duration in minutes between start and end dates
|
||||||
|
* @returns {number|null} Duration in minutes, or null if dates are invalid
|
||||||
|
*/
|
||||||
|
currentDurationMinutes() {
|
||||||
|
if (!this.maintenance.dateRange?.[0] || !this.maintenance.dateRange?.[1]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const start = new Date(this.maintenance.dateRange[0]);
|
||||||
|
const end = new Date(this.maintenance.dateRange[1]);
|
||||||
|
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Math.round((end.getTime() - start.getTime()) / 60000);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
"$route.fullPath"() {
|
"$route.fullPath"() {
|
||||||
@@ -570,6 +679,19 @@ export default {
|
|||||||
this.selectedStatusPages = [];
|
this.selectedStatusPages = [];
|
||||||
|
|
||||||
if (this.isAdd) {
|
if (this.isAdd) {
|
||||||
|
// Get current date/time in local timezone
|
||||||
|
const now = new Date();
|
||||||
|
const oneHourLater = new Date(now.getTime() + 60 * 60000);
|
||||||
|
|
||||||
|
const formatDateTime = (date) => {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
|
const hours = String(date.getHours()).padStart(2, "0");
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||||
|
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||||
|
};
|
||||||
|
|
||||||
this.maintenance = {
|
this.maintenance = {
|
||||||
title: "",
|
title: "",
|
||||||
description: "",
|
description: "",
|
||||||
@@ -578,7 +700,7 @@ export default {
|
|||||||
cron: "30 3 * * *",
|
cron: "30 3 * * *",
|
||||||
durationMinutes: 60,
|
durationMinutes: 60,
|
||||||
intervalDay: 1,
|
intervalDay: 1,
|
||||||
dateRange: [],
|
dateRange: [formatDateTime(now), formatDateTime(oneHourLater)],
|
||||||
timeRange: [
|
timeRange: [
|
||||||
{
|
{
|
||||||
hours: 2,
|
hours: 2,
|
||||||
@@ -591,7 +713,7 @@ export default {
|
|||||||
],
|
],
|
||||||
weekdays: [],
|
weekdays: [],
|
||||||
daysOfMonth: [],
|
daysOfMonth: [],
|
||||||
timezoneOption: null,
|
timezoneOption: "SAME_AS_SERVER",
|
||||||
};
|
};
|
||||||
} else if (this.isEdit || this.isClone) {
|
} else if (this.isEdit || this.isClone) {
|
||||||
this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => {
|
this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => {
|
||||||
@@ -655,6 +777,30 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set quick duration for single maintenance
|
||||||
|
* Calculates end time based on start time + duration in minutes
|
||||||
|
* @param {number} minutes Duration in minutes
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
setQuickDuration(minutes) {
|
||||||
|
if (!this.maintenance.dateRange[0]) {
|
||||||
|
this.$root.toastError(this.$t("Please set start time first"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startDate = new Date(this.maintenance.dateRange[0]);
|
||||||
|
const endDate = new Date(startDate.getTime() + minutes * 60000);
|
||||||
|
|
||||||
|
const year = endDate.getFullYear();
|
||||||
|
const month = String(endDate.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(endDate.getDate()).padStart(2, "0");
|
||||||
|
const hours = String(endDate.getHours()).padStart(2, "0");
|
||||||
|
const mins = String(endDate.getMinutes()).padStart(2, "0");
|
||||||
|
|
||||||
|
this.maintenance.dateRange[1] = `${year}-${month}-${day}T${hours}:${mins}`;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle form submission - show confirmation if no monitors selected
|
* Handle form submission - show confirmation if no monitors selected
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
|
|||||||
Reference in New Issue
Block a user