Update Maintenance UI for recurring

pull/1213/head
Louis Lam 2 years ago
parent 617ba49e6c
commit 9d99c39f30

25
package-lock.json generated

@ -68,6 +68,7 @@
"@vitejs/plugin-legacy": "~2.1.0", "@vitejs/plugin-legacy": "~2.1.0",
"@vitejs/plugin-vue": "~3.1.0", "@vitejs/plugin-vue": "~3.1.0",
"@vue/compiler-sfc": "~3.2.36", "@vue/compiler-sfc": "~3.2.36",
"@vuepic/vue-datepicker": "^3.4.8",
"aedes": "^0.46.3", "aedes": "^0.46.3",
"babel-plugin-rewire": "~1.2.0", "babel-plugin-rewire": "~1.2.0",
"bootstrap": "5.1.3", "bootstrap": "5.1.3",
@ -3941,6 +3942,21 @@
"integrity": "sha512-dTyhTIRmGXBjxJE+skC8tTWCGLCVc4wQgRRLt8+O9p5ewBAjoBwtCAkLPrtToSr1xltoe3st21Pv953aOZ7alg==", "integrity": "sha512-dTyhTIRmGXBjxJE+skC8tTWCGLCVc4wQgRRLt8+O9p5ewBAjoBwtCAkLPrtToSr1xltoe3st21Pv953aOZ7alg==",
"dev": true "dev": true
}, },
"node_modules/@vuepic/vue-datepicker": {
"version": "3.4.8",
"resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-3.4.8.tgz",
"integrity": "sha512-nbuMA7IgjtT99LqcjSTSUcl7omTZSB+7vYSWQ9gQm31Frm/1wn54fT1Q0HaRD9nHXX982AACbqeND4K80SKONw==",
"dev": true,
"dependencies": {
"date-fns": "^2.29.2"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"vue": ">=3.2.0"
}
},
"node_modules/abab": { "node_modules/abab": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@ -20409,6 +20425,15 @@
"integrity": "sha512-dTyhTIRmGXBjxJE+skC8tTWCGLCVc4wQgRRLt8+O9p5ewBAjoBwtCAkLPrtToSr1xltoe3st21Pv953aOZ7alg==", "integrity": "sha512-dTyhTIRmGXBjxJE+skC8tTWCGLCVc4wQgRRLt8+O9p5ewBAjoBwtCAkLPrtToSr1xltoe3st21Pv953aOZ7alg==",
"dev": true "dev": true
}, },
"@vuepic/vue-datepicker": {
"version": "3.4.8",
"resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-3.4.8.tgz",
"integrity": "sha512-nbuMA7IgjtT99LqcjSTSUcl7omTZSB+7vYSWQ9gQm31Frm/1wn54fT1Q0HaRD9nHXX982AACbqeND4K80SKONw==",
"dev": true,
"requires": {
"date-fns": "^2.29.2"
}
},
"abab": { "abab": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",

@ -124,6 +124,7 @@
"@vitejs/plugin-legacy": "~2.1.0", "@vitejs/plugin-legacy": "~2.1.0",
"@vitejs/plugin-vue": "~3.1.0", "@vitejs/plugin-vue": "~3.1.0",
"@vue/compiler-sfc": "~3.2.36", "@vue/compiler-sfc": "~3.2.36",
"@vuepic/vue-datepicker": "^3.4.8",
"aedes": "^0.46.3", "aedes": "^0.46.3",
"babel-plugin-rewire": "~1.2.0", "babel-plugin-rewire": "~1.2.0",
"bootstrap": "5.1.3", "bootstrap": "5.1.3",

@ -19,6 +19,8 @@ class Maintenance extends BeanModel {
description: this.description, description: this.description,
start_date: this.start_date, start_date: this.start_date,
end_date: this.end_date, end_date: this.end_date,
strategy: this.strategy,
active: !!this.active,
}; };
} }
@ -27,13 +29,7 @@ class Maintenance extends BeanModel {
* @returns {Object} * @returns {Object}
*/ */
async toJSON() { async toJSON() {
return { return this.toPublicJSON();
id: this.id,
title: this.title,
description: this.description,
start_date: this.start_date,
end_date: this.end_date,
};
} }
} }

@ -0,0 +1,39 @@
@import "@vuepic/vue-datepicker/dist/main.css";
@import "vars.scss";
// Must use #{ }
// Remark: https://stackoverflow.com/questions/50202991/unable-to-set-scss-variable-to-css-variable
.dp__theme_dark {
--dp-background-color: #{$dark-bg2};
--dp-text-color: #{$dark-font-color};
--dp-hover-color: #484848;
--dp-hover-text-color: #ffffff;
--dp-hover-icon-color: #959595;
--dp-primary-color: #{#5cdd8b};
--dp-primary-text-color: #ffffff;
--dp-secondary-color: #494949;
--dp-border-color: #{$dark-border-color};
--dp-menu-border-color: #2d2d2d;
--dp-border-color-hover: #{$dark-border-color};
--dp-disabled-color: #212121;
--dp-scroll-bar-background: #212121;
--dp-scroll-bar-color: #484848;
--dp-success-color: #{$primary};
--dp-success-color-disabled: #428f59;
--dp-icon-color: #959595;
--dp-danger-color: #e53935;
--dp-highlight-color: rgba(0, 92, 178, 0.2);
}
.dp__input {
border-radius: $border-radius;
}
// Fix: Full width of text input when using "inline textInput inlineWithInput" mode
.dp__main > div[aria-label="Datepicker input"] {
width: 100%;
}
.dp__main > div[aria-label="Datepicker menu"]:nth-child(2) {
margin-top: 20px;
}

