LogEntry.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. <template>
  2. <div v-if="entry" class="divide-y divide-dividerLight">
  3. <div :style="{ color: entryColor }" class="realtime-log">
  4. <div class="flex group">
  5. <div class="flex flex-1 divide-x divide-dividerLight">
  6. <div class="inline-flex items-center p-2">
  7. <SmartIcon
  8. class="svg-icons"
  9. :name="iconName"
  10. :style="{ color: iconColor }"
  11. @click.native="copyQuery(entry.payload)"
  12. />
  13. </div>
  14. <div
  15. v-if="entry.ts !== undefined"
  16. class="items-center hidden px-1 w-18 sm:inline-flex"
  17. >
  18. <span
  19. v-tippy="{ theme: 'tooltip' }"
  20. :title="relativeTime"
  21. class="mx-auto truncate ts-font text-secondaryLight hover:text-secondary hover:text-center"
  22. >
  23. {{ new Date(entry.ts).toLocaleTimeString() }}
  24. </span>
  25. </div>
  26. <div
  27. class="items-center flex-1 min-w-0 p-2 inline-grid"
  28. @click="toggleExpandPayload()"
  29. >
  30. <div class="truncate">
  31. <span v-if="entry.prefix !== undefined" class="!inline">{{
  32. entry.prefix
  33. }}</span>
  34. {{ entry.payload }}
  35. </div>
  36. </div>
  37. </div>
  38. <ButtonSecondary
  39. v-tippy="{ theme: 'tooltip' }"
  40. :title="t('action.copy')"
  41. :svg="`${copyQueryIcon}`"
  42. class="hidden group-hover:inline-flex"
  43. @click.native="copyQuery(entry.payload)"
  44. />
  45. <ButtonSecondary
  46. svg="chevron-down"
  47. class="transform"
  48. :class="{ 'rotate-180': !minimized }"
  49. @click.native="toggleExpandPayload()"
  50. />
  51. </div>
  52. </div>
  53. <div v-if="!minimized" class="overflow-hidden bg-primaryLight">
  54. <SmartTabs
  55. v-model="selectedTab"
  56. styles="bg-primaryLight"
  57. render-inactive-tabs
  58. >
  59. <SmartTab v-if="isJSON(entry.payload)" id="json" label="JSON" />
  60. <SmartTab id="raw" label="Raw" />
  61. </SmartTabs>
  62. <div
  63. class="z-10 flex items-center justify-between pl-4 border-b border-dividerLight top-lowerSecondaryStickyFold"
  64. >
  65. <label class="font-semibold text-secondaryLight">
  66. {{ t("response.body") }}
  67. </label>
  68. <div class="flex">
  69. <ButtonSecondary
  70. v-tippy="{ theme: 'tooltip' }"
  71. :title="t('state.linewrap')"
  72. :class="{ '!text-accent': linewrapEnabled }"
  73. svg="wrap-text"
  74. @click.native.prevent="linewrapEnabled = !linewrapEnabled"
  75. />
  76. <ButtonSecondary
  77. ref="downloadResponse"
  78. v-tippy="{ theme: 'tooltip' }"
  79. :title="t('action.download_file')"
  80. :svg="downloadIcon"
  81. @click.native="downloadResponse"
  82. />
  83. <ButtonSecondary
  84. ref="copyResponse"
  85. v-tippy="{ theme: 'tooltip' }"
  86. :title="t('action.copy')"
  87. :svg="copyIcon"
  88. @click.native="copyResponse"
  89. />
  90. </div>
  91. </div>
  92. <div ref="editor"></div>
  93. <div
  94. v-if="outlinePath && selectedTab === 'json'"
  95. class="sticky bottom-0 z-10 flex px-2 overflow-auto border-t bg-primaryLight border-dividerLight flex-nowrap hide-scrollbar"
  96. >
  97. <div
  98. v-for="(item, index) in outlinePath"
  99. :key="`item-${index}`"
  100. class="flex items-center"
  101. >
  102. <tippy
  103. ref="outlineOptions"
  104. interactive
  105. trigger="click"
  106. theme="popover"
  107. arrow
  108. >
  109. <template #trigger>
  110. <div v-if="item.kind === 'RootObject'" class="outline-item">
  111. {}
  112. </div>
  113. <div v-if="item.kind === 'RootArray'" class="outline-item">
  114. []
  115. </div>
  116. <div v-if="item.kind === 'ArrayMember'" class="outline-item">
  117. {{ item.index }}
  118. </div>
  119. <div v-if="item.kind === 'ObjectMember'" class="outline-item">
  120. {{ item.name }}
  121. </div>
  122. </template>
  123. <div
  124. v-if="item.kind === 'ArrayMember' || item.kind === 'ObjectMember'"
  125. >
  126. <div
  127. v-if="item.kind === 'ArrayMember'"
  128. class="flex flex-col"
  129. role="menu"
  130. >
  131. <SmartItem
  132. v-for="(arrayMember, astIndex) in item.astParent.values"
  133. :key="`ast-${astIndex}`"
  134. :label="`${astIndex}`"
  135. @click.native="
  136. () => {
  137. jumpCursor(arrayMember)
  138. outlineOptions[index].tippy().hide()
  139. }
  140. "
  141. />
  142. </div>
  143. <div
  144. v-if="item.kind === 'ObjectMember'"
  145. class="flex flex-col"
  146. role="menu"
  147. >
  148. <SmartItem
  149. v-for="(objectMember, astIndex) in item.astParent.members"
  150. :key="`ast-${astIndex}`"
  151. :label="objectMember.key.value"
  152. @click.native="
  153. () => {
  154. jumpCursor(objectMember)
  155. outlineOptions[index].tippy().hide()
  156. }
  157. "
  158. />
  159. </div>
  160. </div>
  161. <div
  162. v-if="item.kind === 'RootObject'"
  163. class="flex flex-col"
  164. role="menu"
  165. >
  166. <SmartItem
  167. label="{}"
  168. @click.native="
  169. () => {
  170. jumpCursor(item.astValue)
  171. outlineOptions[index].tippy().hide()
  172. }
  173. "
  174. />
  175. </div>
  176. <div
  177. v-if="item.kind === 'RootArray'"
  178. class="flex flex-col"
  179. role="menu"
  180. >
  181. <SmartItem
  182. label="[]"
  183. @click.native="
  184. () => {
  185. jumpCursor(item.astValue)
  186. outlineOptions[index].tippy().hide()
  187. }
  188. "
  189. />
  190. </div>
  191. </tippy>
  192. <i
  193. v-if="index + 1 !== outlinePath.length"
  194. class="opacity-50 text-secondaryLight material-icons"
  195. >
  196. chevron_right
  197. </i>
  198. </div>
  199. </div>
  200. </div>
  201. </div>
  202. <div v-else>{{ t("response.waiting_for_connection") }}</div>
  203. </template>
  204. <script setup lang="ts">
  205. import * as LJSON from "lossless-json"
  206. import * as O from "fp-ts/Option"
  207. import { pipe } from "fp-ts/function"
  208. import { ref, computed, reactive, watch } from "@nuxtjs/composition-api"
  209. import { refAutoReset, useTimeAgo } from "@vueuse/core"
  210. import { LogEntryData } from "./Log.vue"
  211. import { useI18n } from "~/helpers/utils/composables"
  212. import { copyToClipboard } from "~/helpers/utils/clipboard"
  213. import { isJSON } from "~/helpers/functional/json"
  214. import useCopyResponse from "~/helpers/lenses/composables/useCopyResponse"
  215. import useDownloadResponse from "~/helpers/lenses/composables/useDownloadResponse"
  216. import { useCodemirror } from "~/helpers/editor/codemirror"
  217. import jsonParse, { JSONObjectMember, JSONValue } from "~/helpers/jsonParse"
  218. import { getJSONOutlineAtPos } from "~/helpers/newOutline"
  219. import {
  220. convertIndexToLineCh,
  221. convertLineChToIndex,
  222. } from "~/helpers/editor/utils"
  223. const t = useI18n()
  224. const props = defineProps<{ entry: LogEntryData }>()
  225. const outlineOptions = ref<any | null>(null)
  226. const editor = ref<any | null>(null)
  227. const linewrapEnabled = ref(true)
  228. const logPayload = computed(() => props.entry.payload)
  229. const selectedTab = ref<"json" | "raw">(
  230. isJSON(props.entry.payload) ? "json" : "raw"
  231. )
  232. // CodeMirror Implementation
  233. const jsonBodyText = computed(() =>
  234. pipe(
  235. logPayload.value,
  236. O.tryCatchK(LJSON.parse),
  237. O.map((val) => LJSON.stringify(val, undefined, 2)),
  238. O.getOrElse(() => logPayload.value)
  239. )
  240. )
  241. const ast = computed(() =>
  242. pipe(
  243. jsonBodyText.value,
  244. O.tryCatchK(jsonParse),
  245. O.getOrElseW(() => null)
  246. )
  247. )
  248. const editorText = computed(() => {
  249. if (selectedTab.value === "json") return jsonBodyText.value
  250. else return logPayload.value
  251. })
  252. const editorMode = computed(() => {
  253. if (selectedTab.value === "json") return "application/ld+json"
  254. else return "text/plain"
  255. })
  256. const { cursor } = useCodemirror(
  257. editor,
  258. editorText,
  259. reactive({
  260. extendedEditorConfig: {
  261. mode: editorMode,
  262. readOnly: true,
  263. lineWrapping: linewrapEnabled,
  264. },
  265. linter: null,
  266. completer: null,
  267. environmentHighlights: false,
  268. })
  269. )
  270. const jumpCursor = (ast: JSONValue | JSONObjectMember) => {
  271. const pos = convertIndexToLineCh(jsonBodyText.value, ast.start)
  272. pos.line--
  273. cursor.value = pos
  274. }
  275. const outlinePath = computed(() =>
  276. pipe(
  277. ast.value,
  278. O.fromNullable,
  279. O.map((ast) =>
  280. getJSONOutlineAtPos(
  281. ast,
  282. convertLineChToIndex(jsonBodyText.value, cursor.value)
  283. )
  284. ),
  285. O.getOrElseW(() => null)
  286. )
  287. )
  288. // Code for UI Changes
  289. const minimized = ref(true)
  290. watch(minimized, () => {
  291. selectedTab.value = isJSON(props.entry.payload) ? "json" : "raw"
  292. })
  293. const toggleExpandPayload = () => {
  294. minimized.value = !minimized.value
  295. }
  296. const { copyIcon, copyResponse } = useCopyResponse(logPayload)
  297. const { downloadIcon, downloadResponse } = useDownloadResponse(
  298. "application/json",
  299. logPayload
  300. )
  301. const copyQueryIcon = refAutoReset<"copy" | "check">("copy", 1000)
  302. const copyQuery = (entry: string) => {
  303. copyToClipboard(entry)
  304. copyQueryIcon.value = "check"
  305. }
  306. // Relative Time
  307. // TS could be undefined here. We're just assigning a default value to 0 because we're not showing it in the UI
  308. const relativeTime = useTimeAgo(computed(() => props.entry.ts ?? 0))
  309. const ENTRY_COLORS = {
  310. connected: "#10b981",
  311. connecting: "#10b981",
  312. error: "#ff5555",
  313. disconnected: "#ff5555",
  314. } as const
  315. // Assigns color based on entry event
  316. const entryColor = computed(() => ENTRY_COLORS[props.entry.event])
  317. const ICONS = {
  318. info: {
  319. iconName: "info-realtime",
  320. iconColor: "#10b981",
  321. },
  322. client: {
  323. iconName: "arrow-up-right",
  324. iconColor: "#eaaa45",
  325. },
  326. server: {
  327. iconName: "arrow-down-left",
  328. iconColor: "#38d4ea",
  329. },
  330. disconnected: {
  331. iconName: "info-disconnect",
  332. iconColor: "#ff5555",
  333. },
  334. } as const
  335. const iconColor = computed(() => ICONS[props.entry.source].iconColor)
  336. const iconName = computed(() => ICONS[props.entry.source].iconName)
  337. </script>
  338. <style scoped lang="scss">
  339. .realtime-log {
  340. @apply text-secondary;
  341. @apply overflow-hidden;
  342. &,
  343. span {
  344. @apply select-text;
  345. }
  346. span {
  347. @apply block;
  348. @apply break-words break-all;
  349. }
  350. }
  351. .outline-item {
  352. @apply cursor-pointer;
  353. @apply flex-grow-0 flex-shrink-0;
  354. @apply text-secondaryLight;
  355. @apply inline-flex;
  356. @apply items-center;
  357. @apply px-2;
  358. @apply py-1;
  359. @apply transition;
  360. @apply hover: text-secondary;
  361. }
  362. .ts-font {
  363. font-size: 0.6rem;
  364. }
  365. </style>