mirror of
https://github.com/cesanta/mongoose.git
synced 2025-06-19 17:50:53 +08:00
implemented event log page for the device dashboard
This commit is contained in:
parent
7ea2093a91
commit
bcc044eb92
@ -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);
|
||||||
|
@ -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")) {
|
||||||
|
@ -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
@ -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>`;
|
||||||
|
};
|
@ -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
@ -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">
|
||||||
<//>
|
<//>
|
||||||
<//>
|
<//>
|
||||||
<//>`;
|
<//>`;
|
||||||
|
Loading…
Reference in New Issue
Block a user