socketio.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. <template>
  2. <AppPaneLayout layout-id="socketio">
  3. <template #primary>
  4. <div
  5. class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary hide-scrollbar"
  6. >
  7. <div class="inline-flex flex-1 space-x-2">
  8. <div class="flex flex-1">
  9. <label for="client-version">
  10. <tippy
  11. ref="versionOptions"
  12. interactive
  13. trigger="click"
  14. theme="popover"
  15. arrow
  16. >
  17. <template #trigger>
  18. <span class="select-wrapper">
  19. <input
  20. id="client-version"
  21. v-tippy="{ theme: 'tooltip' }"
  22. title="socket.io-client version"
  23. class="flex px-4 py-2 font-semibold border rounded-l cursor-pointer bg-primaryLight border-divider text-secondaryDark w-26"
  24. :value="`Client ${clientVersion}`"
  25. readonly
  26. :disabled="
  27. connectionState === 'CONNECTED' ||
  28. connectionState === 'CONNECTING'
  29. "
  30. />
  31. </span>
  32. </template>
  33. <div class="flex flex-col" role="menu">
  34. <SmartItem
  35. v-for="version in SIOVersions"
  36. :key="`client-${version}`"
  37. :label="`Client ${version}`"
  38. @click.native="onSelectVersion(version)"
  39. />
  40. </div>
  41. </tippy>
  42. </label>
  43. <input
  44. id="socketio-url"
  45. v-model="url"
  46. type="url"
  47. autocomplete="off"
  48. spellcheck="false"
  49. :class="{ error: !isUrlValid }"
  50. class="flex flex-1 w-full px-4 py-2 border bg-primaryLight border-divider text-secondaryDark"
  51. :placeholder="`${t('socketio.url')}`"
  52. :disabled="
  53. connectionState === 'CONNECTED' ||
  54. connectionState === 'CONNECTING'
  55. "
  56. @keyup.enter="isUrlValid ? toggleConnection() : null"
  57. />
  58. <input
  59. id="socketio-path"
  60. v-model="path"
  61. class="flex flex-1 w-full px-4 py-2 border rounded-r bg-primaryLight border-divider text-secondaryDark"
  62. spellcheck="false"
  63. :disabled="
  64. connectionState === 'CONNECTED' ||
  65. connectionState === 'CONNECTING'
  66. "
  67. @keyup.enter="isUrlValid ? toggleConnection() : null"
  68. />
  69. </div>
  70. <ButtonPrimary
  71. id="connect"
  72. :disabled="!isUrlValid"
  73. name="connect"
  74. class="w-32"
  75. :label="
  76. connectionState === 'DISCONNECTED'
  77. ? t('action.connect')
  78. : t('action.disconnect')
  79. "
  80. :loading="connectionState === 'CONNECTING'"
  81. @click.native="toggleConnection"
  82. />
  83. </div>
  84. </div>
  85. <SmartTabs
  86. v-model="selectedTab"
  87. styles="sticky bg-primary top-upperPrimaryStickyFold z-10"
  88. render-inactive-tabs
  89. >
  90. <SmartTab
  91. :id="'communication'"
  92. :label="`${t('websocket.communication')}`"
  93. render-inactive-tabs
  94. >
  95. <RealtimeCommunication
  96. :show-event-field="true"
  97. :is-connected="connectionState === 'CONNECTED'"
  98. @send-message="sendMessage($event)"
  99. ></RealtimeCommunication>
  100. </SmartTab>
  101. <SmartTab :id="'protocols'" :label="`${t('request.authorization')}`">
  102. <div
  103. class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
  104. >
  105. <span class="flex items-center">
  106. <label class="font-semibold text-secondaryLight">
  107. {{ t("authorization.type") }}
  108. </label>
  109. <tippy
  110. ref="authTypeOptions"
  111. interactive
  112. trigger="click"
  113. theme="popover"
  114. arrow
  115. >
  116. <template #trigger>
  117. <span class="select-wrapper">
  118. <ButtonSecondary
  119. class="pr-8 ml-2 rounded-none"
  120. :label="authType"
  121. />
  122. </span>
  123. </template>
  124. <div class="flex flex-col" role="menu">
  125. <SmartItem
  126. label="None"
  127. :icon="
  128. authType === 'None'
  129. ? 'radio_button_checked'
  130. : 'radio_button_unchecked'
  131. "
  132. :active="authType === 'None'"
  133. @click.native="
  134. () => {
  135. authType = 'None'
  136. authTypeOptions.tippy().hide()
  137. }
  138. "
  139. />
  140. <SmartItem
  141. label="Bearer Token"
  142. :icon="
  143. authType === 'Bearer'
  144. ? 'radio_button_checked'
  145. : 'radio_button_unchecked'
  146. "
  147. :active="authType === 'Bearer'"
  148. @click.native="
  149. () => {
  150. authType = 'Bearer'
  151. authTypeOptions.tippy().hide()
  152. }
  153. "
  154. />
  155. </div>
  156. </tippy>
  157. </span>
  158. <div class="flex">
  159. <SmartCheckbox
  160. :on="authActive"
  161. class="px-2"
  162. @change="authActive = !authActive"
  163. >
  164. {{ t("state.enabled") }}
  165. </SmartCheckbox>
  166. <ButtonSecondary
  167. v-tippy="{ theme: 'tooltip' }"
  168. to="https://docs.hoppscotch.io/features/authorization"
  169. blank
  170. :title="t('app.wiki')"
  171. svg="help-circle"
  172. />
  173. <ButtonSecondary
  174. v-tippy="{ theme: 'tooltip' }"
  175. :title="t('action.clear')"
  176. svg="trash-2"
  177. @click.native="clearContent"
  178. />
  179. </div>
  180. </div>
  181. <div
  182. v-if="authType === 'None'"
  183. class="flex flex-col items-center justify-center p-4 text-secondaryLight"
  184. >
  185. <img
  186. :src="`/images/states/${$colorMode.value}/login.svg`"
  187. loading="lazy"
  188. class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
  189. :alt="`${t('empty.authorization')}`"
  190. />
  191. <span class="pb-4 text-center">
  192. {{ t("socketio.connection_not_authorized") }}
  193. </span>
  194. <ButtonSecondary
  195. outline
  196. :label="t('app.documentation')"
  197. to="https://docs.hoppscotch.io/features/authorization"
  198. blank
  199. svg="external-link"
  200. reverse
  201. class="mb-4"
  202. />
  203. </div>
  204. <div
  205. v-if="authType === 'Bearer'"
  206. class="flex flex-1 border-b border-dividerLight"
  207. >
  208. <div class="w-2/3 border-r border-dividerLight">
  209. <div class="flex flex-1 border-b border-dividerLight">
  210. <SmartEnvInput v-model="bearerToken" placeholder="Token" />
  211. </div>
  212. </div>
  213. <div
  214. class="sticky h-full p-4 overflow-auto bg-primary top-upperTertiaryStickyFold min-w-46 max-w-1/3 z-9"
  215. >
  216. <div class="p-2">
  217. <div class="pb-2 text-secondaryLight">
  218. {{ t("helpers.authorization") }}
  219. </div>
  220. <SmartAnchor
  221. class="link"
  222. :label="`${t('authorization.learn')} \xA0 →`"
  223. to="https://docs.hoppscotch.io/features/authorization"
  224. blank
  225. />
  226. </div>
  227. </div>
  228. </div>
  229. </SmartTab>
  230. </SmartTabs>
  231. </template>
  232. <template #secondary>
  233. <RealtimeLog
  234. :title="t('socketio.log')"
  235. :log="log"
  236. @delete="clearLogEntries()"
  237. />
  238. </template>
  239. </AppPaneLayout>
  240. </template>
  241. <script setup lang="ts">
  242. import { onMounted, onUnmounted, ref, watch } from "@nuxtjs/composition-api"
  243. import debounce from "lodash/debounce"
  244. import {
  245. SIOConnection,
  246. SIOError,
  247. SIOMessage,
  248. SOCKET_CLIENTS,
  249. } from "~/helpers/realtime/SIOConnection"
  250. import {
  251. useI18n,
  252. useNuxt,
  253. useReadonlyStream,
  254. useStream,
  255. useStreamSubscriber,
  256. useToast,
  257. } from "~/helpers/utils/composables"
  258. import {
  259. addSIOLogLine,
  260. setSIOEndpoint,
  261. setSIOLog,
  262. setSIOPath,
  263. setSIOVersion,
  264. SIOClientVersion,
  265. SIOEndpoint$,
  266. SIOLog$,
  267. SIOPath$,
  268. SIOVersion$,
  269. } from "~/newstore/SocketIOSession"
  270. const t = useI18n()
  271. const toast = useToast()
  272. const nuxt = useNuxt()
  273. const { subscribeToStream } = useStreamSubscriber()
  274. type SIOTab = "communication" | "protocols"
  275. const selectedTab = ref<SIOTab>("communication")
  276. const SIOVersions = Object.keys(SOCKET_CLIENTS)
  277. const url = useStream(SIOEndpoint$, "", setSIOEndpoint)
  278. const clientVersion = useStream(SIOVersion$, "v4", setSIOVersion)
  279. const path = useStream(SIOPath$, "", setSIOPath)
  280. const socket = new SIOConnection()
  281. const connectionState = useReadonlyStream(
  282. socket.connectionState$,
  283. "DISCONNECTED"
  284. )
  285. const log = useStream(SIOLog$, [], setSIOLog)
  286. const authTypeOptions = ref<any>(null)
  287. const versionOptions = ref<any | null>(null)
  288. const isUrlValid = ref(true)
  289. const authType = ref<"None" | "Bearer">("None")
  290. const bearerToken = ref("")
  291. const authActive = ref(true)
  292. let worker: Worker
  293. const workerResponseHandler = ({
  294. data,
  295. }: {
  296. data: { url: string; result: boolean }
  297. }) => {
  298. if (data.url === url.value) isUrlValid.value = data.result
  299. }
  300. const getMessagePayload = (data: SIOMessage): string =>
  301. typeof data.value === "object" ? JSON.stringify(data.value) : `${data.value}`
  302. const getErrorPayload = (error: SIOError): string => {
  303. switch (error.type) {
  304. case "CONNECTION":
  305. return t("state.connection_error").toString()
  306. case "RECONNECT_ERROR":
  307. return t("state.reconnection_error").toString()
  308. default:
  309. return t("state.disconnected_from", { name: url.value }).toString()
  310. }
  311. }
  312. onMounted(() => {
  313. worker = nuxt.value.$worker.createRejexWorker()
  314. worker.addEventListener("message", workerResponseHandler)
  315. subscribeToStream(socket.event$, (event) => {
  316. switch (event?.type) {
  317. case "CONNECTING":
  318. log.value = [
  319. {
  320. payload: `${t("state.connecting_to", { name: url.value })}`,
  321. source: "info",
  322. color: "var(--accent-color)",
  323. ts: undefined,
  324. },
  325. ]
  326. break
  327. case "CONNECTED":
  328. log.value = [
  329. {
  330. payload: `${t("state.connected_to", { name: url.value })}`,
  331. source: "info",
  332. color: "var(--accent-color)",
  333. ts: event.time,
  334. },
  335. ]
  336. toast.success(`${t("state.connected")}`)
  337. break
  338. case "MESSAGE_SENT":
  339. addSIOLogLine({
  340. prefix: `[${event.message.eventName}]`,
  341. payload: getMessagePayload(event.message),
  342. source: "client",
  343. ts: event.time,
  344. })
  345. break
  346. case "MESSAGE_RECEIVED":
  347. addSIOLogLine({
  348. prefix: `[${event.message.eventName}]`,
  349. payload: getMessagePayload(event.message),
  350. source: "server",
  351. ts: event.time,
  352. })
  353. break
  354. case "ERROR":
  355. addSIOLogLine({
  356. payload: getErrorPayload(event.error),
  357. source: "info",
  358. color: "#ff5555",
  359. ts: event.time,
  360. })
  361. break
  362. case "DISCONNECTED":
  363. addSIOLogLine({
  364. payload: t("state.disconnected_from", { name: url.value }).toString(),
  365. source: "info",
  366. color: "#ff5555",
  367. ts: event.time,
  368. })
  369. toast.error(`${t("state.disconnected")}`)
  370. break
  371. }
  372. })
  373. })
  374. watch(url, (newUrl) => {
  375. if (newUrl) debouncer()
  376. })
  377. watch(connectionState, (connected) => {
  378. if (connected) versionOptions.value.tippy().disable()
  379. else versionOptions.value.tippy().enable()
  380. })
  381. onUnmounted(() => {
  382. worker.terminate()
  383. })
  384. const debouncer = debounce(function () {
  385. worker.postMessage({ type: "socketio", url: url.value })
  386. }, 1000)
  387. const toggleConnection = () => {
  388. // If it is connecting:
  389. if (connectionState.value === "DISCONNECTED") {
  390. return socket.connect({
  391. url: url.value,
  392. path: path.value || "/socket.io",
  393. clientVersion: clientVersion.value,
  394. auth: authActive.value
  395. ? {
  396. type: authType.value,
  397. token: bearerToken.value,
  398. }
  399. : undefined,
  400. })
  401. }
  402. // Otherwise, it's disconnecting.
  403. socket.disconnect()
  404. }
  405. const sendMessage = (event: { message: string; eventName: string }) => {
  406. socket.sendMessage(event)
  407. }
  408. const onSelectVersion = (version: SIOClientVersion) => {
  409. clientVersion.value = version
  410. versionOptions.value.tippy().hide()
  411. }
  412. const clearLogEntries = () => {
  413. log.value = []
  414. }
  415. </script>