@ -18,7 +18,8 @@ export default {
"All Status Pages": "All Status Pages", "All Status Pages": "All Status Pages",
"Selected status pages": "Selected status pages", "Selected status pages": "Selected status pages",
"Select status pages...": "Select status pages...", "Select status pages...": "Select status pages...",
End: "End", recurringIntervalMessage: "Run once every day | Run once every {0} days",
"End": "End",
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",
atLeastOneMonitor: "Select at least one affected monitor", atLeastOneMonitor: "Select at least one affected monitor",
@ -61,9 +62,7 @@ export default {
List: "List", List: "List",
Add: "Add", Add: "Add",
"Add Monitor": "Add Monitor", "Add Monitor": "Add Monitor",
"Add Maintenance": "Add Maintenance",
"Add New Monitor": "Add New Monitor", "Add New Monitor": "Add New Monitor",
"Add New Maintenance": "Add New Maintenance",
"Quick Stats": "Quick Stats", "Quick Stats": "Quick Stats",
Up: "Up", Up: "Up",
Down: "Down", Down: "Down",
@ -605,4 +604,21 @@ export default {
goAlert: "GoAlert", goAlert: "GoAlert",
backupOutdatedWarning: "Deprecated: Since a lot of features added and this backup feature is a bit unmaintained, it cannot generate or restore a complete backup.", backupOutdatedWarning: "Deprecated: Since a lot of features added and this backup feature is a bit unmaintained, it cannot generate or restore a complete backup.",
backupRecommend: "Please backup the volume or the data folder (./data/) directly instead.", backupRecommend: "Please backup the volume or the data folder (./data/) directly instead.",
recurringInterval: "Interval",
"Recurring": "Recurring",
strategyManual: "Active/Inactive Manually",
warningTimezone: "It is NOT your current browser's timezone. It is your server's timezone.",
weekdayShortMon: "Mon",
weekdayShortTue: "Tue",
weekdayShortWed: "Wed",
weekdayShortThu: "Thu",
weekdayShortFri: "Fri",
weekdayShortSat: "Sat",
weekdayShortSun: "Sun",
dayOfMonth: "Day of Month",
lastDay: "Last Day",
lastDay1: "Last Day of Month",
lastDay2: "2nd Last Day of Month",
lastDay3: "3rd Last Day of Month",
lastDay4: "4th Last Day of Month",
}; };

@ -5,6 +5,7 @@ import Toast from "vue-toastification";
import "vue-toastification/dist/index.css"; import "vue-toastification/dist/index.css";
import App from "./App.vue"; import App from "./App.vue";
import "./assets/app.scss"; import "./assets/app.scss";
import "./assets/vue-datepicker.scss";
import { i18n } from "./i18n"; import { i18n } from "./i18n";
import { FontAwesomeIcon } from "./icon.js"; import { FontAwesomeIcon } from "./icon.js";
import datetime from "./mixins/datetime"; import datetime from "./mixins/datetime";

@ -26,6 +26,15 @@ export default {
return dayjs.tz(value, this.timezone).utc().format(); return dayjs.tz(value, this.timezone).utc().format();
}, },
/**
* Used for <input type="datetime" />
* @param value
* @returns {string}
*/
toDateTimeInputFormat(value) {
return this.datetimeFormat(value, "YYYY-MM-DDTHH:mm");
},
/** /**
* Return a given value in the format YYYY-MM-DD HH:mm:ss * Return a given value in the format YYYY-MM-DD HH:mm:ss
* @param {any} value Value to format as date time * @param {any} value Value to format as date time

@ -46,6 +46,10 @@ export default {
} }
return this.userTheme; return this.userTheme;
} }
},
isDark() {
return this.theme === "dark";
} }
}, },

@ -7,7 +7,7 @@
<div class="row"> <div class="row">
<div class="col-xl-7"> <div class="col-xl-7">
<!-- Title --> <!-- Title -->
<div class="my-3"> <div class="mb-3">
<label for="name" class="form-label">{{ $t("Title") }}</label> <label for="name" class="form-label">{{ $t("Title") }}</label>
<input <input
id="name" v-model="maintenance.title" type="text" class="form-control" id="name" v-model="maintenance.title" type="text" class="form-control"
@ -35,7 +35,6 @@
track-by="id" track-by="id"
label="name" label="name"
:multiple="true" :multiple="true"
:allow-empty="false"
:close-on-select="false" :close-on-select="false"
:clear-on-select="false" :clear-on-select="false"
:preserve-search="true" :preserve-search="true"
@ -70,7 +69,6 @@
track-by="id" track-by="id"
label="name" label="name"
:multiple="true" :multiple="true"
:allow-empty="true"
:close-on-select="false" :close-on-select="false"
:clear-on-select="false" :clear-on-select="false"
:preserve-search="true" :preserve-search="true"
@ -82,26 +80,132 @@
</div> </div>
</div> </div>
<h2 class="mt-5">{{ $t("Effective Date Range") }}</h2> <h2 class="mt-5">{{ $t("Date and Time") }}</h2>
<!-- Start Date Time --> <div> {{ $t("warningTimezone") }}</div>
<div class="my-3">
<label for="start_date" class="form-label">{{ $t("Start Date") }}</label>
<input
id="start_date" v-model="maintenance.start_date" :type="'datetime-local'"
class="form-control" :class="{'dark-calendar': dark }" required
>
</div>
<!-- End Date Time --> <!-- Strategy -->
<div class="my-3"> <div class="my-3">
<label for="end_date" class="form-label">{{ $t("End Date") }}</label> <label for="strategy" class="form-label">{{ $t("Strategy") }}</label>
<input <select id="strategy" v-model="maintenance.strategy" class="form-select">
id="end_date" v-model="maintenance.end_date" :type="'datetime-local'" <option value="manual">{{ $t("strategyManual") }}</option>
class="form-control" :class="{'dark-calendar': dark }" required <option value="single">Single Maintenance Window</option>
> <option value="recurring-interval">{{ $t("Recurring") }} - {{ $t("recurringInterval") }}</option>
<option value="recurring-weekday">{{ $t("Recurring") }} - Weekday</option>
<option value="recurring-day-of-month">{{ $t("Recurring") }} - Day of Month</option>
<option v-if="false" value="recurring-day-of-year">{{ $t("Recurring") }} - Day of Year</option>
</select>
</div> </div>
<!-- Single Maintenance Window -->
<template v-if="maintenance.strategy === 'single'">
<!-- DateTime Range -->
<div class="my-3">
<label class="form-label">{{ $t("DateTime Range") }}</label>
<Datepicker
v-model="maintenance.dateTimeRange"
:dark="$root.isDark"
range textInput
:monthChangeOnScroll="false"
:minDate="minDate"
format="yyyy-MM-dd HH:mm"
utc="preserve"
/>
</div>
</template>
<!-- Recurring - Interval -->
<template v-if="maintenance.strategy === 'recurring-interval'">
<div class="my-3">
<label for="interval-day" class="form-label">
{{ $t("recurringInterval") }}
<template v-if="maintenance.intervalDay >= 1">
({{
$tc("recurringIntervalMessage", maintenance.intervalDay, [
maintenance.intervalDay
])
}})
</template>
</label>
<input id="interval-day" v-model="maintenance.intervalDay" type="number" class="form-control" required min="1" max="3650" step="1">
</div>
</template>
<!-- Recurring - Weekday -->
<template v-if="maintenance.strategy === 'recurring-weekday'">
<div class="my-3">
<label for="interval-day" class="form-label">
{{ $t("Weekday") }}
</label>
<!-- Weekday Picker -->
<div class="weekday-picker">
<div v-for="(weekday, index) in weekdays" :key="index">
<label class="form-check-label" :for="weekday.id">{{ $t(weekday.langKey) }}</label>
<div class="form-check-inline"><input :id="weekday.id" v-model="maintenance.weekdays" type="checkbox" :value="weekday.value" class="form-check-input"></div>
</div>
</div>
</div>
</template>
<!-- Recurring - Day of month -->
<template v-if="maintenance.strategy === 'recurring-day-of-month'">
<div class="my-3">
<label for="interval-day" class="form-label">
{{ $t("dayOfMonth") }}
</label>
<!-- Day Picker -->
<div class="day-picker">
<div v-for="index in 31" :key="index">
<label class="form-check-label" :for="'day' + index">{{ index }}</label>
<div class="form-check-inline">
<input :id="'day' + index" v-model="maintenance.daysOfMonth" type="checkbox" :value="index" class="form-check-input">
</div>
</div>
</div>
<div class="mt-3 mb-2">{{ $t("lastDay") }}</div>
<div v-for="(lastDay, index) in lastDays" :key="index" class="form-check">
<input :id="lastDay.langKey" v-model="maintenance.daysOfMonth" type="checkbox" :value="lastDay.value" class="form-check-input">
<label class="form-check-label" :for="lastDay.langKey">
{{ $t(lastDay.langKey) }}
</label>
</div>
</div>
</template>
<!-- For any recurring types -->
<template v-if="maintenance.strategy === 'recurring-interval' || maintenance.strategy === 'recurring-weekday' || maintenance.strategy === 'recurring-day-of-month'">
<!-- Maintenance Time Window of a Day -->
<div class="my-3">
<label class="form-label">{{ $t("Maintenance Time Window of a Day") }}</label>
<Datepicker
v-model="maintenance.timeRange"
:dark="$root.isDark"
timePicker disableTimeRangeValidation range
placeholder="Select Time"
textInput
/>
</div>
<!-- Date Range -->
<div class="my-3">
<label class="form-label">{{ $t("Effective Date Range") }}</label>
<Datepicker
v-model="maintenance.dateRange"
:dark="$root.isDark"
range textInput datePicker
:monthChangeOnScroll="false"
:minDate="minDate"
:enableTimePicker="false"
utc="preserve"
/>
</div>
</template>
<div class="mt-4 mb-1"> <div class="mt-4 mb-1">
<button <button
id="monitor-submit-btn" class="btn btn-primary" type="submit" id="monitor-submit-btn" class="btn btn-primary" type="submit"
@ -122,12 +226,15 @@
import { useToast } from "vue-toastification"; import { useToast } from "vue-toastification";
import VueMultiselect from "vue-multiselect"; import VueMultiselect from "vue-multiselect";
import dayjs from "dayjs";
import Datepicker from "@vuepic/vue-datepicker";
const toast = useToast(); const toast = useToast();
export default { export default {
components: { components: {
VueMultiselect, VueMultiselect,
Datepicker
}, },
data() { data() {
@ -139,6 +246,63 @@ export default {
showOnAllPages: false, showOnAllPages: false,
selectedStatusPages: [], selectedStatusPages: [],
dark: (this.$root.theme === "dark"), dark: (this.$root.theme === "dark"),
neverEnd: false,
minDate: this.$root.date(dayjs()) + " 00:00",
lastDays: [
{
langKey: "lastDay1",
value: "lastDay1",
},
{
langKey: "lastDay2",
value: "lastDay2",
},
{
langKey: "lastDay3",
value: "lastDay3",
},
{
langKey: "lastDay4",
value: "lastDay4",
}
],
weekdays: [
{
id: "weekday1",
langKey: "weekdayShortMon",
value: 1,
},
{
id: "weekday2",
langKey: "weekdayShortTue",
value: 2,
},
{
id: "weekday3",
langKey: "weekdayShortWed",
value: 3,
},
{
id: "weekday4",
langKey: "weekdayShortTue",
value: 4,
},
{
id: "weekday5",
langKey: "weekdayShortFri",
value: 5,
},
{
id: "weekday6",
langKey: "weekdayShortSat",
value: 6,
},
{
id: "weekday7",
langKey: "weekdayShortSun",
value: 7,
},
],
}; };
}, },
@ -169,8 +333,13 @@ export default {
watch: { watch: {
"$route.fullPath"() { "$route.fullPath"() {
this.init(); this.init();
} },
neverEnd(value) {
if (value) {
this.maintenance.recurringEndDate = "";
}
},
}, },
mounted() { mounted() {
this.init(); this.init();
@ -195,8 +364,21 @@ export default {
this.maintenance = { this.maintenance = {
title: "", title: "",
description: "", description: "",
start_date: "", strategy: "single",
end_date: "", active: 1,
recurringStartDate: this.$root.date(dayjs()),
recurringEndDate: "",
intervalDay: 1,
dateTimeRange: [ this.minDate ],
timeRange: [{
hours: 2,
minutes: 0,
}, {
hours: 3,
minutes: 0,
}],
weekdays: [],
daysOfMonth: [],
}; };
} else if (this.isEdit) { } else if (this.isEdit) {
this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => { this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => {
@ -332,4 +514,38 @@ textarea {
.dark-calendar::-webkit-calendar-picker-indicator { .dark-calendar::-webkit-calendar-picker-indicator {
filter: invert(1); filter: invert(1);
} }
.weekday-picker {
display: flex;
gap: 10px;
& > div {
display: flex;
flex-direction: column;
align-items: center;
width: 40px;
.form-check-inline {
margin-right: 0;
}
}
}
.day-picker {
display: flex;
gap: 10px;
flex-wrap: wrap;
& > div {
display: flex;
flex-direction: column;
align-items: center;
width: 40px;
.form-check-inline {
margin-right: 0;
}
}
}
</style> </style>

@ -44,6 +44,10 @@
</div> </div>
</div> </div>
<div class="text-center mt-3" style="font-size: 13px;">
<a href="https://github.com/louislam/uptime-kuma/wiki/Maintenance" target="_blank">Learn More</a>
</div>
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteMaintenance"> <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteMaintenance">
{{ $t("deleteMaintenanceMsg") }} {{ $t("deleteMaintenanceMsg") }}
</Confirm> </Confirm>

Loading…
Cancel
Save