mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-11-23 14:54:05 +00:00
Feat: Toast notification timeout settings (#3441)
* Add toast timeout to the settings Changing gui, adding timeout with a fix value memo rc rollback readme cleanup code cleanup code Review fixes review fix 2 * Feat: Add clearAll button below toastContainer * Feat: Load & Apply defaults, improve wording Chore: Remove unused * Feat: Change setting to affect monitor notif. only * Apply suggestions from code review Co-authored-by: Matthew Nickson <mnickson@sidingsmedia.com> * Chore: Fix JSDoc --------- Co-authored-by: Berczi Sandor <sandor.berczi@urss.hu> Co-authored-by: Matthew Nickson <mnickson@sidingsmedia.com>
This commit is contained in:
parent
62f4434711
commit
bfc7b498be
7 changed files with 218 additions and 11 deletions
|
@ -609,6 +609,18 @@ $shadow-box-padding: 20px;
|
|||
}
|
||||
}
|
||||
|
||||
@media (max-width: 770px) {
|
||||
.toast-container {
|
||||
margin-bottom: 100px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.toast-container {
|
||||
margin-bottom: 126px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Localization
|
||||
|
||||
@import "localization.scss";
|
||||
|
|
|
@ -20,6 +20,39 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div class="my-4 pt-4">
|
||||
<h5 class="my-4 settings-subheading">{{ $t("monitorToastMessagesLabel") }}</h5>
|
||||
<p>{{ $t("monitorToastMessagesDescription") }}</p>
|
||||
|
||||
<div class="my-4">
|
||||
<label for="toastErrorTimeoutSecs" class="form-label">
|
||||
{{ $t("toastErrorTimeout") }}
|
||||
</label>
|
||||
<input
|
||||
id="toastErrorTimeoutSecs"
|
||||
v-model="toastErrorTimeoutSecs"
|
||||
type="number"
|
||||
class="form-control"
|
||||
min="-1"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="my-4">
|
||||
<label for="toastSuccessTimeoutSecs" class="form-label">
|
||||
{{ $t("toastSuccessTimeout") }}
|
||||
</label>
|
||||
<input
|
||||
id="toastSuccessTimeoutSecs"
|
||||
v-model="toastSuccessTimeoutSecs"
|
||||
type="number"
|
||||
class="form-control"
|
||||
min="-1"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-4 pt-4">
|
||||
<h5 class="my-4 settings-subheading">{{ $t("settingsCertificateExpiry") }}</h5>
|
||||
<p>{{ $t("certificationExpiryDescription") }}</p>
|
||||
|
@ -58,6 +91,8 @@ export default {
|
|||
|
||||
data() {
|
||||
return {
|
||||
toastSuccessTimeoutSecs: 20,
|
||||
toastErrorTimeoutSecs: -1,
|
||||
/**
|
||||
* Variable to store the input for new certificate expiry day.
|
||||
*/
|
||||
|
@ -77,6 +112,26 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
// Parse, store and apply new timeout settings.
|
||||
toastSuccessTimeoutSecs(newTimeout) {
|
||||
const parsedTimeout = parseInt(newTimeout);
|
||||
if (parsedTimeout != null && !Number.isNaN(parsedTimeout)) {
|
||||
localStorage.toastSuccessTimeout = newTimeout > 0 ? newTimeout * 1000 : newTimeout;
|
||||
}
|
||||
},
|
||||
toastErrorTimeoutSecs(newTimeout) {
|
||||
const parsedTimeout = parseInt(newTimeout);
|
||||
if (parsedTimeout != null && !Number.isNaN(parsedTimeout)) {
|
||||
localStorage.toastErrorTimeout = newTimeout > 0 ? newTimeout * 1000 : newTimeout;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.loadToastTimeoutSettings();
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Remove a day from expiry notification days.
|
||||
|
@ -108,6 +163,27 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads toast timeout settings from storage to component data.
|
||||
*/
|
||||
loadToastTimeoutSettings() {
|
||||
const successTimeout = localStorage.toastSuccessTimeout;
|
||||
if (successTimeout !== undefined) {
|
||||
const parsedTimeout = parseInt(successTimeout);
|
||||
if (parsedTimeout != null && !Number.isNaN(parsedTimeout)) {
|
||||
this.toastSuccessTimeoutSecs = parsedTimeout > 0 ? parsedTimeout / 1000 : parsedTimeout;
|
||||
}
|
||||
}
|
||||
|
||||
const errorTimeout = localStorage.toastErrorTimeout;
|
||||
if (errorTimeout !== undefined) {
|
||||
const parsedTimeout = parseInt(errorTimeout);
|
||||
if (parsedTimeout != null && !Number.isNaN(parsedTimeout)) {
|
||||
this.toastErrorTimeoutSecs = parsedTimeout > 0 ? parsedTimeout / 1000 : parsedTimeout;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -785,6 +785,10 @@
|
|||
"Badge URL": "Badge URL",
|
||||
"Group": "Group",
|
||||
"Monitor Group": "Monitor Group",
|
||||
"monitorToastMessagesLabel": "Monitor Toast notifications",
|
||||
"monitorToastMessagesDescription": "Toast notifications for monitors disappear after given time in seconds. Set to -1 disables timeout. Set to 0 disables toast notifications.",
|
||||
"toastErrorTimeout": "Timeout for Error Notifications",
|
||||
"toastSuccessTimeout": "Timeout for Success Notifications",
|
||||
"Kafka Brokers": "Kafka Brokers",
|
||||
"Enter the list of brokers": "Enter the list of brokers",
|
||||
"Press Enter to add broker": "Press Enter to add broker",
|
||||
|
|
|
@ -117,12 +117,23 @@
|
|||
{{ $t("Settings") }}
|
||||
</router-link>
|
||||
</nav>
|
||||
|
||||
<button
|
||||
v-if="numActiveToasts != 0"
|
||||
type="button"
|
||||
class="btn btn-normal clear-all-toast-btn"
|
||||
@click="clearToasts"
|
||||
>
|
||||
<font-awesome-icon icon="times" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Login from "../components/Login.vue";
|
||||
import compareVersions from "compare-versions";
|
||||
import { useToast } from "vue-toastification";
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
|
||||
|
@ -131,7 +142,11 @@ export default {
|
|||
},
|
||||
|
||||
data() {
|
||||
return {};
|
||||
return {
|
||||
toastContainer: null,
|
||||
numActiveToasts: 0,
|
||||
toastContainerObserver: null,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
@ -159,11 +174,33 @@ export default {
|
|||
},
|
||||
|
||||
mounted() {
|
||||
this.toastContainer = document.querySelector(".bottom-right.toast-container");
|
||||
|
||||
// Watch the number of active toasts
|
||||
this.toastContainerObserver = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === "childList") {
|
||||
this.numActiveToasts = mutation.target.children.length;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.toastContainer != null) {
|
||||
this.toastContainerObserver.observe(this.toastContainer, { childList: true });
|
||||
}
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
this.toastContainerObserver.disconnect();
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
/**
|
||||
* Clear all toast notifications.
|
||||
*/
|
||||
clearToasts() {
|
||||
toast.clear();
|
||||
}
|
||||
},
|
||||
|
||||
};
|
||||
|
@ -323,4 +360,22 @@ main {
|
|||
background-color: $dark-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.clear-all-toast-btn {
|
||||
position: fixed;
|
||||
right: 1em;
|
||||
bottom: 1em;
|
||||
font-size: 1.2em;
|
||||
padding: 9px 15px;
|
||||
width: 48px;
|
||||
box-shadow: 2px 2px 30px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
@media (max-width: 770px) {
|
||||
.clear-all-toast-btn {
|
||||
bottom: 72px;
|
||||
z-index: 100;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -20,6 +20,7 @@ import dayjs from "dayjs";
|
|||
import timezone from "./modules/dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { loadToastSettings } from "./util-frontend";
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(relativeTime);
|
||||
|
@ -44,11 +45,7 @@ const app = createApp({
|
|||
app.use(router);
|
||||
app.use(i18n);
|
||||
|
||||
const options = {
|
||||
position: "bottom-right",
|
||||
};
|
||||
|
||||
app.use(Toast, options);
|
||||
app.use(Toast, loadToastSettings());
|
||||
app.component("Editable", contenteditable);
|
||||
app.component("FontAwesomeIcon", FontAwesomeIcon);
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import jwtDecode from "jwt-decode";
|
|||
import Favico from "favico.js";
|
||||
import dayjs from "dayjs";
|
||||
import { DOWN, MAINTENANCE, PENDING, UP } from "../util.ts";
|
||||
import { getDevContainerServerHostname, isDevContainer } from "../util-frontend.js";
|
||||
import { getDevContainerServerHostname, isDevContainer, getToastSuccessTimeout, getToastErrorTimeout } from "../util-frontend.js";
|
||||
const toast = useToast();
|
||||
|
||||
let socket;
|
||||
|
@ -190,11 +190,11 @@ export default {
|
|||
if (this.monitorList[data.monitorID] !== undefined) {
|
||||
if (data.status === 0) {
|
||||
toast.error(`[${this.monitorList[data.monitorID].name}] [DOWN] ${data.msg}`, {
|
||||
timeout: false,
|
||||
timeout: getToastErrorTimeout(),
|
||||
});
|
||||
} else if (data.status === 1) {
|
||||
toast.success(`[${this.monitorList[data.monitorID].name}] [Up] ${data.msg}`, {
|
||||
timeout: 20000,
|
||||
timeout: getToastSuccessTimeout(),
|
||||
});
|
||||
} else {
|
||||
toast(`[${this.monitorList[data.monitorID].name}] ${data.msg}`);
|
||||
|
@ -683,7 +683,7 @@ export default {
|
|||
*/
|
||||
getMonitorBeats(monitorID, period, callback) {
|
||||
socket.emit("getMonitorBeats", monitorID, period, callback);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import dayjs from "dayjs";
|
||||
import timezones from "timezones-list";
|
||||
import { localeDirection, currentLocale } from "./i18n";
|
||||
import { POSITION } from "vue-toastification";
|
||||
|
||||
/**
|
||||
* Returns the offset from UTC in hours for the current locale.
|
||||
|
@ -149,3 +150,65 @@ export function colorOptions(self) {
|
|||
color: "#DB2777" },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the toast timeout settings from storage.
|
||||
* @returns {object} The toast plugin options object.
|
||||
*/
|
||||
export function loadToastSettings() {
|
||||
return {
|
||||
position: POSITION.BOTTOM_RIGHT,
|
||||
containerClassName: "toast-container mb-5",
|
||||
showCloseButtonOnHover: true,
|
||||
|
||||
filterBeforeCreate: (toast, toasts) => {
|
||||
if (toast.timeout === 0) {
|
||||
return false;
|
||||
} else {
|
||||
return toast;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timeout for success toasts
|
||||
* @returns {(number|boolean)} Timeout in ms. If false timeout disabled.
|
||||
*/
|
||||
export function getToastSuccessTimeout() {
|
||||
let successTimeout = 20000;
|
||||
|
||||
if (localStorage.toastSuccessTimeout !== undefined) {
|
||||
const parsedTimeout = parseInt(localStorage.toastSuccessTimeout);
|
||||
if (parsedTimeout != null && !Number.isNaN(parsedTimeout)) {
|
||||
successTimeout = parsedTimeout;
|
||||
}
|
||||
}
|
||||
|
||||
if (successTimeout === -1) {
|
||||
successTimeout = false;
|
||||
}
|
||||
|
||||
return successTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timeout for error toasts
|
||||
* @returns {(number|boolean)} Timeout in ms. If false timeout disabled.
|
||||
*/
|
||||
export function getToastErrorTimeout() {
|
||||
let errorTimeout = -1;
|
||||
|
||||
if (localStorage.toastErrorTimeout !== undefined) {
|
||||
const parsedTimeout = parseInt(localStorage.toastErrorTimeout);
|
||||
if (parsedTimeout != null && !Number.isNaN(parsedTimeout)) {
|
||||
errorTimeout = parsedTimeout;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorTimeout === -1) {
|
||||
errorTimeout = false;
|
||||
}
|
||||
|
||||
return errorTimeout;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue