logs.svelte 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. <script lang="typescript">
  2. import type {
  3. PollOptions,
  4. PollResult,
  5. LogEvent,
  6. } from "src/routes/logs/poll";
  7. import { faSyncAlt } from "@fortawesome/free-solid-svg-icons";
  8. import Icon from "svelte-awesome/components/Icon.svelte";
  9. const sync = (faSyncAlt as unknown) as undefined;
  10. const POLL_INTERVAL = 5000;
  11. let logEvents: LogEvent[] | undefined = undefined;
  12. let syncing = false;
  13. let poller = 0;
  14. let maxMessages = 50;
  15. let error = "";
  16. const LEVEL_COLORS: Record<string, string> = {
  17. "verbose": "#A0AEC0",
  18. "info": "#EFF2F7",
  19. "warn": "#ECC94B",
  20. "error": "#C53030",
  21. };
  22. function getLevelColor(level: string) {
  23. if (level in LEVEL_COLORS) {
  24. return LEVEL_COLORS[level];
  25. }
  26. return "#FFFFFF";
  27. }
  28. function onAutoUpdateChecked(event: Event & { target: HTMLInputElement }) {
  29. if (event.target.checked) {
  30. const pollCallback = async () => {
  31. await pollEvents();
  32. poller = setTimeout(pollCallback, POLL_INTERVAL);
  33. }
  34. if (poller >= 0) {
  35. poller = setTimeout(pollCallback, POLL_INTERVAL);
  36. }
  37. } else {
  38. clearTimeout(poller);
  39. poller = -1;
  40. }
  41. }
  42. async function pollEvents() {
  43. if (syncing) {
  44. return;
  45. }
  46. syncing = true;
  47. const params: PollOptions = {
  48. limit: maxMessages.toString(),
  49. };
  50. const query = Object.entries(params).reduce(
  51. (prev, [key, val]) =>
  52. `${prev}&${encodeURIComponent(key)}=${encodeURIComponent(val)}`,
  53. ""
  54. );
  55. const response = await fetch(`/logs/poll?${query}`, {
  56. method: "get",
  57. credentials: "include",
  58. headers: {
  59. Accept: "application/json",
  60. "Content-Type": "application/json",
  61. },
  62. });
  63. const result = (await response.json()) as PollResult;
  64. if (result.ok) {
  65. logEvents = result.events;
  66. } else {
  67. error = result.error;
  68. }
  69. syncing = false;
  70. }
  71. </script>
  72. <style>
  73. .sync-button {
  74. @apply bg-blue-600 py-1 px-3 text-white cursor-pointer rounded-sm text-sm;
  75. &:hover {
  76. @apply bg-blue-700 text-gray-100;
  77. }
  78. }
  79. .logs-table {
  80. @apply bg-black m-3 block overflow-y-auto;
  81. max-height: 70%;
  82. td,
  83. th {
  84. @apply font-mono px-2 text-sm;
  85. }
  86. thead th {
  87. @apply sticky top-0 bg-black py-2;
  88. }
  89. }
  90. .error-box {
  91. @apply text-gray-100 bg-red-600 py-4 px-3 rounded-sm;
  92. }
  93. .query-row {
  94. @apply flex flex-row justify-end items-center;
  95. }
  96. .event-log-options {
  97. @apply flex flex-col;
  98. input[type="number"] {
  99. @apply text-black w-16;
  100. }
  101. }
  102. </style>
  103. <div class="viewport text-white">
  104. <h1 class="text-3xl pb-4">Event logs</h1>
  105. <form class="py-2" on:submit|preventDefault={pollEvents}>
  106. <div class="event-log-options">
  107. <label>Max messages: <input type="number" min="0" bind:value={maxMessages} /></label>
  108. </div>
  109. <div class="query-row">
  110. {#if syncing}
  111. <Icon class="mx-1" data={sync} spin />
  112. {/if}
  113. <input class="sync-button" type="submit" value="Query" disabled={syncing} />
  114. <label class="mx-1"><input type="checkbox" on:input={onAutoUpdateChecked}/> Auto update</label>
  115. </div>
  116. </form>
  117. {#if error}
  118. <p class="error-box">
  119. Failed to query: <span class="font-mono">{error}</span>
  120. </p>
  121. {/if}
  122. {#if logEvents}
  123. <table class="logs-table">
  124. <thead>
  125. <tr>
  126. <th>Timestamp</th>
  127. <th>Source</th>
  128. <th>Level</th>
  129. <th>Message</th>
  130. </tr>
  131. </thead>
  132. <tbody>
  133. {#each logEvents as logEvent (logEvent._id)}
  134. <tr style="color: {getLevelColor(logEvent.level)};">
  135. <td>{logEvent.timestamp}</td>
  136. <td>{logEvent.label}</td>
  137. <td>{logEvent.level}</td>
  138. <td>{logEvent.message}</td>
  139. </tr>
  140. {/each}
  141. </tbody>
  142. </table>
  143. {/if}
  144. </div>