Log.vue 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. <template>
  2. <div class="flex flex-1 flex-col overflow-auto whitespace-nowrap">
  3. <div
  4. v-if="log.length !== 0"
  5. class="sticky top-0 z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4"
  6. >
  7. <label for="log" class="truncate font-semibold text-secondaryLight">
  8. {{ title }}
  9. </label>
  10. <div class="flex">
  11. <HoppButtonSecondary
  12. v-tippy="{ theme: 'tooltip' }"
  13. :title="t('action.delete')"
  14. :icon="IconTrash"
  15. @click="emit('delete')"
  16. />
  17. <HoppButtonSecondary
  18. id="bottompage"
  19. v-tippy="{ theme: 'tooltip' }"
  20. :title="t('action.scroll_to_top')"
  21. :icon="IconArrowUp"
  22. @click="scrollTo('top')"
  23. />
  24. <HoppButtonSecondary
  25. id="bottompage"
  26. v-tippy="{ theme: 'tooltip' }"
  27. :title="t('action.scroll_to_bottom')"
  28. :icon="IconArrowDown"
  29. @click="scrollTo('bottom')"
  30. />
  31. <HoppButtonSecondary
  32. id="bottompage"
  33. v-tippy="{ theme: 'tooltip' }"
  34. :title="`${t('action.autoscroll')}: ${
  35. autoScrollEnabled ? t('action.turn_off') : t('action.turn_on')
  36. }`"
  37. :icon="IconChevronsDown"
  38. :color="autoScrollEnabled ? 'green' : 'red'"
  39. @click="autoScrollEnabled = !autoScrollEnabled"
  40. />
  41. </div>
  42. </div>
  43. <div
  44. v-if="log.length !== 0"
  45. ref="logs"
  46. class="flex flex-1 flex-col overflow-y-auto"
  47. >
  48. <div class="border-b border-dividerLight">
  49. <div class="flex flex-col divide-y divide-dividerLight">
  50. <RealtimeLogEntry
  51. v-for="(entry, index) in log"
  52. :key="`entry-${index}`"
  53. :entry="entry"
  54. />
  55. </div>
  56. </div>
  57. </div>
  58. <AppShortcutsPrompt v-else class="p-4" />
  59. </div>
  60. </template>
  61. <script setup lang="ts">
  62. import { ref, PropType, watch, Ref } from "vue"
  63. import IconTrash from "~icons/lucide/trash"
  64. import IconArrowUp from "~icons/lucide/arrow-up"
  65. import IconArrowDown from "~icons/lucide/arrow-down"
  66. import IconChevronsDown from "~icons/lucide/chevron-down"
  67. import { useThrottleFn, useScroll } from "@vueuse/core"
  68. import { useI18n } from "@composables/i18n"
  69. export type LogEntryData = {
  70. prefix?: string
  71. ts: number | undefined
  72. source: "info" | "client" | "server" | "disconnected"
  73. payload: string
  74. event?: "connecting" | "connected" | "disconnected" | "error"
  75. }
  76. const props = defineProps({
  77. log: { type: Array as PropType<LogEntryData[]>, default: () => [] },
  78. title: {
  79. type: String,
  80. default: "",
  81. },
  82. })
  83. const emit = defineEmits<{
  84. (e: "delete"): void
  85. }>()
  86. const t = useI18n()
  87. const logs = ref<HTMLElement>()
  88. const autoScrollEnabled = ref(true)
  89. const logListScroll = useScroll(logs as Ref<HTMLElement>)
  90. // Disable autoscroll when scrolling to top
  91. watch(logListScroll.isScrolling, (isScrolling) => {
  92. if (isScrolling && logListScroll.directions.top)
  93. autoScrollEnabled.value = false
  94. })
  95. const scrollTo = (position: "top" | "bottom") => {
  96. if (position === "top") {
  97. logs.value?.scroll({
  98. behavior: "smooth",
  99. top: 0,
  100. })
  101. } else if (position === "bottom") {
  102. logs.value?.scroll({
  103. behavior: "smooth",
  104. top: logs.value?.scrollHeight,
  105. })
  106. }
  107. }
  108. watch(
  109. () => props.log,
  110. useThrottleFn(() => {
  111. if (autoScrollEnabled.value) scrollTo("bottom")
  112. }, 200),
  113. { flush: "post" }
  114. )
  115. </script>