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:
Nelson Chan 2023-09-06 19:52:54 +08:00 committed by GitHub
parent 62f4434711
commit bfc7b498be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 218 additions and 11 deletions

View file

@ -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 // Localization
@import "localization.scss"; @import "localization.scss";

View file

@ -20,6 +20,39 @@
</button> </button>
</div> </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"> <div class="my-4 pt-4">
<h5 class="my-4 settings-subheading">{{ $t("settingsCertificateExpiry") }}</h5> <h5 class="my-4 settings-subheading">{{ $t("settingsCertificateExpiry") }}</h5>
<p>{{ $t("certificationExpiryDescription") }}</p> <p>{{ $t("certificationExpiryDescription") }}</p>
@ -58,6 +91,8 @@ export default {
data() { data() {
return { return {
toastSuccessTimeoutSecs: 20,
toastErrorTimeoutSecs: -1,
/** /**
* Variable to store the input for new certificate expiry day. * 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: { methods: {
/** /**
* Remove a day from expiry notification days. * 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> </script>

View file

@ -785,6 +785,10 @@
"Badge URL": "Badge URL", "Badge URL": "Badge URL",
"Group": "Group", "Group": "Group",
"Monitor Group": "Monitor 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", "Kafka Brokers": "Kafka Brokers",
"Enter the list of brokers": "Enter the list of brokers", "Enter the list of brokers": "Enter the list of brokers",
"Press Enter to add broker": "Press Enter to add broker", "Press Enter to add broker": "Press Enter to add broker",

View file

@ -117,12 +117,23 @@
{{ $t("Settings") }} {{ $t("Settings") }}
</router-link> </router-link>
</nav> </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> </div>
</template> </template>
<script> <script>
import Login from "../components/Login.vue"; import Login from "../components/Login.vue";
import compareVersions from "compare-versions"; import compareVersions from "compare-versions";
import { useToast } from "vue-toastification";
const toast = useToast();
export default { export default {
@ -131,7 +142,11 @@ export default {
}, },
data() { data() {
return {}; return {
toastContainer: null,
numActiveToasts: 0,
toastContainerObserver: null,
};
}, },
computed: { computed: {
@ -159,11 +174,33 @@ export default {
}, },
mounted() { 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: { methods: {
/**
* Clear all toast notifications.
*/
clearToasts() {
toast.clear();
}
}, },
}; };
@ -323,4 +360,22 @@ main {
background-color: $dark-bg; 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> </style>

View file

@ -20,6 +20,7 @@ import dayjs from "dayjs";
import timezone from "./modules/dayjs/plugin/timezone"; import timezone from "./modules/dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
import relativeTime from "dayjs/plugin/relativeTime"; import relativeTime from "dayjs/plugin/relativeTime";
import { loadToastSettings } from "./util-frontend";
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(timezone); dayjs.extend(timezone);
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
@ -44,11 +45,7 @@ const app = createApp({
app.use(router); app.use(router);
app.use(i18n); app.use(i18n);
const options = { app.use(Toast, loadToastSettings());
position: "bottom-right",
};
app.use(Toast, options);
app.component("Editable", contenteditable); app.component("Editable", contenteditable);
app.component("FontAwesomeIcon", FontAwesomeIcon); app.component("FontAwesomeIcon", FontAwesomeIcon);

View file

@ -4,7 +4,7 @@ import jwtDecode from "jwt-decode";
import Favico from "favico.js"; import Favico from "favico.js";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { DOWN, MAINTENANCE, PENDING, UP } from "../util.ts"; 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(); const toast = useToast();
let socket; let socket;
@ -190,11 +190,11 @@ export default {
if (this.monitorList[data.monitorID] !== undefined) { if (this.monitorList[data.monitorID] !== undefined) {
if (data.status === 0) { if (data.status === 0) {
toast.error(`[${this.monitorList[data.monitorID].name}] [DOWN] ${data.msg}`, { toast.error(`[${this.monitorList[data.monitorID].name}] [DOWN] ${data.msg}`, {
timeout: false, timeout: getToastErrorTimeout(),
}); });
} else if (data.status === 1) { } else if (data.status === 1) {
toast.success(`[${this.monitorList[data.monitorID].name}] [Up] ${data.msg}`, { toast.success(`[${this.monitorList[data.monitorID].name}] [Up] ${data.msg}`, {
timeout: 20000, timeout: getToastSuccessTimeout(),
}); });
} else { } else {
toast(`[${this.monitorList[data.monitorID].name}] ${data.msg}`); toast(`[${this.monitorList[data.monitorID].name}] ${data.msg}`);
@ -683,7 +683,7 @@ export default {
*/ */
getMonitorBeats(monitorID, period, callback) { getMonitorBeats(monitorID, period, callback) {
socket.emit("getMonitorBeats", monitorID, period, callback); socket.emit("getMonitorBeats", monitorID, period, callback);
} },
}, },
computed: { computed: {

View file

@ -1,6 +1,7 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import timezones from "timezones-list"; import timezones from "timezones-list";
import { localeDirection, currentLocale } from "./i18n"; import { localeDirection, currentLocale } from "./i18n";
import { POSITION } from "vue-toastification";
/** /**
* Returns the offset from UTC in hours for the current locale. * Returns the offset from UTC in hours for the current locale.
@ -149,3 +150,65 @@ export function colorOptions(self) {
color: "#DB2777" }, 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;
}