logs.svelte 4.4 KB

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