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:
Dorian Grasset
2026-01-14 09:12:19 +01:00
committed by GitHub
parent 2790e3d9e6
commit d893231c6d
5 changed files with 166 additions and 13 deletions

View File

@@ -36,6 +36,9 @@ export default defineConfig({
srcDir: "src",
filename: "serviceWorker.ts",
strategies: "injectManifest",
injectManifest: {
maximumFileSizeToCacheInBytes: 3 * 1024 * 1024, // 3 MiB
},
}),
],
css: {

View File

@@ -85,12 +85,12 @@ export default {
title() {
if (this.type === "1y") {
return `1 ${this.$tc("year", 1)}`;
return this.$tc("years", 1, { n: 1 });
}
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 });
},
},
};

View File

@@ -79,7 +79,7 @@
<ActionInput
v-model="tlsExpiryNotifInput"
:type="'number'"
:placeholder="$t('day')"
:placeholder="$tc('days', 1, { n: 1 })"
:icon="'plus'"
:action="() => addTlsExpiryNotifDay(tlsExpiryNotifInput)"
:action-aria-label="$t('Add a new expiry notification day')"
@@ -117,7 +117,7 @@
<ActionInput
v-model="domainExpiryNotifInput"
:type="'number'"
:placeholder="$t('day')"
:placeholder="$tc('days', 1, { n: 1 })"
:icon="'plus'"
:action="() => addDomainExpiryNotifDay(domainExpiryNotifInput)"
:action-aria-label="$t('Add a new expiry notification day')"

View File

@@ -55,9 +55,11 @@
"Monitor": "Monitor | Monitors",
"now": "now",
"time ago": "{0} ago",
"day": "day | days",
"hour": "hour | hours",
"year": "year | years",
"days": "{n} day | {n} days",
"hours": "{n} hour | {n} hours",
"minutes": "{n} minute | {n} minutes",
"minuteShort": "{n} min | {n} min",
"years": "{n} year | {n} years",
"Response": "Response",
"Ping": "Ping",
"Monitor Type": "Monitor Type",
@@ -669,6 +671,8 @@
"recurringIntervalMessage": "Run once every day | Run once every {0} days",
"affectedMonitorsDescription": "Select monitors that are affected by current maintenance",
"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?",
"noMonitorsOrStatusPagesSelectedError": "Cannot create maintenance without affected monitors or status pages",
"passwordNotMatchMsg": "The repeat password does not match.",

View File

@@ -123,9 +123,6 @@
</select>
</div>
<!-- Single Maintenance Window -->
<template v-if="maintenance.strategy === 'single'"></template>
<template v-if="maintenance.strategy === 'cron'">
<!-- Cron -->
<div class="my-3">
@@ -331,6 +328,102 @@
</div>
</div>
</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>
@@ -511,6 +604,22 @@ export default {
hasStatusPages() {
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: {
"$route.fullPath"() {
@@ -570,6 +679,19 @@ export default {
this.selectedStatusPages = [];
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 = {
title: "",
description: "",
@@ -578,7 +700,7 @@ export default {
cron: "30 3 * * *",
durationMinutes: 60,
intervalDay: 1,
dateRange: [],
dateRange: [formatDateTime(now), formatDateTime(oneHourLater)],
timeRange: [
{
hours: 2,
@@ -591,7 +713,7 @@ export default {
],
weekdays: [],
daysOfMonth: [],
timezoneOption: null,
timezoneOption: "SAME_AS_SERVER",
};
} else if (this.isEdit || this.isClone) {
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
* @returns {void}