Web: add notification history pop-over
Add a notification pop-over, which can be opened by clicking the footer bar.
This commit is contained in:
parent
bc355d68ab
commit
40bed3db5e
@ -67,7 +67,7 @@ body {
|
||||
color-scheme: dark;
|
||||
font-family: var(--font-family-body);
|
||||
font-size: var(--font-size-base);
|
||||
height: 100vh;
|
||||
height: calc(100vh);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
@ -215,6 +215,8 @@ footer {
|
||||
font-size: var(--font-size-sm);
|
||||
grid-area: footer;
|
||||
padding: var(--spacer-sm);
|
||||
|
||||
background-color: var(--color-background-column);
|
||||
}
|
||||
|
||||
.btn-bar {
|
||||
@ -360,7 +362,7 @@ ul.status-filter-bar .status-filter-indicator .indicator {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.tabulator-row {
|
||||
.job-list .tabulator-row, .tasks-list .tabulator-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@ -371,3 +373,36 @@ ul.status-filter-bar .status-filter-indicator .indicator {
|
||||
.tabulator-row.active-row.tabulator-row-even {
|
||||
background-color: var(--table-color-background-row-active-even);
|
||||
}
|
||||
|
||||
footer.window-footer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
section.footer-popup {
|
||||
position: absolute;
|
||||
bottom: var(--grid-gap);
|
||||
left: var(--grid-gap);
|
||||
right: var(--grid-gap);
|
||||
height: 20vh;
|
||||
|
||||
z-index: 42;
|
||||
padding: 0.2rem 0.5rem;
|
||||
|
||||
background-color: var(--color-background-column);
|
||||
border-radius: 0.3rem;
|
||||
border: thin solid var(--color-border);
|
||||
|
||||
box-shadow: 0 0 2rem black;
|
||||
}
|
||||
|
||||
section.footer-popup header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
section.footer-popup header h3 {
|
||||
margin: 0 auto 0 0;
|
||||
}
|
||||
|
||||
section.footer-popup button {
|
||||
float: right;
|
||||
}
|
||||
|
66
web/app/src/components/FooterPopup.vue
Normal file
66
web/app/src/components/FooterPopup.vue
Normal file
@ -0,0 +1,66 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { TabulatorFull as Tabulator } from 'tabulator-tables';
|
||||
import { useNotifs } from '@/stores/notifications'
|
||||
import * as datetime from "@/datetime";
|
||||
|
||||
const notifs = useNotifs();
|
||||
const emit = defineEmits(['clickClose'])
|
||||
|
||||
const tabOptions = {
|
||||
columns: [
|
||||
{
|
||||
title: 'Time', field: 'time',
|
||||
sorter: 'alphanum', sorterParams: { alignEmptyValues: "top" },
|
||||
formatter(cell) {
|
||||
const cellValue = cell.getData().time;
|
||||
return datetime.shortened(cellValue);
|
||||
},
|
||||
widthGrow: 1,
|
||||
resizable: true,
|
||||
},
|
||||
{
|
||||
title: 'Message',
|
||||
field: 'msg',
|
||||
sorter: 'string',
|
||||
widthGrow: 100,
|
||||
resizable: true,
|
||||
},
|
||||
],
|
||||
initialSort: [
|
||||
{ column: "time", dir: "asc" },
|
||||
],
|
||||
layout: "fitDataStretch",
|
||||
resizableColumnFit: true,
|
||||
height: "calc(20vh - 3rem)", // Must be set in order for the virtual DOM to function correctly.
|
||||
data: notifs.history,
|
||||
selectable: false,
|
||||
};
|
||||
|
||||
let tabulator = null;
|
||||
onMounted(() => {
|
||||
tabulator = new Tabulator('#notification_list', tabOptions);
|
||||
tabulator.on("tableBuilt", _scrollToBottom);
|
||||
tabulator.on("tableBuilt", _subscribeToPinia);
|
||||
});
|
||||
|
||||
function _scrollToBottom() {
|
||||
tabulator.scrollToRow(notifs.lastID, "bottom", false);
|
||||
}
|
||||
function _subscribeToPinia() {
|
||||
notifs.$subscribe(() => {
|
||||
tabulator.setData(notifs.history)
|
||||
.then(_scrollToBottom)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="footer-popup">
|
||||
<header>
|
||||
<h3 class="sub-title">Notifications</h3>
|
||||
<button class='close' @click="emit('clickClose')">X</button>
|
||||
</header>
|
||||
<div id="notification_list"></div>
|
||||
</section>
|
||||
</template>
|
@ -5,17 +5,25 @@ const relativeTimeDefaultOptions = {
|
||||
format: DateTime.DATE_MED_WITH_WEEKDAY,
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given timestamp to a Luxon time object.
|
||||
*
|
||||
* @param {Date | string} timestamp either a Date object or an ISO time string.
|
||||
* @returns Luxon time object.
|
||||
*/
|
||||
function parseTimestamp(timestamp) {
|
||||
if (timestamp instanceof Date) {
|
||||
return DateTime.fromJSDate(timestamp);
|
||||
}
|
||||
return DateTime.fromISO(timestamp);
|
||||
}
|
||||
|
||||
// relativeTime parses the timestamp (can be ISO-formatted string or JS Date
|
||||
// object) and returns it in string form. The returned string is either "xxx
|
||||
// time ago" if it's a relatively short time ago, or the formatted absolute time
|
||||
// otherwise.
|
||||
export function relativeTime(timestamp, options) {
|
||||
let parsedTimestamp = null;
|
||||
if (timestamp instanceof Date) {
|
||||
parsedTimestamp = DateTime.fromJSDate(timestamp);
|
||||
} else {
|
||||
parsedTimestamp = DateTime.fromISO(timestamp);
|
||||
}
|
||||
const parsedTimestamp = parseTimestamp(timestamp);
|
||||
|
||||
if (!options) options = relativeTimeDefaultOptions;
|
||||
|
||||
@ -25,3 +33,12 @@ export function relativeTime(timestamp, options) {
|
||||
return parsedTimestamp.toLocaleString(options.format);
|
||||
return parsedTimestamp.toRelative({style: "narrow"});
|
||||
}
|
||||
|
||||
export function shortened(timestamp) {
|
||||
const parsedTimestamp = parseTimestamp(timestamp);
|
||||
const now = DateTime.local();
|
||||
const ageInHours = now.diff(parsedTimestamp).as('hours');
|
||||
if (ageInHours < 24)
|
||||
return parsedTimestamp.toLocaleString(DateTime.TIME_24_SIMPLE);
|
||||
return parsedTimestamp.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY);
|
||||
}
|
||||
|
@ -8,12 +8,19 @@ const MESSAGE_HIDE_DELAY_MS = 5000;
|
||||
*/
|
||||
export const useNotifs = defineStore('notifications', {
|
||||
state: () => ({
|
||||
/** @type {{ msg: string, time: Date }[]} */
|
||||
/**
|
||||
* History of notifications.
|
||||
*
|
||||
* The 'id' is just for Tabulator to uniquely identify rows, in order to be
|
||||
* able to scroll to them.
|
||||
*
|
||||
* @type {{ id: Number, msg: string, time: Date }[]} */
|
||||
history: [],
|
||||
/** @type { msg: string, time: Date } */
|
||||
/** @type { id: Number, msg: string, time: Date } */
|
||||
last: "",
|
||||
|
||||
hideTimerID: 0,
|
||||
lastID: 0,
|
||||
}),
|
||||
actions: {
|
||||
/**
|
||||
@ -21,9 +28,10 @@ export const useNotifs = defineStore('notifications', {
|
||||
* @param {string} message
|
||||
*/
|
||||
add(message) {
|
||||
const notif = {msg: message, time: new Date()};
|
||||
const notif = {id: this._generateID(), msg: message, time: new Date()};
|
||||
this.history.push(notif);
|
||||
this.last = notif;
|
||||
console.log("New notification:", plain(notif));
|
||||
this._prune();
|
||||
this._restartHideTimer();
|
||||
},
|
||||
@ -44,5 +52,8 @@ export const useNotifs = defineStore('notifications', {
|
||||
last: "",
|
||||
});
|
||||
},
|
||||
_generateID() {
|
||||
return ++this.lastID;
|
||||
}
|
||||
},
|
||||
})
|
||||
|
@ -9,12 +9,13 @@
|
||||
<div class="col col-3">
|
||||
<task-details :taskData="tasks.activeTask" />
|
||||
</div>
|
||||
<footer>
|
||||
<notification-bar />
|
||||
<update-listener ref="updateListener" :websocketURL="websocketURL" :subscribedJob="jobID"
|
||||
@jobUpdate="onSioJobUpdate" @taskUpdate="onSioTaskUpdate" @message="onChatMessage"
|
||||
@sioReconnected="onSIOReconnected" @sioDisconnected="onSIODisconnected" />
|
||||
</footer>
|
||||
|
||||
<footer class="window-footer" v-if="!showFooterPopup" @click="showFooterPopup = true"><notification-bar /></footer>
|
||||
<footer-popup v-if="showFooterPopup" ref="footerPopup" @clickClose="showFooterPopup = false" />
|
||||
|
||||
<update-listener ref="updateListener" :websocketURL="websocketURL" :subscribedJob="jobID"
|
||||
@jobUpdate="onSioJobUpdate" @taskUpdate="onSioTaskUpdate" @message="onChatMessage"
|
||||
@sioReconnected="onSIOReconnected" @sioDisconnected="onSIODisconnected" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -24,6 +25,7 @@ import { useJobs } from '@/stores/jobs';
|
||||
import { useTasks } from '@/stores/tasks';
|
||||
import { apiClient } from '@/stores/api-query-count';
|
||||
|
||||
import FooterPopup from '@/components/FooterPopup.vue'
|
||||
import JobDetails from '@/components/JobDetails.vue'
|
||||
import JobsTable from '@/components/JobsTable.vue'
|
||||
import NotificationBar from '@/components/NotificationBar.vue'
|
||||
@ -35,6 +37,7 @@ export default {
|
||||
name: 'JobsView',
|
||||
props: ["jobID", "taskID"], // provided by Vue Router.
|
||||
components: {
|
||||
FooterPopup,
|
||||
JobDetails,
|
||||
JobsTable,
|
||||
NotificationBar,
|
||||
@ -48,9 +51,11 @@ export default {
|
||||
|
||||
jobs: useJobs(),
|
||||
tasks: useTasks(),
|
||||
showFooterPopup: false,
|
||||
}),
|
||||
mounted() {
|
||||
window.jobsView = this;
|
||||
window.footerPopup = this.$refs.footerPopup;
|
||||
|
||||
// Useful for debugging:
|
||||
// this.jobs.$subscribe((mutation, state) => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user