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. "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. },
  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>