|
@@ -1,11 +1,55 @@
|
|
<script lang="typescript">
|
|
<script lang="typescript">
|
|
- import type { PollOptions, PollResult, LogEvent } from "src/routes/logs/poll";
|
|
|
|
|
|
+ import type {
|
|
|
|
+ PollOptions,
|
|
|
|
+ PollResult,
|
|
|
|
+ LogEvent,
|
|
|
|
+ } from "src/routes/logs/poll";
|
|
|
|
+ import { faSyncAlt } from "@fortawesome/free-solid-svg-icons";
|
|
|
|
+ import Icon from "svelte-awesome/components/Icon.svelte";
|
|
|
|
|
|
- let logEvents: LogEvent[] = [];
|
|
|
|
|
|
+ const sync = (faSyncAlt as unknown) as undefined;
|
|
|
|
+ const POLL_INTERVAL = 5000;
|
|
|
|
+ let logEvents: LogEvent[] | undefined = undefined;
|
|
|
|
+ let syncing = false;
|
|
|
|
+ let poller = 0;
|
|
|
|
+ let maxMessages = 50;
|
|
|
|
+ let error = "";
|
|
|
|
+
|
|
|
|
+ const LEVEL_COLORS: Record<string, string> = {
|
|
|
|
+ "info": "#EFF2F7",
|
|
|
|
+ "warn": "#ECC94B",
|
|
|
|
+ "error": "#C53030",
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ function getLevelColor(level: string) {
|
|
|
|
+ if (level in LEVEL_COLORS) {
|
|
|
|
+ return LEVEL_COLORS[level];
|
|
|
|
+ }
|
|
|
|
+ return "#FFFFFF";
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ function onAutoUpdateChecked(event: Event & { target: HTMLInputElement }) {
|
|
|
|
+ if (event.target.checked) {
|
|
|
|
+ const pollCallback = async () => {
|
|
|
|
+ await pollEvents();
|
|
|
|
+ poller = setTimeout(pollCallback, POLL_INTERVAL);
|
|
|
|
+ }
|
|
|
|
+ if (poller >= 0) {
|
|
|
|
+ poller = setTimeout(pollCallback, POLL_INTERVAL);
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ clearTimeout(poller);
|
|
|
|
+ poller = -1;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
|
|
async function pollEvents() {
|
|
async function pollEvents() {
|
|
|
|
+ if (syncing) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ syncing = true;
|
|
const params: PollOptions = {
|
|
const params: PollOptions = {
|
|
- limit: "50",
|
|
|
|
|
|
+ limit: maxMessages.toString(),
|
|
};
|
|
};
|
|
const query = Object.entries(params).reduce(
|
|
const query = Object.entries(params).reduce(
|
|
(prev, [key, val]) =>
|
|
(prev, [key, val]) =>
|
|
@@ -22,42 +66,94 @@
|
|
});
|
|
});
|
|
|
|
|
|
const result = (await response.json()) as PollResult;
|
|
const result = (await response.json()) as PollResult;
|
|
- console.log(result);
|
|
|
|
if (result.ok) {
|
|
if (result.ok) {
|
|
logEvents = result.events;
|
|
logEvents = result.events;
|
|
|
|
+ } else {
|
|
|
|
+ error = result.error;
|
|
}
|
|
}
|
|
|
|
+ syncing = false;
|
|
}
|
|
}
|
|
</script>
|
|
</script>
|
|
|
|
|
|
<style>
|
|
<style>
|
|
|
|
+ .sync-button {
|
|
|
|
+ @apply bg-blue-600 py-1 px-3 text-white cursor-pointer rounded-sm text-sm;
|
|
|
|
+
|
|
|
|
+ &:hover {
|
|
|
|
+ @apply bg-blue-700 text-gray-100;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .logs-table {
|
|
|
|
+ @apply bg-black m-3 block overflow-y-auto;
|
|
|
|
+ max-height: 70%;
|
|
|
|
+
|
|
|
|
+ td,
|
|
|
|
+ th {
|
|
|
|
+ @apply font-mono px-2 text-sm;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ thead th {
|
|
|
|
+ @apply sticky top-0 bg-black py-2;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .error-box {
|
|
|
|
+ @apply text-gray-100 bg-red-600 py-4 px-3 rounded-sm;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .query-row {
|
|
|
|
+ @apply flex flex-row justify-end items-center;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .event-log-options {
|
|
|
|
+ @apply flex flex-col;
|
|
|
|
+
|
|
|
|
+ input[type="number"] {
|
|
|
|
+ @apply text-black w-16;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
</style>
|
|
</style>
|
|
|
|
|
|
<div class="viewport text-white">
|
|
<div class="viewport text-white">
|
|
<h1 class="text-3xl pb-4">Event logs</h1>
|
|
<h1 class="text-3xl pb-4">Event logs</h1>
|
|
<form class="py-2" on:submit|preventDefault={pollEvents}>
|
|
<form class="py-2" on:submit|preventDefault={pollEvents}>
|
|
- <input
|
|
|
|
- class="bg-blue-600 py-2 px-3 text-white cursor-pointer rounded-sm"
|
|
|
|
- type="submit"
|
|
|
|
- value="Query" />
|
|
|
|
|
|
+ <div class="event-log-options">
|
|
|
|
+ <label>Max messages: <input type="number" min="0" bind:value={maxMessages} /></label>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="query-row">
|
|
|
|
+ {#if syncing}
|
|
|
|
+ <Icon class="mx-1" data={sync} spin />
|
|
|
|
+ {/if}
|
|
|
|
+ <input class="sync-button" type="submit" value="Query" disabled={syncing} />
|
|
|
|
+ <label class="mx-1"><input type="checkbox" on:input={onAutoUpdateChecked}/> Auto update</label>
|
|
|
|
+ </div>
|
|
</form>
|
|
</form>
|
|
- <table class="py-3">
|
|
|
|
- <thead>
|
|
|
|
- <tr>
|
|
|
|
- <th>Source</th>
|
|
|
|
- <th>Timestamp</th>
|
|
|
|
- <th>Level</th>
|
|
|
|
- <th>Message</th>
|
|
|
|
- </tr>
|
|
|
|
- </thead>
|
|
|
|
- <tbody>
|
|
|
|
- {#each logEvents as logEvent (logEvent._id)}
|
|
|
|
|
|
+ {#if error}
|
|
|
|
+ <p class="error-box">
|
|
|
|
+ Failed to query: <span class="font-mono">{error}</span>
|
|
|
|
+ </p>
|
|
|
|
+ {/if}
|
|
|
|
+ {#if logEvents}
|
|
|
|
+ <table class="logs-table">
|
|
|
|
+ <thead>
|
|
<tr>
|
|
<tr>
|
|
- <td>{logEvent.label}</td>
|
|
|
|
- <td>{logEvent.timestamp}</td>
|
|
|
|
- <td>{logEvent.level}</td>
|
|
|
|
- <td>{logEvent.message}</td>
|
|
|
|
|
|
+ <th>Timestamp</th>
|
|
|
|
+ <th>Source</th>
|
|
|
|
+ <th>Level</th>
|
|
|
|
+ <th>Message</th>
|
|
</tr>
|
|
</tr>
|
|
- {/each}
|
|
|
|
- </tbody>
|
|
|
|
- </table>
|
|
|
|
|
|
+ </thead>
|
|
|
|
+ <tbody>
|
|
|
|
+ {#each logEvents as logEvent (logEvent._id)}
|
|
|
|
+ <tr style="color: {getLevelColor(logEvent.level)};">
|
|
|
|
+ <td>{logEvent.timestamp}</td>
|
|
|
|
+ <td>{logEvent.label}</td>
|
|
|
|
+ <td>{logEvent.level}</td>
|
|
|
|
+ <td>{logEvent.message}</td>
|
|
|
|
+ </tr>
|
|
|
|
+ {/each}
|
|
|
|
+ </tbody>
|
|
|
|
+ </table>
|
|
|
|
+ {/if}
|
|
</div>
|
|
</div>
|