implemented event log page for the device dashboard

This commit is contained in:
robert 2023-06-15 06:11:07 -04:00
parent 7ea2093a91
commit bcc044eb92
8 changed files with 5303 additions and 4993 deletions

View File

@ -4,6 +4,18 @@
#include "mongoose.h" #include "mongoose.h"
#include "net.h" #include "net.h"
void ui_event_next() {
if (events_no < 0 || events_no >= MAX_EVENTS_NO)
return;
events[events_no].type = rand() % 3;
events[events_no].prio = rand() % 3;
events[events_no].timestamp = events_no;
mg_snprintf(events[events_no].text, MAX_EVENT_TEXT_SIZE,
"event#%d", events_no);
events_no++;
}
static int s_sig_num; static int s_sig_num;
static void signal_handler(int sig_num) { static void signal_handler(int sig_num) {
signal(sig_num, signal_handler); signal(sig_num, signal_handler);
@ -12,17 +24,25 @@ static void signal_handler(int sig_num) {
int main(void) { int main(void) {
struct mg_mgr mgr; struct mg_mgr mgr;
uint64_t last_ts = mg_millis();
uint64_t crt_ts;
signal(SIGPIPE, SIG_IGN); signal(SIGPIPE, SIG_IGN);
signal(SIGINT, signal_handler); signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler); signal(SIGTERM, signal_handler);
srand(time(NULL));
mg_log_set(MG_LL_DEBUG); // Set debug log level mg_log_set(MG_LL_DEBUG); // Set debug log level
mg_mgr_init(&mgr); mg_mgr_init(&mgr);
web_init(&mgr); web_init(&mgr);
while (s_sig_num == 0) { while (s_sig_num == 0) {
mg_mgr_poll(&mgr, 50); mg_mgr_poll(&mgr, 50);
crt_ts = mg_millis();
if (crt_ts - last_ts > 1000) {
last_ts = crt_ts;
ui_event_next(); // generate a new event
}
} }
mg_mgr_free(&mgr); mg_mgr_free(&mgr);

View File

@ -13,12 +13,8 @@ struct user {
const char *name, *pass, *access_token; const char *name, *pass, *access_token;
}; };
// Event log entry int events_no;
struct event { struct ui_event events[MAX_EVENTS_NO];
int type, prio;
unsigned long timestamp;
const char *text;
};
// Settings // Settings
struct settings { struct settings {
@ -30,17 +26,6 @@ struct settings {
static struct settings s_settings = {true, 1, 57, NULL}; static struct settings s_settings = {true, 1, 57, NULL};
// Mocked events
static struct event s_events[] = {
{.type = 0, .prio = 0, .text = "here goes event 1"},
{.type = 1, .prio = 2, .text = "event 2..."},
{.type = 2, .prio = 1, .text = "another event"},
{.type = 1, .prio = 1, .text = "something happened!"},
{.type = 2, .prio = 0, .text = "once more..."},
{.type = 2, .prio = 0, .text = "more again..."},
{.type = 1, .prio = 1, .text = "oops. it happened again"},
};
static const char *s_json_header = static const char *s_json_header =
"Content-Type: application/json\r\n" "Content-Type: application/json\r\n"
"Cache-Control: no-cache\r\n"; "Cache-Control: no-cache\r\n";
@ -66,12 +51,6 @@ static const char *s_ssl_key =
"6YbyU/ZGtdGfbaGYYJwatKNMX00OIwtb8A==\n" "6YbyU/ZGtdGfbaGYYJwatKNMX00OIwtb8A==\n"
"-----END EC PRIVATE KEY-----\n"; "-----END EC PRIVATE KEY-----\n";
static int event_next(int no, struct event *e) {
if (no < 0 || no >= (int) (sizeof(s_events) / sizeof(s_events[0]))) return 0;
*e = s_events[no];
return no + 1;
}
// This is for newlib and TLS (mbedTLS) // This is for newlib and TLS (mbedTLS)
uint64_t mg_now(void) { uint64_t mg_now(void) {
return mg_millis() + s_boot_timestamp; return mg_millis() + s_boot_timestamp;
@ -170,22 +149,34 @@ static void handle_stats_get(struct mg_connection *c) {
static size_t print_events(void (*out)(char, void *), void *ptr, va_list *ap) { static size_t print_events(void (*out)(char, void *), void *ptr, va_list *ap) {
size_t len = 0; size_t len = 0;
struct event e; int page_number = va_arg(*ap, int);
int no = 0; int start = (page_number - 1) * EVENTS_PER_PAGE;
while ((no = event_next(no, &e)) != 0) { int end = start + EVENTS_PER_PAGE;
for (int i = start; i < end && i < events_no; i++) {
len += mg_xprintf(out, ptr, "%s{%m:%lu,%m:%d,%m:%d,%m:%m}", // len += mg_xprintf(out, ptr, "%s{%m:%lu,%m:%d,%m:%d,%m:%m}", //
len == 0 ? "" : ",", // len == 0 ? "" : ",", //
MG_ESC("time"), e.timestamp, // MG_ESC("time"), events[i].timestamp, //
MG_ESC("type"), e.type, // MG_ESC("type"), events[i].type, //
MG_ESC("prio"), e.prio, // MG_ESC("prio"), events[i].prio, //
MG_ESC("text"), MG_ESC(e.text)); MG_ESC("text"), MG_ESC(events[i].text));
} }
(void) ap;
return len; return len;
} }
static void handle_events_get(struct mg_connection *c) { static void handle_events_get(struct mg_connection *c, struct mg_str query) {
mg_http_reply(c, 200, s_json_header, "[%M]", print_events); int page_number;
bool is_last_page;
// query is represented by 'page=<page_id>'
page_number = atoi(query.ptr + 5);
if (page_number > events_no / EVENTS_PER_PAGE + 1 || page_number < 1)
page_number = 1;
mg_http_reply(c, 200, s_json_header, "{%m:[%M], %m:%d}", MG_ESC("arr"),
print_events, page_number,
MG_ESC("totalCount"), events_no);
} }
static void handle_settings_set(struct mg_connection *c, struct mg_str body) { static void handle_settings_set(struct mg_connection *c, struct mg_str body) {
@ -240,7 +231,7 @@ static void fn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
} else if (mg_http_match_uri(hm, "/api/stats/get")) { } else if (mg_http_match_uri(hm, "/api/stats/get")) {
handle_stats_get(c); handle_stats_get(c);
} else if (mg_http_match_uri(hm, "/api/events/get")) { } else if (mg_http_match_uri(hm, "/api/events/get")) {
handle_events_get(c); handle_events_get(c, hm->query);
} else if (mg_http_match_uri(hm, "/api/settings/get")) { } else if (mg_http_match_uri(hm, "/api/settings/get")) {
handle_settings_get(c); handle_settings_get(c);
} else if (mg_http_match_uri(hm, "/api/settings/set")) { } else if (mg_http_match_uri(hm, "/api/settings/set")) {

View File

@ -13,5 +13,19 @@
#endif #endif
#define MAX_DEVICE_NAME 40 #define MAX_DEVICE_NAME 40
#define MAX_EVENTS_NO 1000
#define MAX_EVENT_TEXT_SIZE 10
#define EVENTS_PER_PAGE 20
// Event log entry
struct ui_event {
int type, prio;
unsigned long timestamp;
char text[10];
};
extern int events_no;
extern struct ui_event events[MAX_EVENTS_NO];
void ui_event_next();
void web_init(struct mg_mgr *mgr); void web_init(struct mg_mgr *mgr);

File diff suppressed because it is too large Load Diff

View File

@ -205,3 +205,57 @@ export function Setting(props) {
<//>`; <//>`;
}; };
export function Pagination({ totalItems, itemsPerPage, currentPage, setPageFn }) {
const totalPages = Math.ceil(totalItems / itemsPerPage);
const maxPageRange = 2;
const lessThanSymbol = "<";
const greaterThanSymbol = ">";
const whiteSpace = " ";
// Function to handle rendering of individual page item
const PageItem = ({ page, isActive }) => (
html`<a
onClick=${() => setPageFn(page)}
className=${`relative inline-flex items-center px-4 py-2 text-sm font-semibold ${isActive ? 'bg-blue-600 text-white' : 'text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50'} focus:z-20 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600`}
>
${page}
</a>`
);
return html`
<div className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6">
<div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between space-x-4">
<p className="text-sm text-gray-700">
showing entries <span className="font-medium">${(currentPage - 1) * itemsPerPage + 1}</span> - <span className="font-medium">${Math.min(currentPage * itemsPerPage, totalItems)}</span> of ${whiteSpace}
<span className="font-medium">${totalItems}</span> results
</p>
<div>
<nav className="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
<a
onClick=${() => setPageFn(Math.max(currentPage - 1, 1))}
className="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0">
${lessThanSymbol}
<span className="sr-only">Previous</span>
<ChevronLeftIcon className="h-5 w-5" aria-hidden="true" />
</a>
<${PageItem} page=${1} isActive=${currentPage === 1} />
${currentPage > maxPageRange + 2 ? html`<span className="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-700 ring-1 ring-inset ring-gray-300 focus:outline-offset-0">...</span>` : ''}
${Array.from({length: Math.min(totalPages, maxPageRange * 2 + 1)}, (_, i) => Math.max(2, currentPage - maxPageRange) + i).map(page => page > 1 && page < totalPages && html`
<${PageItem} page=${page} isActive=${currentPage === page} />
`)}
${currentPage < totalPages - (maxPageRange + 1) ? html`<span className="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-700 ring-1 ring-inset ring-gray-300 focus:outline-offset-0">...</span>` : ''}
${totalPages > 1 ? html`<${PageItem} page=${totalPages} isActive=${currentPage === totalPages} />` : ''}
<a
onClick=${() => setPageFn(Math.min(currentPage + 1, totalPages))}
className="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0">
${greaterThanSymbol}
<span className="sr-only">Next</span>
<ChevronRightIcon className="h-5 w-5" aria-hidden="true" />
</a>
</nav>
</div>
</div>
</div>`;
};

View File

@ -9,7 +9,7 @@
<link href="main.css" rel="stylesheet" /> <link href="main.css" rel="stylesheet" />
<link href="https://rsms.me/inter/inter.css" rel="stylesheet" /> <link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
</head> </head>
<body class="h-full"></body> <body class="min-h-screen bg-slate-100"></body>
<script src="history.min.js"></script> <script src="history.min.js"></script>
<script type="module" src="main.js"></script> <script type="module" src="main.js"></script>
</html> </html>

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { h, render, useState, useEffect, useRef, html, Router } from './bundle.js'; import { h, render, useState, useEffect, useRef, html, Router } from './bundle.js';
import { Icons, Login, Setting, Button, Stat, tipColors, Colored, Notification } from './components.js'; import { Icons, Login, Setting, Button, Stat, tipColors, Colored, Notification, Pagination } from './components.js';
const Logo = props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12.87 12.85"><defs><style>.ll-cls-1{fill:none;stroke:#000;stroke-miterlimit:10;stroke-width:0.5px;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="ll-cls-1" d="M12.62,1.82V8.91A1.58,1.58,0,0,1,11,10.48H4a1.44,1.44,0,0,1-1-.37A.69.69,0,0,1,2.84,10l-.1-.12a.81.81,0,0,1-.15-.48V5.57a.87.87,0,0,1,.86-.86H4.73V7.28a.86.86,0,0,0,.86.85H9.42a.85.85,0,0,0,.85-.85V3.45A.86.86,0,0,0,10.13,3,.76.76,0,0,0,10,2.84a.29.29,0,0,0-.12-.1,1.49,1.49,0,0,0-1-.37H2.39V1.82A1.57,1.57,0,0,1,4,.25H11A1.57,1.57,0,0,1,12.62,1.82Z"/><path class="ll-cls-1" d="M10.48,10.48V11A1.58,1.58,0,0,1,8.9,12.6H1.82A1.57,1.57,0,0,1,.25,11V3.94A1.57,1.57,0,0,1,1.82,2.37H8.9a1.49,1.49,0,0,1,1,.37l.12.1a.76.76,0,0,1,.11.14.86.86,0,0,1,.14.47V7.28a.85.85,0,0,1-.85.85H8.13V5.57a.86.86,0,0,0-.85-.86H3.45a.87.87,0,0,0-.86.86V9.4a.81.81,0,0,0,.15.48l.1.12a.69.69,0,0,0,.13.11,1.44,1.44,0,0,0,1,.37Z"/></g></g></svg>`; const Logo = props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12.87 12.85"><defs><style>.ll-cls-1{fill:none;stroke:#000;stroke-miterlimit:10;stroke-width:0.5px;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="ll-cls-1" d="M12.62,1.82V8.91A1.58,1.58,0,0,1,11,10.48H4a1.44,1.44,0,0,1-1-.37A.69.69,0,0,1,2.84,10l-.1-.12a.81.81,0,0,1-.15-.48V5.57a.87.87,0,0,1,.86-.86H4.73V7.28a.86.86,0,0,0,.86.85H9.42a.85.85,0,0,0,.85-.85V3.45A.86.86,0,0,0,10.13,3,.76.76,0,0,0,10,2.84a.29.29,0,0,0-.12-.1,1.49,1.49,0,0,0-1-.37H2.39V1.82A1.57,1.57,0,0,1,4,.25H11A1.57,1.57,0,0,1,12.62,1.82Z"/><path class="ll-cls-1" d="M10.48,10.48V11A1.58,1.58,0,0,1,8.9,12.6H1.82A1.57,1.57,0,0,1,.25,11V3.94A1.57,1.57,0,0,1,1.82,2.37H8.9a1.49,1.49,0,0,1,1,.37l.12.1a.76.76,0,0,1,.11.14.86.86,0,0,1,.14.47V7.28a.85.85,0,0,1-.85.85H8.13V5.57a.86.86,0,0,0-.85-.86H3.45a.87.87,0,0,0-.86.86V9.4a.81.81,0,0,0,.15.48l.1.12a.69.69,0,0,0,.13.11,1.44,1.44,0,0,0,1,.37Z"/></g></g></svg>`;
@ -44,6 +44,7 @@ function Sidebar({url, show}) {
<div class="flex flex-1 flex-col"> <div class="flex flex-1 flex-col">
<${NavLink} title="Dashboard" icon=${Icons.home} href="/" url=${url} /> <${NavLink} title="Dashboard" icon=${Icons.home} href="/" url=${url} />
<${NavLink} title="Settings" icon=${Icons.settings} href="/settings" url=${url} /> <${NavLink} title="Settings" icon=${Icons.settings} href="/settings" url=${url} />
<${NavLink} title="Events" icon=${Icons.alert} href="/events" url=${url} />
<//> <//>
<//> <//>
<//>`; <//>`;
@ -51,8 +52,18 @@ function Sidebar({url, show}) {
function Events({}) { function Events({}) {
const [events, setEvents] = useState([]); const [events, setEvents] = useState([]);
const refresh = () => fetch('api/events/get').then(r => r.json()).then(r => setEvents(r)).catch(e => console.log(e)); const [page, setPage] = useState(1);
useEffect(refresh, []);
const refresh = () => fetch(`api/events/get?page=${page}`).then(r => r.json()).then(r => {console.log(r); setEvents(r)}).catch(e => console.log(e));
useEffect(refresh, [page]);
useEffect(() => {
setPage(JSON.parse(localStorage.getItem('page')));
}, []);
useEffect(() => {
localStorage.setItem('page', page.toString());
}, [page]);
const Th = props => html`<th scope="col" class="sticky top-0 z-10 border-b border-slate-300 bg-white bg-opacity-75 py-1.5 px-4 text-left text-sm font-semibold text-slate-900 backdrop-blur backdrop-filter">${props.title}</th>`; const Th = props => html`<th scope="col" class="sticky top-0 z-10 border-b border-slate-300 bg-white bg-opacity-75 py-1.5 px-4 text-left text-sm font-semibold text-slate-900 backdrop-blur backdrop-filter">${props.title}</th>`;
const Td = props => html`<td class="whitespace-nowrap border-b border-slate-200 py-2 px-4 pr-3 text-sm text-slate-900">${props.text}</td>`; const Td = props => html`<td class="whitespace-nowrap border-b border-slate-200 py-2 px-4 pr-3 text-sm text-slate-900">${props.text}</td>`;
@ -61,6 +72,7 @@ function Events({}) {
const colors = [tipColors.red, tipColors.yellow, tipColors.green][prio]; const colors = [tipColors.red, tipColors.yellow, tipColors.green][prio];
return html`<${Colored} colors=${colors} text=${text} />`; return html`<${Colored} colors=${colors} text=${text} />`;
}; };
const Event = ({e}) => html` const Event = ({e}) => html`
<tr> <tr>
<${Td} text=${['power', 'hardware', 'tier3', 'tier4'][e.type]} /> <${Td} text=${['power', 'hardware', 'tier3', 'tier4'][e.type]} />
@ -68,16 +80,16 @@ function Events({}) {
<${Td} text=${e.time || '1970-01-01'} /> <${Td} text=${e.time || '1970-01-01'} />
<${Td} text=${e.text} /> <${Td} text=${e.text} />
<//>`; <//>`;
//console.log(events);
return html` return html`
<div class="my-4 h-64 divide-y divide-gray-200 rounded bg-white overflow-auto"> <div class="divide-y divide-gray-200 overflow-x-auto shadow-xl rounded-xl bg-white dark:bg-gray-700 my-3 mx-10">
<div class="font-light uppercase flex items-center text-slate-600 px-4 py-2"> <div class="font-light flex items-center text-slate-600 px-4 py-2 justify-between">
Event Log EVENT LOG
<//> <${Pagination} currentPage=${page} setPageFn=${setPage} totalItems=${events.totalCount} itemsPerPage=20 />
<div class=""> </div>
<table class=""> <div class="align-middle inline-block w-full">
<thead> <table class="w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm md:text-base lg:text-lg">
<thead class="bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300">
<tr> <tr>
<${Th} title="Type" /> <${Th} title="Type" />
<${Th} title="Prio" /> <${Th} title="Prio" />
@ -85,8 +97,8 @@ function Events({}) {
<${Th} title="Description" /> <${Th} title="Description" />
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200">
${events.map(e => h(Event, {e}))} ${(events.arr ? events.arr : []).map(e => h(Event, {e}))}
</tbody> </tbody>
</table> </table>
<//> <//>
@ -148,15 +160,6 @@ function Main({}) {
<//> <//>
<//> <//>
<div class="p-4 sm:p-2 mx-auto grid grid-cols-1 lg:grid-cols-2 gap-4"> <div class="p-4 sm:p-2 mx-auto grid grid-cols-1 lg:grid-cols-2 gap-4">
<${Events} />
<div class="my-4 hx-24 bg-white border rounded-md shadow-lg" role="alert">
<${DeveloperNote}
text="Events data is also received from the backend,
via the /api/events/get API call, which returns an array of objects each
representing an event. Events table is scrollable,
Table header is sticky" />
<//>
<${Chart} data=${stats.points} /> <${Chart} data=${stats.points} />
@ -244,6 +247,7 @@ const App = function({}) {
<${Router} onChange=${ev => setUrl(ev.url)} history=${History.createHashHistory()} > <${Router} onChange=${ev => setUrl(ev.url)} history=${History.createHashHistory()} >
<${Main} default=${true} /> <${Main} default=${true} />
<${Settings} path="settings" /> <${Settings} path="settings" />
<${Events} path="events">
<//> <//>
<//> <//>
<//>`; <//>`;